From 102be1ba39b05c2102572b804cfe42bc342f4f86 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 17 Sep 2020 11:05:47 -0400 Subject: [PATCH 01/30] Fix memory leak in query_string_input (#77649) --- .../public/ui/query_string_input/query_string_input.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index f159cac664a9e9..8e1151b387fee7 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -546,13 +546,16 @@ export class QueryStringInputUI extends Component { this.updateSuggestions.cancel(); this.componentIsUnmounting = true; window.removeEventListener('resize', this.handleAutoHeight); - window.removeEventListener('scroll', this.handleListUpdate); + window.removeEventListener('scroll', this.handleListUpdate, { capture: true }); } - handleListUpdate = () => - this.setState({ + handleListUpdate = () => { + if (this.componentIsUnmounting) return; + + return this.setState({ queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(), }); + }; handleAutoHeight = () => { if (this.inputRef !== null && document.activeElement === this.inputRef) { From 4bf0932500e1d74e42bb8e4e42ec61f53fbbfd4a Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 17 Sep 2020 11:11:04 -0400 Subject: [PATCH 02/30] Adding meta data and highlighting to nav search (#77662) --- .../__snapshots__/search_bar.test.tsx.snap | 28 ++++++++++++++++--- .../public/components/search_bar.test.tsx | 4 ++- .../public/components/search_bar.tsx | 25 +++++++++++------ 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap index 0217f039e08bab..7bb9954fa30489 100644 --- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -7,8 +7,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Canvas", "label": "Canvas", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Canvasundefinedundefined", + "title": "Canvas • Kibana", "url": "/app/test/Canvas", }, Object { @@ -16,8 +21,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Discover", "label": "Discover", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Discoverundefinedundefined", + "title": "Discover • Kibana", "url": "/app/test/Discover", }, Object { @@ -25,8 +35,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Graph", "label": "Graph", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Graphundefinedundefined", + "title": "Graph • Kibana", "url": "/app/test/Graph", }, ] @@ -39,8 +54,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Discover", "label": "Discover", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Discoverundefinedundefined", + "title": "Discover • Kibana", "url": "/app/test/Discover", }, Object { diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx index 0d1e8725b4911d..11fbc7931e6201 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -21,6 +21,7 @@ type Result = { id: string; type: string } | string; const createResult = (result: Result): GlobalSearchResult => { const id = typeof result === 'string' ? result : result.id; const type = typeof result === 'string' ? 'application' : result.type; + const meta = type === 'application' ? { categoryLabel: 'Kibana' } : { categoryLabel: null }; return { id, @@ -28,6 +29,7 @@ const createResult = (result: Result): GlobalSearchResult => { title: id, url: `/app/test/${id}`, score: 42, + meta, }; }; @@ -74,7 +76,7 @@ describe('SearchBar', () => { expect(findSpy).toHaveBeenCalledTimes(1); expect(findSpy).toHaveBeenCalledWith('', {}); expect(getSelectableProps(component).options).toMatchSnapshot(); - await wait(() => getSearchProps(component).onSearch('d')); + await wait(() => getSearchProps(component).onKeyUpCapture({ currentTarget: { value: 'd' } })); jest.runAllTimers(); component.update(); expect(getSelectableProps(component).options).toMatchSnapshot(); diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index d00349e21a7e4f..e41f9243198ad9 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -52,14 +52,20 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) { if (!isMounted()) return; _setOptions([ - ..._options.map((option) => ({ - key: option.id, - label: option.title, - url: option.url, - ...(option.icon && { icon: { type: option.icon } }), - ...(option.type && - option.type !== 'application' && { meta: [{ text: cleanMeta(option.type) }] }), - })), + ..._options.map(({ id, title, url, icon, type, meta }) => { + const option: EuiSelectableTemplateSitewideOption = { + key: id, + label: title, + url, + }; + + if (icon) option.icon = { type: icon }; + + if (type === 'application') option.meta = [{ text: meta?.categoryLabel as string }]; + else option.meta = [{ text: cleanMeta(type) }]; + + return option; + }), ]); }, [isMounted, _setOptions] @@ -133,7 +139,8 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) { onChange={onChange} options={options} searchProps={{ - onSearch: setSearchValue, + onKeyUpCapture: (e: React.KeyboardEvent) => + setSearchValue(e.currentTarget.value), 'data-test-subj': 'header-search', inputRef: setSearchRef, compressed: true, From 3cf41674f51de8bc0ff5313d6968d3c49a249037 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 17 Sep 2020 18:38:43 +0300 Subject: [PATCH 03/30] Aligns the y axis settings on horizontal mode (#77585) --- .../shared_components/toolbar_popover.tsx | 1 + .../xy_visualization/xy_config_panel.test.tsx | 46 ++++++++++++++++++- .../xy_visualization/xy_config_panel.tsx | 45 +++++++++++++----- 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx index 98f5878ec927ed..07baf29fdd32aa 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx @@ -56,6 +56,7 @@ export const ToolbarPopover: React.FunctionComponent = ({ onClick={() => { setOpen(!open); }} + title={title} hasArrow={false} isDisabled={isDisabled} groupPosition={groupPosition} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 7e2e8f04535885..2114d63fcfacd4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps, EuiSuperSelect, EuiButtonGroup } from '@elastic/eui'; -import { LayerContextMenu, XyToolbar } from './xy_config_panel'; +import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import { ToolbarPopover } from '../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; import { FramePublicAPI } from '../types'; @@ -171,4 +171,48 @@ describe('XY Config panels', () => { expect(component.find(AxisSettingsPopover).length).toEqual(3); }); }); + + describe('Dimension Editor', () => { + test('shows the correct axis side options when in horizontal mode', () => { + const state = testState(); + const component = mount( + + ); + + const options = component + .find(EuiButtonGroup) + .first() + .prop('options') as EuiButtonGroupProps['options']; + + expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Bottom', 'Top']); + }); + + test('shows the default axis side options when not in horizontal mode', () => { + const state = testState(); + const component = mount( + + ); + + const options = component + .find(EuiButtonGroup) + .first() + .prop('options') as EuiButtonGroupProps['options']; + + expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Left', 'Right']); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index bc98bf53d9f122..4aa5bd62c05a5e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -274,9 +274,15 @@ export function XyToolbar(props: VisualizationToolbarProps) { group.groupId === 'left') || {}).length === 0 } @@ -310,9 +316,15 @@ export function XyToolbar(props: VisualizationToolbarProps) { toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange} /> group.groupId === 'right') || {}).length === 0 } @@ -345,6 +357,7 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps) const { state, setState, layerId, accessor } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; + const isHorizontal = isHorizontalChart(state.layers); const axisMode = (layer.yConfig && layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) || @@ -377,15 +390,23 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps) }, { id: `${idPrefix}left`, - label: i18n.translate('xpack.lens.xyChart.axisSide.left', { - defaultMessage: 'Left', - }), + label: isHorizontal + ? i18n.translate('xpack.lens.xyChart.axisSide.bottom', { + defaultMessage: 'Bottom', + }) + : i18n.translate('xpack.lens.xyChart.axisSide.left', { + defaultMessage: 'Left', + }), }, { id: `${idPrefix}right`, - label: i18n.translate('xpack.lens.xyChart.axisSide.right', { - defaultMessage: 'Right', - }), + label: isHorizontal + ? i18n.translate('xpack.lens.xyChart.axisSide.top', { + defaultMessage: 'Top', + }) + : i18n.translate('xpack.lens.xyChart.axisSide.right', { + defaultMessage: 'Right', + }), }, ]} idSelected={`${idPrefix}${axisMode}`} From 03e3c852288644d961aee7b4f976275b95c83885 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 17 Sep 2020 08:43:40 -0700 Subject: [PATCH 04/30] [Enterprise Search] Add read-only mode interceptor and error handler (#77569) * Add readOnlyMode prop + callout to Layout component * Update HttpLogic to initialize readOnlyMode from config_data + update App Search & Workplace Search layout to pass readOnlyMode state - update passed props test to not refer to readOnlyMode, so as not to confuse distinction between props.readOnlyMode (passed on init, can grow stale) and HttpLogic.values.readOnlyMode (will update on every http call) - DRY out HttpLogic initializeHttp type defs * Update enterpriseSearchRequestHandler to pass read-only mode header + add a custom 503 API response for read-only mode errors that come back from API endpoints (e.g. when attempting to create/edit a document) - this is so we correctly display a flash message error instead of the generic "Error Connecting" state + note that we still need to send back read only mode on ALL headers, not just on handleReadOnlyModeError however - this is so that the read-only mode state can updates dynamically on all API polls (e.g. on a 200 GET) * Add HttpLogic read-only mode interceptor - which should now dynamically listen / update state every time an Enterprise Search API call is made + DRY out isEnterpriseSearchApi helper and making wrapping/branching clearer * PR feedback: Copy --- .../enterprise_search/common/constants.ts | 2 + .../applications/app_search/index.test.tsx | 13 ++- .../public/applications/app_search/index.tsx | 4 +- .../public/applications/index.tsx | 6 +- .../shared/http/http_logic.test.ts | 96 ++++++++++++++++--- .../applications/shared/http/http_logic.ts | 63 ++++++++---- .../shared/http/http_provider.test.tsx | 1 + .../shared/http/http_provider.tsx | 3 +- .../applications/shared/layout/layout.scss | 11 +++ .../shared/layout/layout.test.tsx | 8 +- .../applications/shared/layout/layout.tsx | 21 +++- .../workplace_search/index.test.tsx | 14 ++- .../applications/workplace_search/index.tsx | 4 +- .../enterprise_search_request_handler.test.ts | 61 ++++++++++-- .../lib/enterprise_search_request_handler.ts | 50 ++++++++-- 15 files changed, 298 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index d6a51e8b482d0a..5df25f11e50705 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -76,4 +76,6 @@ export const JSON_HEADER = { Accept: 'application/json', // Required for Enterprise Search APIs }; +export const READ_ONLY_MODE_HEADER = 'x-ent-search-read-only-mode'; + export const ENGINES_PAGE_SIZE = 10; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 94e9127bbed74b..31c7680fd2f1c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -54,6 +54,7 @@ describe('AppSearchConfigured', () => { const wrapper = shallow(); expect(wrapper.find(Layout)).toHaveLength(1); + expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(EngineOverview)).toHaveLength(1); }); @@ -61,9 +62,9 @@ describe('AppSearchConfigured', () => { const initializeAppData = jest.fn(); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); - shallow(); + shallow(); - expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true }); + expect(initializeAppData).toHaveBeenCalledWith({ ilmEnabled: true }); }); it('does not re-initialize app data', () => { @@ -83,6 +84,14 @@ describe('AppSearchConfigured', () => { expect(wrapper.find(ErrorConnecting)).toHaveLength(1); }); + + it('passes readOnlyMode state', () => { + (useValues as jest.Mock).mockImplementation(() => ({ readOnlyMode: true })); + + const wrapper = shallow(); + + expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true); + }); }); describe('AppSearchNav', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index c4a366930d22aa..643c4b5ccc8731 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -51,7 +51,7 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData } = useActions(AppLogic); - const { errorConnecting } = useValues(HttpLogic); + const { errorConnecting, readOnlyMode } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); @@ -63,7 +63,7 @@ export const AppSearchConfigured: React.FC = (props) => { - }> + } readOnlyMode={readOnlyMode}> {errorConnecting ? ( ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index a54295548004a8..82f884644be4ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -69,7 +69,11 @@ export const renderApp = ( > - + diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts index c032e3b04ebe62..b65499be2f7c03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts @@ -16,6 +16,7 @@ describe('HttpLogic', () => { http: null, httpInterceptors: [], errorConnecting: false, + readOnlyMode: false, }; beforeEach(() => { @@ -31,12 +32,17 @@ describe('HttpLogic', () => { describe('initializeHttp()', () => { it('sets values based on passed props', () => { HttpLogic.mount(); - HttpLogic.actions.initializeHttp({ http: mockHttp, errorConnecting: true }); + HttpLogic.actions.initializeHttp({ + http: mockHttp, + errorConnecting: true, + readOnlyMode: true, + }); expect(HttpLogic.values).toEqual({ http: mockHttp, httpInterceptors: [], errorConnecting: true, + readOnlyMode: true, }); }); }); @@ -52,50 +58,110 @@ describe('HttpLogic', () => { }); }); + describe('setReadOnlyMode()', () => { + it('sets readOnlyMode value', () => { + HttpLogic.mount(); + HttpLogic.actions.setReadOnlyMode(true); + expect(HttpLogic.values.readOnlyMode).toEqual(true); + + HttpLogic.actions.setReadOnlyMode(false); + expect(HttpLogic.values.readOnlyMode).toEqual(false); + }); + }); + describe('http interceptors', () => { describe('initializeHttpInterceptors()', () => { beforeEach(() => { HttpLogic.mount(); jest.spyOn(HttpLogic.actions, 'setHttpInterceptors'); - jest.spyOn(HttpLogic.actions, 'setErrorConnecting'); HttpLogic.actions.initializeHttp({ http: mockHttp }); - HttpLogic.actions.initializeHttpInterceptors(); }); it('calls http.intercept and sets an array of interceptors', () => { - mockHttp.intercept.mockImplementationOnce(() => 'removeInterceptorFn' as any); + mockHttp.intercept + .mockImplementationOnce(() => 'removeErrorInterceptorFn' as any) + .mockImplementationOnce(() => 'removeReadOnlyInterceptorFn' as any); HttpLogic.actions.initializeHttpInterceptors(); expect(mockHttp.intercept).toHaveBeenCalled(); - expect(HttpLogic.actions.setHttpInterceptors).toHaveBeenCalledWith(['removeInterceptorFn']); + expect(HttpLogic.actions.setHttpInterceptors).toHaveBeenCalledWith([ + 'removeErrorInterceptorFn', + 'removeReadOnlyInterceptorFn', + ]); }); describe('errorConnectingInterceptor', () => { + let interceptedResponse: any; + + beforeEach(() => { + interceptedResponse = mockHttp.intercept.mock.calls[0][0].responseError; + jest.spyOn(HttpLogic.actions, 'setErrorConnecting'); + }); + it('handles errors connecting to Enterprise Search', async () => { - const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - const httpResponse = { response: { url: '/api/app_search/engines', status: 502 } }; - await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); + const httpResponse = { + response: { url: '/api/app_search/engines', status: 502 }, + }; + await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).toHaveBeenCalled(); }); it('does not handle non-502 Enterprise Search errors', async () => { - const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - const httpResponse = { response: { url: '/api/workplace_search/overview', status: 404 } }; - await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); + const httpResponse = { + response: { url: '/api/workplace_search/overview', status: 404 }, + }; + await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); - it('does not handle errors for unrelated calls', async () => { - const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - const httpResponse = { response: { url: '/api/some_other_plugin/', status: 502 } }; - await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); + it('does not handle errors for non-Enterprise Search API calls', async () => { + const httpResponse = { + response: { url: '/api/some_other_plugin/', status: 502 }, + }; + await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); }); + + describe('readOnlyModeInterceptor', () => { + let interceptedResponse: any; + + beforeEach(() => { + interceptedResponse = mockHttp.intercept.mock.calls[1][0].response; + jest.spyOn(HttpLogic.actions, 'setReadOnlyMode'); + }); + + it('sets readOnlyMode to true if the response header is true', async () => { + const httpResponse = { + response: { url: '/api/app_search/engines', headers: { get: () => 'true' } }, + }; + await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse); + + expect(HttpLogic.actions.setReadOnlyMode).toHaveBeenCalledWith(true); + }); + + it('sets readOnlyMode to false if the response header is false', async () => { + const httpResponse = { + response: { url: '/api/workplace_search/overview', headers: { get: () => 'false' } }, + }; + await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse); + + expect(HttpLogic.actions.setReadOnlyMode).toHaveBeenCalledWith(false); + }); + + it('does not handle headers for non-Enterprise Search API calls', async () => { + const httpResponse = { + response: { url: '/api/some_other_plugin/', headers: { get: () => 'true' } }, + }; + await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse); + + expect(HttpLogic.actions.setReadOnlyMode).not.toHaveBeenCalled(); + }); + }); }); it('sets httpInterceptors and calls all valid remove functions on unmount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index ec9db30ddef3ba..5e2b5a9ed6b06b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -6,32 +6,32 @@ import { kea, MakeLogicType } from 'kea'; -import { HttpSetup, HttpInterceptorResponseError } from 'src/core/public'; +import { HttpSetup, HttpInterceptorResponseError, HttpResponse } from 'src/core/public'; +import { IHttpProviderProps } from './http_provider'; + +import { READ_ONLY_MODE_HEADER } from '../../../../common/constants'; export interface IHttpValues { http: HttpSetup; httpInterceptors: Function[]; errorConnecting: boolean; + readOnlyMode: boolean; } export interface IHttpActions { - initializeHttp({ - http, - errorConnecting, - }: { - http: HttpSetup; - errorConnecting?: boolean; - }): { http: HttpSetup; errorConnecting?: boolean }; + initializeHttp({ http, errorConnecting, readOnlyMode }: IHttpProviderProps): IHttpProviderProps; initializeHttpInterceptors(): void; setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] }; setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean }; + setReadOnlyMode(readOnlyMode: boolean): { readOnlyMode: boolean }; } export const HttpLogic = kea>({ actions: { - initializeHttp: ({ http, errorConnecting }) => ({ http, errorConnecting }), + initializeHttp: (props) => props, initializeHttpInterceptors: () => null, setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }), setErrorConnecting: (errorConnecting) => ({ errorConnecting }), + setReadOnlyMode: (readOnlyMode) => ({ readOnlyMode }), }, reducers: { http: [ @@ -53,6 +53,13 @@ export const HttpLogic = kea>({ setErrorConnecting: (_, { errorConnecting }) => errorConnecting, }, ], + readOnlyMode: [ + false, + { + initializeHttp: (_, { readOnlyMode }) => !!readOnlyMode, + setReadOnlyMode: (_, { readOnlyMode }) => readOnlyMode, + }, + ], }, listeners: ({ values, actions }) => ({ initializeHttpInterceptors: () => { @@ -60,13 +67,13 @@ export const HttpLogic = kea>({ const errorConnectingInterceptor = values.http.intercept({ responseError: async (httpResponse) => { - const { url, status } = httpResponse.response!; - const hasErrorConnecting = status === 502; - const isApiResponse = - url.includes('/api/app_search/') || url.includes('/api/workplace_search/'); + if (isEnterpriseSearchApi(httpResponse)) { + const { status } = httpResponse.response!; + const hasErrorConnecting = status === 502; - if (isApiResponse && hasErrorConnecting) { - actions.setErrorConnecting(true); + if (hasErrorConnecting) { + actions.setErrorConnecting(true); + } } // Re-throw error so that downstream catches work as expected @@ -75,7 +82,23 @@ export const HttpLogic = kea>({ }); httpInterceptors.push(errorConnectingInterceptor); - // TODO: Read only mode interceptor + const readOnlyModeInterceptor = values.http.intercept({ + response: async (httpResponse) => { + if (isEnterpriseSearchApi(httpResponse)) { + const readOnlyMode = httpResponse.response!.headers.get(READ_ONLY_MODE_HEADER); + + if (readOnlyMode === 'true') { + actions.setReadOnlyMode(true); + } else { + actions.setReadOnlyMode(false); + } + } + + return Promise.resolve(httpResponse); + }, + }); + httpInterceptors.push(readOnlyModeInterceptor); + actions.setHttpInterceptors(httpInterceptors); }, }), @@ -87,3 +110,11 @@ export const HttpLogic = kea>({ }, }), }); + +/** + * Small helper that checks whether or not an http call is for an Enterprise Search API + */ +const isEnterpriseSearchApi = (httpResponse: HttpResponse) => { + const { url } = httpResponse.response!; + return url.includes('/api/app_search/') || url.includes('/api/workplace_search/'); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx index 81106235780d6d..902c910f10d7c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx @@ -17,6 +17,7 @@ describe('HttpProvider', () => { const props = { http: {} as any, errorConnecting: false, + readOnlyMode: false, }; const initializeHttp = jest.fn(); const initializeHttpInterceptors = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx index 4c2160195a1af3..db1b0d611079a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx @@ -11,9 +11,10 @@ import { HttpSetup } from 'src/core/public'; import { HttpLogic } from './http_logic'; -interface IHttpProviderProps { +export interface IHttpProviderProps { http: HttpSetup; errorConnecting?: boolean; + readOnlyMode?: boolean; } export const HttpProvider: React.FC = (props) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss index f6c83888413d37..e867e9cf5a445b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss @@ -81,4 +81,15 @@ padding: $euiSize; } } + + &__readOnlyMode { + margin: -$euiSizeM 0 $euiSizeL; + + @include euiBreakpoint('m') { + margin: 0 0 $euiSizeL; + } + @include euiBreakpoint('xs', 's') { + margin: 0; + } + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx index 623e6e47167d26..7b876d81527fac 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageSideBar, EuiButton, EuiPageBody } from '@elastic/eui'; +import { EuiPageSideBar, EuiButton, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { Layout, INavContext } from './layout'; @@ -55,6 +55,12 @@ describe('Layout', () => { expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen'); }); + it('renders a read-only mode callout', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); + it('renders children', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx index e122c4d5cfdfaf..ef8216e8b6711c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import classNames from 'classnames'; -import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton } from '@elastic/eui'; +import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import './layout.scss'; @@ -15,6 +15,7 @@ import './layout.scss'; interface ILayoutProps { navigation: React.ReactNode; restrictWidth?: boolean; + readOnlyMode?: boolean; } export interface INavContext { @@ -22,7 +23,12 @@ export interface INavContext { } export const NavContext = React.createContext({}); -export const Layout: React.FC = ({ children, navigation, restrictWidth }) => { +export const Layout: React.FC = ({ + children, + navigation, + restrictWidth, + readOnlyMode, +}) => { const [isNavOpen, setIsNavOpen] = useState(false); const toggleNavigation = () => setIsNavOpen(!isNavOpen); const closeNavigation = () => setIsNavOpen(false); @@ -56,6 +62,17 @@ export const Layout: React.FC = ({ children, navigation, restrictW {navigation} + {readOnlyMode && ( + + )} {children} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 39280ad6f4be4a..fc1943264d72bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -12,6 +12,7 @@ import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; import { useValues, useActions } from 'kea'; +import { Layout } from '../shared/layout'; import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; import { Overview } from './views/overview'; @@ -53,6 +54,7 @@ describe('WorkplaceSearchConfigured', () => { it('renders with layout', () => { const wrapper = shallow(); + expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(Overview)).toHaveLength(1); }); @@ -60,9 +62,9 @@ describe('WorkplaceSearchConfigured', () => { const initializeAppData = jest.fn(); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); - shallow(); + shallow(); - expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true }); + expect(initializeAppData).toHaveBeenCalledWith({ isFederatedAuth: true }); }); it('does not re-initialize app data', () => { @@ -82,4 +84,12 @@ describe('WorkplaceSearchConfigured', () => { expect(wrapper.find(ErrorState)).toHaveLength(2); }); + + it('passes readOnlyMode state', () => { + (useValues as jest.Mock).mockImplementation(() => ({ readOnlyMode: true })); + + const wrapper = shallow(); + + expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 6a51b49869eafb..a68dfaf8ea4711 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -31,7 +31,7 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData } = useActions(AppLogic); - const { errorConnecting } = useValues(HttpLogic); + const { errorConnecting, readOnlyMode } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); @@ -46,7 +46,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { {errorConnecting ? : } - }> + } readOnlyMode={readOnlyMode}> {errorConnecting ? ( ) : ( diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index 0c1e81e3aba462..3d0a3181f8ab87 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -5,7 +5,7 @@ */ import { mockConfig, mockLogger } from '../__mocks__'; -import { JSON_HEADER } from '../../common/constants'; +import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; import { EnterpriseSearchRequestHandler } from './enterprise_search_request_handler'; @@ -18,6 +18,9 @@ const responseMock = { custom: jest.fn(), customError: jest.fn(), }; +const mockExpectedResponseHeaders = { + [READ_ONLY_MODE_HEADER]: 'false', +}; describe('EnterpriseSearchRequestHandler', () => { const enterpriseSearchRequestHandler = new EnterpriseSearchRequestHandler({ @@ -58,6 +61,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.custom).toHaveBeenCalledWith({ body: responseBody, statusCode: 200, + headers: mockExpectedResponseHeaders, }); }); @@ -112,11 +116,12 @@ describe('EnterpriseSearchRequestHandler', () => { await makeAPICall(requestHandler); EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example'); - expect(responseMock.custom).toHaveBeenCalledWith({ body: {}, statusCode: 201 }); + expect(responseMock.custom).toHaveBeenCalledWith({ + body: {}, + statusCode: 201, + headers: mockExpectedResponseHeaders, + }); }); - - // TODO: It's possible we may also pass back headers at some point - // from Enterprise Search, e.g. the x-read-only mode header }); }); @@ -140,6 +145,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'some error message', attributes: { errors: ['some error message'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -156,6 +162,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'one,two,three', attributes: { errors: ['one', 'two', 'three'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -171,6 +178,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'Bad Request', attributes: { errors: ['Bad Request'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -186,6 +194,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'Bad Request', attributes: { errors: ['Bad Request'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -201,6 +210,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'Not Found', attributes: { errors: ['Not Found'] }, }, + headers: mockExpectedResponseHeaders, }); }); }); @@ -215,12 +225,33 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: expect.stringContaining('Enterprise Search encountered an internal server error'), + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalledWith( 'Enterprise Search Server Error 500 at : "something crashed!"' ); }); + it('handleReadOnlyModeError()', async () => { + EnterpriseSearchAPI.mockReturn( + { errors: ['Read only mode'] }, + { status: 503, headers: { ...JSON_HEADER, [READ_ONLY_MODE_HEADER]: 'true' } } + ); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/503' }); + + await makeAPICall(requestHandler); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/503'); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 503, + body: expect.stringContaining('Enterprise Search is in read-only mode'), + headers: { [READ_ONLY_MODE_HEADER]: 'true' }, + }); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Cannot perform action: Enterprise Search is in read-only mode. Actions that create, update, or delete information are disabled.' + ); + }); + it('handleInvalidDataError()', async () => { EnterpriseSearchAPI.mockReturn({ results: false }); const requestHandler = enterpriseSearchRequestHandler.createRequest({ @@ -234,6 +265,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Invalid data received from Enterprise Search', + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalledWith( 'Invalid data received from : {"results":false}' @@ -250,6 +282,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Error connecting to Enterprise Search: Failed', + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalled(); }); @@ -265,6 +298,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Cannot authenticate Enterprise Search user', + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalled(); }); @@ -279,6 +313,18 @@ describe('EnterpriseSearchRequestHandler', () => { }); }); + it('setResponseHeaders', async () => { + EnterpriseSearchAPI.mockReturn('anything' as any, { + headers: { [READ_ONLY_MODE_HEADER]: 'true' }, + }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/' }); + await makeAPICall(requestHandler); + + expect(enterpriseSearchRequestHandler.headers).toEqual({ + [READ_ONLY_MODE_HEADER]: 'true', + }); + }); + it('isEmptyObj', async () => { expect(enterpriseSearchRequestHandler.isEmptyObj({})).toEqual(true); expect(enterpriseSearchRequestHandler.isEmptyObj({ empty: false })).toEqual(false); @@ -304,9 +350,10 @@ const EnterpriseSearchAPI = { ...expectedParams, }); }, - mockReturn(response: object, options?: object) { + mockReturn(response: object, options?: any) { fetchMock.mockImplementation(() => { - return Promise.resolve(new Response(JSON.stringify(response), options)); + const headers = Object.assign({}, mockExpectedResponseHeaders, options?.headers); + return Promise.resolve(new Response(JSON.stringify(response), { ...options, headers })); }); }, mockReturnError() { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 00d5eaf5d6a83b..6b65c16c832fd8 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -14,7 +14,7 @@ import { Logger, } from 'src/core/server'; import { ConfigType } from '../index'; -import { JSON_HEADER } from '../../common/constants'; +import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; interface IConstructorDependencies { config: ConfigType; @@ -46,6 +46,7 @@ export interface IEnterpriseSearchRequestHandler { export class EnterpriseSearchRequestHandler { private enterpriseSearchUrl: string; private log: Logger; + private headers: Record = {}; constructor({ config, log }: IConstructorDependencies) { this.log = log; @@ -80,6 +81,9 @@ export class EnterpriseSearchRequestHandler { // Call the Enterprise Search API const apiResponse = await fetch(url, { method, headers, body }); + // Handle response headers + this.setResponseHeaders(apiResponse); + // Handle authentication redirects if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) { return this.handleAuthenticationError(response); @@ -88,7 +92,13 @@ export class EnterpriseSearchRequestHandler { // Handle 400-500+ responses from the Enterprise Search server const { status } = apiResponse; if (status >= 500) { - return this.handleServerError(response, apiResponse, url); + if (this.headers[READ_ONLY_MODE_HEADER] === 'true') { + // Handle 503 read-only mode errors + return this.handleReadOnlyModeError(response); + } else { + // Handle unexpected server errors + return this.handleServerError(response, apiResponse, url); + } } else if (status >= 400) { return this.handleClientError(response, apiResponse); } @@ -100,7 +110,11 @@ export class EnterpriseSearchRequestHandler { } // Pass successful responses back to the front-end - return response.custom({ statusCode: status, body: json }); + return response.custom({ + statusCode: status, + headers: this.headers, + body: json, + }); } catch (e) { // Catch connection/auth errors return this.handleConnectionError(response, e); @@ -160,7 +174,7 @@ export class EnterpriseSearchRequestHandler { const { status } = apiResponse; const body = await this.getErrorResponseBody(apiResponse); - return response.customError({ statusCode: status, body }); + return response.customError({ statusCode: status, headers: this.headers, body }); } async handleServerError(response: KibanaResponseFactory, apiResponse: Response, url: string) { @@ -172,14 +186,22 @@ export class EnterpriseSearchRequestHandler { 'Enterprise Search encountered an internal server error. Please contact your system administrator if the problem persists.'; this.log.error(`Enterprise Search Server Error ${status} at <${url}>: ${message}`); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); + } + + handleReadOnlyModeError(response: KibanaResponseFactory) { + const errorMessage = + 'Enterprise Search is in read-only mode. Actions that create, update, or delete information are disabled.'; + + this.log.error(`Cannot perform action: ${errorMessage}`); + return response.customError({ statusCode: 503, headers: this.headers, body: errorMessage }); } handleInvalidDataError(response: KibanaResponseFactory, url: string, json: object) { const errorMessage = 'Invalid data received from Enterprise Search'; this.log.error(`Invalid data received from <${url}>: ${JSON.stringify(json)}`); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); } handleConnectionError(response: KibanaResponseFactory, e: Error) { @@ -188,14 +210,26 @@ export class EnterpriseSearchRequestHandler { this.log.error(errorMessage); if (e instanceof Error) this.log.debug(e.stack as string); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); } handleAuthenticationError(response: KibanaResponseFactory) { const errorMessage = 'Cannot authenticate Enterprise Search user'; this.log.error(errorMessage); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); + } + + /** + * Set response headers + * + * Currently just forwards the read-only mode header, but we can expand this + * in the future to pass more headers from Enterprise Search as we need them + */ + + setResponseHeaders(apiResponse: Response) { + const readOnlyMode = apiResponse.headers.get(READ_ONLY_MODE_HEADER); + this.headers[READ_ONLY_MODE_HEADER] = readOnlyMode as 'true' | 'false'; } /** From 161a74f732dc1c5a06c1c5c342e33399b2993077 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 17 Sep 2020 18:00:04 +0200 Subject: [PATCH 05/30] remove legacy security plugin (#75648) * remove legacy security plugin * remove legacy plugin check * adapt mocha tests * update CODEOWNERS --- .github/CODEOWNERS | 4 +--- x-pack/index.js | 3 +-- x-pack/legacy/plugins/security/index.ts | 21 --------------------- 3 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 x-pack/legacy/plugins/security/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d81f6af4cec28f..1077230812b60c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -167,7 +167,6 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform -/x-pack/legacy/plugins/security/ @elastic/kibana-security /x-pack/legacy/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security @@ -287,7 +286,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers /src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers -/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers @@ -297,7 +295,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/infra/**/*.scss @elastic/observability-design /x-pack/plugins/ingest_manager/**/*.scss @elastic/observability-design /x-pack/plugins/observability/**/*.scss @elastic/observability-design -/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design +/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design # Ent. Search design /x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design diff --git a/x-pack/index.js b/x-pack/index.js index 074b8e6859dc2b..745b4bd72dde8e 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,9 +5,8 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { security } from './legacy/plugins/security'; import { spaces } from './legacy/plugins/spaces'; module.exports = function (kibana) { - return [xpackMain(kibana), spaces(kibana), security(kibana)]; + return [xpackMain(kibana), spaces(kibana)]; }; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts deleted file mode 100644 index c3596d3745e57e..00000000000000 --- a/x-pack/legacy/plugins/security/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Root } from 'joi'; -import { resolve } from 'path'; - -export const security = (kibana: Record) => - new kibana.Plugin({ - id: 'security', - publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch'], - configPrefix: 'xpack.security', - config: (Joi: Root) => - Joi.object({ enabled: Joi.boolean().default(true) }) - .unknown() - .default(), - init() {}, - }); From 9c90c14b25bb02e05c9bddc8e14cd04e82160f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 17 Sep 2020 17:13:06 +0100 Subject: [PATCH 06/30] [APM] Show accurate metrics for containerized applications (#76768) * adding cgroup fields * calculate the memory based on the cgroup fields otherwise use the system fields * updating script * adding api tests * using cgroup fields on service map * addressing PR comment * fixing unit test * fixing test * changing api tests to use snapshot * removing inactive_files from calculation * fixing test * refactoring painless script * addressing PR comment * addressing pr comments Co-authored-by: Elastic Machine --- .../elasticsearch_fieldnames.test.ts.snap | 12 + .../apm/common/elasticsearch_fieldnames.ts | 4 + .../__snapshots__/queries.test.ts.snap | 273 +- .../metrics/by_agent/shared/memory/index.ts | 64 +- .../get_service_map_service_node_info.ts | 66 +- .../apm_api_integration/basic/tests/index.ts | 4 + .../tests/metrics_charts/metrics_charts.ts | 440 ++ .../es_archiver/metrics_8.0.0/data.json.gz | Bin 0 -> 177873 bytes .../es_archiver/metrics_8.0.0/mappings.json | 4092 +++++++++++++++++ 9 files changed, 4879 insertions(+), 76 deletions(-) create mode 100644 x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts create mode 100644 x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/data.json.gz create mode 100644 x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 3524d41646d50a..8c233d3691c7fa 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -52,6 +52,10 @@ exports[`Error LABEL_NAME 1`] = `undefined`; exports[`Error LCP_FIELD 1`] = `undefined`; +exports[`Error METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Error METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Error METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Error METRIC_JAVA_GC_TIME 1`] = `undefined`; @@ -220,6 +224,10 @@ exports[`Span LABEL_NAME 1`] = `undefined`; exports[`Span LCP_FIELD 1`] = `undefined`; +exports[`Span METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Span METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Span METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Span METRIC_JAVA_GC_TIME 1`] = `undefined`; @@ -388,6 +396,10 @@ exports[`Transaction LABEL_NAME 1`] = `undefined`; exports[`Transaction LCP_FIELD 1`] = `undefined`; +exports[`Transaction METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Transaction METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Transaction METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Transaction METRIC_JAVA_GC_TIME 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 612cb18bbe190b..cc6a1fffb22885 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -79,6 +79,10 @@ export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total'; export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct'; export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct'; +export const METRIC_CGROUP_MEMORY_LIMIT_BYTES = + 'system.process.cgroup.memory.mem.limit.bytes'; +export const METRIC_CGROUP_MEMORY_USAGE_BYTES = + 'system.process.cgroup.memory.mem.usage.bytes'; export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max'; export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed'; diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index b88c90a213c671..2868dcfda97b6e 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -203,16 +203,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -221,16 +255,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -275,12 +343,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], @@ -682,16 +745,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -700,16 +797,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -760,12 +891,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], @@ -1157,16 +1283,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -1175,16 +1335,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -1224,12 +1418,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index 316b0d59d2c5b6..a60576ca0c175e 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; import { + METRIC_CGROUP_MEMORY_LIMIT_BYTES, + METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, } from '../../../../../../common/elasticsearch_fieldnames'; @@ -14,8 +16,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../../../../helpers/setup_request'; -import { ChartBase } from '../../../types'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; +import { ChartBase } from '../../../types'; const series = { memoryUsedMax: { @@ -43,36 +45,68 @@ const chartBase: ChartBase = { series, }; -export const percentMemoryUsedScript = { +export const percentSystemMemoryUsedScript = { lang: 'expression', source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']`, }; +export const percentCgroupMemoryUsedScript = { + lang: 'painless', + source: ` + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = '${METRIC_CGROUP_MEMORY_LIMIT_BYTES}'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['${METRIC_SYSTEM_TOTAL_MEMORY}'].value; + + double used = doc['${METRIC_CGROUP_MEMORY_USAGE_BYTES}'].value; + + return used / total; + `, +}; + export async function getMemoryChartData( setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { - return fetchAndTransformMetrics({ + const cgroupResponse = await fetchAndTransformMetrics({ setup, serviceName, serviceNodeName, chartBase, aggs: { - memoryUsedAvg: { avg: { script: percentMemoryUsedScript } }, - memoryUsedMax: { max: { script: percentMemoryUsedScript } }, + memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } }, }, additionalFilters: [ - { - exists: { - field: METRIC_SYSTEM_FREE_MEMORY, - }, - }, - { - exists: { - field: METRIC_SYSTEM_TOTAL_MEMORY, - }, - }, + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, ], }); + + if (cgroupResponse.noHits) { + return await fetchAndTransformMetrics({ + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } }, + }, + additionalFilters: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + }); + } + + return cgroupResponse; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 88cc26608b8508..5c183fd9150dd0 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -14,13 +14,17 @@ import { METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, + METRIC_CGROUP_MEMORY_USAGE_BYTES, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; +import { + percentCgroupMemoryUsedScript, + percentSystemMemoryUsedScript, +} from '../metrics/by_agent/shared/memory'; import { getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, @@ -205,26 +209,50 @@ async function getMemoryStats({ filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { const { apmEventClient } = setup; - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.metric], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...filter, - { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, - { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ], + + const getAvgMemoryUsage = async ({ + additionalFilters, + script, + }: { + additionalFilters: ESFilter[]; + script: typeof percentCgroupMemoryUsedScript; + }) => { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [...filter, ...additionalFilters], + }, + }, + aggs: { + avgMemoryUsage: { avg: { script } }, }, }, - aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, - }, - }); + }); - return { - avgMemoryUsage: response.aggregations?.avgMemoryUsage.value ?? null, + return response.aggregations?.avgMemoryUsage.value ?? null; }; + + let avgMemoryUsage = await getAvgMemoryUsage({ + additionalFilters: [ + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, + ], + script: percentCgroupMemoryUsedScript, + }); + + if (!avgMemoryUsage) { + avgMemoryUsage = await getAvgMemoryUsage({ + additionalFilters: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + script: percentSystemMemoryUsedScript, + }); + } + + return { avgMemoryUsage }; } diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index bae94d89e74573..8aa509b0899ceb 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -52,5 +52,9 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./observability_overview/has_data')); loadTestFile(require.resolve('./observability_overview/observability_overview')); }); + + describe('Metrics', function () { + loadTestFile(require.resolve('./metrics_charts/metrics_charts')); + }); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts new file mode 100644 index 00000000000000..f82e16e090eaeb --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts @@ -0,0 +1,440 @@ +/* + * 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 { first } from 'lodash'; +import { MetricsChartsByAgentAPIResponse } from '../../../../../plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent'; +import { GenericMetricsChart } from '../../../../../plugins/apm/server/lib/metrics/transform_metrics_chart'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { expectSnapshot } from '../../../common/match_snapshot'; + +interface ChartResponse { + body: MetricsChartsByAgentAPIResponse; + status: number; +} + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('when data is loaded', () => { + before(() => esArchiver.load('metrics_8.0.0')); + after(() => esArchiver.unload('metrics_8.0.0')); + + describe('for opbeans-node', () => { + const start = encodeURIComponent('2020-09-08T14:50:00.000Z'); + const end = encodeURIComponent('2020-09-08T14:55:00.000Z'); + const uiFilters = encodeURIComponent(JSON.stringify({})); + const agentName = 'nodejs'; + + describe('returns metrics data', () => { + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await supertest.get( + `/api/apm/services/opbeans-node/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` + ); + }); + it('contains CPU usage and System memory usage chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.714, + 0.38770000000000004, + 0.75, + 0.2543, + ] + `); + }); + }); + + describe("System memory usage (using 'system.memory' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.7220939209255549, + 0.7181735467963479, + ] + `); + }); + }); + }); + }); + + describe('for opbeans-java', () => { + const uiFilters = encodeURIComponent(JSON.stringify({})); + const agentName = 'java'; + + describe('returns metrics data', () => { + const start = encodeURIComponent('2020-09-08T14:55:30.000Z'); + const end = encodeURIComponent('2020-09-08T15:00:00.000Z'); + + let chartsResponse: ChartResponse; + before(async () => { + chartsResponse = await supertest.get( + `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` + ); + }); + + it('has correct chart data', async () => { + expect(chartsResponse.status).to.be(200); + expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` + Array [ + "CPU usage", + "System memory usage", + "Heap Memory", + "Non-Heap Memory", + "Thread Count", + "Garbage collection per minute", + "Garbage collection time spent per minute", + ] + `); + }); + + describe('CPU usage', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find(({ key }) => key === 'cpu_usage_chart'); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.203, + 0.17877777777777779, + 0.01, + 0.009000000000000001, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.193, + 0.193, + 0.009000000000000001, + 0.009000000000000001, + ] + `); + }); + }); + + describe("System memory usage (using 'system.process.cgroup' fields to calculate the memory usage)", () => { + let systemMemoryUsageChart: GenericMetricsChart | undefined; + before(() => { + systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + }); + + it('has correct series', () => { + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.7079247035578369, + 0.7053959808411816, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.7079247035578369, + 0.7079247035578369, + ] + `); + }); + }); + + describe('Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + "Avg. limit", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 222501617.7777778, + 374341632, + 1560281088, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 211472896, + 374341632, + 1560281088, + ] + `); + }); + }); + + describe('Non-Heap Memory', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'non_heap_memory_area_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. used", + "Avg. committed", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 138573397.33333334, + 147677639.1111111, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 138162752, + 147386368, + ] + `); + }); + }); + + describe('Thread Count', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'thread_count_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Avg. count", + "Max count", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 44.44444444444444, + 45, + ] + `); + }); + + it('has the correct rate', async () => { + const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 44, + 44, + ] + `); + }); + }); + + describe('Garbage collection per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_rate_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 15, + ] + `); + }); + }); + + describe('Garbage collection time spent per minute', () => { + let cpuUsageChart: GenericMetricsChart | undefined; + before(() => { + cpuUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'gc_time_line_chart' + ); + }); + + it('has correct series', () => { + expect(cpuUsageChart).to.not.empty(); + expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); + }); + + it('has correct series overall values', () => { + expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0, + 187.5, + ] + `); + }); + }); + }); + + // 9223372036854771712 = memory limit for a c-group when no memory limit is specified + it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => { + const start = encodeURIComponent('2020-09-08T15:00:30.000Z'); + const end = encodeURIComponent('2020-09-08T15:05:00.000Z'); + + const chartsResponse: ChartResponse = await supertest.get( + `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&uiFilters=${uiFilters}&agentName=${agentName}` + ); + + const systemMemoryUsageChart = chartsResponse.body.charts.find( + ({ key }) => key === 'memory_usage_chart' + ); + + expect(systemMemoryUsageChart).to.not.empty(); + expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` + Array [ + "Max", + "Average", + ] + `); + expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) + .toMatchInline(` + Array [ + 0.11452389642649889, + 0.11400237609041514, + ] + `); + + const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); + expectSnapshot(yValues).toMatchInline(` + Array [ + 0.11383724014063981, + 0.11383724014063981, + ] + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/data.json.gz b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..7b38da28496a57defd128fae94e0fce1648b3afc GIT binary patch literal 177873 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%0Dy0p_AzSn{1~HTnRdG%q8(v*X)jP1=Si)< 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�(iPjo~nBT+Vr$-8)Rt~v^izq)tnpsL3-Cb1qPUM5)!WTAW0;OxsXY&?H>+$TpUwu!!-oxK##0ikbVZ9tCb#tY zeTQbZ9`8w>&V_WSj#=Lx$KeKa?3UZ+bhM|X=O)SGA)P%vL?L*Vj`oZdJDSO;@+P_T~0t@vL@#k7eDaagh2rh_=PM zvu(b$-PUtkCl}EbFL71zZY|bUJ3XA4asZ1%A%-ajSCmAaeHDq@in%xg-b9;d)VitB zF8k%+OK-KaM@3PghXNs{?5byW~qVb#G#;yPdfz$ zW<@|%|8ND=*l~qBJ$lrHb!#~RiZ#nB(&sMS1*gzggQ)o-JQmc@Yu(0Uu*Xp@d|UaT zeyQ$ugL*(hQs7B=OC9ZUN)I7~sW!3Y=*(#qB+-gOL_VosRT%Q+y%MCyd4$Jltj5JP z4&ABmP+JJko_?#kH`+WauEyw69do`tRs*hx3cg+&b*Wz*^=j4$*SnhX!|e@e1cQ?{ zjdaOss*FD%huiy9Db@E??OZwt2CmVvAUzdxpgrL{p9x328=O)w^54|!AbC*?@o~+o zr|T!rT7w7lET&Dl{8ecWk&UY|L3GFte z?h*3QgHAv$B?)%!@fOQ)z8v2q0(&d_0W7a=DN!ugt@8u40pj@)M)r$n&es=lj%e;% zQX73ZeHu5j`_<^M=`AP}TVru5qFu^ZcqynIhxj;o6?AHCT&65fXK&S6#u(l%gj#Q_ zKBV}tJiiGl%)o;N=Y9plpMupqMaieeOV12i+6#Y9TG{x$w^heW4Ph}UPb}l{;2O`& zw{LAn7`$-(F=Gm1!)i zTeioO=)on;y)92jYB2~evp65;VH+|WwtOu53xh407w%>8X(xFUX0ncR@5ofK6-l{t u@D+C#Z|5m=^HHj?A`abCojDLu;ZIgN_iNJMYMVzsJpY)cywEOJf%q@N&KXJo literal 0 HcmV?d00001 diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json new file mode 100644 index 00000000000000..b3b3281be3cdbb --- /dev/null +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0/mappings.json @@ -0,0 +1,4092 @@ +{ + "type": "index", + "value": { + "aliases": { + "apm-8.0.0-metric": { + "is_write_index": true + } + }, + "index": "apm-8.0.0-metric-000001", + "mappings": { + "_meta": { + "beat": "apm", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "dns.answers": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "dns.answers.*" + } + }, + { + "log.syslog": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "log.syslog.*" + } + }, + { + "network.inner": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "network.inner.*" + } + }, + { + "observer.egress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.egress.*" + } + }, + { + "observer.ingress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.ingress.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "transaction.marks": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "transaction.marks.*" + } + }, + { + "transaction.marks.*.*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "transaction.marks.*.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "dynamic": "false", + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "path": "agent.name", + "type": "alias" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "child": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "dynamic": "false", + "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": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "dynamic": "false", + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "dynamic": "false", + "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" + } + } + }, + "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" + } + } + }, + "container": { + "dynamic": "false", + "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" + } + } + }, + "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" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "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" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "dynamic": "false", + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "culprit": { + "ignore_above": 1024, + "type": "keyword" + }, + "exception": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handled": { + "type": "boolean" + }, + "message": { + "norms": false, + "type": "text" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "grouping_key": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "param_message": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "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": { + "ignore_above": 1024, + "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" + } + } + }, + "experimental": { + "dynamic": "true", + "type": "object" + }, + "fields": { + "type": "object" + }, + "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" + }, + "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" + }, + "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": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "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" + } + } + }, + "golang": { + "properties": { + "goroutines": { + "type": "long" + }, + "heap": { + "properties": { + "allocations": { + "properties": { + "active": { + "type": "float" + }, + "allocated": { + "type": "float" + }, + "frees": { + "type": "long" + }, + "idle": { + "type": "float" + }, + "mallocs": { + "type": "long" + }, + "objects": { + "type": "long" + }, + "total": { + "type": "float" + } + } + }, + "gc": { + "properties": { + "cpu_fraction": { + "type": "float" + }, + "next_gc_limit": { + "type": "float" + }, + "total_count": { + "type": "long" + }, + "total_pause": { + "properties": { + "ns": { + "type": "float" + } + } + } + } + }, + "system": { + "properties": { + "obtained": { + "type": "float" + }, + "released": { + "type": "float" + }, + "stack": { + "type": "long" + }, + "total": { + "type": "float" + } + } + } + } + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "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" + } + } + }, + "host": { + "dynamic": "false", + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "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": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "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" + }, + "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": { + "dynamic": "false", + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "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" + }, + "finished": { + "type": "boolean" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jvm": { + "properties": { + "gc": { + "properties": { + "alloc": { + "type": "float" + }, + "count": { + "type": "long" + }, + "time": { + "type": "long" + } + } + }, + "memory": { + "properties": { + "heap": { + "properties": { + "committed": { + "type": "float" + }, + "max": { + "type": "float" + }, + "pool": { + "properties": { + "committed": { + "type": "float" + }, + "max": { + "type": "long" + }, + "used": { + "type": "float" + } + } + }, + "used": { + "type": "float" + } + } + }, + "non_heap": { + "properties": { + "committed": { + "type": "float" + }, + "max": { + "type": "long" + }, + "used": { + "type": "float" + } + } + } + } + }, + "thread": { + "properties": { + "count": { + "type": "long" + } + } + } + } + }, + "kubernetes": { + "dynamic": "false", + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "dynamic": "true", + "properties": { + "env": { + "type": "keyword" + }, + "hostname": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "ignore_above": 1024, + "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" + } + } + } + } + }, + "nodejs": { + "properties": { + "eventloop": { + "properties": { + "delay": { + "properties": { + "avg": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "ns": { + "type": "long" + } + } + } + } + }, + "handles": { + "properties": { + "active": { + "type": "long" + } + } + }, + "memory": { + "properties": { + "arrayBuffers": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "external": { + "properties": { + "bytes": { + "type": "float" + } + } + }, + "heap": { + "properties": { + "allocated": { + "properties": { + "bytes": { + "type": "float" + } + } + }, + "used": { + "properties": { + "bytes": { + "type": "float" + } + } + } + } + } + } + }, + "requests": { + "properties": { + "active": { + "type": "long" + } + } + } + } + }, + "observer": { + "dynamic": "false", + "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" + }, + "listening": { + "ignore_above": 1024, + "type": "keyword" + }, + "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" + }, + "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" + }, + "version_major": { + "type": "byte" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "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" + }, + "version": { + "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" + } + } + }, + "parent": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "dynamic": "false", + "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" + }, + "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": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "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" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "processor": { + "properties": { + "event": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "profile": { + "dynamic": "false", + "properties": { + "alloc_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "alloc_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "cpu": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "duration": { + "type": "long" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "inuse_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "inuse_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "samples": { + "properties": { + "count": { + "type": "long" + } + } + }, + "stack": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + }, + "top": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + } + } + }, + "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": { + "dynamic": "false", + "properties": { + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "framework": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "language": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "dynamic": "false", + "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" + } + } + } + } + }, + "sourcemap": { + "dynamic": "false", + "properties": { + "bundle_filepath": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "span": { + "dynamic": "false", + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "db": { + "dynamic": "false", + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "rows_affected": { + "type": "long" + } + } + }, + "destination": { + "dynamic": "false", + "properties": { + "service": { + "dynamic": "false", + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "start": { + "properties": { + "us": { + "type": "long" + } + } + }, + "subtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "sync": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "system": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "actual": { + "properties": { + "free": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "process": { + "properties": { + "cgroup": { + "properties": { + "memory": { + "properties": { + "mem": { + "properties": { + "limit": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "usage": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "stats": { + "properties": { + "inactive_file": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "cpu": { + "properties": { + "system": { + "properties": { + "norm": { + "properties": { + "pct": { + "type": "float" + } + } + } + } + }, + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + }, + "user": { + "properties": { + "norm": { + "properties": { + "pct": { + "type": "float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "size": { + "type": "long" + } + } + } + } + } + } + }, + "tags": { + "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" + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timestamp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "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" + } + } + }, + "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" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tracing": { + "properties": { + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "transaction": { + "dynamic": "false", + "properties": { + "breakdown": { + "properties": { + "count": { + "type": "long" + } + } + }, + "duration": { + "properties": { + "count": { + "type": "long" + }, + "histogram": { + "type": "histogram" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + }, + "us": { + "type": "long" + } + } + }, + "experience": { + "properties": { + "cls": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "fid": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "tbt": { + "scaling_factor": 1000000, + "type": "scaled_float" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "marks": { + "dynamic": "true", + "properties": { + "*": { + "properties": { + "*": { + "dynamic": "true", + "type": "object" + } + } + } + } + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "root": { + "type": "boolean" + }, + "sampled": { + "type": "boolean" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "span_count": { + "properties": { + "dropped": { + "type": "long" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "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": { + "dynamic": "false", + "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": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "dynamic": "false", + "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": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "view spans": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "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": { + "codec": "best_compression", + "lifecycle": { + "name": "apm-rollover-30-days", + "rollover_alias": "apm-8.0.0-metric" + }, + "mapping": { + "total_fields": { + "limit": "2000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "100", + "refresh_interval": "1ms" + } + } + } +} \ No newline at end of file From 53c8fad2333842b12f37cae1bdda5c174b5ba7c3 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 17 Sep 2020 12:30:07 -0400 Subject: [PATCH 07/30] [Resolver] Fixup assets file (#77766) * Fix useUiSetting import * remove unused exports --- .../security_solution/public/resolver/view/assets.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index a066eb9421fc1f..e7c3a87c9c0f60 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -12,9 +12,8 @@ import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light. import { htmlIdGenerator, ButtonColor } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { useUiSetting } from '../../common/lib/kibana'; -import { DEFAULT_DARK_MODE as defaultDarkMode } from '../../../common/constants'; import { ResolverProcessType } from '../types'; +import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; type ResolverColorNames = | 'descriptionText' @@ -38,7 +37,7 @@ interface NodeStyleConfig { strokeColor: string; } -export interface NodeStyleMap { +interface NodeStyleMap { runningProcessCube: NodeStyleConfig; runningTriggerCube: NodeStyleConfig; terminatedProcessCube: NodeStyleConfig; @@ -50,7 +49,7 @@ const idGenerator = htmlIdGenerator(); /** * Ids of paint servers to be referenced by fill and stroke attributes */ -export const PaintServerIds = { +const PaintServerIds = { runningProcessCube: idGenerator('psRunningProcessCube'), runningTriggerCube: idGenerator('psRunningTriggerCube'), terminatedProcessCube: idGenerator('psTerminatedProcessCube'), @@ -385,7 +384,7 @@ const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( * 3. `` elements can be handled by compositor (faster) */ const SymbolDefinitionsComponent = memo(({ className }: { className?: string }) => { - const isDarkMode = useUiSetting(defaultDarkMode); + const isDarkMode = useUiSetting('theme:darkMode'); return ( @@ -421,7 +420,7 @@ export const useResolverTheme = (): { nodeAssets: NodeStyleMap; cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessTrigger: boolean) => NodeStyleConfig; } => { - const isDarkMode = useUiSetting(defaultDarkMode); + const isDarkMode = useUiSetting('theme:darkMode'); const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; const getThemedOption = (lightOption: string, darkOption: string): string => { From 11f100b1ac994cb5178b886432a31dee58f37cfa Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 17 Sep 2020 12:41:21 -0400 Subject: [PATCH 08/30] [Mappings editor] Add support for histogram field type (#76671) --- .../document_fields/field_parameters/index.ts | 2 + .../field_parameters/meta_parameter.tsx | 50 +++++++++++++++++++ .../fields/field_types/binary_type.tsx | 15 +++++- .../fields/field_types/boolean_type.tsx | 4 ++ .../fields/field_types/completion_type.tsx | 5 +- .../field_types/constant_keyword_type.tsx | 50 ++++--------------- .../fields/field_types/date_type.tsx | 4 ++ .../fields/field_types/flattened_type.tsx | 4 ++ .../fields/field_types/geo_point_type.tsx | 5 ++ .../fields/field_types/histogram_type.tsx | 29 +++++++++++ .../fields/field_types/index.ts | 2 + .../fields/field_types/numeric_type.tsx | 4 ++ .../fields/field_types/range_type.tsx | 7 ++- .../fields/field_types/search_as_you_type.tsx | 4 ++ .../fields/field_types/text_type.tsx | 6 ++- .../fields/field_types/token_count_type.tsx | 4 ++ .../constants/data_types_definition.tsx | 18 +++++++ .../mappings_editor/types/document_fields.ts | 1 + 18 files changed, 167 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts index a2d5c7c8d53085..b3bf0719489562 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -69,6 +69,8 @@ export * from './other_type_name_parameter'; export * from './other_type_json_parameter'; +export * from './meta_parameter'; + export * from './ignore_above_parameter'; export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx new file mode 100644 index 00000000000000..c8af296318b61c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx @@ -0,0 +1,50 @@ +/* + * 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, { FunctionComponent } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { documentationService } from '../../../../../services/documentation'; +import { UseField, JsonEditorField } from '../../../shared_imports'; +import { getFieldConfig } from '../../../lib'; +import { EditFieldFormRow } from '../fields/edit_field'; + +interface Props { + defaultToggleValue: boolean; +} + +export const MetaParameter: FunctionComponent = ({ defaultToggleValue }) => ( + + + +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx index ba9c75baa1987d..1550485ebad934 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx @@ -5,14 +5,25 @@ */ import React from 'react'; -import { StoreParameter, DocValuesParameter } from '../../field_parameters'; +import { NormalizedField, ParameterName, Field as FieldType } from '../../../../types'; +import { getFieldConfig } from '../../../../lib'; +import { StoreParameter, DocValuesParameter, MetaParameter } from '../../field_parameters'; import { AdvancedParametersSection } from '../edit_field'; -export const BinaryType = () => { +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + +interface Props { + field: NormalizedField; +} + +export const BinaryType = ({ field }: Props) => { return ( + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx index 962606b2f4ffd8..1ee2bf22edb444 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx @@ -16,11 +16,13 @@ import { DocValuesParameter, BoostParameter, NullValueParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { + case 'meta': case 'boost': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -90,6 +92,8 @@ export const BooleanType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx index 74331cb1b6b221..748dc54838270e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx @@ -10,11 +10,12 @@ import { i18n } from '@kbn/i18n'; import { NormalizedField, Field as FieldType } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; import { UseField, Field } from '../../../../shared_imports'; -import { AnalyzersParameter } from '../../field_parameters'; +import { AnalyzersParameter, MetaParameter } from '../../field_parameters'; import { EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { + case 'meta': case 'max_input_length': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -88,6 +89,8 @@ export const CompletionType = ({ field }: Props) => { )} formFieldPath="preserve_position_increments" /> + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx index 4c02171d49eec8..aa8aefba921e77 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx @@ -6,16 +6,20 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { documentationService } from '../../../../../../services/documentation'; -import { UseField, Field, JsonEditorField } from '../../../../shared_imports'; +import { UseField, Field } from '../../../../shared_imports'; import { getFieldConfig } from '../../../../lib'; -import { NormalizedField } from '../../../../types'; +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { MetaParameter } from '../../field_parameters'; import { AdvancedParametersSection, EditFieldFormRow, BasicParametersSection } from '../edit_field'; interface Props { field: NormalizedField; } +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + export const ConstantKeywordType: FunctionComponent = ({ field }) => { return ( <> @@ -32,50 +36,14 @@ export const ConstantKeywordType: FunctionComponent = ({ field }) => { 'The value of this field for all documents in the index. If not specified, defaults to the value specified in the first document indexed.', } )} - defaultToggleValue={field.source?.value !== undefined} + defaultToggleValue={getDefaultToggleValue('value', field.source)} > - {/* Meta field */} - - - + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx index 0c067d09046d73..35382506a3cd9f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx @@ -19,6 +19,7 @@ import { IgnoreMalformedParameter, FormatParameter, LocaleParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; @@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'locale': case 'format': + case 'meta': case 'boost': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -73,6 +75,8 @@ export const DateType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx index e96426ece27e8e..b1545d44885c8f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx @@ -18,6 +18,7 @@ import { NullValueParameter, SimilarityParameter, SplitQueriesOnWhitespaceParameter, + MetaParameter, IgnoreAboveParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; @@ -30,6 +31,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'boost': case 'ignore_above': + case 'meta': case 'similarity': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -83,6 +85,8 @@ export const FlattenedType = React.memo(({ field }: Props) => { defaultToggleValue={getDefaultToggleValue('null_value', field.source)} /> + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx index 997e866da35f0e..0f28c5080d26d5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx @@ -14,11 +14,14 @@ import { IgnoreMalformedParameter, NullValueParameter, IgnoreZValueParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { + case 'meta': + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; case 'null_value': { return field.null_value !== undefined; } @@ -65,6 +68,8 @@ export const GeoPointType = ({ field }: Props) => { config={getFieldConfig('null_value_geo_point')} /> + + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx new file mode 100644 index 00000000000000..1ff97a8d72a217 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx @@ -0,0 +1,29 @@ +/* + * 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 { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { getFieldConfig } from '../../../../lib'; +import { IgnoreMalformedParameter, MetaParameter } from '../../field_parameters'; +import { AdvancedParametersSection } from '../edit_field'; + +interface Props { + field: NormalizedField; +} + +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + +export const HistogramType = ({ field }: Props) => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index 0cf921f66451b8..8fcd02e4a362ea 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -28,6 +28,7 @@ import { ObjectType } from './object_type'; import { OtherType } from './other_type'; import { NestedType } from './nested_type'; import { JoinType } from './join_type'; +import { HistogramType } from './histogram_type'; import { ConstantKeywordType } from './constant_keyword_type'; import { RankFeatureType } from './rank_feature_type'; import { WildcardType } from './wildcard_type'; @@ -55,6 +56,7 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { other: OtherType, nested: NestedType, join: JoinType, + histogram: HistogramType, constant_keyword: ConstantKeywordType, rank_feature: RankFeatureType, wildcard: WildcardType, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx index 3d78205934eeaa..6ad3c9c5d0bd4c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx @@ -18,6 +18,7 @@ import { CoerceNumberParameter, IgnoreMalformedParameter, CopyToParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; import { PARAMETERS_DEFINITION } from '../../../../constants'; @@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'copy_to': case 'boost': + case 'meta': case 'ignore_malformed': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -95,6 +97,8 @@ export const NumericType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx index f87d1f94001018..9a37f55ac8e9d2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { NormalizedField, Field as FieldType } from '../../../../types'; +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; import { StoreParameter, @@ -14,11 +14,12 @@ import { CoerceNumberParameter, FormatParameter, LocaleParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; import { FormDataProvider } from '../../../../shared_imports'; -const getDefaultToggleValue = (param: 'locale' | 'format' | 'boost', field: FieldType) => { +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; }; @@ -57,6 +58,8 @@ export const RangeType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx index dafbebd24b3fa7..3fa456c33f5e90 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx @@ -15,6 +15,7 @@ import { SimilarityParameter, TermVectorParameter, MaxShingleSizeParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; @@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'similarity': case 'term_vector': + case 'meta': case 'max_shingle_size': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -65,6 +67,8 @@ export const SearchAsYouType = React.memo(({ field }: Props) => { /> + + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx index c4ed11097b6098..07def791096e7c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx @@ -28,6 +28,7 @@ import { CopyToParameter, TermVectorParameter, FieldDataParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; @@ -40,6 +41,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { case 'boost': case 'position_increment_gap': case 'similarity': + case 'meta': case 'term_vector': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -47,7 +49,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { return field.search_analyzer !== undefined && field.search_analyzer !== field.analyzer; } case 'copy_to': { - return field.null_value !== undefined && field.null_value !== ''; + return field[param] !== undefined && field[param] !== ''; } case 'indexPrefixes': { if (field.index_prefixes === undefined) { @@ -241,6 +243,8 @@ export const TextType = React.memo(({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx index 42854673269aee..5cc2addba53b87 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx @@ -20,12 +20,14 @@ import { BoostParameter, AnalyzerParameter, NullValueParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'analyzer': + case 'meta': case 'boost': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -107,6 +109,8 @@ export const TokenCountType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index 18d9c637bd45b9..a4d3bf3832d5c5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -719,6 +719,23 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {

), }, + histogram: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.histogramDescription', { + defaultMessage: 'Histogram', + }), + value: 'histogram', + documentation: { + main: '/histogram.html', + }, + description: () => ( +

+ +

+ ), + }, join: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.joinDescription', { defaultMessage: 'Join', @@ -863,6 +880,7 @@ export const MAIN_TYPES: MainType[] = [ 'shape', 'text', 'token_count', + 'histogram', 'wildcard', 'other', ]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index c2a44152ae1ee8..97dca49fc93ed5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -59,6 +59,7 @@ export type MainType = | 'geo_point' | 'geo_shape' | 'token_count' + | 'histogram' | 'constant_keyword' | 'wildcard' /** From d88b3a6ddea2e3016d95cadec98be9426776f124 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 17 Sep 2020 12:09:08 -0500 Subject: [PATCH 09/30] [ML] Migrate internal urls to non-hash paths (#76735) Co-authored-by: Elastic Machine --- x-pack/plugins/ml/common/constants/app.ts | 5 + .../common/constants/data_frame_analytics.ts | 5 + .../ml/common/constants/ml_url_generator.ts | 8 + .../ml/common/types/data_frame_analytics.ts | 7 +- .../ml/common/types/ml_url_generator.ts | 82 ++- .../plugins/ml/common/util/analytics_utils.ts | 2 +- x-pack/plugins/ml/kibana.json | 3 +- x-pack/plugins/ml/public/application/app.tsx | 11 + .../capabilities/check_capabilities.ts | 38 +- .../annotations_table.test.js.snap | 663 ++++++++++++++---- .../annotations_table/annotations_table.js | 86 ++- .../components/anomalies_table/links_menu.js | 57 +- .../anomaly_results_view_selector.test.tsx | 34 +- .../anomaly_results_view_selector.tsx | 25 +- .../custom_hooks/use_create_ad_links.ts | 7 +- .../components/data_grid/data_grid.tsx | 3 +- .../decision_path_popover.tsx | 3 +- .../data_recognizer/recognized_result.js | 9 +- .../components/navigation_menu/main_tabs.tsx | 118 +++- .../components/rule_editor/scope_section.js | 13 +- .../application/contexts/kibana/index.ts | 2 +- .../contexts/kibana/kibana_context.ts | 2 +- .../contexts/kibana/use_create_url.ts | 59 ++ .../data_frame_analytics/common/analytics.ts | 7 +- .../data_frame_analytics/common/index.ts | 2 +- .../configuration_step/job_type.tsx | 2 +- .../supported_fields_message.tsx | 5 +- .../create_step_footer/create_step_footer.tsx | 4 +- .../view_results_panel/view_results_panel.tsx | 4 +- .../exploration_results_table.tsx | 4 +- .../pages/analytics_exploration/page.tsx | 5 +- .../action_view/use_view_action.tsx | 24 +- .../analytics_list/analytics_list.tsx | 8 +- .../components/analytics_list/common.ts | 16 +- .../components/analytics_list/use_columns.tsx | 18 +- .../models_management/models_list.tsx | 26 +- .../use_create_analytics_form/reducer.ts | 2 +- .../hooks/use_create_analytics_form/state.ts | 9 +- .../analytics_service/get_analytics.ts | 5 +- .../datavisualizer_selector.tsx | 10 +- .../results_links/results_links.tsx | 88 ++- .../actions_panel/actions_panel.tsx | 38 +- .../explorer_no_jobs_found.test.js.snap | 23 +- .../explorer_no_jobs_found.js | 51 +- .../explorer_no_jobs_found.test.js | 3 + .../explorer_charts_container.js | 44 +- .../explorer_charts_container.test.js | 26 +- .../components/job_actions/results.js | 69 +- .../job_details/extract_job_details.js | 4 +- .../forecasts_table/forecasts_table.js | 93 ++- .../components/jobs_list/job_description.js | 8 +- .../components/jobs_list/job_id_link.tsx | 63 ++ .../components/jobs_list/jobs_list.js | 8 +- .../new_job_button/new_job_button.js | 8 +- .../jobs/jobs_list/components/utils.js | 2 +- .../calendars/calendars_selection.tsx | 3 +- .../pages/components/summary_step/summary.tsx | 7 +- .../preconfigured_job_redirect.ts | 9 +- .../jobs/new_job/pages/job_type/page.tsx | 8 +- .../jobs/new_job/recognize/page.tsx | 30 +- .../jobs/new_job/recognize/resolvers.ts | 24 +- .../jobs_list_page/jobs_list_page.tsx | 6 +- .../application/management/jobs_list/index.ts | 10 +- .../ml_nodes_check/check_ml_nodes.ts | 4 +- .../components/analytics_panel/actions.tsx | 58 +- .../analytics_panel/analytics_panel.tsx | 18 +- .../anomaly_detection_panel/actions.tsx | 28 +- .../anomaly_detection_panel.tsx | 28 +- .../anomaly_detection_panel/table.tsx | 3 +- .../public/application/routing/breadcrumbs.ts | 22 +- .../public/application/routing/resolvers.ts | 8 +- .../ml/public/application/routing/router.tsx | 11 +- .../analytics_job_creation.tsx | 7 +- .../analytics_job_exploration.tsx | 35 +- .../analytics_jobs_list.tsx | 9 +- .../data_frame_analytics/models_list.tsx | 9 +- .../routes/datavisualizer/datavisualizer.tsx | 14 +- .../routes/datavisualizer/file_based.tsx | 14 +- .../routes/datavisualizer/index_based.tsx | 20 +- .../application/routing/routes/explorer.tsx | 9 +- .../application/routing/routes/jobs_list.tsx | 6 +- .../routes/new_job/index_or_search.tsx | 42 +- .../routing/routes/new_job/job_type.tsx | 6 +- .../routing/routes/new_job/recognize.tsx | 20 +- .../routing/routes/new_job/wizard.tsx | 71 +- .../application/routing/routes/overview.tsx | 13 +- .../routing/routes/settings/calendar_list.tsx | 23 +- .../routes/settings/calendar_new_edit.tsx | 30 +- .../routing/routes/settings/filter_list.tsx | 23 +- .../routes/settings/filter_list_new_edit.tsx | 31 +- .../routing/routes/settings/settings.tsx | 13 +- .../routes/timeseriesexplorer.test.tsx | 5 + .../routing/routes/timeseriesexplorer.tsx | 9 +- .../application/routing/use_resolver.ts | 7 +- .../application/services/job_service.js | 1 - .../settings/anomaly_detection_settings.tsx | 16 +- .../__snapshots__/calendar_form.test.js.snap | 64 +- .../edit/calendar_form/calendar_form.js | 5 +- .../edit/calendar_form/calendar_form.test.js | 4 + .../settings/calendars/edit/new_calendar.js | 15 +- .../calendars/edit/new_calendar.test.js | 4 + .../table/__snapshots__/table.test.js.snap | 1 - .../settings/calendars/list/table/table.js | 18 +- .../calendars/list/table/table.test.js | 23 +- .../filter_lists/edit/edit_filter_list.js | 19 +- .../settings/filter_lists/list/table.js | 17 +- .../application/settings/settings.test.tsx | 4 + .../timeseriesexplorer_no_jobs_found.tsx | 58 +- .../ml/public/application/util/chart_utils.js | 62 +- .../application/util/chart_utils.test.js | 15 - .../application/util/get_selected_ids_url.ts | 39 -- .../application/util/recently_accessed.ts | 2 +- .../anomaly_detection_urls_generator.ts | 123 ++-- .../ml/public/ml_url_generator/common.ts | 49 +- .../data_frame_analytics_urls_generator.ts | 37 +- .../data_visualizer_urls_generator.ts | 29 - .../ml_url_generator/ml_url_generator.test.ts | 2 +- .../ml_url_generator/ml_url_generator.ts | 42 +- .../settings_urls_generator.tsx | 45 ++ x-pack/plugins/ml/public/register_feature.ts | 2 +- .../services/ml/settings_filter_list.ts | 2 +- 121 files changed, 2284 insertions(+), 1028 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx delete mode 100644 x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts delete mode 100644 x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/settings_urls_generator.tsx diff --git a/x-pack/plugins/ml/common/constants/app.ts b/x-pack/plugins/ml/common/constants/app.ts index 97dd7a7b0fef54..3d54e9e150fefa 100644 --- a/x-pack/plugins/ml/common/constants/app.ts +++ b/x-pack/plugins/ml/common/constants/app.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + export const PLUGIN_ID = 'ml'; export const PLUGIN_ICON = 'machineLearningApp'; export const PLUGIN_ICON_SOLUTION = 'logoKibana'; +export const ML_APP_NAME = i18n.translate('xpack.ml.navMenu.mlAppNameText', { + defaultMessage: 'Machine Learning', +}); diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 830537cbadbc8d..9a7af2496c03fe 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +export const ANALYSIS_CONFIG_TYPE = { + OUTLIER_DETECTION: 'outlier_detection', + REGRESSION: 'regression', + CLASSIFICATION: 'classification', +} as const; export const DEFAULT_RESULTS_FIELD = 'ml'; diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts index 44f33aa329e7ae..541b8af6fc0fc7 100644 --- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -31,8 +31,16 @@ export const ML_PAGES = { * Open index data visualizer viewer page */ DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer', + ANOMALY_DETECTION_CREATE_JOB: `jobs/new_job`, ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`, + ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: `jobs/new_job/step/index_or_search`, SETTINGS: 'settings', CALENDARS_MANAGE: 'settings/calendars_list', + CALENDARS_NEW: 'settings/calendars_list/new_calendar', + CALENDARS_EDIT: 'settings/calendars_list/edit_calendar', FILTER_LISTS_MANAGE: 'settings/filter_lists', + FILTER_LISTS_NEW: 'settings/filter_lists/new_filter_list', + FILTER_LISTS_EDIT: 'settings/filter_lists/edit_filter_list', + ACCESS_DENIED: 'access-denied', + OVERVIEW: 'overview', } as const; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 96d6c81a3d309b..5d0ecf96fb6b5f 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { EsErrorBody } from '../util/errors'; +import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; export interface DeleteDataFrameAnalyticsWithIndexStatus { success: boolean; @@ -81,8 +82,4 @@ export interface DataFrameAnalyticsConfig { allow_lazy_start?: boolean; } -export enum ANALYSIS_CONFIG_TYPE { - OUTLIER_DETECTION = 'outlier_detection', - REGRESSION = 'regression', - CLASSIFICATION = 'classification', -} +export type DataFrameAnalysisConfigType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index 234be8b6faf909..d176c22bdbb62f 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -5,27 +5,21 @@ */ import { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; -import { JobId } from '../../../reporting/common/types'; +import { JobId } from './anomaly_detection_jobs/job'; import { ML_PAGES } from '../constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from './data_frame_analytics'; type OptionalPageState = object | undefined; export type MLPageState = PageState extends OptionalPageState - ? { page: PageType; pageState?: PageState } + ? { page: PageType; pageState?: PageState; excludeBasePath?: boolean } : PageState extends object - ? { page: PageType; pageState: PageState } - : { page: PageType }; - -export const ANALYSIS_CONFIG_TYPE = { - OUTLIER_DETECTION: 'outlier_detection', - REGRESSION: 'regression', - CLASSIFICATION: 'classification', -} as const; - -type DataFrameAnalyticsType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; + ? { page: PageType; pageState: PageState; excludeBasePath?: boolean } + : { page: PageType; excludeBasePath?: boolean }; export interface MlCommonGlobalState { time?: TimeRange; + refreshInterval?: RefreshInterval; } export interface MlCommonAppState { [key: string]: any; @@ -42,16 +36,28 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState { [key: string]: any; } -export interface MlGenericUrlState { - page: - | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER - | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE; - pageState: MlGenericUrlPageState; -} +export type MlGenericUrlState = MLPageState< + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX + | typeof ML_PAGES.OVERVIEW + | typeof ML_PAGES.CALENDARS_MANAGE + | typeof ML_PAGES.CALENDARS_NEW + | typeof ML_PAGES.FILTER_LISTS_MANAGE + | typeof ML_PAGES.FILTER_LISTS_NEW + | typeof ML_PAGES.SETTINGS + | typeof ML_PAGES.ACCESS_DENIED + | typeof ML_PAGES.DATA_VISUALIZER + | typeof ML_PAGES.DATA_VISUALIZER_FILE + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT, + MlGenericUrlPageState | undefined +>; export interface AnomalyDetectionQueryState { jobId?: JobId; groupIds?: string[]; + globalState?: MlCommonGlobalState; } export type AnomalyDetectionUrlState = MLPageState< @@ -86,7 +92,7 @@ export interface ExplorerUrlPageState { /** * Job IDs */ - jobIds: JobId[]; + jobIds?: JobId[]; /** * Optionally set the time range in the time picker. */ @@ -104,6 +110,7 @@ export interface ExplorerUrlPageState { */ mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; + globalState?: MlCommonGlobalState; } export type ExplorerUrlState = MLPageState; @@ -122,6 +129,7 @@ export interface TimeSeriesExplorerAppState { to?: string; }; mlTimeSeriesExplorer?: { + forecastId?: string; detectorIndex?: number; entities?: Record; }; @@ -131,10 +139,12 @@ export interface TimeSeriesExplorerAppState { export interface TimeSeriesExplorerPageState extends Pick, Pick { - jobIds: JobId[]; + jobIds?: JobId[]; timeRange?: TimeRange; detectorIndex?: number; entities?: Record; + forecastId?: string; + globalState?: MlCommonGlobalState; } export type TimeSeriesExplorerUrlState = MLPageState< @@ -145,6 +155,7 @@ export type TimeSeriesExplorerUrlState = MLPageState< export interface DataFrameAnalyticsQueryState { jobId?: JobId | JobId[]; groupIds?: string[]; + globalState?: MlCommonGlobalState; } export type DataFrameAnalyticsUrlState = MLPageState< @@ -152,17 +163,10 @@ export type DataFrameAnalyticsUrlState = MLPageState< DataFrameAnalyticsQueryState | undefined >; -export interface DataVisualizerUrlState { - page: - | typeof ML_PAGES.DATA_VISUALIZER - | typeof ML_PAGES.DATA_VISUALIZER_FILE - | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT; -} - export interface DataFrameAnalyticsExplorationQueryState { ml: { jobId: JobId; - analysisType: DataFrameAnalyticsType; + analysisType: DataFrameAnalysisConfigType; }; } @@ -170,7 +174,24 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState< typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, { jobId: JobId; - analysisType: DataFrameAnalyticsType; + analysisType: DataFrameAnalysisConfigType; + globalState?: MlCommonGlobalState; + } +>; + +export type CalendarEditUrlState = MLPageState< + typeof ML_PAGES.CALENDARS_EDIT, + { + calendarId: string; + globalState?: MlCommonGlobalState; + } +>; + +export type FilterEditUrlState = MLPageState< + typeof ML_PAGES.FILTER_LISTS_EDIT, + { + filterId: string; + globalState?: MlCommonGlobalState; } >; @@ -183,5 +204,6 @@ export type MlUrlGeneratorState = | TimeSeriesExplorerUrlState | DataFrameAnalyticsUrlState | DataFrameAnalyticsExplorationUrlState - | DataVisualizerUrlState + | CalendarEditUrlState + | FilterEditUrlState | MlGenericUrlState; diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts index d725984a47d661..d231ed43443892 100644 --- a/x-pack/plugins/ml/common/util/analytics_utils.ts +++ b/x-pack/plugins/ml/common/util/analytics_utils.ts @@ -9,8 +9,8 @@ import { ClassificationAnalysis, OutlierAnalysis, RegressionAnalysis, - ANALYSIS_CONFIG_TYPE, } from '../types/data_frame_analytics'; +import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => { const keys = Object.keys(arg); diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index fc673397ef177d..2c5dbe108ab1e9 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,8 @@ "embeddable", "uiActions", "kibanaLegacy", - "indexPatternManagement" + "indexPatternManagement", + "discover" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index c281dc4e9ae059..e3bcc53fe697fc 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -20,6 +20,7 @@ import { MlSetupDependencies, MlStartDependencies } from '../plugin'; import { MlRouter } from './routing'; import { mlApiServicesProvider } from './services/ml_api_service'; import { HttpService } from './services/http_service'; +import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../common/constants/ml_url_generator'; export type MlDependencies = Omit & MlStartDependencies; @@ -50,11 +51,21 @@ export interface MlServicesContext { export type MlGlobalServices = ReturnType; const App: FC = ({ coreStart, deps, appMountParams }) => { + const redirectToMlAccessDeniedPage = async () => { + const accessDeniedPageUrl = await deps.share.urlGenerators + .getUrlGenerator(ML_APP_URL_GENERATOR) + .createUrl({ + page: ML_PAGES.ACCESS_DENIED, + }); + await coreStart.application.navigateToUrl(accessDeniedPageUrl); + }; + const pageDeps = { history: appMountParams.history, indexPatterns: deps.data.indexPatterns, config: coreStart.uiSettings!, setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, + redirectToMlAccessDeniedPage, }; const services = { appName: 'ML', diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index 653eca126006db..cdd25821ea5ca2 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -33,10 +33,12 @@ export function checkGetManagementMlJobsResolver() { }); } -export function checkGetJobsCapabilitiesResolver(): Promise { +export function checkGetJobsCapabilitiesResolver( + redirectToMlAccessDeniedPage: () => Promise +): Promise { return new Promise((resolve, reject) => { getCapabilities() - .then(({ capabilities, isPlatinumOrTrialLicense }) => { + .then(async ({ capabilities, isPlatinumOrTrialLicense }) => { _capabilities = capabilities; // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. // all other functionality is controlled by the return capabilities object. @@ -46,21 +48,23 @@ export function checkGetJobsCapabilitiesResolver(): Promise { if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) { return resolve(_capabilities); } else { - window.location.href = '#/access-denied'; + await redirectToMlAccessDeniedPage(); return reject(); } }) - .catch((e) => { - window.location.href = '#/access-denied'; + .catch(async (e) => { + await redirectToMlAccessDeniedPage(); return reject(); }); }); } -export function checkCreateJobsCapabilitiesResolver(): Promise { +export function checkCreateJobsCapabilitiesResolver( + redirectToJobsManagementPage: () => Promise +): Promise { return new Promise((resolve, reject) => { getCapabilities() - .then(({ capabilities, isPlatinumOrTrialLicense }) => { + .then(async ({ capabilities, isPlatinumOrTrialLicense }) => { _capabilities = capabilities; // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, // allow the promise to resolve as the separate license check will redirect then user to @@ -69,34 +73,36 @@ export function checkCreateJobsCapabilitiesResolver(): Promise { return resolve(_capabilities); } else { // if the user has no permission to create a job, - // redirect them back to the Transforms Management page - window.location.href = '#/jobs'; + // redirect them back to the Anomaly Detection Management page + await redirectToJobsManagementPage(); return reject(); } }) - .catch((e) => { - window.location.href = '#/jobs'; + .catch(async (e) => { + await redirectToJobsManagementPage(); return reject(); }); }); } -export function checkFindFileStructurePrivilegeResolver(): Promise { +export function checkFindFileStructurePrivilegeResolver( + redirectToMlAccessDeniedPage: () => Promise +): Promise { return new Promise((resolve, reject) => { getCapabilities() - .then(({ capabilities }) => { + .then(async ({ capabilities }) => { _capabilities = capabilities; // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. // all other functionality is controlled by the return _capabilities object if (_capabilities.canFindFileStructure) { return resolve(_capabilities); } else { - window.location.href = '#/access-denied'; + await redirectToMlAccessDeniedPage(); return reject(); } }) - .catch((e) => { - window.location.href = '#/access-denied'; + .catch(async (e) => { + await redirectToMlAccessDeniedPage(); return reject(); }); }); diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index 9eb44c71aa7996..114a6b235d1adf 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -1,170 +1,527 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` - - ", + "end_timestamp": 1455041968976, + "job_id": "farequote", + "modified_time": 1546417097181, + "modified_username": "", + "timestamp": 1455026177994, + "type": "annotation", + }, + ] + } + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } + kibana={ + Object { + "notifications": Object { + "toasts": Object { + "danger": [Function], + "show": [Function], + "success": [Function], + "warning": [Function], + }, + }, + "overlays": Object { + "openFlyout": [Function], + "openModal": [Function], + }, + "services": Object {}, + } + } +/> +`; + +exports[`AnnotationsTable Initialization with job config prop. 1`] = ` +", - "end_timestamp": 1455041968976, "job_id": "farequote", - "modified_time": 1546417097181, - "modified_username": "", - "timestamp": 1455026177994, - "type": "annotation", + "query": Object { + "bool": Object { + "adjust_pure_negative": true, + "boost": 1, + "must": Array [ + Object { + "query_string": Object { + "analyze_wildcard": true, + "auto_generate_synonyms_phrase_query": true, + "boost": 1, + "default_operator": "or", + "enable_position_increments": true, + "escape": false, + "fields": Array [], + "fuzziness": "AUTO", + "fuzzy_max_expansions": 50, + "fuzzy_prefix_length": 0, + "fuzzy_transpositions": true, + "max_determinized_states": 10000, + "phrase_slop": 0, + "query": "*", + "type": "best_fields", + }, + }, + ], + }, + }, + "query_delay": "115823ms", + "scroll_size": 1000, + "state": "stopped", }, - ] - } - pagination={ - Object { - "pageSizeOptions": Array [ - 5, - 10, - 25, - ], - } - } - responsive={true} - rowProps={[Function]} - search={ - Object { - "box": Object { - "incremental": true, - "schema": true, - }, - "defaultQuery": "event:(user or delayed_data)", - "filters": Array [ - Object { - "field": "event", - "multiSelect": "or", - "name": "Event", - "options": Array [], - "type": "field_value_selection", - }, - ], - } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "timestamp", + "description": "", + "established_model_memory": 42102, + "finished_time": 1546418359427, + "job_id": "farequote", + "job_type": "anomaly_detector", + "job_version": "7.0.0", + "model_plot_config": Object { + "enabled": true, }, - } + "model_size_stats": Object { + "bucket_allocation_failures_count": 0, + "job_id": "farequote", + "log_time": 1546418359000, + "memory_status": "ok", + "model_bytes": 42102, + "result_type": "model_size_stats", + "timestamp": 1455232500000, + "total_by_field_count": 3, + "total_over_field_count": 0, + "total_partition_field_count": 2, + }, + "model_snapshot_id": "1546418359", + "model_snapshot_min_version": "6.4.0", + "model_snapshot_retention_days": 1, + "results_index_name": "shared", + "state": "closed", + }, + ] + } + kibana={ + Object { + "notifications": Object { + "toasts": Object { + "danger": [Function], + "show": [Function], + "success": [Function], + "warning": [Function], + }, + }, + "overlays": Object { + "openFlyout": [Function], + "openModal": [Function], + }, + "services": Object {}, } - tableLayout="fixed" - /> - -`; - -exports[`AnnotationsTable Initialization with job config prop. 1`] = ` - - - - - + } +/> `; exports[`AnnotationsTable Minimal initialization without props. 1`] = ` - + `; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 9dabfce163dbb3..d5025fd3c36492 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -13,7 +13,6 @@ import uniq from 'lodash/uniq'; import PropTypes from 'prop-types'; -import rison from 'rison-node'; import React, { Component, Fragment } from 'react'; import memoizeOne from 'memoize-one'; import { @@ -54,12 +53,15 @@ import { ANNOTATION_EVENT_USER, ANNOTATION_EVENT_DELAYED_DATA, } from '../../../../../common/constants/annotations'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../../common/constants/app'; const CURRENT_SERIES = 'current_series'; /** * Table component for rendering the lists of annotations for an ML job. */ -export class AnnotationsTable extends Component { +class AnnotationsTableUI extends Component { static propTypes = { annotations: PropTypes.array, jobs: PropTypes.array, @@ -199,7 +201,17 @@ export class AnnotationsTable extends Component { } } - openSingleMetricView = (annotation = {}) => { + openSingleMetricView = async (annotation = {}) => { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = this.props.kibana; + // Creates the link to the Single Metric Viewer. // Set the total time range from the start to the end of the annotation. const job = this.getJob(annotation.job_id); @@ -210,30 +222,10 @@ export class AnnotationsTable extends Component { ); const from = new Date(dataCounts.earliest_record_timestamp).toISOString(); const to = new Date(resultLatest).toISOString(); - - const globalSettings = { - ml: { - jobIds: [job.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from, - to, - mode: 'absolute', - }, - }; - - const appState = { - query: { - query_string: { - analyze_wildcard: true, - query: '*', - }, - }, + const timeRange = { + from, + to, + mode: 'absolute', }; let mlTimeSeriesExplorer = {}; const entityCondition = {}; @@ -247,11 +239,11 @@ export class AnnotationsTable extends Component { }; if (annotation.timestamp < dataCounts.earliest_record_timestamp) { - globalSettings.time.from = new Date(annotation.timestamp).toISOString(); + timeRange.from = new Date(annotation.timestamp).toISOString(); } if (annotation.end_timestamp > dataCounts.latest_record_timestamp) { - globalSettings.time.to = new Date(annotation.end_timestamp).toISOString(); + timeRange.to = new Date(annotation.end_timestamp).toISOString(); } } @@ -274,14 +266,34 @@ export class AnnotationsTable extends Component { entityCondition[annotation.by_field_name] = annotation.by_field_value; } mlTimeSeriesExplorer.entities = entityCondition; - appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; - - const _g = rison.encode(globalSettings); - const _a = rison.encode(appState); + // appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; + + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + const singleMetricViewerLink = await mlUrlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + timeRange, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + jobIds: [job.job_id], + query: { + query_string: { + analyze_wildcard: true, + query: '*', + }, + }, + ...mlTimeSeriesExplorer, + }, + excludeBasePath: true, + }); - const url = `?_g=${_g}&_a=${_a}`; - addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); - window.open(`#/timeseriesexplorer${url}`, '_self'); + addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, singleMetricViewerLink); + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerLink, + }); }; onMouseOverRow = (record) => { @@ -686,3 +698,5 @@ export class AnnotationsTable extends Component { ); } } + +export const AnnotationsTable = withKibana(AnnotationsTableUI); diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index fdeab0c49e32b4..6025dd1c7433e7 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -29,6 +29,8 @@ import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_util import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; import { getIndexPatternIdFromName } from '../../util/index_utils'; import { replaceStringTokens } from '../../util/string_utils'; +import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../common/constants/app'; /* * Component for rendering the links menu inside a cell in the anomalies table. */ @@ -142,7 +144,18 @@ class LinksMenuUI extends Component { } }; - viewSeries = () => { + viewSeries = async () => { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = this.props.kibana; + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + const record = this.props.anomaly.source; const bounds = this.props.bounds; const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z @@ -171,44 +184,36 @@ class LinksMenuUI extends Component { entityCondition[record.by_field_name] = record.by_field_value; } - // Use rison to build the URL . - const _g = rison.encode({ - ml: { + const singleMetricViewerLink = await mlUrlGenerator.createUrl({ + excludeBasePath: true, + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { jobIds: [record.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from: from, - to: to, - mode: 'absolute', - }, - }); - - const _a = rison.encode({ - mlTimeSeriesExplorer: { + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + timeRange: { + from: from, + to: to, + mode: 'absolute', + }, zoom: { from: zoomFrom, to: zoomTo, }, detectorIndex: record.detector_index, entities: entityCondition, - }, - query: { query_string: { analyze_wildcard: true, query: '*', }, }, }); - - // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. - let path = '#/timeseriesexplorer'; - path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; - window.open(path, '_blank'); + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerLink, + }); }; viewExamples = () => { diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx index 4a63a8cd7e7163..d54a7fe81e8580 100644 --- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx +++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx @@ -6,13 +6,22 @@ import React from 'react'; import { Router } from 'react-router-dom'; -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { createBrowserHistory } from 'history'; import { I18nProvider } from '@kbn/i18n/react'; import { AnomalyResultsViewSelector } from './index'; +jest.mock('../../contexts/kibana', () => { + return { + useMlUrlGenerator: () => ({ + createUrl: jest.fn(), + }), + useNavigateToPath: () => jest.fn(), + }; +}); + describe('AnomalyResultsViewSelector', () => { test('should create selector with correctly selected value', () => { const history = createBrowserHistory(); @@ -31,27 +40,4 @@ describe('AnomalyResultsViewSelector', () => { getByTestId('mlAnomalyResultsViewSelectorSingleMetricViewer').hasAttribute('checked') ).toBe(true); }); - - test('should open window to other results view when clicking on non-checked input', () => { - // Create mock for window.open - const mockedOpen = jest.fn(); - const originalOpen = window.open; - window.open = mockedOpen; - - const history = createBrowserHistory(); - - const { getByTestId } = render( - - - - - - ); - - fireEvent.click(getByTestId('mlAnomalyResultsViewSelectorExplorer')); - expect(mockedOpen).toHaveBeenCalledWith('#/explorer', '_self'); - - // Clean-up window.open. - window.open = originalOpen; - }); }); diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx index 78acb422851e36..c4c8f06bbbc3a0 100644 --- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx @@ -5,21 +5,25 @@ */ import React, { FC, useMemo } from 'react'; -import { encode } from 'rison-node'; import { EuiButtonGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useUrlState } from '../../util/url_state'; +import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; interface Props { - viewId: 'timeseriesexplorer' | 'explorer'; + viewId: typeof ML_PAGES.SINGLE_METRIC_VIEWER | typeof ML_PAGES.ANOMALY_EXPLORER; } // Component for rendering a set of buttons for switching between the Anomaly Detection results views. export const AnomalyResultsViewSelector: FC = ({ viewId }) => { + const urlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + const toggleButtonsIcons = useMemo( () => [ { @@ -28,7 +32,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { defaultMessage: 'View results in the Single Metric Viewer', }), iconType: 'visLine', - value: 'timeseriesexplorer', + value: ML_PAGES.SINGLE_METRIC_VIEWER, 'data-test-subj': 'mlAnomalyResultsViewSelectorSingleMetricViewer', }, { @@ -37,7 +41,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { defaultMessage: 'View results in the Anomaly Explorer', }), iconType: 'visTable', - value: 'explorer', + value: ML_PAGES.ANOMALY_EXPLORER, 'data-test-subj': 'mlAnomalyResultsViewSelectorExplorer', }, ], @@ -46,9 +50,14 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { const [globalState] = useUrlState('_g'); - const onChangeView = (newViewId: string) => { - const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : ''; - window.open(`#/${newViewId}${fullGlobalStateString}`, '_self'); + const onChangeView = async (newViewId: Props['viewId']) => { + const url = await urlGenerator.createUrl({ + page: newViewId, + pageState: { + globalState, + }, + }); + await navigateToPath(url); }; return ( @@ -60,7 +69,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { data-test-subj="mlAnomalyResultsViewSelector" options={toggleButtonsIcons} idSelected={viewId} - onChange={onChangeView} + onChange={(newViewId: string) => onChangeView(newViewId as Props['viewId'])} isIconOnly /> ); diff --git a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts index 368e758a027c49..b4668810b94210 100644 --- a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts +++ b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts @@ -22,16 +22,19 @@ export const useCreateADLinks = () => { const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE); const createLinkWithUserDefaults = useCallback( (location, jobList) => { - const resultsPageUrl = mlJobService.createResultsUrlForJobs( + return mlJobService.createResultsUrlForJobs( jobList, location, useUserTimeSettings === true && userTimeSettings !== undefined ? userTimeSettings : undefined ); - return `${basePath.get()}/app/ml${resultsPageUrl}`; }, [basePath] ); return { createLinkWithUserDefaults }; }; + +export type CreateLinkWithUserDefaults = ReturnType< + typeof useCreateADLinks +>['createLinkWithUserDefaults']; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 22815fe593d57a..6aad5d53c3a3c9 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -32,6 +32,7 @@ import { UseIndexDataReturnType } from './types'; import { DecisionPathPopover } from './feature_importance/decision_path_popover'; import { TopClasses } from '../../../../common/types/feature_importance'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; +import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics'; // TODO Fix row hovering + bar highlighting // import { hoveredRow$ } from './column_chart'; @@ -44,7 +45,7 @@ export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( interface PropsWithoutHeader extends UseIndexDataReturnType { baseline?: number; - analysisType?: ANALYSIS_CONFIG_TYPE; + analysisType?: DataFrameAnalysisConfigType; resultsField?: string; dataTestSubj: string; toastNotifications: CoreSetup['notifications']['toasts']; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx index 263337f93e9a8c..7c4428db71b3b6 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx @@ -13,10 +13,11 @@ import { FeatureImportance, TopClasses } from '../../../../../common/types/featu import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common'; import { ClassificationDecisionPath } from './decision_path_classification'; import { useMlKibana } from '../../../contexts/kibana'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; interface DecisionPathPopoverProps { featureImportance: FeatureImportance[]; - analysisType: ANALYSIS_CONFIG_TYPE; + analysisType: DataFrameAnalysisConfigType; predictionFieldName?: string; baseline?: number; predictedValue?: number | string | undefined; diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js index 1f03dbe1347569..279afc8c503399 100644 --- a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js +++ b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js @@ -9,11 +9,16 @@ import PropTypes from 'prop-types'; import { EuiIcon, EuiFlexItem } from '@elastic/eui'; import { CreateJobLinkCard } from '../create_job_link_card'; +import { useMlKibana } from '../../contexts/kibana'; export const RecognizedResult = ({ config, indexPattern, savedSearch }) => { + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); const id = savedSearch === null ? `index=${indexPattern.id}` : `savedSearchId=${savedSearch.id}`; - - const href = `#/jobs/new_job/recognize?id=${config.id}&${id}`; + const href = `${basePath.get()}/app/ml/jobs/new_job/recognize?id=${config.id}&${id}`; let logo = null; // if a logo is available, use that, otherwise display the id diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index 3a4875fa243fda..671f0b196ce35f 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState } from 'react'; -import { encode } from 'rison-node'; +import React, { FC, useState, useEffect } from 'react'; -import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui'; +import { EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - -import { useUrlState } from '../../util/url_state'; - import { TabId } from './navigation_menu'; +import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; +import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator'; +import { useUrlState } from '../../util/url_state'; +import { ML_APP_NAME } from '../../../../common/constants/app'; export interface Tab { id: TabId; @@ -66,20 +66,57 @@ function getTabs(disableLinks: boolean): Tab[] { } interface TabData { testSubject: string; - pathId?: string; + pathId?: MlUrlGeneratorState['page']; + name: string; } const TAB_DATA: Record = { - overview: { testSubject: 'mlMainTab overview' }, + overview: { + testSubject: 'mlMainTab overview', + name: i18n.translate('xpack.ml.overviewTabLabel', { + defaultMessage: 'Overview', + }), + }, // Note that anomaly detection jobs list is mapped to ml#/jobs. - anomaly_detection: { testSubject: 'mlMainTab anomalyDetection', pathId: 'jobs' }, - data_frame_analytics: { testSubject: 'mlMainTab dataFrameAnalytics' }, - datavisualizer: { testSubject: 'mlMainTab dataVisualizer' }, - settings: { testSubject: 'mlMainTab settings' }, - 'access-denied': { testSubject: 'mlMainTab overview' }, + anomaly_detection: { + testSubject: 'mlMainTab anomalyDetection', + name: i18n.translate('xpack.ml.anomalyDetectionTabLabel', { + defaultMessage: 'Anomaly Detection', + }), + pathId: 'jobs', + }, + data_frame_analytics: { + testSubject: 'mlMainTab dataFrameAnalytics', + name: i18n.translate('xpack.ml.dataFrameAnalyticsTabLabel', { + defaultMessage: 'Data Frame Analytics', + }), + }, + datavisualizer: { + testSubject: 'mlMainTab dataVisualizer', + name: i18n.translate('xpack.ml.dataVisualizerTabLabel', { + defaultMessage: 'Data Visualizer', + }), + }, + settings: { + testSubject: 'mlMainTab settings', + name: i18n.translate('xpack.ml.settingsTabLabel', { + defaultMessage: 'Settings', + }), + }, + 'access-denied': { + testSubject: 'mlMainTab overview', + name: i18n.translate('xpack.ml.accessDeniedTabLabel', { + defaultMessage: 'Access Denied', + }), + }, }; export const MainTabs: FC = ({ tabId, disableLinks }) => { + const { + services: { + chrome: { docTitle }, + }, + } = useMlKibana(); const [globalState] = useUrlState('_g'); const [selectedTabId, setSelectedTabId] = useState(tabId); function onSelectedTabChanged(id: TabId) { @@ -87,16 +124,40 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { } const tabs = getTabs(disableLinks); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToTab = async (defaultPathId: MlUrlGeneratorState['page']) => { + const pageState = + globalState?.refreshInterval !== undefined + ? { + globalState: { + refreshInterval: globalState.refreshInterval, + }, + } + : undefined; + // TODO - Fix ts so passing pageState won't default to MlGenericUrlState when pageState is passed in + // @ts-ignore + const path = await mlUrlGenerator.createUrl({ + page: defaultPathId, + // only retain the refreshInterval part of globalState + // appState will not be considered. + pageState, + }); + + await navigateToPath(path, false); + }; + + useEffect(() => { + docTitle.change([TAB_DATA[selectedTabId].name, ML_APP_NAME]); + }, [selectedTabId]); return ( {tabs.map((tab: Tab) => { const { id, disabled } = tab; const testSubject = TAB_DATA[id].testSubject; - const defaultPathId = TAB_DATA[id].pathId || id; - // globalState (e.g. selected jobs and time range) should be retained when changing pages. - // appState will not be considered. - const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : ''; + const defaultPathId = (TAB_DATA[id].pathId || id) as MlUrlGeneratorState['page']; return disabled ? ( @@ -104,21 +165,18 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { ) : (
- { + onSelectedTabChanged(id); + redirectToTab(defaultPathId); + }} + isSelected={id === selectedTabId} + key={`tab-${id}-key`} > - onSelectedTabChanged(id)} - isSelected={id === selectedTabId} - key={`tab-${id}-key`} - > - {tab.name} - - + {tab.name} +
); })} diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js index 48e0da72f067cd..eb12cb7679674c 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js @@ -17,8 +17,19 @@ import { ScopeExpression } from './scope_expression'; import { checkPermission } from '../../capabilities/check_capabilities'; import { getScopeFieldDefaults } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; function NoFilterListsCallOut() { + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + const redirectToFilterManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.FILTER_LISTS_MANAGE, + }); + await navigateToPath(path, true); + }; + return ( + useKibana(); export type MlKibanaReactContextValue = KibanaReactContextValue; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts index 48385ad3ae6a82..d448185c914b86 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { useCallback, useEffect, useState } from 'react'; import { useMlKibana } from './kibana_context'; import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; +import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator'; +import { useUrlState } from '../../util/url_state'; export const useMlUrlGenerator = () => { const { @@ -18,3 +21,59 @@ export const useMlUrlGenerator = () => { return getUrlGenerator(ML_APP_URL_GENERATOR); }; + +export const useMlLink = (params: MlUrlGeneratorState): string => { + const [href, setHref] = useState(params.page); + const mlUrlGenerator = useMlUrlGenerator(); + + useEffect(() => { + let isCancelled = false; + const generateUrl = async (_params: MlUrlGeneratorState) => { + const url = await mlUrlGenerator.createUrl(_params); + if (!isCancelled) { + setHref(url); + } + }; + generateUrl(params); + return () => { + isCancelled = true; + }; + }, [params]); + + return href; +}; + +export const useCreateAndNavigateToMlLink = ( + page: MlUrlGeneratorState['page'] +): (() => Promise) => { + const mlUrlGenerator = useMlUrlGenerator(); + const [globalState] = useUrlState('_g'); + + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + + const redirectToMlPage = useCallback( + async (_page: MlUrlGeneratorState['page']) => { + const pageState = + globalState?.refreshInterval !== undefined + ? { + globalState: { + refreshInterval: globalState.refreshInterval, + }, + } + : undefined; + + // TODO: fix ts only interpreting it as MlUrlGenericState if pageState is passed + // @ts-ignore + const url = await mlUrlGenerator.createUrl({ page: _page, pageState }); + await navigateToUrl(url); + }, + [mlUrlGenerator, navigateToUrl] + ); + + // returns the onClick callback + return useCallback(() => redirectToMlPage(page), [redirectToMlPage, page]); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 60681fb6e7bbe2..d22bba7738db42 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -15,8 +15,8 @@ import { SavedSearchQuery } from '../../contexts/ml'; import { AnalysisConfig, ClassificationAnalysis, + DataFrameAnalysisConfigType, RegressionAnalysis, - ANALYSIS_CONFIG_TYPE, } from '../../../../common/types/data_frame_analytics'; import { isOutlierAnalysis, @@ -26,6 +26,7 @@ import { getDependentVar, getPredictedFieldName, } from '../../../../common/util/analytics_utils'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/constants/data_frame_analytics'; export type IndexPattern = string; export enum ANALYSIS_ADVANCED_FIELDS { @@ -429,7 +430,7 @@ interface LoadEvalDataConfig { predictionFieldName?: string; searchQuery?: ResultsSearchQuery; ignoreDefaultQuery?: boolean; - jobType: ANALYSIS_CONFIG_TYPE; + jobType: DataFrameAnalysisConfigType; requiresKeyword?: boolean; } @@ -550,7 +551,7 @@ export { isRegressionAnalysis, isClassificationAnalysis, getPredictionFieldName, - ANALYSIS_CONFIG_TYPE, getDependentVar, getPredictedFieldName, + ANALYSIS_CONFIG_TYPE, }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 00d735d9a866e9..83eebccd310e3c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -14,7 +14,6 @@ export { UpdateDataFrameAnalyticsConfig, IndexPattern, REFRESH_ANALYTICS_LIST_STATE, - ANALYSIS_CONFIG_TYPE, OUTLIER_ANALYSIS_METHOD, RegressionEvaluateResponse, getValuesFromResponse, @@ -26,6 +25,7 @@ export { SEARCH_SIZE, defaultSearchQuery, SearchQuery, + ANALYSIS_CONFIG_TYPE, } from './analytics'; export { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx index 1e5dbee3499bda..1e6a616fedd64d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; -import { ANALYSIS_CONFIG_TYPE } from '../../../../common'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx index 88c89df86b29ab..310cd4e3b3a79c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -16,6 +16,7 @@ import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../com import { CATEGORICAL_TYPES } from './form_options_validation'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; const containsClassificationFieldsCb = ({ name, type }: Field) => !OMIT_FIELDS.includes(name) && @@ -32,13 +33,13 @@ const containsRegressionFieldsCb = ({ name, type }: Field) => const containsOutlierFieldsCb = ({ name, type }: Field) => !OMIT_FIELDS.includes(name) && name !== EVENT_RATE_FIELD_ID && BASIC_NUMERICAL_TYPES.has(type); -const callbacks: Record boolean> = { +const callbacks: Record boolean> = { [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: containsClassificationFieldsCb, [ANALYSIS_CONFIG_TYPE.REGRESSION]: containsRegressionFieldsCb, [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: containsOutlierFieldsCb, }; -const messages: Record = { +const messages: Record = { [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: ( = ({ jobId, analysisType }) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index eea579ef1d064f..84b1c4241aaf20 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -29,7 +29,6 @@ import { SEARCH_SIZE, defaultSearchQuery, getAnalysisType, - ANALYSIS_CONFIG_TYPE, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; @@ -39,6 +38,7 @@ import { IndexPatternPrompt } from '../index_pattern_prompt'; import { useExplorationResults } from './use_exploration_results'; import { useMlKibana } from '../../../../../contexts/kibana'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', @@ -195,7 +195,7 @@ export const ExplorationResultsTable: FC = React.memo( {...classificationData} dataTestSubj="mlExplorationDataGrid" toastNotifications={getToastNotifications()} - analysisType={(analysisType as unknown) as ANALYSIS_CONFIG_TYPE} + analysisType={(analysisType as unknown) as DataFrameAnalysisConfigType} />
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index c8349084dbda88..f4f01330271fce 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -26,11 +26,12 @@ import { OutlierExploration } from './components/outlier_exploration'; import { RegressionExploration } from './components/regression_exploration'; import { ClassificationExploration } from './components/classification_exploration'; -import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../common/constants/data_frame_analytics'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; export const Page: FC<{ jobId: string; - analysisType: ANALYSIS_CONFIG_TYPE; + analysisType: DataFrameAnalysisConfigType; }> = ({ jobId, analysisType }) => ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx index a3595b51d0a596..2363e6fbecc9d8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx @@ -7,24 +7,32 @@ import React, { useCallback, useMemo } from 'react'; import { getAnalysisType } from '../../../../common/analytics'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../../../contexts/kibana'; -import { - getResultsUrl, - DataFrameAnalyticsListAction, - DataFrameAnalyticsListRow, -} from '../analytics_list/common'; +import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common'; import { getViewLinkStatus } from './get_view_link_status'; import { viewActionButtonText, ViewButton } from './view_button'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; export type ViewAction = ReturnType; export const useViewAction = () => { + const mlUrlGenerator = useMlUrlGenerator(); const navigateToPath = useNavigateToPath(); + const redirectToTab = async (jobId: string, analysisType: DataFrameAnalysisConfigType) => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { jobId, analysisType }, + }); + + await navigateToPath(path, false); + }; + const clickHandler = useCallback((item: DataFrameAnalyticsListRow) => { - const analysisType = getAnalysisType(item.config.analysis); - navigateToPath(getResultsUrl(item.id, analysisType)); + const analysisType = getAnalysisType(item.config.analysis) as DataFrameAnalysisConfigType; + redirectToTab(item.id, analysisType); }, []); const action: DataFrameAnalyticsListAction = useMemo( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 0c3bff58c25cdd..2f8e087a6a3f0e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -15,12 +15,8 @@ import { EuiSearchBarProps, EuiSpacer, } from '@elastic/eui'; - -import { - DataFrameAnalyticsId, - useRefreshAnalyticsList, - ANALYSIS_CONFIG_TYPE, -} from '../../../../common'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; +import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 994357412510df..37076d400f0211 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -9,11 +9,8 @@ import { EuiTableActionsColumnType, Query, Ast } from '@elastic/eui'; import { DATA_FRAME_TASK_STATE } from './data_frame_task_state'; export { DATA_FRAME_TASK_STATE }; -import { - DataFrameAnalyticsId, - DataFrameAnalyticsConfig, - ANALYSIS_CONFIG_TYPE, -} from '../../../../common'; +import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; export enum DATA_FRAME_MODE { BATCH = 'batch', @@ -111,10 +108,7 @@ export interface DataFrameAnalyticsListRow { checkpointing: object; config: DataFrameAnalyticsConfig; id: DataFrameAnalyticsId; - job_type: - | ANALYSIS_CONFIG_TYPE.CLASSIFICATION - | ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION - | ANALYSIS_CONFIG_TYPE.REGRESSION; + job_type: DataFrameAnalysisConfigType; mode: string; state: DataFrameAnalyticsStats['state']; stats: DataFrameAnalyticsStats; @@ -137,10 +131,6 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) { return stats.state === DATA_FRAME_TASK_STATE.STOPPED && progress === 100; } -export function getResultsUrl(jobId: string, analysisType: ANALYSIS_CONFIG_TYPE | string) { - return `#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId},analysisType:${analysisType}))`; -} - // The single Action type is not exported as is // from EUI so we use that code to get the single // Action type from the array of actions. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index ef1d373a55a124..1af99d2a1ed00c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -19,8 +19,6 @@ import { EuiLink, RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url'; - import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common'; import { getDataFrameAnalyticsProgressPhase, @@ -32,6 +30,8 @@ import { DataFrameAnalyticsStats, } from './common'; import { useActions } from './use_actions'; +import { useMlLink } from '../../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; enum TASK_STATE_COLOR { analyzing = 'primary', @@ -134,9 +134,14 @@ export const progressColumn = { 'data-test-subj': 'mlAnalyticsTableColumnProgress', }; -export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => ( - {item.id} -); +export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => { + const href = useMlLink({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { jobId: item.id }, + }); + + return {item.id}; +}; export const useColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], @@ -145,7 +150,6 @@ export const useColumns = ( isMlEnabledInSpace: boolean = true ) => { const { actions, modals } = useActions(isManagementTable); - function toggleDetails(item: DataFrameAnalyticsListRow) { const index = expandedRowItemIds.indexOf(item.config.id); if (index !== -1) { @@ -200,7 +204,7 @@ export const useColumns = ( 'data-test-subj': 'mlAnalyticsTableColumnId', scope: 'row', render: (item: DataFrameAnalyticsListRow) => - isManagementTable ? getDFAnalyticsJobIdLink(item) : item.id, + isManagementTable ? : item.id, }, { field: DataFrameAnalyticsListColumn.description, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 338b6444671a68..dbc7a23f2258ba 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -29,21 +29,23 @@ import { useInferenceApiService } from '../../../../../services/ml_api_service/i import { ModelsTableToConfigMapping } from './index'; import { TIME_FORMAT } from '../../../../../../../common/constants/time_format'; import { DeleteModelsModal } from './delete_models_modal'; -import { useMlKibana, useNotifications } from '../../../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator, useNotifications } from '../../../../../contexts/kibana'; import { ExpandedRow } from './expanded_row'; -import { getResultsUrl } from '../analytics_list/common'; import { ModelConfigResponse, ModelPipelines, TrainedModelStat, } from '../../../../../../../common/types/inference'; import { + getAnalysisType, REFRESH_ANALYTICS_LIST_STATE, refreshAnalyticsList$, useRefreshAnalyticsList, } from '../../../../common'; import { useTableSettings } from '../analytics_list/use_table_settings'; import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; type Stats = Omit; @@ -61,6 +63,7 @@ export const ModelsList: FC = () => { application: { navigateToUrl, capabilities }, }, } = useMlKibana(); + const urlGenerator = useMlUrlGenerator(); const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean; @@ -278,12 +281,19 @@ export const ModelsList: FC = () => { type: 'icon', available: (item) => item.metadata?.analytics_config?.id, onClick: async (item) => { - await navigateToUrl( - getResultsUrl( - item.metadata?.analytics_config.id, - Object.keys(item.metadata?.analytics_config.analysis)[0] - ) - ); + if (item.metadata?.analytics_config === undefined) return; + + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId: item.metadata?.analytics_config.id as string, + analysisType: getAnalysisType( + item.metadata?.analytics_config.analysis + ) as DataFrameAnalysisConfigType, + }, + }); + + await navigateToUrl(url); }, isPrimary: true, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 7cd9fcc052f1a5..178638322bacdb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -33,13 +33,13 @@ import { JOB_ID_MAX_LENGTH, ALLOWED_DATA_UNITS, } from '../../../../../../../common/constants/validation'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; import { getDependentVar, getNumTopFeatureImportanceValues, getTrainingPercent, isRegressionAnalysis, isClassificationAnalysis, - ANALYSIS_CONFIG_TYPE, NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN, TRAINING_PERCENT_MIN, TRAINING_PERCENT_MAX, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 4926decaa7f9c0..2a89c5a5fd686c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -8,13 +8,14 @@ import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/com import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; -import { ANALYSIS_CONFIG_TYPE, defaultSearchQuery } from '../../../../common/analytics'; +import { defaultSearchQuery, getAnalysisType } from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; import { DataFrameAnalyticsConfig, DataFrameAnalyticsId, + DataFrameAnalysisConfigType, } from '../../../../../../../common/types/data_frame_analytics'; - +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', outlier_detection = '50mb', @@ -28,7 +29,7 @@ export const UNSET_CONFIG_ITEM = '--'; export type EsIndexName = string; export type DependentVariable = string; export type IndexPatternTitle = string; -export type AnalyticsJobType = ANALYSIS_CONFIG_TYPE | undefined; +export type AnalyticsJobType = DataFrameAnalysisConfigType | undefined; type IndexPatternId = string; export type SourceIndexMap = Record< IndexPatternTitle, @@ -290,7 +291,7 @@ export function getFormStateFromJobConfig( analyticsJobConfig: Readonly, isClone: boolean = true ): Partial { - const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; + const jobType = getAnalysisType(analyticsJobConfig.analysis) as DataFrameAnalysisConfigType; const resultState: Partial = { jobType, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index 41f3bab8113f0e..14427dd5c6ef28 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -11,7 +11,7 @@ import { GetDataFrameAnalyticsStatsResponseOk, } from '../../../../../services/ml_api_service/data_frame_analytics'; import { - ANALYSIS_CONFIG_TYPE, + getAnalysisType, REFRESH_ANALYTICS_LIST_STATE, refreshAnalyticsList$, } from '../../../../common'; @@ -25,6 +25,7 @@ import { isDataFrameAnalyticsStopped, } from '../../components/analytics_list/common'; import { AnalyticStatsBarStats } from '../../../../../components/stats_bar'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; export const isGetDataFrameAnalyticsStatsResponseOk = ( arg: any @@ -143,7 +144,7 @@ export const getAnalyticsFactory = ( checkpointing: {}, config, id: config.id, - job_type: Object.keys(config.analysis)[0] as ANALYSIS_CONFIG_TYPE, + job_type: getAnalysisType(config.analysis) as DataFrameAnalysisConfigType, mode: DATA_FRAME_MODE.BATCH, state: stats.state, stats, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 769b83c03110b1..7c30dc0cac6901 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -52,7 +52,10 @@ function startTrialDescription() { export const DatavisualizerSelector: FC = () => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { - services: { licenseManagement }, + services: { + licenseManagement, + http: { basePath }, + }, } = useMlKibana(); const navigateToPath = useNavigateToPath(); @@ -183,7 +186,10 @@ export const DatavisualizerSelector: FC = () => { } description={startTrialDescription()} footer={ - + = ({ to: 'now', }); const [showCreateJobLink, setShowCreateJobLink] = useState(false); - const [globalStateString, setGlobalStateString] = useState(''); + const [globalState, setGlobalState] = useState(); + + const [discoverLink, setDiscoverLink] = useState(''); const { services: { http: { basePath }, }, } = useMlKibana(); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const { + services: { + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = useMlKibana(); + + useEffect(() => { + let unmounted = false; + + const getDiscoverUrl = async (): Promise => { + const state: DiscoverUrlGeneratorState = { + indexPatternId, + }; + + if (globalState?.time) { + state.timeRange = globalState.time; + } + if (!unmounted) { + const discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR); + const discoverUrl = await discoverUrlGenerator.createUrl(state); + setDiscoverLink(discoverUrl); + } + }; + getDiscoverUrl(); + + return () => { + unmounted = true; + }; + }, [indexPatternId, getUrlGenerator]); + + const openInDataVisualizer = useCallback(async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: indexPatternId, + globalState, + }, + }); + await navigateToPath(path); + }, [indexPatternId, globalState]); + + const redirectToADCreateJobsSelectTypePage = useCallback(async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: indexPatternId, + globalState, + }, + }); + await navigateToPath(path); + }, [indexPatternId, globalState]); useEffect(() => { setShowCreateJobLink(checkPermission('canCreateJob') && mlNodesAvailable()); @@ -49,11 +113,13 @@ export const ResultsLinks: FC = ({ }, []); useEffect(() => { - const _g = - timeFieldName !== undefined - ? `&_g=(time:(from:'${duration.from}',mode:quick,to:'${duration.to}'))` - : ''; - setGlobalStateString(_g); + const _globalState: MlCommonGlobalState = { + time: { + from: duration.from, + to: duration.to, + }, + }; + setGlobalState(_globalState); }, [duration]); async function updateTimeValues(recheck = true) { @@ -89,7 +155,7 @@ export const ResultsLinks: FC = ({ /> } description="" - href={`${basePath.get()}/app/discover#/?&_a=(index:'${indexPatternId}')${globalStateString}`} + href={discoverLink} /> )} @@ -108,7 +174,7 @@ export const ResultsLinks: FC = ({ /> } description="" - href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${globalStateString}`} + onClick={redirectToADCreateJobsSelectTypePage} /> )} @@ -124,7 +190,7 @@ export const ResultsLinks: FC = ({ /> } description="" - href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${globalStateString}`} + onClick={openInDataVisualizer} /> )} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 1f2c97b128e3f8..ab738ca0f1545b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -9,11 +9,11 @@ import React, { FC, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; - +import { Link } from 'react-router-dom'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; -import { getBasePath } from '../../../../util/dependency_cache'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; interface Props { indexPattern: IndexPattern; @@ -21,7 +21,6 @@ interface Props { export const ActionsPanel: FC = ({ indexPattern }) => { const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); - const basePath = getBasePath(); const recognizerResults = { count: 0, @@ -29,12 +28,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { setRecognizerResultsCount(recognizerResults.count); }, }; - - function openAdvancedJobWizard() { - // TODO - pass the search string to the advanced job page as well as the index pattern - // (add in with new advanced job wizard?) - window.open(`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`, '_self'); - } + const createJobLink = `/${ML_PAGES.ANOMALY_DETECTION_CREATE_JOB}/advanced?index=${indexPattern.id}`; // Note we use display:none for the DataRecognizer section as it needs to be // passed the recognizerResults object, and then run the recognizer check which @@ -78,19 +72,19 @@ export const ActionsPanel: FC = ({ indexPattern }) => {

- + + + ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap index c6503a639997db..826f7b707cfdf6 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap @@ -3,17 +3,20 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = ` - -
+ + + + } data-test-subj="mlNoJobsFound" iconType="alert" diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js index 6f391f9746f232..029ca0475015f8 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js @@ -7,25 +7,40 @@ /* * React component for rendering EuiEmptyPrompt when no jobs were found. */ - +import { Link } from 'react-router-dom'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { useMlLink } from '../../../contexts/kibana/use_create_url'; -export const ExplorerNoJobsFound = () => ( - - - - } - actions={ - - - - } - data-test-subj="mlNoJobsFound" - /> -); +export const ExplorerNoJobsFound = () => { + const ADJobsManagementUrl = useMlLink({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + excludeBasePath: true, + }); + return ( + + + + } + actions={ + + + + + + } + data-test-subj="mlNoJobsFound" + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js index bcb11cad9674ce..c9645b787a8e02 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js @@ -8,6 +8,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ExplorerNoJobsFound } from './explorer_no_jobs_found'; +jest.mock('../../../contexts/kibana/use_create_url', () => ({ + useMlLink: jest.fn().mockReturnValue('/jobs'), +})); describe('ExplorerNoInfluencersFound', () => { test('snapshot', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 4fb783bfb60063..8f03b1903800a4 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiButtonEmpty, @@ -28,6 +28,10 @@ import { CHART_TYPE } from '../explorer_constants'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { MlTooltipComponent } from '../../components/chart_tooltip'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../common/constants/app'; +import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', { defaultMessage: @@ -51,7 +55,23 @@ function getChartId(series) { } // Wrapper for a single explorer chart -function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) { +function ExplorerChartContainer({ + series, + severity, + tooManyBuckets, + wrapLabel, + navigateToApp, + mlUrlGenerator, +}) { + const redirectToSingleMetricViewer = useCallback(async () => { + const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); + addItemToRecentlyAccessed('timeseriesexplorer', series.jobId, singleMetricViewerLink); + + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerLink, + }); + }, [mlUrlGenerator]); + const { detectorLabel, entityFields } = series; const chartType = getChartType(series); @@ -106,7 +126,7 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) iconSide="right" iconType="visLine" size="xs" - onClick={() => window.open(getExploreSeriesLink(series), '_blank')} + onClick={redirectToSingleMetricViewer} > @@ -150,12 +170,24 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) } // Flex layout wrapper for all explorer charts -export const ExplorerChartsContainer = ({ +export const ExplorerChartsContainerUI = ({ chartsPerRow, seriesToPlot, severity, tooManyBuckets, + kibana, }) => { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = kibana; + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + // doesn't allow a setting of `columns={1}` when chartsPerRow would be 1. // If that's the case we trick it doing that with the following settings: const chartsWidth = chartsPerRow === 1 ? 'calc(100% - 20px)' : 'auto'; @@ -177,9 +209,13 @@ export const ExplorerChartsContainer = ({ severity={severity} tooManyBuckets={tooManyBuckets} wrapLabel={wrapLabel} + navigateToApp={navigateToApp} + mlUrlGenerator={mlUrlGenerator} /> ))} ); }; + +export const ExplorerChartsContainer = withKibana(ExplorerChartsContainerUI); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js index 8257ac2b3a7036..2da212c8f2f293 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js @@ -40,6 +40,12 @@ jest.mock('../../services/job_service', () => ({ }, })); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: (comp) => { + return comp; + }, +})); + describe('ExplorerChartsContainer', () => { const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; const originalGetBBox = SVGElement.prototype.getBBox; @@ -47,10 +53,22 @@ describe('ExplorerChartsContainer', () => { beforeEach(() => (SVGElement.prototype.getBBox = () => mockedGetBBox)); afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); + const kibanaContextMock = { + services: { + application: { navigateToApp: jest.fn() }, + share: { + urlGenerators: { getUrlGenerator: jest.fn() }, + }, + }, + }; test('Minimal Initialization', () => { const wrapper = shallow( - + ); @@ -71,10 +89,11 @@ describe('ExplorerChartsContainer', () => { ], chartsPerRow: 1, tooManyBuckets: false, + severity: 10, }; const wrapper = mount( - + ); @@ -98,10 +117,11 @@ describe('ExplorerChartsContainer', () => { ], chartsPerRow: 1, tooManyBuckets: false, + severity: 10, }; const wrapper = mount( - + ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index d0d0442dd4aeed..85a342838a5062 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -5,13 +5,20 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useCreateADLinks } from '../../../../components/custom_hooks/use_create_ad_links'; +import { Link } from 'react-router-dom'; +import { useMlKibana } from '../../../../contexts/kibana'; -export function ResultLinks({ jobs }) { +export function ResultLinks({ jobs, isManagementTable }) { + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); const openJobsInSingleMetricViewerText = i18n.translate( 'xpack.ml.jobsList.resultActions.openJobsInSingleMetricViewerText', { @@ -37,29 +44,59 @@ export function ResultLinks({ jobs }) { const singleMetricEnabled = jobs.length === 1 && jobs[0].isSingleMetricViewerJob; const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true; const { createLinkWithUserDefaults } = useCreateADLinks(); + const timeSeriesExplorerLink = useMemo( + () => createLinkWithUserDefaults('timeseriesexplorer', jobs), + [jobs] + ); + const anomalyExplorerLink = useMemo(() => createLinkWithUserDefaults('explorer', jobs), [jobs]); + return ( {singleMetricVisible && ( + {isManagementTable ? ( + + ) : ( + + + + )} + + )} + + {isManagementTable ? ( - - )} - - + ) : ( + + + + )}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 8f89c4a049189e..73b212b97b4ccd 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -5,10 +5,10 @@ */ import React from 'react'; -import { EuiLink } from '@elastic/eui'; import { detectorToString } from '../../../../util/string_utils'; import { formatValues, filterObjects } from './format_values'; import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; export function extractJobDetails(job) { if (Object.keys(job).length === 0) { @@ -61,7 +61,7 @@ export function extractJobDetails(job) { if (job.calendars) { calendars.items = job.calendars.map((c) => [ '', - {c}, + {c}, ]); // remove the calendars list from the general section // so not to show it twice. diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index b6157c8694a185..b32070fff73aad 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -5,8 +5,6 @@ */ import PropTypes from 'prop-types'; -import rison from 'rison-node'; - import React, { Component } from 'react'; import { @@ -30,13 +28,19 @@ import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, } from '../../../../../../../common/util/job_utils'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { + ML_APP_URL_GENERATOR, + ML_PAGES, +} from '../../../../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../../../../common/constants/app'; const MAX_FORECASTS = 500; /** * Table component for rendering the lists of forecasts run on an ML job. */ -export class ForecastsTable extends Component { +export class ForecastsTableUI extends Component { constructor(props) { super(props); this.state = { @@ -78,7 +82,17 @@ export class ForecastsTable extends Component { } } - openSingleMetricView(forecast) { + async openSingleMetricView(forecast) { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = this.props.kibana; + // Creates the link to the Single Metric Viewer. // Set the total time range from the start of the job data to the end of the forecast, const dataCounts = this.props.job.data_counts; @@ -93,31 +107,7 @@ export class ForecastsTable extends Component { ? new Date(forecast.forecast_end_timestamp).toISOString() : new Date(resultLatest).toISOString(); - const _g = rison.encode({ - ml: { - jobIds: [this.props.job.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from, - to, - mode: 'absolute', - }, - }); - - const appState = { - query: { - query_string: { - analyze_wildcard: true, - query: '*', - }, - }, - }; - + let mlTimeSeriesExplorer = {}; if (forecast !== undefined) { // Set the zoom to show duration before the forecast equal to the length of the forecast. const forecastDurationMs = @@ -126,8 +116,7 @@ export class ForecastsTable extends Component { forecast.forecast_start_timestamp - forecastDurationMs, jobEarliest ); - - appState.mlTimeSeriesExplorer = { + mlTimeSeriesExplorer = { forecastId: forecast.forecast_id, zoom: { from: new Date(zoomFrom).toISOString(), @@ -136,11 +125,39 @@ export class ForecastsTable extends Component { }; } - const _a = rison.encode(appState); - - const url = `?_g=${_g}&_a=${_a}`; - addItemToRecentlyAccessed('timeseriesexplorer', this.props.job.job_id, url); - window.open(`#/timeseriesexplorer${url}`, '_self'); + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + const singleMetricViewerForecastLink = await mlUrlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + timeRange: { + from, + to, + mode: 'absolute', + }, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + jobIds: [this.props.job.job_id], + query: { + query_string: { + analyze_wildcard: true, + query: '*', + }, + }, + ...mlTimeSeriesExplorer, + }, + excludeBasePath: true, + }); + addItemToRecentlyAccessed( + 'timeseriesexplorer', + this.props.job.job_id, + singleMetricViewerForecastLink + ); + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerForecastLink, + }); } render() { @@ -322,6 +339,8 @@ export class ForecastsTable extends Component { ); } } -ForecastsTable.propTypes = { +ForecastsTableUI.propTypes = { job: PropTypes.object.isRequired, }; + +export const ForecastsTable = withKibana(ForecastsTableUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js index a5469357ba1a1b..8b5d6009cc61ed 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { JobGroup } from '../job_group'; -import { getGroupIdsUrl, TAB_IDS } from '../../../../util/get_selected_ids_url'; +import { AnomalyDetectionJobIdLink } from './job_id_link'; export function JobDescription({ job, isManagementTable }) { return ( @@ -17,11 +17,7 @@ export function JobDescription({ job, isManagementTable }) { {job.description}   {job.groups.map((group) => { if (isManagementTable === true) { - return ( - - - - ); + return ; } return ; })} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx new file mode 100644 index 00000000000000..0e84619899d71b --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx @@ -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. + */ + +import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { useMlKibana, useMlUrlGenerator } from '../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; +import { AnomalyDetectionQueryState } from '../../../../../../common/types/ml_url_generator'; +// @ts-ignore +import { JobGroup } from '../job_group'; + +interface JobIdLink { + id: string; +} + +interface GroupIdLink { + groupId: string; + children: string; +} + +type AnomalyDetectionJobIdLinkProps = JobIdLink | GroupIdLink; + +function isGroupIdLink(props: JobIdLink | GroupIdLink): props is GroupIdLink { + return (props as GroupIdLink).groupId !== undefined; +} +export const AnomalyDetectionJobIdLink = (props: AnomalyDetectionJobIdLinkProps) => { + const mlUrlGenerator = useMlUrlGenerator(); + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + + const redirectToJobsManagementPage = async () => { + const pageState: AnomalyDetectionQueryState = {}; + if (isGroupIdLink(props)) { + pageState.groupIds = [props.groupId]; + } else { + pageState.jobId = props.id; + } + const url = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState, + }); + await navigateToUrl(url); + }; + if (isGroupIdLink(props)) { + return ( + redirectToJobsManagementPage()}> + + + ); + } else { + return ( + redirectToJobsManagementPage()}> + {props.id} + + ); + } +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index fa4ea09b89ff91..8bc0057b27d6db 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -14,12 +14,12 @@ import { toLocaleString } from '../../../../util/string_utils'; import { ResultLinks, actionsMenuContent } from '../job_actions'; import { JobDescription } from './job_description'; import { JobIcon } from '../../../../components/job_message_icon'; -import { getJobIdUrl, TAB_IDS } from '../../../../util/get_selected_ids_url'; import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; -import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; +import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AnomalyDetectionJobIdLink } from './job_id_link'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; @@ -71,7 +71,7 @@ export class JobsList extends Component { return id; } - return {id}; + return ; } getPageOfJobs(index, size, sortField, sortDirection) { @@ -241,7 +241,7 @@ export class JobsList extends Component { name: i18n.translate('xpack.ml.jobsList.actionsLabel', { defaultMessage: 'Actions', }), - render: (item) => , + render: (item) => , }, ]; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index fdffa8b38ae04c..81effe8d3ebebd 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -11,13 +11,13 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - -function newJob() { - window.location.href = `#/jobs/new_job`; -} +import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; export function NewJobButton() { const buttonEnabled = checkPermission('canCreateJob') && mlNodesAvailable(); + const newJob = useCreateAndNavigateToMlLink(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX); + return ( { const { @@ -73,7 +74,7 @@ export const CalendarsSelection: FC = () => { }; const manageCalendarsHref = getUrlForApp(PLUGIN_ID, { - path: '/settings/calendars_list', + path: ML_PAGES.CALENDARS_MANAGE, }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 669b8837e74b50..021039c06e3209 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -39,7 +39,10 @@ import { JobSectionTitle, DatafeedSectionTitle } from './components/common'; export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { - services: { notifications }, + services: { + notifications, + http: { basePath }, + }, } = useMlKibana(); const navigateToPath = useNavigateToPath(); @@ -108,7 +111,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => jobCreator.end, isSingleMetricJobCreator(jobCreator) === true ? 'timeseriesexplorer' : 'explorer' ); - window.open(url, '_blank'); + navigateToPath(`${basePath.get()}/app/ml/${url}`); } function clickResetJob() { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 69df2773f9f8d7..cedaaa3b5dfaac 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -4,19 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ApplicationStart } from 'kibana/public'; import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; import { mlJobService } from '../../../../services/job_service'; import { loadIndexPatterns, getIndexPatternIdFromName } from '../../../../util/index_utils'; import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job'; -export async function preConfiguredJobRedirect(indexPatterns: IndexPatternsContract) { +export async function preConfiguredJobRedirect( + indexPatterns: IndexPatternsContract, + basePath: string, + navigateToUrl: ApplicationStart['navigateToUrl'] +) { const { job } = mlJobService.tempJobCloningObjects; if (job) { try { await loadIndexPatterns(indexPatterns); const redirectUrl = getWizardUrlFromCloningJob(job); - window.location.href = `#/${redirectUrl}`; + await navigateToUrl(`${basePath}/app/ml/${redirectUrl}`); return Promise.reject(); } catch (error) { return Promise.resolve(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index be0135ec3f1e09..1a91f6d51ed4d0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useNavigateToPath } from '../../../../contexts/kibana'; + import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -26,10 +27,15 @@ import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; import { timeBasedIndexCheck } from '../../../../util/index_utils'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { CategorizationIcon } from './categorization_job_icon'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url'; export const Page: FC = () => { const mlContext = useMlContext(); const navigateToPath = useNavigateToPath(); + const onSelectDifferentIndex = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX + ); const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); @@ -193,7 +199,7 @@ export const Page: FC = () => { defaultMessage="Anomaly detection can only be run over indices which are time based." />
- + = ({ moduleId, existingGroupIds }) => { const { services: { notifications }, } = useMlKibana(); + const urlGenerator = useMlUrlGenerator(); + // #region State const [jobPrefix, setJobPrefix] = useState(''); const [jobs, setJobs] = useState([]); @@ -185,14 +189,20 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { }) ); setKibanaObjects(merge(kibanaObjects, kibanaResponse)); - setResultsUrl( - mlJobService.createResultsUrl( - jobsResponse.filter(({ success }) => success).map(({ id }) => id), - resultTimeRange.start, - resultTimeRange.end, - 'explorer' - ) - ); + + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: jobsResponse.filter(({ success }) => success).map(({ id }) => id), + timeRange: { + from: moment(resultTimeRange.start).format(TIME_FORMAT), + to: moment(resultTimeRange.end).format(TIME_FORMAT), + mode: 'absolute', + }, + }, + }); + + setResultsUrl(url); const failedJobsCount = jobsResponse.reduce((count, { success }) => { return success ? count : count + 1; }, 0); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts index e3b0fd4cefe0ca..97a03fa21035fe 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts @@ -6,33 +6,40 @@ import { i18n } from '@kbn/i18n'; import { getToastNotifications, getSavedObjectsClient } from '../../../util/dependency_cache'; -import { mlJobService } from '../../../services/job_service'; import { ml } from '../../../services/ml_api_service'; import { KibanaObjects } from './page'; +import { NavigateToPath } from '../../../contexts/kibana'; +import { CreateLinkWithUserDefaults } from '../../../components/custom_hooks/use_create_ad_links'; /** * Checks whether the jobs in a data recognizer module have been created. * Redirects to the Anomaly Explorer to view the jobs if they have been created, * or the recognizer job wizard for the module if not. */ -export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): Promise { +export function checkViewOrCreateJobs( + moduleId: string, + indexPatternId: string, + createLinkWithUserDefaults: CreateLinkWithUserDefaults, + navigateToPath: NavigateToPath +): Promise { return new Promise((resolve, reject) => { // Load the module, and check if the job(s) in the module have been created. // If so, load the jobs in the Anomaly Explorer. // Otherwise open the data recognizer wizard for the module. // Always want to call reject() so as not to load original page. ml.dataRecognizerModuleJobsExist({ moduleId }) - .then((resp: any) => { + .then(async (resp: any) => { if (resp.jobsExist === true) { - const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); - window.location.href = resultsPageUrl; + // also honor user's time filter setting in Advanced Settings + const url = createLinkWithUserDefaults('explorer', resp.jobs); + await navigateToPath(url); reject(); } else { - window.location.href = `#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; + await navigateToPath(`/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`); reject(); } }) - .catch((err: Error) => { + .catch(async (err: Error) => { // eslint-disable-next-line no-console console.error(`Error checking whether jobs in module ${moduleId} exists`, err); const toastNotifications = getToastNotifications(); @@ -46,8 +53,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): 'An error occurred trying to check whether the jobs in the module have been created.', }), }); - - window.location.href = '#/jobs'; + await navigateToPath(`/jobs`); reject(); }); }); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 0af6030df28b19..9c9096dfdfc21b 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -31,6 +31,7 @@ import { getDocLinks } from '../../../../util/dependency_cache'; import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; +import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; interface Tab { 'data-test-subj': string; @@ -75,8 +76,9 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] { export const JobsListPage: FC<{ coreStart: CoreStart; + share: SharePluginStart; history: ManagementAppMountParams['history']; -}> = ({ coreStart, history }) => { +}> = ({ coreStart, share, history }) => { const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); @@ -136,7 +138,7 @@ export const JobsListPage: FC<{ return ( - + { - ReactDOM.render(React.createElement(JobsListPage, { coreStart, history }), element); + ReactDOM.render(React.createElement(JobsListPage, { coreStart, history, share }), element); return () => { unmountComponentAtNode(element); clearCache(); @@ -30,7 +32,7 @@ export async function mountApp( core: CoreSetup, params: ManagementAppMountParams ) { - const [coreStart] = await core.getStartServices(); + const [coreStart, pluginsStart] = await core.getStartServices(); setDependencyCache({ docLinks: coreStart.docLinks!, @@ -41,5 +43,5 @@ export async function mountApp( params.setBreadcrumbs(getJobsListBreadcrumbs()); - return renderApp(params.element, params.history, coreStart); + return renderApp(params.element, params.history, coreStart, pluginsStart.share); } diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts index 1792999eee4c2e..d0cfd16d7562f2 100644 --- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts +++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts @@ -9,7 +9,7 @@ import { ml } from '../services/ml_api_service'; let mlNodeCount: number = 0; let userHasPermissionToViewMlNodeCount: boolean = false; -export async function checkMlNodesAvailable() { +export async function checkMlNodesAvailable(redirectToJobsManagementPage: () => Promise) { try { const nodes = await getMlNodeCount(); if (nodes.count !== undefined && nodes.count > 0) { @@ -20,7 +20,7 @@ export async function checkMlNodesAvailable() { } catch (error) { // eslint-disable-next-line no-console console.error(error); - window.location.href = '#/jobs'; + await redirectToJobsManagementPage(); Promise.reject(); } } diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx index 395a570083c0de..4f0cbc0adddf2c 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx @@ -4,30 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../contexts/kibana'; +import { Link } from 'react-router-dom'; +import { useMlLink } from '../../../contexts/kibana'; import { getAnalysisType } from '../../../data_frame_analytics/common/analytics'; -import { - getResultsUrl, - DataFrameAnalyticsListRow, -} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { getViewLinkStatus } from '../../../data_frame_analytics/pages/analytics_management/components/action_view/get_view_link_status'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; interface Props { item: DataFrameAnalyticsListRow; } export const ViewLink: FC = ({ item }) => { - const navigateToPath = useNavigateToPath(); - - const clickHandler = useCallback(() => { - const analysisType = getAnalysisType(item.config.analysis); - navigateToPath(getResultsUrl(item.id, analysisType)); - }, []); - const { disabled, tooltipContent } = getViewLinkStatus(item); const viewJobResultsButtonText = i18n.translate( @@ -38,23 +31,34 @@ export const ViewLink: FC = ({ item }) => { ); const tooltipText = disabled === false ? viewJobResultsButtonText : tooltipContent; + const analysisType = useMemo(() => getAnalysisType(item.config.analysis), [item]); + + const viewAnalyticsResultsLink = useMlLink({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId: item.id, + analysisType: analysisType as DataFrameAnalysisConfigType, + }, + excludeBasePath: true, + }); return ( - - {i18n.translate('xpack.ml.overview.analytics.viewActionName', { - defaultMessage: 'View', - })} - + + + {i18n.translate('xpack.ml.overview.analytics.viewActionName', { + defaultMessage: 'View', + })} + + ); }; diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index be8038cc5049d2..4d810c47415a79 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -23,6 +23,8 @@ import { AnalyticsTable } from './table'; import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service'; import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { AnalyticStatsBarStats, StatsBar } from '../../../components/stats_bar'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; interface Props { jobCreationDisabled: boolean; @@ -35,6 +37,16 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { const [errorMessage, setErrorMessage] = useState(undefined); const [isInitialized, setIsInitialized] = useState(false); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToDataFrameAnalyticsManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + }); + await navigateToPath(path, true); + }; + const getAnalytics = getAnalyticsFactory( setAnalytics, setAnalyticsStats, @@ -75,7 +87,6 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { {isInitialized === false && ( )} -      {errorMessage === undefined && isInitialized === true && analytics.length === 0 && ( = ({ jobCreationDisabled }) => { } actions={ = ({ jobCreationDisabled }) => { )} {isInitialized === true && analytics.length > 0 && ( <> + @@ -136,7 +148,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { defaultMessage: 'Refresh', })} - + {i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', { defaultMessage: 'Manage jobs', })} diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx index a71141d0356d07..dfba7c96512667 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx @@ -7,6 +7,7 @@ import React, { FC } from 'react'; import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs'; import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links'; @@ -26,19 +27,20 @@ export const ExplorerLink: FC = ({ jobsList }) => { return ( - - {i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', { - defaultMessage: 'View', - })} - + + + {i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', { + defaultMessage: 'View', + })} + + ); }; diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 0bfd2c2e492323..1cb6bab7fd7683 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -16,12 +16,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { useMlKibana } from '../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana'; import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils'; import { Dictionary } from '../../../../../common/types/common'; import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; export type GroupsDictionary = Dictionary; @@ -39,8 +40,6 @@ type MaxScoresByGroup = Dictionary<{ index?: number; }>; -const createJobLink = '#/jobs/new_job/step/index_or_search'; - function getDefaultAnomalyScores(groups: Group[]): MaxScoresByGroup { const anomalyScores: MaxScoresByGroup = {}; groups.forEach((group) => { @@ -58,6 +57,23 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { const { services: { notifications }, } = useMlKibana(); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToJobsManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + }); + await navigateToPath(path, true); + }; + + const redirectToCreateJobSelectIndexPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX, + }); + await navigateToPath(path, true); + }; + const [isLoading, setIsLoading] = useState(false); const [groups, setGroups] = useState({}); const [groupsCount, setGroupsCount] = useState(0); @@ -157,7 +173,7 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { return ( {typeof errorMessage !== 'undefined' && errorDisplay} - {isLoading && }    + {isLoading && } {isLoading === false && typeof errorMessage === 'undefined' && groupsCount === 0 && ( = ({ jobCreationDisabled }) => { actions={ = ({ jobCreationDisabled }) => { defaultMessage: 'Refresh', })} - + {i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', { defaultMessage: 'Manage jobs', })} diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx index 945116b0534bb8..8515431d49b17c 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx @@ -88,7 +88,7 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData {i18n.translate('xpack.ml.overview.anomalyDetection.tableMaxScore', { defaultMessage: 'Max anomaly score', - })}{' '} + })} @@ -203,6 +203,7 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData return ( + diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index d0a4f999af7582..398ec5b4759d24 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -54,6 +54,20 @@ export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ href: '/jobs/new_job', }); +export const CALENDAR_MANAGEMENT_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + href: '/settings/calendars_list', +}); + +export const FILTER_LISTS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + href: '/settings/filter_lists', +}); + const breadcrumbs = { ML_BREADCRUMB, SETTINGS_BREADCRUMB, @@ -61,6 +75,8 @@ const breadcrumbs = { DATA_FRAME_ANALYTICS_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, CREATE_JOB_BREADCRUMB, + CALENDAR_MANAGEMENT_BREADCRUMB, + FILTER_LISTS_BREADCRUMB, }; type Breadcrumb = keyof typeof breadcrumbs; @@ -76,10 +92,12 @@ export const breadcrumbOnClickFactory = ( export const getBreadcrumbWithUrlForApp = ( breadcrumbName: Breadcrumb, - navigateToPath: NavigateToPath + navigateToPath: NavigateToPath, + basePath: string ): EuiBreadcrumb => { return { - ...breadcrumbs[breadcrumbName], + text: breadcrumbs[breadcrumbName].text, + href: `${basePath}/app/ml${breadcrumbs[breadcrumbName].href}`, onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath), }; }; diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts index 958221df8a6361..9cebb67166a666 100644 --- a/x-pack/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts @@ -21,13 +21,17 @@ export interface ResolverResults { interface BasicResolverDependencies { indexPatterns: IndexPatternsContract; + redirectToMlAccessDeniedPage: () => Promise; } -export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Resolvers => ({ +export const basicResolvers = ({ + indexPatterns, + redirectToMlAccessDeniedPage, +}: BasicResolverDependencies): Resolvers => ({ checkFullLicense, getMlNodeCount, loadMlServerInfo, loadIndexPatterns: () => loadIndexPatterns(indexPatterns), - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), loadSavedSearches, }); diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 22a17c4ea089a9..7cb3a2f07c2ee2 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -12,7 +12,7 @@ import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/publi import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; -import { useNavigateToPath } from '../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { MlContext, MlContextValue } from '../contexts/ml'; import { UrlStateProvider } from '../util/url_state'; @@ -39,6 +39,7 @@ interface PageDependencies { history: AppMountParameters['history']; indexPatterns: IndexPatternsContract; setBreadcrumbs: ChromeStart['setBreadcrumbs']; + redirectToMlAccessDeniedPage: () => Promise; } export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { @@ -75,10 +76,16 @@ const MlRoutes: FC<{ pageDeps: PageDependencies; }> = ({ pageDeps }) => { const navigateToPath = useNavigateToPath(); + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); + return ( <> {Object.entries(routes).map(([name, routeFactory]) => { - const route = routeFactory(navigateToPath); + const route = routeFactory(navigateToPath, basePath.get()); return ( ({ +export const analyticsJobsCreationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics/new_job', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { defaultMessage: 'Data Frame Analytics', diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 47cc002ab4d830..f9f2ebe48f4aa2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -10,21 +10,25 @@ import { decode } from 'rison-node'; import { i18n } from '@kbn/i18n'; -import { NavigateToPath } from '../../../contexts/kibana'; +import { NavigateToPath, useMlKibana, useMlUrlGenerator } from '../../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; -import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; -export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const analyticsJobExplorationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics/exploration', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { defaultMessage: 'Exploration', @@ -38,16 +42,31 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); const { _g }: Record = parse(location.search, { sort: false }); + const urlGenerator = useMlUrlGenerator(); + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + + const redirectToAnalyticsManagementPage = async () => { + const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE }); + await navigateToUrl(url); + }; + let globalState: any = null; try { globalState = decode(_g); } catch (error) { // eslint-disable-next-line no-console - console.error('Could not parse global state'); - window.location.href = '#data_frame_analytics'; + console.error( + 'Could not parse global state. Redirecting to Data Frame Analytics Management Page.' + ); + redirectToAnalyticsManagementPage(); + return <>; } const jobId: string = globalState.ml.jobId; - const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType; + const analysisType: DataFrameAnalysisConfigType = globalState.ml.analysisType; return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index b6ef9ea81b4bab..80706a82121d51 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const analyticsJobsListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { defaultMessage: 'Job Management', diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx index 7bf7784d1b5598..b1fd6e93a744c8 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx @@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const modelsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const modelsListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics/models', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.modelsListLabel', { defaultMessage: 'Model Management', diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index efe5c3cba04a55..f40b754a23ccb1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -21,19 +21,25 @@ import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const selectorRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/datavisualizer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, - checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, + checkFindFileStructurePrivilege: () => + checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 485af52c45a551..837616a8a76d2a 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -24,12 +24,15 @@ import { loadIndexPatterns } from '../../../util/index_utils'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const fileBasedRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/filedatavisualizer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { defaultMessage: 'File', @@ -40,10 +43,13 @@ export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute = }); const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, + checkFindFileStructurePrivilege: () => + checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index 358b8773e3460e..e3d0e5050fca5f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -20,13 +20,18 @@ import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_ca import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; -export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const indexBasedRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/datavisualizer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { defaultMessage: 'Index', @@ -37,12 +42,17 @@ export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute }); const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 30b9bc2af219f7..00d64a2f1bd1df 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -35,12 +35,15 @@ import { useTimefilter } from '../../contexts/kibana'; import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; import { JOB_ID } from '../../../../common/constants/anomalies'; -export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const explorerRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/explorer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { defaultMessage: 'Anomaly Explorer', diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index 38a7900916ba83..2863e59508e351 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -20,12 +20,12 @@ import { JobsPage } from '../../jobs/jobs_list'; import { useTimefilter } from '../../contexts/kibana'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ path: '/jobs', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { defaultMessage: 'Job Management', diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index d8605c4cc91152..0ef3b384dcf5de 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { NavigateToPath } from '../../../contexts/kibana'; +import { NavigateToPath, useMlKibana } from '../../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -19,6 +19,8 @@ import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; enum MODE { NEW_JOB, @@ -30,9 +32,9 @@ interface IndexOrSearchPageProps extends PageProps { mode: MODE; } -const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), +const getBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { defaultMessage: 'Create job', @@ -41,7 +43,10 @@ const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const indexOrSearchRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/step/index_or_search', render: (props, deps) => ( ), - breadcrumbs: getBreadcrumbs(navigateToPath), + breadcrumbs: getBreadcrumbs(navigateToPath, basePath), }); -export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const dataVizIndexOrSearchRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/datavisualizer_index_select', render: (props, deps) => ( ), - breadcrumbs: getBreadcrumbs(navigateToPath), + breadcrumbs: getBreadcrumbs(navigateToPath, basePath), }); const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { + const { + services: { + http: { basePath }, + application: { navigateToUrl }, + }, + } = useMlKibana(); + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); + const newJobResolvers = { ...basicResolvers(deps), - preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns), + preConfiguredJobRedirect: () => + preConfiguredJobRedirect(deps.indexPatterns, basePath.get(), navigateToUrl), }; const dataVizResolvers = { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }; const { context } = useResolver( diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index b8ab29d40fa1f7..543e01fbd326d5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -17,12 +17,12 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/pages/job_type'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const jobTypeRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ path: '/jobs/new_job/step/job_type', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { defaultMessage: 'Create job', diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 6be58828ee1a55..654d7184cfcf27 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { NavigateToPath } from '../../../contexts/kibana'; +import { NavigateToPath, useNavigateToPath } from '../../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -18,14 +18,18 @@ import { Page } from '../../../jobs/new_job/recognize'; import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; import { mlJobService } from '../../../services/job_service'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links'; -export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const recognizeRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/recognize', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { defaultMessage: 'Recognized index', @@ -60,10 +64,14 @@ const CheckViewOrCreateWrapper: FC = ({ location, deps }) => { const { id: moduleId, index: indexPatternId }: Record = parse(location.search, { sort: false, }); + const { createLinkWithUserDefaults } = useCreateADLinks(); + + const navigateToPath = useNavigateToPath(); // the single resolver checkViewOrCreateJobs redirects only. so will always reject useResolver(undefined, undefined, deps.config, { - checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), + checkViewOrCreateJobs: () => + checkViewOrCreateJobs(moduleId, indexPatternId, createLinkWithUserDefaults, navigateToPath), }); return null; }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 35085fd5575773..8a82a9a8dbc49f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -19,19 +19,21 @@ import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; interface WizardPageProps extends PageProps { jobType: JOB_TYPE; } -const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), +const getBaseBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath), ]; -const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { defaultMessage: 'Single metric', @@ -40,8 +42,8 @@ const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { defaultMessage: 'Multi-metric', @@ -50,8 +52,8 @@ const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { defaultMessage: 'Population', @@ -60,8 +62,8 @@ const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { defaultMessage: 'Advanced configuration', @@ -70,8 +72,8 @@ const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', { defaultMessage: 'Categorization', @@ -80,41 +82,60 @@ const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const singleMetricRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/single_metric', render: (props, deps) => , - breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath), + breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath, basePath), }); -export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const multiMetricRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/multi_metric', render: (props, deps) => , - breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath), + breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath, basePath), }); -export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const populationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/population', render: (props, deps) => , - breadcrumbs: getPopulationBreadcrumbs(navigateToPath), + breadcrumbs: getPopulationBreadcrumbs(navigateToPath, basePath), }); -export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const advancedRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/advanced', render: (props, deps) => , - breadcrumbs: getAdvancedBreadcrumbs(navigateToPath), + breadcrumbs: getAdvancedBreadcrumbs(navigateToPath, basePath), }); -export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const categorizationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/categorization', render: (props, deps) => , - breadcrumbs: getCategorizationBreadcrumbs(navigateToPath), + breadcrumbs: getCategorizationBreadcrumbs(navigateToPath, basePath), }); const PageWrapper: FC = ({ location, jobType, deps }) => { + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), - privileges: checkCreateJobsCapabilitiesResolver, + privileges: () => checkCreateJobsCapabilitiesResolver(redirectToJobsManagementPage), jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index 174e9804b96893..0e07b0edfbe560 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -22,11 +22,14 @@ import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const overviewRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/overview', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.overview.overviewLabel', { defaultMessage: 'Overview', @@ -37,9 +40,11 @@ export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, loadMlServerInfo, }); @@ -52,7 +57,7 @@ const PageWrapper: FC = ({ deps }) => { ); }; -export const appRootRouteFactory = (): MlRoute => ({ +export const appRootRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ path: '/', render: () => , breadcrumbs: [], diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index f2ae57f1ec961b..24609712396184 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -10,7 +10,6 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { NavigateToPath } from '../../../contexts/kibana'; @@ -25,27 +24,27 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; -import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const calendarListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/calendars_list', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath), - }, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index a5c30e1eaaacc3..4e0a8340590a4d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -26,6 +26,8 @@ import { import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; enum MODE { NEW, @@ -36,12 +38,16 @@ interface NewCalendarPageProps extends PageProps { mode: MODE; } -export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const newCalendarRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/calendars_list/new_calendar', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { defaultMessage: 'Create', @@ -51,12 +57,16 @@ export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute ], }); -export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const editCalendarRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/calendars_list/edit_calendar/:calendarId', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { defaultMessage: 'Edit', @@ -72,11 +82,15 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index d734e18d72babc..4e39cfce82e36f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -10,7 +10,6 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { NavigateToPath } from '../../../contexts/kibana'; @@ -26,27 +25,27 @@ import { import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; -import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const filterListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/filter_lists', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath), - }, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index c6f17bc7f6f683..5fe56b024e413e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -27,6 +27,8 @@ import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; enum MODE { NEW, @@ -37,12 +39,17 @@ interface NewFilterPageProps extends PageProps { mode: MODE; } -export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const newFilterListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/filter_lists/new_filter_list', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath), + { text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { defaultMessage: 'Create', @@ -52,12 +59,16 @@ export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRou ], }); -export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const editFilterListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/filter_lists/edit_filter_list/:filterId', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { defaultMessage: 'Edit', @@ -73,11 +84,15 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index 3f4b2698514691..3159c2ae881663 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -26,19 +26,24 @@ import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { AnomalyDetectionSettingsContext, Settings } from '../../../settings'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const settingsRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index 11ec074bac1db4..b60a2655604551 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -19,6 +19,11 @@ jest.mock('../../contexts/kibana/kibana_context', () => { useMlKibana: () => { return { services: { + chrome: { docTitle: { change: jest.fn() } }, + application: { getUrlForApp: jest.fn(), navigateToUrl: jest.fn() }, + share: { + urlGenerators: { getUrlGenerator: jest.fn() }, + }, uiSettings: { get: jest.fn() }, data: { query: { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 817c9754159971..03588872d6be03 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -39,12 +39,15 @@ import { basicResolvers } from '../resolvers'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; -export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const timeSeriesExplorerRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/timeseriesexplorer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { defaultMessage: 'Single Metric Viewer', diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.ts index 4967e3a684a6b8..e4cd90145bee40 100644 --- a/x-pack/plugins/ml/public/application/routing/use_resolver.ts +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.ts @@ -16,6 +16,8 @@ import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; import { ResolverResults, Resolvers } from './resolvers'; import { MlContextValue } from '../contexts/ml'; import { useNotifications } from '../contexts/kibana'; +import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; export const useResolver = ( indexPatternId: string | undefined, @@ -34,6 +36,9 @@ export const useResolver = ( const [context, setContext] = useState(null); const [results, setResults] = useState(tempResults); + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); useEffect(() => { (async () => { @@ -73,7 +78,7 @@ export const useResolver = ( defaultMessage: 'An error has occurred', }), }); - window.location.href = '#/'; + await redirectToJobsManagementPage(); } } else { setContext({}); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index dfa1b5f4e68cd0..ea97492ae0f5ad 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -797,7 +797,6 @@ function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') { let path = ''; if (resultsPage !== undefined) { - path += '#/'; path += resultsPage; } diff --git a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx index 16d7e1605263c6..57caa56b2f10e3 100644 --- a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx +++ b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx @@ -25,6 +25,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyDetectionSettingsContext } from './anomaly_detection_settings_context'; import { useNotifications } from '../contexts/kibana'; import { ml } from '../services/ml_api_service'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; export const AnomalyDetectionSettings: FC = () => { const [calendarsCount, setCalendarsCount] = useState(0); @@ -35,6 +37,10 @@ export const AnomalyDetectionSettings: FC = () => { ); const { toasts } = useNotifications(); + const redirectToCalendarList = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_MANAGE); + const redirectToNewCalendarPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_NEW); + const redirectToFilterLists = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_MANAGE); + const redirectToNewFilterListPage = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_NEW); useEffect(() => { loadSummaryStats(); @@ -126,7 +132,7 @@ export const AnomalyDetectionSettings: FC = () => { flush="left" size="l" color="primary" - href="#/settings/calendars_list" + onClick={redirectToCalendarList} isDisabled={canGetCalendars === false} > { flush="left" size="l" color="primary" - href="#/settings/calendars_list/new_calendar" + onClick={redirectToNewCalendarPage} isDisabled={canCreateCalendar === false} > {

@@ -194,7 +200,7 @@ export const AnomalyDetectionSettings: FC = () => { flush="left" size="l" color="primary" - href="#/settings/filter_lists" + onClick={redirectToFilterLists} isDisabled={canGetFilters === false} > { data-test-subj="mlFilterListsCreateButton" size="l" color="primary" - href="#/settings/filter_lists/new_filter_list" + onClick={redirectToNewFilterListPage} isDisabled={canCreateFilter === false} > + + + } + labelType="label" + > + + + + } + labelType="label" + > + + @@ -137,7 +200,6 @@ exports[`CalendarForm Renders calendar form 1`] = ` grow={false} > @@ -215,7 +218,7 @@ export const CalendarForm = ({ - + ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); const testProps = { calendarId: '', canCreateCalendar: true, @@ -31,6 +34,7 @@ const testProps = { selectedGroupOptions: [], selectedJobOptions: [], showNewEventModal: jest.fn(), + isGlobalCalendar: false, }; describe('CalendarForm', () => { diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 1fe16e4588bd76..a5eb212ba127ee 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -20,6 +20,7 @@ import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { GLOBAL_CALENDAR } from '../../../../../common/constants/calendars'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; class NewCalendarUI extends Component { static propTypes = { @@ -55,6 +56,16 @@ class NewCalendarUI extends Component { this.formSetup(); } + returnToCalendarsManagementPage = async () => { + const { + services: { + http: { basePath }, + application: { navigateToUrl }, + }, + } = this.props.kibana; + await navigateToUrl(`${basePath.get()}/app/ml/${ML_PAGES.CALENDARS_MANAGE}`, true); + }; + async formSetup() { try { const { jobIds, groupIds, calendars } = await getCalendarSettingsData(); @@ -146,7 +157,7 @@ class NewCalendarUI extends Component { try { await ml.addCalendar(calendar); - window.location = '#/settings/calendars_list'; + await this.returnToCalendarsManagementPage(); } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); @@ -167,7 +178,7 @@ class NewCalendarUI extends Component { try { await ml.updateCalendar(calendar); - window.location = '#/settings/calendars_list'; + await this.returnToCalendarsManagementPage(); } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 2cff255bd1ce3a..068d4433000883 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('../../../contexts/kibana/use_create_url', () => ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); + jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index cc1c524c19b57b..50cacd7b3545a5 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -77,7 +77,6 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` "toolsRight": Array [ diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js index 77331c4a987dca..6b4403aef7c7b0 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -7,12 +7,14 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { EuiButton, EuiLink, EuiInMemoryTable } from '@elastic/eui'; - +import { EuiButton, EuiInMemoryTable } from '@elastic/eui'; +import { Link } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { GLOBAL_CALENDAR } from '../../../../../../common/constants/calendars'; +import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; export const CalendarsListTable = ({ calendarsList, @@ -24,6 +26,8 @@ export const CalendarsListTable = ({ mlNodesAvailable, itemsSelected, }) => { + const redirectToNewCalendarPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_NEW); + const sorting = { sort: { field: 'calendar_id', @@ -46,12 +50,9 @@ export const CalendarsListTable = ({ truncateText: true, scope: 'row', render: (id) => ( - + {id} - + ), 'data-test-subj': 'mlCalendarListColumnId', }, @@ -101,7 +102,7 @@ export const CalendarsListTable = ({ size="s" data-test-subj="mlCalendarButtonCreate" key="new_calendar_button" - href="#/settings/calendars_list/new_calendar" + onClick={redirectToNewCalendarPage} isDisabled={canCreateCalendar === false || mlNodesAvailable === false} > @@ -115,6 +116,7 @@ export const CalendarsListTable = ({ canDeleteCalendar === false || mlNodesAvailable === false || itemsSelected === false } data-test-subj="mlCalendarButtonDelete" + key="delete_calendar_button" > ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); const calendars = [ { @@ -42,7 +47,11 @@ describe('CalendarsListTable', () => { }); test('New button enabled if permission available', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -56,7 +65,11 @@ describe('CalendarsListTable', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -70,7 +83,11 @@ describe('CalendarsListTable', () => { mlNodesAvailable: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js index 41b7aa63f55ef5..681c54ca9eee07 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js @@ -34,6 +34,7 @@ import { ItemsGrid } from '../../../components/items_grid'; import { NavigationMenu } from '../../../components/navigation_menu'; import { isValidFilterListId, saveFilterList } from './utils'; import { ml } from '../../../services/ml_api_service'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; const DEFAULT_ITEMS_PER_PAGE = 50; @@ -67,10 +68,6 @@ function getActivePage(activePageState, itemsPerPage, numMatchingItems) { return activePage; } -function returnToFiltersList() { - window.location.href = `#/settings/filter_lists`; -} - export class EditFilterListUI extends Component { static displayName = 'EditFilterList'; static propTypes = { @@ -105,6 +102,16 @@ export class EditFilterListUI extends Component { } } + returnToFiltersList = async () => { + const { + services: { + http: { basePath }, + application: { navigateToUrl }, + }, + } = this.props.kibana; + await navigateToUrl(`${basePath.get()}/app/ml/${ML_PAGES.FILTER_LISTS_MANAGE}`, true); + }; + loadFilterList = (filterId) => { ml.filters .filters({ filterId }) @@ -279,7 +286,7 @@ export class EditFilterListUI extends Component { saveFilterList(filterId, description, items, loadedFilter) .then((savedFilter) => { this.setLoadedFilterState(savedFilter); - returnToFiltersList(); + this.returnToFiltersList(); }) .catch((resp) => { console.log(`Error saving filter ${filterId}:`, resp); @@ -355,7 +362,7 @@ export class EditFilterListUI extends Component { /> - + this.returnToFiltersList()}> @@ -84,12 +88,9 @@ function getColumns() { defaultMessage: 'ID', }), render: (id) => ( - + {id} - + ), sortable: true, scope: 'row', @@ -213,7 +214,7 @@ export function FilterListsTable({ isSelectable={true} data-test-subj="mlFilterListsTable" rowProps={(item) => ({ - 'data-test-subj': `mlCalendarListRow row-${item.filter_id}`, + 'data-test-subj': `mlFilterListsRow row-${item.filter_id}`, })} />
diff --git a/x-pack/plugins/ml/public/application/settings/settings.test.tsx b/x-pack/plugins/ml/public/application/settings/settings.test.tsx index f16bf626321524..a5e69f233e2df8 100644 --- a/x-pack/plugins/ml/public/application/settings/settings.test.tsx +++ b/x-pack/plugins/ml/public/application/settings/settings.test.tsx @@ -22,6 +22,10 @@ jest.mock('../contexts/kibana', () => ({ }, })); +jest.mock('../contexts/kibana/use_create_url', () => ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); + describe('Settings', () => { function runCheckButtonsDisabledTest( canGetFilters: boolean, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx index deecb9fb45b514..88bf769aa29365 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx @@ -12,26 +12,40 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; -export const TimeseriesexplorerNoJobsFound = () => ( - - - - } - actions={ - - - - } - /> -); +export const TimeseriesexplorerNoJobsFound = () => { + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToJobsManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + }); + await navigateToPath(path, true); + }; + + return ( + + + + } + actions={ + + + + } + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 4ec7c5cb6d819a..ca55bb10b13d56 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -8,11 +8,9 @@ import d3 from 'd3'; import { calculateTextWidth } from './string_utils'; import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; import moment from 'moment'; -import rison from 'rison-node'; - import { getTimefilter } from './dependency_cache'; - import { CHART_TYPE } from '../explorer/explorer_constants'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; export const LINE_CHART_ANOMALY_RADIUS = 7; export const MULTI_BUCKET_SYMBOL_SIZE = 100; // In square pixels for use with d3 symbol.size @@ -212,7 +210,7 @@ export function getChartType(config) { return chartType; } -export function getExploreSeriesLink(series) { +export async function getExploreSeriesLink(mlUrlGenerator, series) { // Open the Single Metric dashboard over the same overall bounds and // zoomed in to the same time as the current chart. const timefilter = getTimefilter(); @@ -227,46 +225,44 @@ export function getExploreSeriesLink(series) { // to identify the particular series to view. // Initially pass them in the mlTimeSeriesExplorer part of the AppState. // TODO - do we want to pass the entities via the filter? - const entityCondition = {}; - series.entityFields.forEach((entity) => { - entityCondition[entity.fieldName] = entity.fieldValue; - }); + let entityCondition; + if (series.entityFields.length > 0) { + entityCondition = {}; + series.entityFields.forEach((entity) => { + entityCondition[entity.fieldName] = entity.fieldValue; + }); + } - // Use rison to build the URL . - const _g = rison.encode({ - ml: { + const url = await mlUrlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { jobIds: [series.jobId], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from: from, - to: to, - mode: 'absolute', - }, - }); - - const _a = rison.encode({ - mlTimeSeriesExplorer: { + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + timeRange: { + from: from, + to: to, + mode: 'absolute', + }, zoom: { from: zoomFrom, to: zoomTo, }, detectorIndex: series.detectorIndex, entities: entityCondition, - }, - query: { - query_string: { - analyze_wildcard: true, - query: '*', + query: { + query_string: { + analyze_wildcard: true, + query: '*', + }, }, }, + excludeBasePath: true, }); - - return `#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; + return url; } export function showMultiBucketAnomalyMarker(point) { diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/plugins/ml/public/application/util/chart_utils.test.js index b7cf11c088a1ec..955dd7cbea0a18 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.test.js @@ -35,7 +35,6 @@ import { render } from '@testing-library/react'; import { chartLimits, getChartType, - getExploreSeriesLink, getTickValues, getXTransform, isLabelLengthAboveThreshold, @@ -238,20 +237,6 @@ describe('ML - chart utils', () => { }); }); - describe('getExploreSeriesLink', () => { - test('get timeseriesexplorer link', () => { - const link = getExploreSeriesLink(seriesConfig); - const expectedLink = - `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + - `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + - `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + - `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + - `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`; - - expect(link).toBe(expectedLink); - }); - }); - describe('numTicks', () => { test('returns 10 for 1000', () => { expect(numTicks(1000)).toBe(10); diff --git a/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts b/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts deleted file mode 100644 index 806626577008e0..00000000000000 --- a/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import rison from 'rison-node'; -import { getBasePath } from './dependency_cache'; - -export enum TAB_IDS { - DATA_FRAME_ANALYTICS = 'data_frame_analytics', - ANOMALY_DETECTION = 'jobs', -} - -function getSelectedIdsUrl(tabId: TAB_IDS, settings: { [key: string]: string | string[] }): string { - // Create url for filtering by job id or group ids for kibana management table - const encoded = rison.encode(settings); - const url = `?mlManagement=${encoded}`; - const basePath = getBasePath(); - - return `${basePath.get()}/app/ml#/${tabId}${url}`; -} - -// Create url for filtering by group ids for kibana management table -export function getGroupIdsUrl(tabId: TAB_IDS, ids: string[]): string { - const settings = { - groupIds: ids, - }; - - return getSelectedIdsUrl(tabId, settings); -} - -// Create url for filtering by job id for kibana management table -export function getJobIdUrl(tabId: TAB_IDS, id: string): string { - const settings = { - jobId: id, - }; - - return getSelectedIdsUrl(tabId, settings); -} diff --git a/x-pack/plugins/ml/public/application/util/recently_accessed.ts b/x-pack/plugins/ml/public/application/util/recently_accessed.ts index ab879e421cb094..04ccd84c561bb3 100644 --- a/x-pack/plugins/ml/public/application/util/recently_accessed.ts +++ b/x-pack/plugins/ml/public/application/util/recently_accessed.ts @@ -37,7 +37,7 @@ export function addItemToRecentlyAccessed(page: string, itemId: string, url: str return; } - url = `ml#/${page}/${url}`; + url = url.startsWith('/') ? `/app/ml${url}` : `/app/ml/${page}/${url}`; const recentlyAccessed = getRecentlyAccessed(); recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id); } diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index c4aebb108e7b92..6a44756412fe39 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -11,13 +11,14 @@ import { ExplorerAppState, ExplorerGlobalState, ExplorerUrlState, + MlCommonGlobalState, MlGenericUrlState, TimeSeriesExplorerAppState, TimeSeriesExplorerGlobalState, TimeSeriesExplorerUrlState, } from '../../common/types/ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; -import { createIndexBasedMlUrl } from './common'; +import { createGenericMlUrl } from './common'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; /** * Creates URL to the Anomaly Detection Job management page @@ -30,18 +31,29 @@ export function createAnomalyDetectionJobManagementUrl( if (!params || isEmpty(params)) { return url; } - const { jobId, groupIds } = params; - const queryState: AnomalyDetectionQueryState = { - jobId, - groupIds, - }; + const { jobId, groupIds, globalState } = params; + if (jobId || groupIds) { + const queryState: AnomalyDetectionQueryState = { + jobId, + groupIds, + }; - url = setStateToKbnUrl( - 'mlManagement', - queryState, - { useHash: false, storeInHashQuery: false }, - url - ); + url = setStateToKbnUrl( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + if (globalState) { + url = setStateToKbnUrl>( + '_g', + globalState, + { useHash: false, storeInHashQuery: false }, + url + ); + } return url; } @@ -49,13 +61,24 @@ export function createAnomalyDetectionCreateJobSelectType( appBasePath: string, pageState: MlGenericUrlState['pageState'] ): string { - return createIndexBasedMlUrl( + return createGenericMlUrl( appBasePath, ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, pageState ); } +export function createAnomalyDetectionCreateJobSelectIndex( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createGenericMlUrl( + appBasePath, + ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX, + pageState + ); +} + /** * Creates URL to the Anomaly Explorer page */ @@ -75,36 +98,35 @@ export function createExplorerUrl( query, mlExplorerSwimlane = {}, mlExplorerFilter = {}, + globalState, } = params; const appState: Partial = { mlExplorerSwimlane, mlExplorerFilter, }; + let queryState: Partial = {}; + if (globalState) queryState = globalState; if (query) appState.query = query; - if (jobIds) { - const queryState: Partial = { - ml: { - jobIds, - }, + queryState.ml = { + jobIds, }; - - if (timeRange) queryState.time = timeRange; - if (refreshInterval) queryState.refreshInterval = refreshInterval; - - url = setStateToKbnUrl>( - '_g', - queryState, - { useHash: false, storeInHashQuery: false }, - url - ); - url = setStateToKbnUrl>( - '_a', - appState, - { useHash: false, storeInHashQuery: false }, - url - ); } + if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (timeRange) queryState.time = timeRange; + + url = setStateToKbnUrl>( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl>( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); return url; } @@ -120,19 +142,36 @@ export function createSingleMetricViewerUrl( if (!params) { return url; } - const { timeRange, jobIds, refreshInterval, zoom, query, detectorIndex, entities } = params; + const { + timeRange, + jobIds, + refreshInterval, + zoom, + query, + detectorIndex, + forecastId, + entities, + globalState, + } = params; + + let queryState: Partial = {}; + if (globalState) queryState = globalState; - const queryState: TimeSeriesExplorerGlobalState = { - ml: { + if (jobIds) { + queryState.ml = { jobIds, - }, - refreshInterval, - time: timeRange, - }; + }; + } + if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (timeRange) queryState.time = timeRange; const appState: Partial = {}; const mlTimeSeriesExplorer: Partial = {}; + if (forecastId !== undefined) { + mlTimeSeriesExplorer.forecastId = forecastId; + } + if (detectorIndex !== undefined) { mlTimeSeriesExplorer.detectorIndex = detectorIndex; } @@ -146,7 +185,7 @@ export function createSingleMetricViewerUrl( appState.query = { query_string: query, }; - url = setStateToKbnUrl( + url = setStateToKbnUrl>( '_g', queryState, { useHash: false, storeInHashQuery: false }, diff --git a/x-pack/plugins/ml/public/ml_url_generator/common.ts b/x-pack/plugins/ml/public/ml_url_generator/common.ts index 57cfc52045282b..f929e513e618a0 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/common.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/common.ts @@ -19,37 +19,40 @@ export function extractParams(urlState: UrlState) { * Creates generic index based search ML url * e.g. `jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a` */ -export function createIndexBasedMlUrl( +export function createGenericMlUrl( appBasePath: string, page: MlGenericUrlState['page'], pageState: MlGenericUrlState['pageState'] ): string { - const { globalState, appState, index, savedSearchId, ...restParams } = pageState; let url = `${appBasePath}/${page}`; - if (index !== undefined && savedSearchId === undefined) { - url = `${url}?index=${index}`; - } - if (index === undefined && savedSearchId !== undefined) { - url = `${url}?savedSearchId=${savedSearchId}`; - } + if (pageState) { + const { globalState, appState, index, savedSearchId, ...restParams } = pageState; + if (index !== undefined && savedSearchId === undefined) { + url = `${url}?index=${index}`; + } + if (index === undefined && savedSearchId !== undefined) { + url = `${url}?savedSearchId=${savedSearchId}`; + } - if (!isEmpty(restParams)) { - Object.keys(restParams).forEach((key) => { - url = setStateToKbnUrl( - key, - restParams[key], - { useHash: false, storeInHashQuery: false }, - url - ); - }); - } + if (!isEmpty(restParams)) { + Object.keys(restParams).forEach((key) => { + url = setStateToKbnUrl( + key, + restParams[key], + { useHash: false, storeInHashQuery: false }, + url + ); + }); + } - if (globalState) { - url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url); - } - if (appState) { - url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url); + if (globalState) { + url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url); + } + if (appState) { + url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url); + } } + return url; } diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 8cf10a2acb64f2..88761edf241a97 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -12,6 +12,7 @@ import { DataFrameAnalyticsExplorationUrlState, DataFrameAnalyticsQueryState, DataFrameAnalyticsUrlState, + MlCommonGlobalState, } from '../../common/types/ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; @@ -23,18 +24,28 @@ export function createDataFrameAnalyticsJobManagementUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`; if (mlUrlGeneratorState) { - const { jobId, groupIds } = mlUrlGeneratorState; - const queryState: Partial = { - jobId, - groupIds, - }; + const { jobId, groupIds, globalState } = mlUrlGeneratorState; + if (jobId || groupIds) { + const queryState: Partial = { + jobId, + groupIds, + }; - url = setStateToKbnUrl>( - 'mlManagement', - queryState, - { useHash: false, storeInHashQuery: false }, - url - ); + url = setStateToKbnUrl>( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + if (globalState) { + url = setStateToKbnUrl>( + '_g', + globalState, + { useHash: false, storeInHashQuery: false }, + url + ); + } } return url; @@ -50,12 +61,14 @@ export function createDataFrameAnalyticsExplorationUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`; if (mlUrlGeneratorState) { - const { jobId, analysisType } = mlUrlGeneratorState; + const { jobId, analysisType, globalState } = mlUrlGeneratorState; + const queryState: DataFrameAnalyticsExplorationQueryState = { ml: { jobId, analysisType, }, + ...globalState, }; url = setStateToKbnUrl( diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts deleted file mode 100644 index 24693df5025d9d..00000000000000 --- a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts +++ /dev/null @@ -1,29 +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. - */ - -/** - * Creates URL to the Data Visualizer page - */ -import { DataVisualizerUrlState, MlGenericUrlState } from '../../common/types/ml_url_generator'; -import { createIndexBasedMlUrl } from './common'; -import { ML_PAGES } from '../../common/constants/ml_url_generator'; - -export function createDataVisualizerUrl( - appBasePath: string, - { page }: DataVisualizerUrlState -): string { - return `${appBasePath}/${page}`; -} - -/** - * Creates URL to the Index Data Visualizer - */ -export function createIndexDataVisualizerUrl( - appBasePath: string, - pageState: MlGenericUrlState['pageState'] -): string { - return createIndexBasedMlUrl(appBasePath, ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, pageState); -} diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index 55bc6d3668de78..754f5bec57a07b 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -6,7 +6,7 @@ import { MlUrlGenerator } from './ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; -import { ANALYSIS_CONFIG_TYPE } from '../../common/types/ml_url_generator'; +import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; describe('MlUrlGenerator', () => { const urlGenerator = new MlUrlGenerator({ diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts index b69260d8d4157b..abec5cc2b7d1ef 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -16,6 +16,7 @@ import { MlUrlGeneratorState } from '../../common/types/ml_url_generator'; import { createAnomalyDetectionJobManagementUrl, createAnomalyDetectionCreateJobSelectType, + createAnomalyDetectionCreateJobSelectIndex, createExplorerUrl, createSingleMetricViewerUrl, } from './anomaly_detection_urls_generator'; @@ -23,10 +24,8 @@ import { createDataFrameAnalyticsJobManagementUrl, createDataFrameAnalyticsExplorationUrl, } from './data_frame_analytics_urls_generator'; -import { - createIndexDataVisualizerUrl, - createDataVisualizerUrl, -} from './data_visualizer_urls_generator'; +import { createGenericMlUrl } from './common'; +import { createEditCalendarUrl, createEditFilterUrl } from './settings_urls_generator'; declare module '../../../../../src/plugins/share/public' { export interface UrlGeneratorStateMapping { @@ -44,8 +43,12 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition => { - const appBasePath = this.params.appBasePath; + public readonly createUrl = async ( + mlUrlGeneratorParams: MlUrlGeneratorState + ): Promise => { + const { excludeBasePath, ...mlUrlGeneratorState } = mlUrlGeneratorParams; + const appBasePath = excludeBasePath === true ? '' : this.params.appBasePath; + switch (mlUrlGeneratorState.page) { case ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE: return createAnomalyDetectionJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); @@ -56,18 +59,39 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition { defaultMessage: 'Import your own CSV, NDJSON, or log file.', }), icon: 'document', - path: '/app/ml#/filedatavisualizer', + path: '/app/ml/filedatavisualizer', showOnHomePage: true, category: FeatureCatalogueCategory.DATA, order: 520, diff --git a/x-pack/test/functional/services/ml/settings_filter_list.ts b/x-pack/test/functional/services/ml/settings_filter_list.ts index fb1f203b655628..0afe9f21b03a69 100644 --- a/x-pack/test/functional/services/ml/settings_filter_list.ts +++ b/x-pack/test/functional/services/ml/settings_filter_list.ts @@ -17,7 +17,7 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro const $ = await table.parseDomContent(); const rows = []; - for (const tr of $.findTestSubjects('~mlCalendarListRow').toArray()) { + for (const tr of $.findTestSubjects('~mlFilterListsRow').toArray()) { const $tr = $(tr); const inUseSubject = $tr From 9497d62d4af164491ccddd90b0594d78b729cb6a Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 17 Sep 2020 11:41:57 -0600 Subject: [PATCH 10/30] [Security Solution][Detection Engine] Fixes degraded state when signals detected are between 0 and 100 (#77658) ## Summary Fixes: https://github.com/elastic/kibana/issues/77342 No easy way to unit test/end to end test this as it was operating correctly before and passed all of our tests, it was just running in a slow state if you had between 0 and 100 signals. The best bet is that you hand run the tests from 77342 or use a large look back time to ensure de-duplicate does not run as outlined in 77342. Also this PR removes a TODO block, a complexity linter issue we had, a few await that were there by accident, and pushes down arrays to make things to be cleaner. --- .../signals/search_after_bulk_create.test.ts | 2 +- .../signals/search_after_bulk_create.ts | 60 +++++++------------ .../signals/signal_rule_alert_type.ts | 4 +- .../detection_engine/signals/utils.test.ts | 34 +---------- .../lib/detection_engine/signals/utils.ts | 30 ++++------ 5 files changed, 36 insertions(+), 94 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 58dcd7f6bd1c1c..0cf0c3880fc98f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -559,7 +559,7 @@ describe('searchAfterAndBulkCreate', () => { // I don't like testing log statements since logs change but this is the best // way I can think of to ensure this section is getting hit with this test case. expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[15][0]).toContain( - 'sortIds was empty on filteredEvents' + 'sortIds was empty on searchResult name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index e90e5996877f81..be1c44de593a49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable complexity */ - import moment from 'moment'; import { AlertServices } from '../../../../../alerts/server'; @@ -25,7 +23,7 @@ interface SearchAfterAndBulkCreateParams { previousStartedAt: Date | null | undefined; ruleParams: RuleTypeParams; services: AlertServices; - listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged + listClient: ListClient; exceptionsList: ExceptionListItemSchema[]; logger: Logger; id: string; @@ -91,7 +89,7 @@ export const searchAfterAndBulkCreate = async ({ }; // sortId tells us where to start our next consecutive search_after query - let sortId; + let sortId: string | undefined; // signalsCreatedCount keeps track of how many signals we have created, // to ensure we don't exceed maxSignals @@ -155,7 +153,7 @@ export const searchAfterAndBulkCreate = async ({ // yields zero hits, but there were hits using the previous // sortIds. // e.g. totalHits was 156, index 50 of 100 results, do another search-after - // this time with a new sortId, index 22 of the remainding 56, get another sortId + // this time with a new sortId, index 22 of the remaining 56, get another sortId // search with that sortId, total is still 156 but the hits.hits array is empty. if (totalHits === 0 || searchResult.hits.hits.length === 0) { logger.debug( @@ -178,16 +176,13 @@ export const searchAfterAndBulkCreate = async ({ // filter out the search results that match with the values found in the list. // the resulting set are signals to be indexed, given they are not duplicates // of signals already present in the signals index. - const filteredEvents: SignalSearchResponse = - listClient != null - ? await filterEventsAgainstList({ - listClient, - exceptionsList, - logger, - eventSearchResult: searchResult, - buildRuleMessage, - }) - : searchResult; + const filteredEvents: SignalSearchResponse = await filterEventsAgainstList({ + listClient, + exceptionsList, + logger, + eventSearchResult: searchResult, + buildRuleMessage, + }); // only bulk create if there are filteredEvents leftover // if there isn't anything after going through the value list filter @@ -234,33 +229,18 @@ export const searchAfterAndBulkCreate = async ({ logger.debug( buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) ); + } - if ( - filteredEvents.hits.hits[0].sort != null && - filteredEvents.hits.hits[0].sort.length !== 0 - ) { - sortId = filteredEvents.hits.hits[0].sort - ? filteredEvents.hits.hits[0].sort[0] - : undefined; - } else { - logger.debug(buildRuleMessage('sortIds was empty on filteredEvents')); - toReturn.success = true; - break; - } + // we are guaranteed to have searchResult hits at this point + // because we check before if the totalHits or + // searchResult.hits.hits.length is 0 + const lastSortId = searchResult.hits.hits[searchResult.hits.hits.length - 1].sort; + if (lastSortId != null && lastSortId.length !== 0) { + sortId = lastSortId[0]; } else { - // we are guaranteed to have searchResult hits at this point - // because we check before if the totalHits or - // searchResult.hits.hits.length is 0 - if ( - searchResult.hits.hits[0].sort != null && - searchResult.hits.hits[0].sort.length !== 0 - ) { - sortId = searchResult.hits.hits[0].sort ? searchResult.hits.hits[0].sort[0] : undefined; - } else { - logger.debug(buildRuleMessage('sortIds was empty on searchResult')); - toReturn.success = true; - break; - } + logger.debug(buildRuleMessage('sortIds was empty on searchResult')); + toReturn.success = true; + break; } } catch (exc) { logger.error(buildRuleMessage(`[-] search_after and bulk threw an error ${exc}`)); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 2b6587300a581e..d48b5b434c9c0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -159,7 +159,7 @@ export const signalRulesAlertType = ({ } } try { - const { listClient, exceptionsClient } = await getListsClient({ + const { listClient, exceptionsClient } = getListsClient({ services, updatedByUser, spaceId, @@ -168,7 +168,7 @@ export const signalRulesAlertType = ({ }); const exceptionItems = await getExceptions({ client: exceptionsClient, - lists: exceptionsList, + lists: exceptionsList ?? [], }); if (isMlRule(type)) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index d053a8e1089ad3..9d22ba9dcc02b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -558,7 +558,7 @@ describe('utils', () => { }); test('it successfully returns list and exceptions list client', async () => { - const { listClient, exceptionsClient } = await getListsClient({ + const { listClient, exceptionsClient } = getListsClient({ services: alertServices, savedObjectClient: alertServices.savedObjectsClient, updatedByUser: 'some_user', @@ -569,18 +569,6 @@ describe('utils', () => { expect(listClient).toBeDefined(); expect(exceptionsClient).toBeDefined(); }); - - test('it throws if "lists" is undefined', async () => { - await expect(() => - getListsClient({ - services: alertServices, - savedObjectClient: alertServices.savedObjectsClient, - updatedByUser: 'some_user', - spaceId: '', - lists: undefined, - }) - ).rejects.toThrowError('lists plugin unavailable during rule execution'); - }); }); describe('getSignalTimeTuples', () => { @@ -743,24 +731,6 @@ describe('utils', () => { expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); }); - test('it throws if "client" is undefined', async () => { - await expect(() => - getExceptions({ - client: undefined, - lists: getListArrayMock(), - }) - ).rejects.toThrowError('lists plugin unavailable during rule execution'); - }); - - test('it returns empty array if "lists" is undefined', async () => { - const exceptions = await getExceptions({ - client: listMock.getExceptionListClient(), - lists: undefined, - }); - - expect(exceptions).toEqual([]); - }); - test('it throws if "getExceptionListClient" fails', async () => { const err = new Error('error fetching list'); listMock.getExceptionListClient = () => @@ -799,7 +769,7 @@ describe('utils', () => { const exceptions = await getExceptions({ client: listMock.getExceptionListClient(), - lists: undefined, + lists: [], }); expect(exceptions).toEqual([]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index dc09c6d5386fce..4a6ea96e1854bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -11,7 +11,7 @@ import { Logger, SavedObjectsClientContract } from '../../../../../../../src/cor import { AlertServices, parseDuration } from '../../../../../alerts/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; -import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; +import { ListArray } from '../../../../common/detection_engine/schemas/types/lists'; import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; import { BuildRuleMessage } from './rule_messages'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; @@ -118,7 +118,7 @@ export const getGapMaxCatchupRatio = ({ }; }; -export const getListsClient = async ({ +export const getListsClient = ({ lists, spaceId, updatedByUser, @@ -130,20 +130,16 @@ export const getListsClient = async ({ updatedByUser: string | null; services: AlertServices; savedObjectClient: SavedObjectsClientContract; -}): Promise<{ - listClient: ListClient | undefined; - exceptionsClient: ExceptionListClient | undefined; -}> => { +}): { + listClient: ListClient; + exceptionsClient: ExceptionListClient; +} => { if (lists == null) { throw new Error('lists plugin unavailable during rule execution'); } - const listClient = await lists.getListClient( - services.callCluster, - spaceId, - updatedByUser ?? 'elastic' - ); - const exceptionsClient = await lists.getExceptionListClient( + const listClient = lists.getListClient(services.callCluster, spaceId, updatedByUser ?? 'elastic'); + const exceptionsClient = lists.getExceptionListClient( savedObjectClient, updatedByUser ?? 'elastic' ); @@ -155,14 +151,10 @@ export const getExceptions = async ({ client, lists, }: { - client: ExceptionListClient | undefined; - lists: ListArrayOrUndefined; + client: ExceptionListClient; + lists: ListArray; }): Promise => { - if (client == null) { - throw new Error('lists plugin unavailable during rule execution'); - } - - if (lists != null && lists.length > 0) { + if (lists.length > 0) { try { const listIds = lists.map(({ list_id: listId }) => listId); const namespaceTypes = lists.map(({ namespace_type: namespaceType }) => namespaceType); From 12cd179e1282677c2ac9f444b64a6dcba8d52456 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Thu, 17 Sep 2020 10:45:39 -0700 Subject: [PATCH 11/30] docs: fix temp link (#77005) --- docs/apm/troubleshooting.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index e00a67f6c78a48..b4c9c6a4ec39e4 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -49,7 +49,7 @@ GET /_template/apm-{version} *Using Logstash, Kafka, etc.* If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka), then the index template will not be set up automatically. Instead, you'll need to -{apm-server-ref}/configuration-template.html[load the template manually]. +{apm-server-ref}/apm-server-template.html[load the template manually]. *Using a custom index names* This problem can also occur if you've customized the index name that you write APM data to. From 2e15faeadc64f86a0f94876c9771cdcc15c8840d Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 17 Sep 2020 14:40:36 -0400 Subject: [PATCH 12/30] Fix isInitialRequest (#76984) --- .../public/request/use_request.test.helpers.tsx | 9 ++++++++- src/plugins/es_ui_shared/public/request/use_request.ts | 7 +++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx index 7a42ed7fad4274..b175066b81c8ee 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx +++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { act } from 'react-dom/test-utils'; import { mount, ReactWrapper } from 'enzyme'; import sinon from 'sinon'; @@ -111,6 +111,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { requestConfig ); + // Force a re-render of the component to stress-test the useRequest hook and verify its + // state remains unaffected. + const [, setState] = useState(false); + useEffect(() => { + setState(true); + }, []); + hookResult.isInitialRequest = isInitialRequest; hookResult.isLoading = isLoading; hookResult.error = error; diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts index e04f84a67b8a3c..9d40291423cac2 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -49,7 +49,7 @@ export const useRequest = ( // Consumers can use isInitialRequest to implement a polling UX. const requestCountRef = useRef(0); - const isInitialRequest = requestCountRef.current === 0; + const isInitialRequestRef = useRef(true); const pollIntervalIdRef = useRef(null); const clearPollInterval = useCallback(() => { @@ -98,6 +98,9 @@ export const useRequest = ( return; } + // Surface to consumers that at least one request has resolved. + isInitialRequestRef.current = false; + setError(responseError); // If there's an error, keep the data from the last request in case it's still useful to the user. if (!responseError) { @@ -146,7 +149,7 @@ export const useRequest = ( }, [clearPollInterval]); return { - isInitialRequest, + isInitialRequest: isInitialRequestRef.current, isLoading, error, data, From 791eb515b8e54431c70ff62c1f90bc444527f712 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 17 Sep 2020 15:01:39 -0400 Subject: [PATCH 13/30] Update server-log.asciidoc (#77794) This is a re-do of PR #74063, which was merged to the wrong branch Co-authored-by: Nimex94 <34445912+Nimex94@users.noreply.github.com> --- docs/user/alerting/action-types/server-log.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc index eadca229bc19c6..7022320328c859 100644 --- a/docs/user/alerting/action-types/server-log.asciidoc +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -2,7 +2,7 @@ [[server-log-action-type]] === Server log action -This action type writes and entry to the {kib} server log. +This action type writes an entry to the {kib} server log. [float] [[server-log-connector-configuration]] From d22c47d47b8ea0d1a2a7cf122b59f2e472d75431 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 17 Sep 2020 12:04:01 -0700 Subject: [PATCH 14/30] [Reporting/Config] allow string representations of duration settings (#74202) * [Reporting/Config] use better schema methods * add createMockConfig * update documentation * fix observable.test * add docs links to common options page * make the schema match the docs * wording edits per feedback * self edits * todo comment * fix tests * feedback change 1 * schema utils * fix goof * use objects for the defaults when available * fix pollInterval * fix snapshots * Update report_listing.tsx * call new ByteSizeValue on server side only * revert xpack.reporting.poll * fix ts * fix snapshot * use correct input for duration * revert reorganize imports Co-authored-by: Elastic Machine --- docs/settings/reporting-settings.asciidoc | 54 ++- .../plugins/reporting/common/schema_utils.ts | 28 ++ .../public/components/report_listing.tsx | 11 +- x-pack/plugins/reporting/public/plugin.tsx | 4 +- .../browsers/chromium/driver_factory/index.ts | 3 +- .../plugins/reporting/server/config/index.ts | 2 + .../reporting/server/config/schema.test.ts | 313 +++++++++++++----- .../plugins/reporting/server/config/schema.ts | 50 +-- .../common/get_conditional_headers.test.ts | 50 +-- .../common/get_custom_logo.test.ts | 16 +- .../export_types/common/get_full_urls.test.ts | 12 +- .../export_types/csv/execute_job.test.ts | 2 + .../export_types/csv/generate_csv/index.ts | 5 +- .../printable_pdf/execute_job/index.test.ts | 22 +- .../server/lib/create_worker.test.ts | 30 +- .../reporting/server/lib/create_worker.ts | 3 +- .../lib/screenshots/get_number_of_items.ts | 7 +- .../server/lib/screenshots/inject_css.ts | 1 + .../server/lib/screenshots/observable.test.ts | 42 ++- .../server/lib/screenshots/open_url.ts | 11 +- .../server/lib/screenshots/wait_for_render.ts | 3 +- .../screenshots/wait_for_visualizations.ts | 20 +- .../server/lib/store/index_timestamp.ts | 1 - .../reporting/server/lib/store/store.test.ts | 38 ++- .../reporting/server/lib/store/store.ts | 3 +- .../plugins/reporting/server/plugin.test.ts | 4 +- .../server/routes/diagnostic/browser.test.ts | 10 +- .../server/routes/diagnostic/browser.ts | 37 ++- .../server/routes/diagnostic/config.test.ts | 10 +- .../server/routes/diagnostic/config.ts | 22 +- .../routes/diagnostic/screenshot.test.ts | 6 +- .../reporting/server/routes/jobs.test.ts | 11 +- .../lib/authorized_user_pre_routing.test.ts | 23 +- .../create_mock_browserdriverfactory.ts | 20 +- .../create_mock_reportingplugin.ts | 62 +++- .../reporting/server/test_helpers/index.ts | 8 +- .../usage/reporting_usage_collector.test.ts | 12 +- 37 files changed, 607 insertions(+), 349 deletions(-) create mode 100644 x-pack/plugins/reporting/common/schema_utils.ts diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 9c8d753a2d6681..3489dcd0182934 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -104,15 +104,14 @@ security is enabled, `xpack.security.encryptionKey`. [cols="2*<"] |=== | `xpack.reporting.queue.pollInterval` - | Specifies the number of milliseconds that the reporting poller waits between polling the - index for any pending Reporting jobs. Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[time] that the reporting poller waits between polling the index for any + pending Reporting jobs. Can be specified as number of milliseconds. Defaults to `3s`. | [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon} - | How long each worker has to produce a report. If your machine is slow or under - heavy load, you might need to increase this timeout. Specified in milliseconds. - If a Reporting job execution time goes over this time limit, the job will be - marked as a failure and there will not be a download available. - Defaults to `120000` (two minutes). + | {ref}/common-options.html#time-units[How long] each worker has to produce a report. If your machine is slow or under heavy + load, you might need to increase this timeout. If a Reporting job execution goes over this time limit, the job is marked as a + failure and no download will be available. Can be specified as number of milliseconds. + Defaults to `2m`. |=== @@ -127,24 +126,24 @@ control the capturing process. |=== a| `xpack.reporting.capture.timeouts` `.openUrl` {ess-icon} - | Specify how long to allow the Reporting browser to wait for the "Loading..." screen - to dismiss and find the initial data for the Kibana page. If the time is - exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. - Defaults to `60000` (1 minute). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for the "Loading..." screen + to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current + page, and the download link shows a warning message. Can be specified as number of milliseconds. + Defaults to `1m`. a| `xpack.reporting.capture.timeouts` `.waitForElements` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualization - panels to load on the Kibana page. If the time is exceeded, a page screenshot - is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30 - seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualization panels + to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows + a warning message. Can be specified as number of milliseconds. + Defaults to `30s`. a| `xpack.reporting.capture.timeouts` `.renderComplete` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualizations to - fetch and render the data. If the time is exceeded, a - page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to - `30000` (30 seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualizations to + fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a + warning message. Can be specified as number of milliseconds. + Defaults to `30s`. |=== @@ -163,11 +162,10 @@ available, but there will likely be errors in the visualizations in the report. job, as many times as this setting. Defaults to `3`. | `xpack.reporting.capture.loadDelay` - | When visualizations are not evented, this is the amount of time before - taking a screenshot. All visualizations that ship with {kib} are evented, so this - setting should not have much effect. If you are seeing empty images instead of - visualizations, try increasing this value. - Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[amount of time] before taking a screenshot when visualizations are not evented. + All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images + instead of visualizations, try increasing this value. + Defaults to `3s`. | [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` {ess-icon} | Specifies the browser to use to capture screenshots. This setting exists for @@ -213,9 +211,9 @@ a| `xpack.reporting.capture.browser` [cols="2*<"] |=== | [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` {ess-icon} - | The maximum size of a CSV file before being truncated. This setting exists to prevent - large exports from causing performance and storage issues. - Defaults to `10485760` (10mB). + | The maximum {ref}/common-options.html#byte-units[byte size] of a CSV file before being truncated. This setting exists to + prevent large exports from causing performance and storage issues. Can be specified as number of bytes. + Defaults to `10mb`. | `xpack.reporting.csv.scroll.size` | Number of documents retrieved from {es} for each scroll iteration during a CSV @@ -223,7 +221,7 @@ a| `xpack.reporting.capture.browser` Defaults to `500`. | `xpack.reporting.csv.scroll.duration` - | Amount of time allowed before {kib} cleans the scroll context during a CSV export. + | Amount of {ref}/common-options.html#time-units[time] allowed before {kib} cleans the scroll context during a CSV export. Defaults to `30s`. | `xpack.reporting.csv.checkForFormulas` diff --git a/x-pack/plugins/reporting/common/schema_utils.ts b/x-pack/plugins/reporting/common/schema_utils.ts new file mode 100644 index 00000000000000..f9b5c90e3c366b --- /dev/null +++ b/x-pack/plugins/reporting/common/schema_utils.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ByteSizeValue } from '@kbn/config-schema'; +import moment from 'moment'; + +/* + * For cleaner code: use these functions when a config schema value could be + * one type or another. This allows you to treat the value as one type. + */ + +export const durationToNumber = (value: number | moment.Duration): number => { + if (typeof value === 'number') { + return value; + } + return value.asMilliseconds(); +}; + +export const byteSizeValueToNumber = (value: number | ByteSizeValue) => { + if (typeof value === 'number') { + return value; + } + + return value.getValueInBytes(); +}; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 65db13f22788b5..f326d365351f20 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -6,8 +6,8 @@ import { EuiBasicTable, - EuiFlexItem, EuiFlexGroup, + EuiFlexItem, EuiPageContent, EuiSpacer, EuiText, @@ -23,6 +23,7 @@ import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { Poller } from '../../common/poller'; +import { durationToNumber } from '../../common/schema_utils'; import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; @@ -183,17 +184,19 @@ class ReportListingUi extends Component { public componentDidMount() { this.mounted = true; + const { pollConfig, license$ } = this.props; + const pollFrequencyInMillis = durationToNumber(pollConfig.jobsRefresh.interval); this.poller = new Poller({ functionToPoll: () => { return this.fetchJobs(); }, - pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval, + pollFrequencyInMillis, trailing: false, continuePollingOnError: true, - pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier, + pollFrequencyErrorMultiplier: pollConfig.jobsRefresh.intervalErrorMultiplier, }); this.poller.start(); - this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); + this.licenseSubscription = license$.subscribe(this.licenseHandler); } private licenseHandler = (license: ILicense) => { diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index d003d4c581699f..a134377e194b8b 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -26,6 +26,7 @@ import { import { ManagementSetup } from '../../../../src/plugins/management/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../licensing/public'; +import { durationToNumber } from '../common/schema_utils'; import { JobId, JobStatusBuckets, ReportingConfigType } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; import { getGeneralErrorToast } from './components'; @@ -158,8 +159,7 @@ export class ReportingPublicPlugin implements Plugin { const { http, notifications } = core; const apiClient = new ReportingAPIClient(http); const streamHandler = new StreamHandler(notifications, apiClient); - const { interval } = this.config.poll.jobsRefresh; - + const interval = durationToNumber(this.config.poll.jobsRefresh.interval); Rx.timer(0, interval) .pipe( takeUntil(this.stop$), // stop the interval when stop method is called diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 88be86d1ecc308..6897f07c45e2bb 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -21,6 +21,7 @@ import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { getChromiumDisconnectedError } from '../'; import { BROWSER_TYPE } from '../../../../common/constants'; +import { durationToNumber } from '../../../../common/schema_utils'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; import { safeChildProcess } from '../../safe_child_process'; @@ -90,7 +91,7 @@ export class HeadlessChromiumDriverFactory { // Set the default timeout for all navigation methods to the openUrl timeout (30 seconds) // All waitFor methods have their own timeout config passed in to them - page.setDefaultTimeout(this.captureConfig.timeouts.openUrl); + page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); logger.debug(`Browser page driver created`); } catch (err) { diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index a89b952702e1b5..b9c6f8e7591e34 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -17,6 +17,8 @@ export const config: PluginConfigDescriptor = { unused('capture.concurrency'), unused('capture.settleTime'), unused('capture.timeout'), + unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), + unused('poll.jobsRefresh.intervalErrorMultiplier'), unused('kibanaApp'), ], }; diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 69e4d443cf0402..9fc3d4329879eb 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -8,101 +8,242 @@ import { ConfigSchema } from './schema'; describe('Reporting Config Schema', () => { it(`context {"dev":false,"dist":false} produces correct config`, () => { - expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchObject({ - capture: { - browser: { - autoDownload: true, - chromium: { proxy: { enabled: false } }, - type: 'chromium', + expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "autoDownload": true, + "chromium": Object { + "proxy": Object { + "enabled": false, + }, + }, + "type": "chromium", + }, + "loadDelay": "PT3S", + "maxAttempts": 1, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + "timeouts": Object { + "openUrl": "PT1M", + "renderComplete": "PT30S", + "waitForElements": "PT30S", + }, + "viewport": Object { + "height": 1200, + "width": 1950, + }, + "zoom": 2, + }, + "csv": Object { + "checkForFormulas": true, + "enablePanelActionDownload": true, + "escapeFormulaValues": false, + "maxSizeBytes": ByteSizeValue { + "valueInBytes": 10485760, + }, + "scroll": Object { + "duration": "30s", + "size": 500, + }, + "useByteOrderMarkEncoding": false, }, - loadDelay: 3000, - maxAttempts: 1, - networkPolicy: { - enabled: true, - rules: [ - { allow: true, host: undefined, protocol: 'http:' }, - { allow: true, host: undefined, protocol: 'https:' }, - { allow: true, host: undefined, protocol: 'ws:' }, - { allow: true, host: undefined, protocol: 'wss:' }, - { allow: true, host: undefined, protocol: 'data:' }, - { allow: false, host: undefined, protocol: undefined }, + "enabled": true, + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": ".reporting", + "kibanaServer": Object {}, + "poll": Object { + "jobCompletionNotifier": Object { + "interval": 10000, + "intervalErrorMultiplier": 5, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 5, + }, + }, + "queue": Object { + "indexInterval": "week", + "pollEnabled": true, + "pollInterval": "PT3S", + "pollIntervalErrorMultiplier": 10, + "timeout": "PT2M", + }, + "roles": Object { + "allow": Array [ + "reporting_user", ], }, - viewport: { height: 1200, width: 1950 }, - zoom: 2, - }, - csv: { - checkForFormulas: true, - enablePanelActionDownload: true, - maxSizeBytes: 10485760, - scroll: { duration: '30s', size: 500 }, - }, - encryptionKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - index: '.reporting', - kibanaServer: {}, - poll: { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, - }, - queue: { - indexInterval: 'week', - pollEnabled: true, - pollInterval: 3000, - pollIntervalErrorMultiplier: 10, - timeout: 120000, - }, - roles: { allow: ['reporting_user'] }, - }); + } + `); }); it(`context {"dev":false,"dist":true} produces correct config`, () => { - expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchObject({ - capture: { - browser: { - autoDownload: false, - chromium: { - inspect: false, - proxy: { enabled: false }, - }, - type: 'chromium', + expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "autoDownload": false, + "chromium": Object { + "inspect": false, + "proxy": Object { + "enabled": false, + }, + }, + "type": "chromium", + }, + "loadDelay": "PT3S", + "maxAttempts": 3, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + "timeouts": Object { + "openUrl": "PT1M", + "renderComplete": "PT30S", + "waitForElements": "PT30S", + }, + "viewport": Object { + "height": 1200, + "width": 1950, + }, + "zoom": 2, + }, + "csv": Object { + "checkForFormulas": true, + "enablePanelActionDownload": true, + "escapeFormulaValues": false, + "maxSizeBytes": ByteSizeValue { + "valueInBytes": 10485760, + }, + "scroll": Object { + "duration": "30s", + "size": 500, + }, + "useByteOrderMarkEncoding": false, }, - loadDelay: 3000, - maxAttempts: 3, - networkPolicy: { - enabled: true, - rules: [ - { allow: true, host: undefined, protocol: 'http:' }, - { allow: true, host: undefined, protocol: 'https:' }, - { allow: true, host: undefined, protocol: 'ws:' }, - { allow: true, host: undefined, protocol: 'wss:' }, - { allow: true, host: undefined, protocol: 'data:' }, - { allow: false, host: undefined, protocol: undefined }, + "enabled": true, + "index": ".reporting", + "kibanaServer": Object {}, + "poll": Object { + "jobCompletionNotifier": Object { + "interval": 10000, + "intervalErrorMultiplier": 5, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 5, + }, + }, + "queue": Object { + "indexInterval": "week", + "pollEnabled": true, + "pollInterval": "PT3S", + "pollIntervalErrorMultiplier": 10, + "timeout": "PT2M", + }, + "roles": Object { + "allow": Array [ + "reporting_user", ], }, - viewport: { height: 1200, width: 1950 }, - zoom: 2, - }, - csv: { - checkForFormulas: true, - enablePanelActionDownload: true, - maxSizeBytes: 10485760, - scroll: { duration: '30s', size: 500 }, - }, - index: '.reporting', - kibanaServer: {}, - poll: { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, - }, - queue: { - indexInterval: 'week', - pollEnabled: true, - pollInterval: 3000, - pollIntervalErrorMultiplier: 10, - timeout: 120000, - }, - roles: { allow: ['reporting_user'] }, - }); + } + `); + }); + + it('allows Duration values for certain keys', () => { + expect(ConfigSchema.validate({ queue: { timeout: '2m' } }).queue.timeout).toMatchInlineSnapshot( + `"PT2M"` + ); + + expect( + ConfigSchema.validate({ capture: { loadDelay: '3s' } }).capture.loadDelay + ).toMatchInlineSnapshot(`"PT3S"`); + + expect( + ConfigSchema.validate({ + capture: { timeouts: { openUrl: '1m', waitForElements: '30s', renderComplete: '10s' } }, + }).capture.timeouts + ).toMatchInlineSnapshot(` + Object { + "openUrl": "PT1M", + "renderComplete": "PT10S", + "waitForElements": "PT30S", + } + `); + }); + + it('allows ByteSizeValue values for certain keys', () => { + expect(ConfigSchema.validate({ csv: { maxSizeBytes: '12mb' } }).csv.maxSizeBytes) + .toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 12582912, + } + `); }); it(`allows optional settings`, () => { diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index a81ffd754946bf..8276e8b49d3483 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import moment from 'moment'; const KibanaServerSchema = schema.object({ @@ -33,9 +33,13 @@ const KibanaServerSchema = schema.object({ const QueueSchema = schema.object({ indexInterval: schema.string({ defaultValue: 'week' }), pollEnabled: schema.boolean({ defaultValue: true }), - pollInterval: schema.number({ defaultValue: 3000 }), + pollInterval: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 3 }), + }), pollIntervalErrorMultiplier: schema.number({ defaultValue: 10 }), - timeout: schema.number({ defaultValue: moment.duration(2, 'm').asMilliseconds() }), + timeout: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ minutes: 2 }), + }), }); const RulesSchema = schema.object({ @@ -46,9 +50,15 @@ const RulesSchema = schema.object({ const CaptureSchema = schema.object({ timeouts: schema.object({ - openUrl: schema.number({ defaultValue: 60000 }), - waitForElements: schema.number({ defaultValue: 30000 }), - renderComplete: schema.number({ defaultValue: 30000 }), + openUrl: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ minutes: 1 }), + }), + waitForElements: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 30 }), + }), + renderComplete: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 30 }), + }), }), networkPolicy: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -68,9 +78,9 @@ const CaptureSchema = schema.object({ width: schema.number({ defaultValue: 1950 }), height: schema.number({ defaultValue: 1200 }), }), - loadDelay: schema.number({ - defaultValue: moment.duration(3, 's').asMilliseconds(), - }), // TODO: use schema.duration + loadDelay: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 3 }), + }), browser: schema.object({ autoDownload: schema.conditional( schema.contextRef('dist'), @@ -116,13 +126,13 @@ const CsvSchema = schema.object({ checkForFormulas: schema.boolean({ defaultValue: true }), escapeFormulaValues: schema.boolean({ defaultValue: false }), enablePanelActionDownload: schema.boolean({ defaultValue: true }), - maxSizeBytes: schema.number({ - defaultValue: 1024 * 1024 * 10, // 10MB - }), // TODO: use schema.byteSize + maxSizeBytes: schema.oneOf([schema.number(), schema.byteSize()], { + defaultValue: ByteSizeValue.parse('10mb'), + }), useByteOrderMarkEncoding: schema.boolean({ defaultValue: false }), scroll: schema.object({ duration: schema.string({ - defaultValue: '30s', + defaultValue: '30s', // this value is passed directly to ES, so string only format is preferred validate(value) { if (!/^[0-9]+(d|h|m|s|ms|micros|nanos)$/.test(value)) { return 'must be a duration string'; @@ -146,18 +156,16 @@ const RolesSchema = schema.object({ const IndexSchema = schema.string({ defaultValue: '.reporting' }); +// Browser side polling: job completion notifier, management table auto-refresh +// NOTE: can not use schema.duration, a bug prevents it being passed to the browser correctly const PollSchema = schema.object({ jobCompletionNotifier: schema.object({ - interval: schema.number({ - defaultValue: moment.duration(10, 's').asMilliseconds(), - }), // TODO: use schema.duration - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + interval: schema.number({ defaultValue: 10000 }), + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused }), jobsRefresh: schema.object({ - interval: schema.number({ - defaultValue: moment.duration(5, 's').asMilliseconds(), - }), // TODO: use schema.duration - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + interval: schema.number({ defaultValue: 5000 }), + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused }), }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 754bc7bc75cb53..a0d8ff08525447 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; import { ReportingConfig } from '../../'; import { ReportingCore } from '../../core'; -import { createMockReportingCore } from '../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; import { BasePayload } from '../../types'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; @@ -15,17 +18,10 @@ import { getConditionalHeaders, getCustomLogo } from './'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; -const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, -}); - beforeEach(async () => { - const mockConfigGet = sinon - .stub() - .withArgs('kibanaServer', 'hostname') - .returns('custom-hostname'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { kibanaServer: { hostname: 'custom-hostname' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockReportingPlugin = await createMockReportingCore(mockConfig); }); @@ -84,10 +80,9 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav const mockGetSavedObjectsClient = jest.fn(); mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient; - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('kibanaServer', 'hostname').returns('localhost'); - mockConfigGet.withArgs('server', 'basePath').returns('/sbp'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { kibanaServer: { hostname: 'localhost' }, server: { basePath: '/sbp' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); const permittedHeaders = { foo: 'bar', @@ -134,25 +129,12 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav }); describe('config formatting', () => { - test(`lowercases server.host`, async () => { - const mockConfigGet = sinon.stub().withArgs('server', 'host').returns('COOL-HOSTNAME'); - mockConfig = getMockConfig(mockConfigGet); - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as BasePayload, - filteredHeaders: {}, - config: mockConfig, - }); - expect(conditionalHeaders.conditions.hostname).toEqual('cool-hostname'); - }); - test(`lowercases kibanaServer.hostname`, async () => { - const mockConfigGet = sinon - .stub() - .withArgs('kibanaServer', 'hostname') - .returns('GREAT-HOSTNAME'); - mockConfig = getMockConfig(mockConfigGet); - const conditionalHeaders = await getConditionalHeaders({ + const reportingConfig = { kibanaServer: { hostname: 'GREAT-HOSTNAME' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); + + const conditionalHeaders = getConditionalHeaders({ job: { title: 'cool-job-bro', type: 'csv', diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index 8c02fdd69de8bd..ec4e54632eef55 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../../core'; -import { createMockReportingCore } from '../../test_helpers'; +import { ReportingConfig, ReportingCore } from '../../'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; -const mockConfigGet = jest.fn().mockImplementation((key: string) => { - return 'localhost'; -}); -const mockConfig = { get: mockConfigGet, kbnConfig: { get: mockConfigGet } }; - +let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; + beforeEach(async () => { + mockConfig = createMockConfig(createMockConfigSchema()); mockReportingPlugin = await createMockReportingCore(mockConfig); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index 355536000326e7..fae66b26a83e0c 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -5,6 +5,7 @@ */ import { ReportingConfig } from '../../'; +import { createMockConfig } from '../../test_helpers'; import { TaskPayloadPNG } from '../png/types'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; @@ -15,12 +16,6 @@ interface FullUrlsOpts { } let mockConfig: ReportingConfig; -const getMockConfig = (mockConfigGet: jest.Mock) => { - return { - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, - }; -}; beforeEach(() => { const reportingConfig: Record = { @@ -29,10 +24,7 @@ beforeEach(() => { 'kibanaServer.protocol': 'http', 'server.basePath': '/sbp', }; - const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => { - return reportingConfig[keys.join('.') as string]; - }); - mockConfig = getMockConfig(mockConfigGet); + mockConfig = createMockConfig(reportingConfig); }); const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF; diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 15432d0cbd1474..72b42143a24f70 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -6,6 +6,7 @@ import nodeCrypto from '@elastic/node-crypto'; import { ElasticsearchServiceSetup, IUiSettingsClient } from 'kibana/server'; +import moment from 'moment'; // @ts-ignore import Puid from 'puid'; import sinon from 'sinon'; @@ -73,6 +74,7 @@ describe('CSV Execute Job', function () { beforeEach(async function () { configGetStub = sinon.stub(); + configGetStub.withArgs('queue', 'timeout').returns(moment.duration('2m')); configGetStub.withArgs('index').returns('.reporting-foo-test'); configGetStub.withArgs('encryptionKey').returns(encryptionKey); configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index 06aa2434afc3f4..e383f21143149c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -6,11 +6,12 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'src/core/server'; -import { getFieldFormats } from '../../../services'; import { ReportingConfig } from '../../../'; import { CancellationToken } from '../../../../../../plugins/reporting/common'; import { CSV_BOM_CHARS } from '../../../../common/constants'; +import { byteSizeValueToNumber } from '../../../../common/schema_utils'; import { LevelLogger } from '../../../lib'; +import { getFieldFormats } from '../../../services'; import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../types'; import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; import { createEscapeValue } from './escape_value'; @@ -64,7 +65,7 @@ export function createGenerateCsv(logger: LevelLogger) { ); const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; - const builder = new MaxSizeStringBuilder(settings.maxSizeBytes, bom); + const builder = new MaxSizeStringBuilder(byteSizeValueToNumber(settings.maxSizeBytes), bom); const { fields, metaFields, conflictedTypesFields } = job; const header = `${fields.map(escapeValue).join(settings.separator)}\n`; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index fdc51dc1c9c878..e7322bdc0d4084 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -10,7 +10,11 @@ import * as Rx from 'rxjs'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; -import { createMockReportingCore } from '../../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; import { runTaskFnFactory } from './'; @@ -39,20 +43,16 @@ const encryptHeaders = async (headers: Record) => { const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF; beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; const reportingConfig = { + 'server.basePath': '/sbp', index: '.reports-test', encryptionKey: mockEncryptionKey, 'kibanaServer.hostname': 'localhost', 'kibanaServer.port': 5601, 'kibanaServer.protocol': 'http', }; - const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, - }; + const mockSchema = createMockConfigSchema(reportingConfig); + const mockReportingConfig = createMockConfig(mockSchema); mockReporting = await createMockReportingCore(mockReportingConfig); @@ -79,7 +79,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; await runTask( 'pdfJobId', @@ -98,7 +98,7 @@ test(`passes browserTimezone to generatePdf`, async () => { test(`returns content_type of application/pdf`, async () => { const logger = getMockLogger(); - const runTask = await runTaskFnFactory(mockReporting, logger); + const runTask = runTaskFnFactory(mockReporting, logger); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = await generatePdfObservableFactory(mockReporting); @@ -117,7 +117,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const { content } = await runTask( 'pdfJobId', diff --git a/x-pack/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/plugins/reporting/server/lib/create_worker.test.ts index 85188c07eeb207..1fcd7508493312 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.test.ts @@ -6,7 +6,12 @@ import * as sinon from 'sinon'; import { ReportingConfig, ReportingCore } from '../../server'; -import { createMockReportingCore } from '../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../test_helpers'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; @@ -14,16 +19,13 @@ import { Esqueue } from './esqueue'; import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch'; import { ExportTypesRegistry } from './export_types_registry'; -const configGetStub = sinon.stub(); -configGetStub.withArgs('queue').returns({ - pollInterval: 3300, - pollIntervalErrorMultiplier: 10, -}); -configGetStub.withArgs('server', 'name').returns('test-server-123'); -configGetStub.withArgs('server', 'uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr'); +const logger = createMockLevelLogger(); +const reportingConfig = { + queue: { pollInterval: 3300, pollIntervalErrorMultiplier: 10 }, + server: { name: 'test-server-123', uuid: 'g9ymiujthvy6v8yrh7567g6fwzgzftzfr' }, +}; const executeJobFactoryStub = sinon.stub(); -const getMockLogger = sinon.stub(); const getMockExportTypesRegistry = ( exportTypes: any[] = [{ runTaskFnFactory: executeJobFactoryStub }] @@ -39,18 +41,18 @@ describe('Create Worker', () => { let client: ClientMock; beforeEach(async () => { - mockConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockReporting = await createMockReportingCore(mockConfig); mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry(); - // @ts-ignore over-riding config manually - mockReporting.config = mockConfig; + client = new ClientMock(); queue = new Esqueue('reporting-queue', { client }); executeJobFactoryStub.reset(); }); test('Creates a single Esqueue worker for Reporting', async () => { - const createWorker = createWorkerFactory(mockReporting, getMockLogger()); + const createWorker = createWorkerFactory(mockReporting, logger); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); @@ -82,7 +84,7 @@ Object { { runTaskFnFactory: executeJobFactoryStub }, ]); mockReporting.getExportTypesRegistry = () => exportTypesRegistry; - const createWorker = createWorkerFactory(mockReporting, getMockLogger()); + const createWorker = createWorkerFactory(mockReporting, logger); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index dd5c5604552743..c1c88dd8a54bae 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../common'; import { PLUGIN_ID } from '../../common/constants'; +import { durationToNumber } from '../../common/schema_utils'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types'; @@ -57,7 +58,7 @@ export function createWorkerFactory(reporting: ReportingCore, log const workerOptions = { kibanaName, kibanaId, - interval: queueConfig.pollInterval, + interval: durationToNumber(queueConfig.pollInterval), intervalErrorMultiplier: queueConfig.pollIntervalErrorMultiplier, }; const worker = queue.registerWorker(PLUGIN_ID, workerFn, workerOptions); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts index 49c690e8c024d9..89cb4221c96b2c 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts @@ -5,9 +5,10 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; +import { LevelLogger, startTrace } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { LevelLogger, startTrace } from '../'; import { LayoutInstance } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; @@ -31,9 +32,10 @@ export const getNumberOfItems = async ( // the dashboard is using the `itemsCountAttribute` attribute to let us // know how many items to expect since gridster incrementally adds panels // we have to use this hint to wait for all of them + const timeout = durationToNumber(captureConfig.timeouts.waitForElements); await browser.waitForSelector( `${renderCompleteSelector},[${itemsCountAttribute}]`, - { timeout: captureConfig.timeouts.waitForElements }, + { timeout }, { context: CONTEXT_READMETADATA }, logger ); @@ -59,6 +61,7 @@ export const getNumberOfItems = async ( logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.readVisualizationsError', { defaultMessage: `An error occurred when trying to read the page for visualization panel info. You may need to increase '{configKey}'. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts index f893951815e9ef..2fc711d4d6f07d 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts @@ -43,6 +43,7 @@ export const injectCustomCss = async ( logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.injectCss', { defaultMessage: `An error occurred when trying to update Kibana CSS for reporting. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 3749e4372bdab0..5b671e9f5b47e9 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -15,12 +15,17 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({ }), })); +import moment from 'moment'; import * as Rx from 'rxjs'; -import { LevelLogger } from '../'; -import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { HeadlessChromiumDriver } from '../../browsers'; -import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers'; -import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { + createMockBrowserDriverFactory, + createMockConfig, + createMockConfigSchema, + createMockLayoutInstance, + createMockLevelLogger, +} from '../../test_helpers'; +import { ConditionalHeaders } from '../../types'; import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; @@ -28,11 +33,22 @@ import { screenshotsObservableFactory } from './observable'; /* * Mocks */ -const mockLogger = jest.fn(loggingSystemMock.create); -const logger = new LevelLogger(mockLogger()); +const logger = createMockLevelLogger(); -const mockConfig = { timeouts: { openUrl: 13 } } as CaptureConfig; -const mockLayout = createMockLayoutInstance(mockConfig); +const reportingConfig = { + capture: { + loadDelay: moment.duration(2, 's'), + timeouts: { + openUrl: moment.duration(2, 'm'), + waitForElements: moment.duration(20, 's'), + renderComplete: moment.duration(10, 's'), + }, + }, +}; +const mockSchema = createMockConfigSchema(reportingConfig); +const mockConfig = createMockConfig(mockSchema); +const captureConfig = mockConfig.get('capture'); +const mockLayout = createMockLayoutInstance(captureConfig); /* * Tests @@ -45,7 +61,7 @@ describe('Screenshot Observable Pipeline', () => { }); it('pipelines a single url into screenshot and timeRange', async () => { - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index.htm'], @@ -106,7 +122,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], @@ -205,7 +221,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, @@ -300,7 +316,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, @@ -333,7 +349,7 @@ describe('Screenshot Observable Pipeline', () => { mockLayout.getViewport = () => null; // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index c21ef3b91fab3f..e28f50851f4d91 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -5,9 +5,10 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; +import { LevelLogger, startTrace } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig, ConditionalHeaders } from '../../types'; -import { LevelLogger, startTrace } from '../'; export const openUrl = async ( captureConfig: CaptureConfig, @@ -19,16 +20,14 @@ export const openUrl = async ( ): Promise => { const endTrace = startTrace('open_url', 'wait'); try { + const timeout = durationToNumber(captureConfig.timeouts.openUrl); await browser.open( url, - { - conditionalHeaders, - waitForSelector: pageLoadSelector, - timeout: captureConfig.timeouts.openUrl, - }, + { conditionalHeaders, waitForSelector: pageLoadSelector, timeout }, logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', { defaultMessage: `An error occurred when trying to open the Kibana URL. You may need to increase '{configKey}'. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts index f36a7b6f73664a..edd4f71b2adacc 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; import { LevelLogger, startTrace } from '../'; @@ -67,7 +68,7 @@ export const waitForRenderComplete = async ( return Promise.all(renderedTasks).then(hackyWaitForVisualizations); }, - args: [layout.selectors.renderComplete, captureConfig.loadDelay], + args: [layout.selectors.renderComplete, durationToNumber(captureConfig.loadDelay)], }, { context: CONTEXT_WAITFORRENDER }, logger diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index 779d00442522d0..5f86a2b3bf00b9 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -5,8 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../browsers'; +import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; @@ -25,7 +26,7 @@ const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { export const waitForVisualizations = async ( captureConfig: CaptureConfig, browser: HeadlessChromiumDriver, - itemsCount: number, + toEqual: number, layout: LayoutInstance, logger: LevelLogger ): Promise => { @@ -35,29 +36,26 @@ export const waitForVisualizations = async ( logger.debug( i18n.translate('xpack.reporting.screencapture.waitingForRenderedElements', { defaultMessage: `waiting for {itemsCount} rendered elements to be in the DOM`, - values: { itemsCount }, + values: { itemsCount: toEqual }, }) ); try { + const timeout = durationToNumber(captureConfig.timeouts.renderComplete); await browser.waitFor( - { - fn: getCompletedItemsCount, - args: [{ renderCompleteSelector }], - toEqual: itemsCount, - timeout: captureConfig.timeouts.renderComplete, - }, + { fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual, timeout }, { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, logger ); - logger.debug(`found ${itemsCount} rendered elements in the DOM`); + logger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.couldntFinishRendering', { defaultMessage: `An error occurred when trying to wait for {count} visualizations to finish rendering. You may need to increase '{configKey}'. {error}`, values: { - count: itemsCount, + count: toEqual, configKey: 'xpack.reporting.capture.timeouts.renderComplete', error: err, }, diff --git a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts index 71ce0b1e572f84..7b8b851f5bd729 100644 --- a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts +++ b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts @@ -8,7 +8,6 @@ import moment, { unitOfTime } from 'moment'; export const intervals = ['year', 'month', 'week', 'day', 'hour', 'minute']; -// TODO: This helper function can be removed by using `schema.duration` objects in the reporting config schema export function indexTimestamp(intervalStr: string, separator = '-') { const startOf = intervalStr as unitOfTime.StartOf; if (separator.match(/[a-z]/i)) throw new Error('Interval separator can not be a letter'); diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index b87466ca289cfd..8dc4edd2000527 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -7,15 +7,15 @@ import sinon from 'sinon'; import { ElasticsearchServiceSetup } from 'src/core/server'; import { ReportingConfig, ReportingCore } from '../..'; -import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../test_helpers'; import { Report } from './report'; import { ReportingStore } from './store'; -const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, -}); - describe('ReportingStore', () => { const mockLogger = createMockLevelLogger(); let mockConfig: ReportingConfig; @@ -25,10 +25,12 @@ describe('ReportingStore', () => { const mockElasticsearch = { legacy: { client: { callAsInternalUser: callClusterStub } } }; beforeEach(async () => { - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('index').returns('.reporting-test'); - mockConfigGet.withArgs('queue', 'indexInterval').returns('week'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { + index: '.reporting-test', + queue: { indexInterval: 'week' }, + }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockCore = await createMockReportingCore(mockConfig); callClusterStub.reset(); @@ -67,15 +69,17 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); it('throws if options has invalid indexInterval', async () => { - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('index').returns('.reporting-test'); - mockConfigGet.withArgs('queue', 'indexInterval').returns('centurially'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { + index: '.reporting-test', + queue: { indexInterval: 'centurially' }, + }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockCore = await createMockReportingCore(mockConfig); const store = new ReportingStore(mockCore, mockLogger); @@ -159,7 +163,7 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); @@ -190,7 +194,7 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index b1309cbdeb94db..0aae8b567bcdb1 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -5,6 +5,7 @@ */ import { ElasticsearchServiceSetup } from 'src/core/server'; +import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types'; @@ -45,7 +46,7 @@ export class ReportingStore { this.indexPrefix = config.get('index'); this.indexInterval = config.get('queue', 'indexInterval'); this.jobSettings = { - timeout: config.get('queue', 'timeout'), + timeout: durationToNumber(config.get('queue', 'timeout')), browser_type: config.get('capture', 'browser', 'type'), max_attempts: config.get('capture', 'maxAttempts'), priority: 10, // unused diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index d323a281c06ffb..3f2f472ab06230 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -32,8 +32,8 @@ describe('Reporting Plugin', () => { beforeEach(async () => { configSchema = createMockConfigSchema(); initContext = coreMock.createPluginInitializerContext(configSchema); - coreSetup = await coreMock.createSetup(configSchema); - coreStart = await coreMock.createStart(); + coreSetup = coreMock.createSetup(configSchema); + coreStart = coreMock.createStart(); pluginSetup = ({ licensing: {}, features: featuresPluginMock.createSetup(), diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index f92fbfc7013cfe..71ca0661a42a91 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -33,7 +33,15 @@ describe('POST /diagnose/browser', () => { const mockedCreateInterface: any = createInterface; const config = { - get: jest.fn().mockImplementation(() => ({})), + get: jest.fn().mockImplementation((...keys) => { + const key = keys.join('.'); + switch (key) { + case 'queue.timeout': + return 120000; + case 'capture.browser.chromium.proxy': + return { enabled: false }; + } + }), kbnConfig: { get: jest.fn() }, }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index 24b85220defb4c..33620bc9a00383 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -54,25 +54,30 @@ export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger validate: {}, }, userHandler(async (user, context, req, res) => { - const logs = await browserStartLogs(reporting, logger).toPromise(); - const knownIssues = Object.keys(logsToHelpMap) as Array; + try { + const logs = await browserStartLogs(reporting, logger).toPromise(); + const knownIssues = Object.keys(logsToHelpMap) as Array; - const boundSuccessfully = logs.includes(`DevTools listening on`); - const help = knownIssues.reduce((helpTexts: string[], knownIssue) => { - const helpText = logsToHelpMap[knownIssue]; - if (logs.includes(knownIssue)) { - helpTexts.push(helpText); - } - return helpTexts; - }, []); + const boundSuccessfully = logs.includes(`DevTools listening on`); + const help = knownIssues.reduce((helpTexts: string[], knownIssue) => { + const helpText = logsToHelpMap[knownIssue]; + if (logs.includes(knownIssue)) { + helpTexts.push(helpText); + } + return helpTexts; + }, []); - const response: DiagnosticResponse = { - success: boundSuccessfully && !help.length, - help, - logs, - }; + const response: DiagnosticResponse = { + success: boundSuccessfully && !help.length, + help, + logs, + }; - return res.ok({ body: response }); + return res.ok({ body: response }); + } catch (err) { + logger.error(err); + return res.custom({ statusCode: 500 }); + } }) ); }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts index 624397246656d4..a112d04f38c7b5 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts @@ -35,7 +35,15 @@ describe('POST /diagnose/config', () => { } as unknown) as any; config = { - get: jest.fn(), + get: jest.fn().mockImplementation((...keys) => { + const key = keys.join('.'); + switch (key) { + case 'queue.timeout': + return 120000; + case 'csv.maxSizeBytes': + return 1024; + } + }), kbnConfig: { get: jest.fn() }, }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts index 198ba63e2614db..95c3a05bbf6809 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ByteSizeValue } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import numeral from '@elastic/numeral'; import { defaults, get } from 'lodash'; import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; @@ -16,6 +16,14 @@ import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_rout const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; +const numberToByteSizeValue = (value: number | ByteSizeValue) => { + if (typeof value === 'number') { + return new ByteSizeValue(value); + } + + return value; +}; + export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); const userHandler = authorizedUserPreRoutingFactory(reporting); @@ -42,12 +50,10 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) 'http.max_content_length', '100mb' ); - const elasticSearchMaxContentBytes = numeral().unformat( - elasticSearchMaxContent.toUpperCase() - ); - const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes'); + const elasticSearchMaxContentBytes = ByteSizeValue.parse(elasticSearchMaxContent); + const kibanaMaxContentBytes = numberToByteSizeValue(config.get('csv', 'maxSizeBytes')); - if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) { + if (kibanaMaxContentBytes.isGreaterThan(elasticSearchMaxContentBytes)) { const maxContentSizeWarning = i18n.translate( 'xpack.reporting.diagnostic.configSizeMismatch', { @@ -55,8 +61,8 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) `xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) is higher than ElasticSearch's {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}). ` + `Please set {ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} in Kibana.`, values: { - kibanaMaxContentBytes, - elasticSearchMaxContentBytes, + kibanaMaxContentBytes: kibanaMaxContentBytes.getValueInBytes(), + elasticSearchMaxContentBytes: elasticSearchMaxContentBytes.getValueInBytes(), KIBANA_MAX_SIZE_BYTES_PATH, ES_MAX_SIZE_BYTES_PATH, }, diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index ec4ab0446ae5f0..287da0d2ed5ecd 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -33,7 +33,11 @@ describe('POST /diagnose/screenshot', () => { }; const config = { - get: jest.fn(), + get: jest.fn().mockImplementation((...keys) => { + if (keys.join('.') === 'queue.timeout') { + return 120000; + } + }), kbnConfig: { get: jest.fn() }, }; const mockLogger = createMockLevelLogger(); diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts index 2957bc76f46826..187c69f4a72ef7 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts @@ -10,9 +10,8 @@ import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '..'; import { ReportingInternalSetup } from '../core'; -import { LevelLogger } from '../lib'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { createMockReportingCore } from '../test_helpers'; +import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; import { ExportTypeDefinition } from '../types'; import { registerJobInfoRoutes } from './jobs'; @@ -25,11 +24,7 @@ describe('GET /api/reporting/jobs/download', () => { 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 config = createMockConfig(createMockConfigSchema()); const getHits = (...sources: any) => { return { @@ -86,8 +81,6 @@ describe('GET /api/reporting/jobs/download', () => { }); afterEach(async () => { - mockLogger.debug.mockReset(); - mockLogger.error.mockReset(); await server.stop(); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index 50780a577af02b..932ebfdd22bbca 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -4,24 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'kibana/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; -import { createMockReportingCore } from '../../test_helpers'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; import { ReportingInternalSetup } from '../../core'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; +import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; let mockCore: ReportingCore; -const kbnConfig = { - 'server.basePath': '/sbp', -}; -const reportingConfig = { - 'roles.allow': ['reporting_user'], -}; -const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')] || 'whoah!', - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, -}; +const mockConfig: any = { 'server.basePath': '/sbp', 'roles.allow': ['reporting_user'] }; +const mockReportingConfigSchema = createMockConfigSchema(mockConfig); +const mockReportingConfig = createMockConfig(mockReportingConfigSchema); const getMockContext = () => (({ diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index f2785bce10964a..d6996d2caf1bcf 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; @@ -15,6 +16,7 @@ import { CaptureConfig } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; waitForSelector: jest.Mock, any[]>; + waitFor: jest.Mock, any[]>; screenshot: jest.Mock, any[]>; open: jest.Mock, any[]>; getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock; @@ -86,6 +88,7 @@ const getCreatePage = (driver: HeadlessChromiumDriver) => const defaultOpts: CreateMockBrowserDriverFactoryOpts = { evaluate: mockBrowserEvaluate, waitForSelector: mockWaitForSelector, + waitFor: jest.fn(), screenshot: mockScreenshot, open: jest.fn(), getCreatePage, @@ -96,7 +99,11 @@ export const createMockBrowserDriverFactory = async ( opts: Partial = {} ): Promise => { const captureConfig: CaptureConfig = { - timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, + timeouts: { + openUrl: moment.duration(60, 's'), + waitForElements: moment.duration(30, 's'), + renderComplete: moment.duration(30, 's'), + }, browser: { type: 'chromium', chromium: { @@ -108,18 +115,14 @@ export const createMockBrowserDriverFactory = async ( }, networkPolicy: { enabled: true, rules: [] }, viewport: { width: 800, height: 600 }, - loadDelay: 2000, + loadDelay: moment.duration(2, 's'), zoom: 2, maxAttempts: 1, }; const binaryPath = '/usr/local/share/common/secure/super_awesome_binary'; - const mockBrowserDriverFactory = await chromium.createDriverFactory( - binaryPath, - captureConfig, - logger - ); - const mockPage = {} as Page; + const mockBrowserDriverFactory = chromium.createDriverFactory(binaryPath, captureConfig, logger); + const mockPage = ({ setViewport: () => {} } as unknown) as Page; const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { inspect: true, networkPolicy: captureConfig.networkPolicy, @@ -127,6 +130,7 @@ export const createMockBrowserDriverFactory = async ( // mock the driver methods as either default mocks or passed-in mockBrowserDriver.waitForSelector = opts.waitForSelector ? opts.waitForSelector : defaultOpts.waitForSelector; // prettier-ignore + mockBrowserDriver.waitFor = opts.waitFor ? opts.waitFor : defaultOpts.waitFor; mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open; diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 559726e0b8a993..6ec35db5caec66 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -9,14 +9,16 @@ jest.mock('../usage'); jest.mock('../browsers'); jest.mock('../lib/create_queue'); +import _ from 'lodash'; import * as Rx from 'rxjs'; -import { featuresPluginMock } from '../../../features/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { chromium, HeadlessChromiumDriverFactory, initializeBrowserDriverFactory, } from '../browsers'; +import { ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { ReportingStartDeps } from '../types'; @@ -57,12 +59,58 @@ const createMockPluginStart = ( }; }; -export const createMockConfigSchema = (overrides?: any) => ({ - index: '.reporting', - kibanaServer: { hostname: 'localhost', port: '80' }, - capture: { browser: { chromium: { disableSandbox: true } } }, - ...overrides, -}); +interface ReportingConfigTestType { + index: string; + encryptionKey: string; + queue: Partial; + kibanaServer: Partial; + csv: Partial; + capture: any; + server?: any; +} + +export const createMockConfigSchema = ( + overrides: Partial = {} +): ReportingConfigTestType => { + // deeply merge the defaults and the provided partial schema + return { + index: '.reporting', + encryptionKey: 'cool-encryption-key-where-did-you-find-it', + ...overrides, + kibanaServer: { + hostname: 'localhost', + port: 80, + ...overrides.kibanaServer, + }, + capture: { + browser: { + chromium: { + disableSandbox: true, + }, + }, + ...overrides.capture, + }, + queue: { + timeout: 120000, + ...overrides.queue, + }, + csv: { + ...overrides.csv, + }, + }; +}; + +export const createMockConfig = ( + reportingConfig: Partial +): ReportingConfig => { + const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => { + return _.get(reportingConfig, keys.join('.')); + }); + return { + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, + }; +}; export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({ data: startMock.data, diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index 2d5ef9fdd768d0..96357dc915eef5 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createMockServer } from './create_mock_server'; -export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin'; export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory'; export { createMockLayoutInstance } from './create_mock_layoutinstance'; export { createMockLevelLogger } from './create_mock_levellogger'; +export { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from './create_mock_reportingplugin'; +export { createMockServer } from './create_mock_server'; diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index ed2abef2542deb..fc2dce441c6214 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -8,8 +8,8 @@ import * as Rx from 'rxjs'; import sinon from 'sinon'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingConfig, ReportingCore } from '../'; -import { createMockReportingCore } from '../test_helpers'; import { getExportTypesRegistry } from '../lib/export_types_registry'; +import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; import { ReportingSetupDeps } from '../types'; import { FeaturesAvailability } from './'; import { @@ -54,17 +54,13 @@ function getPluginsMock( } as unknown) as ReportingSetupDeps & { usageCollection: UsageCollectionSetup }; } -const getMockReportingConfig = () => ({ - get: () => {}, - kbnConfig: { get: () => '' }, -}); const getResponseMock = (base = {}) => base; describe('license checks', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; beforeAll(async () => { - mockConfig = getMockReportingConfig(); + mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); }); @@ -189,7 +185,7 @@ describe('data modeling', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; beforeAll(async () => { - mockConfig = getMockReportingConfig(); + mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); }); test('with normal looking usage data', async () => { @@ -455,7 +451,7 @@ describe('data modeling', () => { describe('Ready for collection observable', () => { test('converts observable to promise', async () => { - const mockConfig = getMockReportingConfig(); + const mockConfig = createMockConfig(createMockConfigSchema()); const mockReporting = await createMockReportingCore(mockConfig); const usageCollection = getMockUsageCollection(); From f64aa5b58d1ef9ab056dd5b30085f78f08c22f1b Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 17 Sep 2020 15:14:21 -0400 Subject: [PATCH 15/30] [Security Solution][Resolver] Analyzed event panel styling (#77671) --- .../public/resolver/view/assets.tsx | 2 +- .../public/resolver/view/panel.test.tsx | 8 +++ .../resolver/view/panels/cube_for_process.tsx | 47 ++++++++++++---- .../resolver/view/panels/node_details.tsx | 8 ++- .../public/resolver/view/panels/node_list.tsx | 56 ++++++++++++++++++- 5 files changed, 105 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index e7c3a87c9c0f60..db4b514a6c748b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -428,7 +428,7 @@ export const useResolverTheme = (): { }; const colorMap = { - descriptionText: theme.euiColorDarkestShade, + descriptionText: theme.euiTextColor, full: theme.euiColorFullShade, graphControls: theme.euiColorDarkestShade, graphControlsBackground: theme.euiColorEmptyShade, diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 3c32d448ac2a79..7cfbd9a794669d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -136,6 +136,14 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an }); }); + it('should show a single analyzed event in the node list', async () => { + await expect( + simulator().map( + () => simulator().testSubject('resolver:node-list:node-link:analyzed-event').length + ) + ).toYieldEqualTo(1); + }); + it('should have 3 nodes (with icons) in the node list', async () => { await expect( simulator().map(() => simulator().testSubject('resolver:node-list:node-link:title').length) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx index b7c8ed0dfd7db7..deddd171982298 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx @@ -11,13 +11,19 @@ import { i18n } from '@kbn/i18n'; /* eslint-disable react/display-name */ import React, { memo } from 'react'; -import { useResolverTheme } from '../assets'; +import { useResolverTheme, SymbolIds } from '../assets'; + +interface StyledSVGCube { + readonly isOrigin?: boolean; +} /** * Icon representing a process node. */ export const CubeForProcess = memo(function ({ + className, running, + isOrigin, 'data-test-subj': dataTestSubj, }: { 'data-test-subj'?: string; @@ -25,25 +31,46 @@ export const CubeForProcess = memo(function ({ * True if the process represented by the node is still running. */ running: boolean; + isOrigin?: boolean; + className?: string; }) { const { cubeAssetsForNode } = useResolverTheme(); - const { cubeSymbol } = cubeAssetsForNode(!running, false); + const { cubeSymbol, strokeColor } = cubeAssetsForNode(!running, false); return ( - + {i18n.translate('xpack.securitySolution.resolver.node_icon', { defaultMessage: '{running, select, true {Running Process} false {Terminated Process}}', values: { running }, })} + {isOrigin && ( + + )} @@ -51,8 +78,6 @@ export const CubeForProcess = memo(function ({ ); }); -const StyledSVG = styled.svg` - position: relative; - top: 0.4em; - margin-right: 0.25em; +const StyledSVG = styled.svg` + margin-right: ${(props) => (props.isOrigin ? '0.15em' : 0)}; `; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx index cf7b646c6c50e8..d72a98811bd55c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx @@ -11,6 +11,7 @@ import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { htmlIdGenerator, EuiSpacer, EuiTitle, EuiText, EuiTextColor, EuiLink } from '@elastic/eui'; import { FormattedMessage } from 'react-intl'; +import styled from 'styled-components'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import { StyledDescriptionList, StyledTitle } from './styles'; import * as selectors from '../../store/selectors'; @@ -32,6 +33,11 @@ import { PanelLoading } from './panel_loading'; import { StyledPanel } from '../styles'; import { useNavigateOrReplace } from '../use_navigate_or_replace'; +const StyledCubeForProcess = styled(CubeForProcess)` + position: relative; + top: 0.75em; +`; + export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { const processEvent = useSelector((state: ResolverState) => selectors.processEventForID(state)(nodeID) @@ -186,7 +192,7 @@ const NodeDetailView = memo(function NodeDetailView({ - diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index aaa81a6f747e29..fd564cde9d15cd 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -26,6 +26,7 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { LimitWarning } from '../limit_warnings'; import { ResolverState } from '../../types'; import { useNavigateOrReplace } from '../use_navigate_or_replace'; +import { useResolverTheme } from '../assets'; const StyledLimitWarning = styled(LimitWarning)` flex-flow: row wrap; @@ -46,6 +47,35 @@ const StyledLimitWarning = styled(LimitWarning)` display: inline; } `; + +const StyledButtonTextContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; +`; + +const StyledAnalyzedEvent = styled.div` + color: ${(props) => props.color}; + font-size: 10.5px; + font-weight: 700; +`; + +const StyledLabelTitle = styled.div``; + +const StyledLabelContainer = styled.div` + display: inline-block; + flex: 3; + min-width: 0; + + ${StyledAnalyzedEvent}, + ${StyledLabelTitle} { + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } +`; + interface ProcessTableView { name?: string; timestamp?: Date; @@ -173,9 +203,14 @@ export const NodeList = memo(() => { function NodeDetailLink({ name, item }: { name: string; item: ProcessTableView }) { const entityID = event.entityIDSafeVersion(item.event); + const originID = useSelector(selectors.originID); + const isOrigin = originID === entityID; const isTerminated = useSelector((state: ResolverState) => entityID === undefined ? false : selectors.isProcessTerminated(state)(entityID) ); + const { + colorMap: { descriptionText }, + } = useResolverTheme(); return ( {name === '' ? ( @@ -188,13 +223,28 @@ function NodeDetailLink({ name, item }: { name: string; item: ProcessTableView } )} ) : ( - <> + - {name} - + + {isOrigin && ( + + {i18n.translate('xpack.securitySolution.resolver.panel.table.row.analyzedEvent', { + defaultMessage: 'ANALYZED EVENT', + })} + + )} + + {name} + + + )} ); From 6c57afcddcd2185b61f68c51319f5c43cafdc88b Mon Sep 17 00:00:00 2001 From: Charlie Pichette <56399229+charlie-pichette@users.noreply.github.com> Date: Thu, 17 Sep 2020 13:28:26 -0600 Subject: [PATCH 16/30] Issue 77701 Remove flaky tests (#77790) --- .../apps/endpoint/endpoint_list.ts | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index f58862ff4c725f..4beb64affc46b9 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -65,8 +65,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ], ]; - // Failing: See https://github.com/elastic/kibana/issues/77701 - describe.skip('endpoint list', function () { + describe('endpoint list', function () { this.tags('ciGroup7'); const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -161,20 +160,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); expect(endpointDetailTitleNew).to.equal(endpointDetailTitleInitial); }); - - // Just check the href link is correct - would need to load ingest data to validate the integration - it('navigates to ingest fleet when the Reassign Policy link is clicked', async () => { - // The prior test results in a tooltip. We need to move the mouse to clear it and allow the click - await (await testSubjects.find('hostnameCellLink')).moveMouseTo(); - await (await testSubjects.find('hostnameCellLink')).click(); - const endpointDetailsLinkToIngestButton = await testSubjects.find( - 'endpointDetailsLinkToIngest' - ); - const hrefLink = await endpointDetailsLinkToIngestButton.getAttribute('href'); - expect(hrefLink).to.contain( - '/app/ingestManager#/fleet/agents/023fa40c-411d-4188-a941-4147bfadd095/activity?openReassignFlyout=true' - ); - }); }); // This set of tests fails the flyout does not open in the before() and will be fixed in soon @@ -315,17 +300,5 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(tableData).to.eql(expectedDataFromQuery); }); }); - - describe.skip('when there is no data,', () => { - before(async () => { - // clear out the data and reload the page - await deleteMetadataStream(getService); - await deleteMetadataCurrentStream(getService); - await pageObjects.endpoint.navigateToEndpointList(); - }); - it('displays empty Policy Table page.', async () => { - await testSubjects.existOrFail('emptyPolicyTable'); - }); - }); }); }; From 8fd71ac48926d5abe2edb4098bdbf8cabf723bac Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 17 Sep 2020 15:32:29 -0400 Subject: [PATCH 17/30] [Ingest Manager] Return 400 (not 500) when given a bad `kuery` param (#77796) * Add tests. Add default error handler to 2 routes * Add more API tests for /fleet/agent/events?kuery= Co-authored-by: Elastic Machine --- .../server/routes/agent/handlers.ts | 18 ++++++------------ .../apis/fleet/agents/events.ts | 19 +++++++++++++++++++ .../apis/fleet/agents/list.ts | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 605e4db230ce59..b9789b770eb2e2 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -30,6 +30,7 @@ import { import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; import { appContextService } from '../../services/app_context'; +import { defaultIngestErrorHandler } from '../../errors'; export const getAgentHandler: RequestHandler { + await supertest + .get(`/api/ingest_manager/fleet/agents/agent1/events?kuery=m`) // missing saved object type + .expect(400); + }); + it('should accept a valid "kuery" value', async () => { + const filter = encodeURIComponent('fleet-agent-events.subtype : "STOPPED"'); + const { body: apiResponse } = await supertest + .get(`/api/ingest_manager/fleet/agents/agent1/events?kuery=${filter}`) + .expect(200); + + expect(apiResponse).to.have.keys(['list', 'total', 'page']); + expect(apiResponse.total).to.be(1); + expect(apiResponse.page).to.be(1); + + const event = apiResponse.list[0]; + expect(event.subtype).to.eql('STOPPED'); + }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts index 1ee00ed7201692..0c6c39dde0547b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts @@ -95,5 +95,22 @@ export default function ({ getService }: FtrProviderContext) { .auth(users.kibana_basic_user.username, users.kibana_basic_user.password) .expect(404); }); + it('should return a 400 when given an invalid "kuery" value', async () => { + await supertest + .get(`/api/ingest_manager/fleet/agents?kuery=m`) // missing saved object type + .auth(users.fleet_user.username, users.fleet_user.password) + .expect(400); + }); + it('should accept a valid "kuery" value', async () => { + const filter = encodeURIComponent('fleet-agents.shared_id : "agent2_filebeat"'); + const { body: apiResponse } = await supertest + .get(`/api/ingest_manager/fleet/agents?kuery=${filter}`) + .auth(users.fleet_user.username, users.fleet_user.password) + .expect(200); + + expect(apiResponse.total).to.eql(1); + const agent = apiResponse.list[0]; + expect(agent.shared_id).to.eql('agent2_filebeat'); + }); }); } From 0a21d70560ab65dd4430546eb3fd7618f5774b57 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 17 Sep 2020 13:04:00 -0700 Subject: [PATCH 18/30] [Enterprise Search][tech debt] Add Kea logic paths for easier debugging/defaults (#77698) * Add Kea logic paths for easier debugging/defaults * PR feedback: prefer snake_case to match file names, use exact file name for last path key * Document Kea + casing logic --- x-pack/plugins/enterprise_search/README.md | 8 ++++++++ .../public/applications/app_search/app_logic.ts | 1 + .../shared/flash_messages/flash_messages_logic.ts | 1 + .../public/applications/shared/http/http_logic.ts | 1 + .../public/applications/workplace_search/app_logic.ts | 1 + .../workplace_search/views/overview/overview_logic.ts | 1 + 6 files changed, 13 insertions(+) diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index 31ee304fe22477..ba14be5564be17 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -13,6 +13,14 @@ This plugin's goal is to provide a Kibana user interface to the Enterprise Searc 2. Update `config/kibana.dev.yml` with `enterpriseSearch.host: 'http://localhost:3002'` 3. For faster QA/development, run Enterprise Search on [elasticsearch-native auth](https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm) and log in as the `elastic` superuser on Kibana. +### Kea + +Enterprise Search uses [Kea.js](https://github.com/keajs/kea) to manage our React/Redux state for us. Kea state is handled in our `*Logic` files and exposes [values](https://kea.js.org/docs/guide/concepts#values) and [actions](https://kea.js.org/docs/guide/concepts#actions) for our components to get and set state with. + +#### Debugging Kea + +To debug Kea state in-browser, Kea recommends [Redux Devtools](https://kea.js.org/docs/guide/debugging). To facilitate debugging, we use the [path](https://kea.js.org/docs/guide/debugging/#setting-the-path-manually) key with `snake_case`d paths. The path key should always end with the logic filename (e.g. `['enterprise_search', 'some_logic']`) to make it easy for devs to quickly find/jump to files via IDE tooling. + ## Testing ### Unit tests diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 3f71759390879a..9388d61041b13e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -16,6 +16,7 @@ export interface IAppActions { } export const AppLogic = kea>({ + path: ['enterprise_search', 'app_search', 'app_logic'], actions: { initializeAppData: (props) => props, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 3ae48f352b2c18..37a8f16acad6df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -32,6 +32,7 @@ const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => !Array.isArray(messages) ? [messages] : messages; export const FlashMessagesLogic = kea>({ + path: ['enterprise_search', 'flash_messages_logic'], actions: { setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), clearFlashMessages: () => null, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index 5e2b5a9ed6b06b..72380142fe3998 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -26,6 +26,7 @@ export interface IHttpActions { } export const HttpLogic = kea>({ + path: ['enterprise_search', 'http_logic'], actions: { initializeHttp: (props) => props, initializeHttpInterceptors: () => null, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index f88a00f63f4873..94bd1d529b65ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -22,6 +22,7 @@ export interface IAppActions { } export const AppLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'app_logic'], actions: { initializeAppData: ({ workplaceSearch, isFederatedAuth }) => ({ workplaceSearch, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 787d5295db1cf2..a156b8a8009f9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -31,6 +31,7 @@ export interface IOverviewValues extends IOverviewServerData { } export const OverviewLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'overview_logic'], actions: { setServerData: (serverData) => serverData, initializeOverview: () => null, From becb137835a720ddc248563a75ba9ec8ab309087 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Thu, 17 Sep 2020 16:27:44 -0400 Subject: [PATCH 19/30] [Security Solution] [Detections] Remove file validation on import route (#77770) * utlize schema.any() for validation on file in body of import rules request, adds new functional tests and unit tests to make sure we can reach and don't go past bounds. These tests would have helped uncover performance issues io-ts gave us with validating the import rules file object * fix type check failure * updates getSimpleRule and getSimpleRuleAsNdjson to accept an enabled param defaulted to false * updates comments in e2e tests for import rules route * fix tests after adding enabled boolean in test utils --- .../routes/rules/import_rules_route.test.ts | 25 ++++++++ .../routes/rules/import_rules_route.ts | 10 +-- .../basic/tests/import_rules.ts | 62 +++++++++++++++++-- .../security_and_spaces/tests/import_rules.ts | 12 ++-- .../detection_engine_api_integration/utils.ts | 8 ++- 5 files changed, 99 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 81c9387c6f39c0..a033c16cd5e997 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -55,6 +55,18 @@ describe('import_rules_route', () => { expect(response.status).toEqual(200); }); + test('returns 500 if more than 10,000 rules are imported', async () => { + const ruleIds = new Array(10001).fill(undefined).map((_, index) => `rule-${index}`); + const multiRequest = getImportRulesRequest(buildHapiStream(ruleIdsToNdJsonString(ruleIds))); + const response = await server.inject(multiRequest, context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: "Can't import more than 10000 rules", + status_code: 500, + }); + }); + test('returns 404 if alertClient is not available on the route', async () => { context.alerting!.getAlertsClient = jest.fn(); const response = await server.inject(request, context); @@ -229,6 +241,19 @@ describe('import_rules_route', () => { }); }); + test('returns 200 if many rules are imported successfully', async () => { + const ruleIds = new Array(9999).fill(undefined).map((_, index) => `rule-${index}`); + const multiRequest = getImportRulesRequest(buildHapiStream(ruleIdsToNdJsonString(ruleIds))); + const response = await server.inject(multiRequest, context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + errors: [], + success: true, + success_count: 9999, + }); + }); + test('returns 200 with errors if all rules are missing rule_ids and import fails on validation', async () => { const rulesWithoutRuleIds = ['rule-1', 'rule-2'].map((ruleId) => getImportRulesWithIdSchemaMock(ruleId) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 60bb8c79243d7c..0f44b50d4bc747 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -6,13 +6,12 @@ import { chunk } from 'lodash/fp'; import { extname } from 'path'; +import { schema } from '@kbn/config-schema'; import { validate } from '../../../../../common/validate'; import { importRulesQuerySchema, ImportRulesQuerySchemaDecoded, - importRulesPayloadSchema, - ImportRulesPayloadSchemaDecoded, ImportRulesSchemaDecoded, } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { @@ -48,7 +47,7 @@ import { PartialFilter } from '../../types'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; -const CHUNK_PARSED_OBJECT_SIZE = 10; +const CHUNK_PARSED_OBJECT_SIZE = 50; export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupPlugins['ml']) => { router.post( @@ -58,10 +57,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP query: buildRouteValidation( importRulesQuerySchema ), - body: buildRouteValidation< - typeof importRulesPayloadSchema, - ImportRulesPayloadSchemaDecoded - >(importRulesPayloadSchema), + body: schema.any(), // validation on file object is accomplished later in the handler. }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts index 108ca365bc14ff..c6294cfe6ec28b 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts @@ -129,7 +129,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') .expect(200); const { body } = await supertest @@ -192,6 +192,56 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + // import is very slow in 7.10+ due to the alerts client find api + // when importing 100 rules it takes about 30 seconds for this + // test to complete so at 10 rules completing in about 10 seconds + // I figured this is enough to make sure the import route is doing its job. + it('should be able to import 10 rules', async () => { + const ruleIds = new Array(10).fill(undefined).map((_, index) => `rule-${index}`); + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 10, + }); + }); + + // uncomment the below test once we speed up the alerts client find api + // in another PR. + // it('should be able to import 10000 rules', async () => { + // const ruleIds = new Array(10000).fill(undefined).map((_, index) => `rule-${index}`); + // const { body } = await supertest + // .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + // .set('kbn-xsrf', 'true') + // .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson') + // .expect(200); + + // expect(body).to.eql({ + // errors: [], + // success: true, + // success_count: 10000, + // }); + // }); + + it('should NOT be able to import more than 10,000 rules', async () => { + const ruleIds = new Array(10001).fill(undefined).map((_, index) => `rule-${index}`); + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson') + .expect(500); + + expect(body).to.eql({ + status_code: 500, + message: "Can't import more than 10000 rules", + }); + }); + it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) @@ -280,7 +330,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') .expect(200); const simpleRule = getSimpleRule('rule-1'); @@ -372,13 +422,17 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2'], true), 'rules.ndjson') .expect(200); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .attach( + 'file', + getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3'], true), + 'rules.ndjson' + ) .expect(200); const { body: bodyOfRule1 } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index e0b60ae1fbeebb..664077d5a4fab9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -129,7 +129,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') .expect(200); const { body } = await supertest @@ -243,7 +243,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson') .expect(200); const simpleRule = getSimpleRule('rule-1'); @@ -335,13 +335,17 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2'], true), 'rules.ndjson') .expect(200); await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .attach( + 'file', + getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3'], true), + 'rules.ndjson' + ) .expect(200); const { body: bodyOfRule1 } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 4cbbc142edd40f..1dba1a154373b3 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -56,10 +56,12 @@ export const removeServerGeneratedPropertiesIncludingRuleId = ( /** * This is a typical simple rule for testing that is easy for most basic testing * @param ruleId + * @param enabled Enables the rule on creation or not. Defaulted to false to enable it on import */ -export const getSimpleRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ +export const getSimpleRule = (ruleId = 'rule-1', enabled = true): CreateRulesSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', + enabled, risk_score: 1, rule_id: ruleId, severity: 'high', @@ -360,9 +362,9 @@ export const deleteSignalsIndex = async ( * for testing uploads. * @param ruleIds Array of strings of rule_ids */ -export const getSimpleRuleAsNdjson = (ruleIds: string[]): Buffer => { +export const getSimpleRuleAsNdjson = (ruleIds: string[], enabled = false): Buffer => { const stringOfRules = ruleIds.map((ruleId) => { - const simpleRule = getSimpleRule(ruleId); + const simpleRule = getSimpleRule(ruleId, enabled); return JSON.stringify(simpleRule); }); return Buffer.from(stringOfRules.join('\n')); From fb6292965b546718cd236a3ef771f91c2cf59c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 17 Sep 2020 22:38:14 +0200 Subject: [PATCH 20/30] [APM] Add transaction error rate alert (#76933) --- x-pack/plugins/apm/common/alert_types.ts | 86 ++++------- .../ErrorCountAlertTrigger}/index.stories.tsx | 8 +- .../ErrorCountAlertTrigger}/index.tsx | 72 +++------ .../PopoverExpression/index.tsx | 1 - .../ServiceAlertTrigger/index.tsx | 0 .../index.stories.tsx | 0 .../TransactionDurationAlertTrigger/index.tsx | 138 ++++++++---------- .../SelectAnomalySeverity.tsx | 0 .../index.tsx | 50 +++---- .../index.tsx | 110 ++++++++++++++ .../apm/public/components/alerting/fields.tsx | 108 ++++++++++++++ .../alerting/register_apm_alerts.ts | 72 +++++++++ .../AlertIntegrations/index.tsx | 54 +++++-- x-pack/plugins/apm/public/plugin.ts | 51 +------ .../apm/server/lib/alerts/action_variables.ts | 48 ++++++ .../server/lib/alerts/register_apm_alerts.ts | 9 +- ....ts => register_error_count_alert_type.ts} | 65 +++------ ...egister_transaction_duration_alert_type.ts | 78 +++------- ...transaction_duration_anomaly_alert_type.ts | 32 ++-- ...ister_transaction_error_rate_alert_type.ts | 131 +++++++++++++++++ .../create_anomaly_detection_jobs.ts | 3 +- .../collect_data_telemetry/tasks.ts | 11 +- .../get_environment_ui_filter_es.ts | 7 +- .../lib/rum_client/get_page_view_trends.ts | 2 +- .../translations/translations/ja-JP.json | 14 -- .../translations/translations/zh-CN.json | 14 -- 26 files changed, 726 insertions(+), 438 deletions(-) rename x-pack/plugins/apm/public/components/{shared/ErrorRateAlertTrigger => alerting/ErrorCountAlertTrigger}/index.stories.tsx (83%) rename x-pack/plugins/apm/public/components/{shared/ErrorRateAlertTrigger => alerting/ErrorCountAlertTrigger}/index.tsx (54%) rename x-pack/plugins/apm/public/components/{shared => alerting}/ServiceAlertTrigger/PopoverExpression/index.tsx (99%) rename x-pack/plugins/apm/public/components/{shared => alerting}/ServiceAlertTrigger/index.tsx (100%) rename x-pack/plugins/apm/public/components/{shared => alerting}/TransactionDurationAlertTrigger/index.stories.tsx (100%) rename x-pack/plugins/apm/public/components/{shared => alerting}/TransactionDurationAlertTrigger/index.tsx (57%) rename x-pack/plugins/apm/public/components/{shared => alerting}/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx (100%) rename x-pack/plugins/apm/public/components/{shared => alerting}/TransactionDurationAnomalyAlertTrigger/index.tsx (75%) create mode 100644 x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx create mode 100644 x-pack/plugins/apm/public/components/alerting/fields.tsx create mode 100644 x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts create mode 100644 x-pack/plugins/apm/server/lib/alerts/action_variables.ts rename x-pack/plugins/apm/server/lib/alerts/{register_error_rate_alert_type.ts => register_error_count_alert_type.ts} (66%) create mode 100644 x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index a1161354e04f45..15a3c642faf324 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -7,42 +7,33 @@ import { i18n } from '@kbn/i18n'; export enum AlertType { - ErrorRate = 'apm.error_rate', + ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. + TransactionErrorRate = 'apm.transaction_error_rate', TransactionDuration = 'apm.transaction_duration', TransactionDurationAnomaly = 'apm.transaction_duration_anomaly', } +const THRESHOLD_MET_GROUP = { + id: 'threshold_met', + name: i18n.translate('xpack.apm.a.thresholdMet', { + defaultMessage: 'Threshold met', + }), +}; + export const ALERT_TYPES_CONFIG = { - [AlertType.ErrorRate]: { - name: i18n.translate('xpack.apm.errorRateAlert.name', { - defaultMessage: 'Error rate', + [AlertType.ErrorCount]: { + name: i18n.translate('xpack.apm.errorCountAlert.name', { + defaultMessage: 'Error count threshold', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate('xpack.apm.errorRateAlert.thresholdMet', { - defaultMessage: 'Threshold met', - }), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, [AlertType.TransactionDuration]: { name: i18n.translate('xpack.apm.transactionDurationAlert.name', { - defaultMessage: 'Transaction duration', + defaultMessage: 'Transaction duration threshold', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate( - 'xpack.apm.transactionDurationAlert.thresholdMet', - { - defaultMessage: 'Threshold met', - } - ), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, @@ -50,39 +41,24 @@ export const ALERT_TYPES_CONFIG = { name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', { defaultMessage: 'Transaction duration anomaly', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate( - 'xpack.apm.transactionDurationAlert.thresholdMet', - { - defaultMessage: 'Threshold met', - } - ), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], + defaultActionGroupId: 'threshold_met', + producer: 'apm', + }, + [AlertType.TransactionErrorRate]: { + name: i18n.translate('xpack.apm.transactionErrorRateAlert.name', { + defaultMessage: 'Transaction error rate threshold', + }), + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, }; -export const TRANSACTION_ALERT_AGGREGATION_TYPES = { - avg: i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.avg', - { - defaultMessage: 'Average', - } - ), - '95th': i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.95th', - { - defaultMessage: '95th percentile', - } - ), - '99th': i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.99th', - { - defaultMessage: '99th percentile', - } - ), -}; +// Server side registrations +// x-pack/plugins/apm/server/lib/alerts/.ts +// x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts + +// Client side registrations: +// x-pack/plugins/apm/public/components/alerting//index.tsx +// x-pack/plugins/apm/public/components/alerting/register_apm_alerts diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx similarity index 83% rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx index 632d53a9c63b65..c30cef7210a435 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx @@ -6,14 +6,14 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { ErrorRateAlertTrigger } from '.'; +import { ErrorCountAlertTrigger } from '.'; import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../context/ApmPluginContext/MockApmPluginContext'; -storiesOf('app/ErrorRateAlertTrigger', module).add( +storiesOf('app/ErrorCountAlertTrigger', module).add( 'example', () => { const params = { @@ -26,7 +26,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add( value={(mockApmPluginContextValue as unknown) as ApmPluginContextValue} >
- undefined} setAlertProperty={() => undefined} @@ -37,7 +37,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add( }, { info: { - propTablesExclude: [ErrorRateAlertTrigger, MockApmPluginContextWrapper], + propTablesExclude: [ErrorCountAlertTrigger, MockApmPluginContextWrapper], source: false, }, } diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx similarity index 54% rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx index 7b284696477f3f..a465b90e7bf05f 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx @@ -3,36 +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. */ -import { EuiFieldNumber, EuiSelect } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; -import { isFinite } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { - ENVIRONMENT_ALL, - getEnvironmentLabel, -} from '../../../../common/environment_filter_values'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { useEnvironments } from '../../../hooks/useEnvironments'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { EnvironmentField, ServiceField, IsAboveField } from '../fields'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; -import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; -export interface ErrorRateAlertTriggerParams { +export interface AlertParams { windowSize: number; windowUnit: string; threshold: number; + serviceName: string; environment: string; } interface Props { - alertParams: ErrorRateAlertTriggerParams; + alertParams: AlertParams; setAlertParams: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; } -export function ErrorRateAlertTrigger(props: Props) { +export function ErrorCountAlertTrigger(props: Props) { const { setAlertParams, setAlertProperty, alertParams } = props; const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); @@ -51,45 +48,20 @@ export function ErrorRateAlertTrigger(props: Props) { ...alertParams, }; - const threshold = isFinite(params.threshold) ? params.threshold : ''; - const fields = [ - - - setAlertParams( - 'environment', - e.target.value as ErrorRateAlertTriggerParams['environment'] - ) - } - compressed - /> - , - , + setAlertParams('environment', e.target.value)} + />, + - - setAlertParams('threshold', parseInt(e.target.value, 10)) - } - compressed - append={i18n.translate('xpack.apm.errorRateAlertTrigger.errors', { - defaultMessage: 'errors', - })} - /> - , + onChange={(value) => setAlertParams('threshold', value)} + />, setAlertParams('windowSize', windowSize || '') @@ -108,7 +80,7 @@ export function ErrorRateAlertTrigger(props: Props) { return ( void; setAlertProperty: (key: string, value: any) => void; } export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; - const { serviceName } = alertParams; const { urlParams } = useUrlParams(); - const transactionTypes = useServiceTransactionTypes(urlParams); - - const { start, end } = urlParams; + const { serviceName } = useParams<{ serviceName?: string }>(); + const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); - if (!transactionTypes.length) { + if (!transactionTypes.length || !serviceName) { return null; } @@ -57,7 +77,9 @@ export function TransactionDurationAlertTrigger(props: Props) { aggregationType: 'avg', windowSize: 5, windowUnit: 'm', - transactionType: transactionTypes[0], + + // use the current transaction type or default to the first in the list + transactionType: transactionType || transactionTypes[0], environment: urlParams.environment || ENVIRONMENT_ALL.value, }; @@ -67,47 +89,17 @@ export function TransactionDurationAlertTrigger(props: Props) { }; const fields = [ - - - setAlertParams('environment', e.target.value as Params['environment']) - } - compressed - /> - , - - { - return { - text: key, - value: key, - }; - })} - onChange={(e) => - setAlertParams( - 'transactionType', - e.target.value as Params['transactionType'] - ) - } - compressed - /> - , + , + setAlertParams('environment', e.target.value)} + />, + ({ text: key, value: key }))} + onChange={(e) => setAlertParams('transactionType', e.target.value)} + />, - setAlertParams( - 'aggregationType', - e.target.value as Params['aggregationType'] - ) - } - compressed - /> - , - - setAlertParams('threshold', e.target.value)} - append={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', { - defaultMessage: 'ms', - })} + onChange={(e) => setAlertParams('aggregationType', e.target.value)} compressed /> , + setAlertParams('threshold', value)} + />, setAlertParams('windowSize', timeWindowSize || '') diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index 20e0a3f27c4a45..fb4cda56fce048 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiExpression, EuiSelect } from '@elastic/eui'; + +import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; @@ -16,14 +17,16 @@ import { AnomalySeverity, SelectAnomalySeverity, } from './SelectAnomalySeverity'; -import { - ENVIRONMENT_ALL, - getEnvironmentLabel, -} from '../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../../common/transaction_types'; +import { + EnvironmentField, + ServiceField, + TransactionTypeField, +} from '../fields'; interface Params { windowSize: number; @@ -42,9 +45,9 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; - const { serviceName } = alertParams; const { urlParams } = useUrlParams(); const transactionTypes = useServiceTransactionTypes(urlParams); + const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); const supportedTransactionTypes = transactionTypes.filter((transactionType) => @@ -55,10 +58,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { return null; } + // 'page-load' for RUM, 'request' otherwise + const transactionType = supportedTransactionTypes[0]; + const defaults: Params = { windowSize: 15, windowUnit: 'm', - transactionType: supportedTransactionTypes[0], // 'page-load' for RUM, 'request' otherwise + transactionType, serviceName, environment: urlParams.environment || ENVIRONMENT_ALL.value, anomalyScore: 75, @@ -70,31 +76,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { }; const fields = [ - , + , + setAlertParams('environment', e.target.value)} />, - - setAlertParams('environment', e.target.value)} - compressed - /> - , } title={i18n.translate( diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx new file mode 100644 index 00000000000000..4dbf4dc10a9076 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -0,0 +1,110 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import React from 'react'; +import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; +import { useEnvironments } from '../../../hooks/useEnvironments'; +import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; + +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { + ServiceField, + TransactionTypeField, + EnvironmentField, + IsAboveField, +} from '../fields'; + +interface AlertParams { + windowSize: number; + windowUnit: string; + threshold: number; + serviceName: string; + transactionType: string; + environment: string; +} + +interface Props { + alertParams: AlertParams; + setAlertParams: (key: string, value: any) => void; + setAlertProperty: (key: string, value: any) => void; +} + +export function TransactionErrorRateAlertTrigger(props: Props) { + const { setAlertParams, alertParams, setAlertProperty } = props; + const { urlParams } = useUrlParams(); + const transactionTypes = useServiceTransactionTypes(urlParams); + const { serviceName } = useParams<{ serviceName?: string }>(); + const { start, end, transactionType } = urlParams; + const { environmentOptions } = useEnvironments({ serviceName, start, end }); + + if (!transactionTypes.length || !serviceName) { + return null; + } + + const defaultParams = { + threshold: 30, + windowSize: 5, + windowUnit: 'm', + transactionType: transactionType || transactionTypes[0], + environment: urlParams.environment || ENVIRONMENT_ALL.value, + }; + + const params = { + ...defaultParams, + ...alertParams, + }; + + const fields = [ + , + setAlertParams('environment', e.target.value)} + />, + ({ text: key, value: key }))} + onChange={(e) => setAlertParams('transactionType', e.target.value)} + />, + setAlertParams('threshold', value)} + />, + + setAlertParams('windowSize', timeWindowSize || '') + } + onChangeWindowUnit={(timeWindowUnit) => + setAlertParams('windowUnit', timeWindowUnit) + } + timeWindowSize={params.windowSize} + timeWindowUnit={params.windowUnit} + errors={{ + timeWindowSize: [], + timeWindowUnit: [], + }} + />, + ]; + + return ( + + ); +} + +// Default export is required for React.lazy loading +// +// eslint-disable-next-line import/no-default-export +export default TransactionErrorRateAlertTrigger; diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx new file mode 100644 index 00000000000000..e145d03671a180 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -0,0 +1,108 @@ +/* + * 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 { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSelectOption } from '@elastic/eui'; +import { getEnvironmentLabel } from '../../../common/environment_filter_values'; +import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression'; + +export function ServiceField({ value }: { value?: string }) { + return ( + + ); +} + +export function EnvironmentField({ + currentValue, + options, + onChange, +}: { + currentValue: string; + options: EuiSelectOption[]; + onChange: (event: React.ChangeEvent) => void; +}) { + return ( + + + + ); +} + +export function TransactionTypeField({ + currentValue, + options, + onChange, +}: { + currentValue: string; + options?: EuiSelectOption[]; + onChange?: (event: React.ChangeEvent) => void; +}) { + const label = i18n.translate('xpack.apm.alerting.fields.type', { + defaultMessage: 'Type', + }); + + if (!options || options.length === 1) { + return ; + } + + return ( + + + + ); +} + +export function IsAboveField({ + value, + unit, + onChange, + step, +}: { + value: number; + unit: string; + onChange: (value: number) => void; + step?: number; +}) { + return ( + + onChange(parseInt(e.target.value, 10))} + append={unit} + compressed + step={step} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts new file mode 100644 index 00000000000000..c0a1955e2cc8a4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { lazy } from 'react'; +import { AlertType } from '../../../common/alert_types'; +import { ApmPluginStartDeps } from '../../plugin'; + +export function registerApmAlerts( + alertTypeRegistry: ApmPluginStartDeps['triggers_actions_ui']['alertTypeRegistry'] +) { + alertTypeRegistry.register({ + id: AlertType.ErrorCount, + name: i18n.translate('xpack.apm.alertTypes.errorCount', { + defaultMessage: 'Error count threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionDuration, + name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { + defaultMessage: 'Transaction duration threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionDurationAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionErrorRate, + name: i18n.translate('xpack.apm.alertTypes.transactionErrorRate', { + defaultMessage: 'Transaction error rate threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionErrorRateAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionDurationAnomaly, + name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { + defaultMessage: 'Transaction duration anomaly', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionDurationAnomalyAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx index 27c4a37e09c008..c11bfdeae945be 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -24,9 +24,13 @@ const transactionDurationLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', { defaultMessage: 'Transaction duration' } ); -const errorRateLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.errorRate', - { defaultMessage: 'Error rate' } +const transactionErrorRateLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.transactionErrorRate', + { defaultMessage: 'Transaction error rate' } +); +const errorCountLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.errorCount', + { defaultMessage: 'Error count' } ); const createThresholdAlertLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert', @@ -38,8 +42,10 @@ const createAnomalyAlertAlertLabel = i18n.translate( ); const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = - 'create_transaction_duration'; -const CREATE_ERROR_RATE_ALERT_PANEL_ID = 'create_error_rate'; + 'create_transaction_duration_panel'; +const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID = + 'create_transaction_error_rate_panel'; +const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel'; interface Props { canReadAlerts: boolean; @@ -77,7 +83,14 @@ export function AlertIntegrations(props: Props) { name: transactionDurationLabel, panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, }, - { name: errorRateLabel, panel: CREATE_ERROR_RATE_ALERT_PANEL_ID }, + { + name: transactionErrorRateLabel, + panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + }, + { + name: errorCountLabel, + panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + }, ] : []), ...(canReadAlerts @@ -96,10 +109,13 @@ export function AlertIntegrations(props: Props) { : []), ], }, + + // transaction duration panel { id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, title: transactionDurationLabel, items: [ + // threshold alerts { name: createThresholdAlertLabel, onClick: () => { @@ -107,6 +123,8 @@ export function AlertIntegrations(props: Props) { setPopoverOpen(false); }, }, + + // anomaly alerts ...(canReadAnomalies ? [ { @@ -120,14 +138,32 @@ export function AlertIntegrations(props: Props) { : []), ], }, + + // transaction error rate panel + { + id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + title: transactionErrorRateLabel, + items: [ + // threshold alerts + { + name: createThresholdAlertLabel, + onClick: () => { + setAlertType(AlertType.TransactionErrorRate); + setPopoverOpen(false); + }, + }, + ], + }, + + // error alerts panel { - id: CREATE_ERROR_RATE_ALERT_PANEL_ID, - title: errorRateLabel, + id: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + title: errorCountLabel, items: [ { name: createThresholdAlertLabel, onClick: () => { - setAlertType(AlertType.ErrorRate); + setAlertType(AlertType.ErrorCount); setPopoverOpen(false); }, }, diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 51ac6673251fb8..ab3f1026a92dd4 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -import { lazy } from 'react'; import { ConfigSchema } from '.'; import { FetchDataParams, @@ -34,10 +32,10 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; -import { AlertType } from '../common/alert_types'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { registerApmAlerts } from './components/alerting/register_apm_alerts'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -147,51 +145,6 @@ export class ApmPlugin implements Plugin { } public start(core: CoreStart, plugins: ApmPluginStartDeps) { toggleAppLinkInNav(core, this.initializerContext.config.get()); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.ErrorRate, - name: i18n.translate('xpack.apm.alertTypes.errorRate', { - defaultMessage: 'Error rate', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => import('./components/shared/ErrorRateAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDuration, - name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { - defaultMessage: 'Transaction duration', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => import('./components/shared/TransactionDurationAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDurationAnomaly, - name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { - defaultMessage: 'Transaction duration anomaly', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => - import('./components/shared/TransactionDurationAnomalyAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); + registerApmAlerts(plugins.triggers_actions_ui.alertTypeRegistry); } } diff --git a/x-pack/plugins/apm/server/lib/alerts/action_variables.ts b/x-pack/plugins/apm/server/lib/alerts/action_variables.ts new file mode 100644 index 00000000000000..f2558da3a30e48 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/action_variables.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const apmActionVariables = { + serviceName: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.serviceName', + { defaultMessage: 'The service the alert is created for' } + ), + name: 'serviceName', + }, + transactionType: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.transactionType', + { defaultMessage: 'The transaction type the alert is created for' } + ), + name: 'transactionType', + }, + environment: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.environment', + { defaultMessage: 'The transaction type the alert is created for' } + ), + name: 'environment', + }, + threshold: { + description: i18n.translate('xpack.apm.alerts.action_variables.threshold', { + defaultMessage: + 'Any trigger value above this value will cause the alert to fire', + }), + name: 'threshold', + }, + triggerValue: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.triggerValue', + { + defaultMessage: + 'The value that breached the threshold and triggered the alert', + } + ), + name: 'triggerValue', + }, +}; 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 44ca80143bcd9a..fcbb4cc5950e06 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 @@ -9,9 +9,10 @@ import { AlertingPlugin } from '../../../../alerts/server'; import { ActionsPlugin } from '../../../../actions/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; -import { registerErrorRateAlertType } from './register_error_rate_alert_type'; +import { registerErrorCountAlertType } from './register_error_count_alert_type'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; +import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type'; interface Params { alerts: AlertingPlugin['setup']; @@ -30,7 +31,11 @@ export function registerApmAlerts(params: Params) { ml: params.ml, config$: params.config$, }); - registerErrorRateAlertType({ + registerErrorCountAlertType({ + alerts: params.alerts, + config$: params.config$, + }); + registerTransactionErrorRateAlertType({ 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_count_alert_type.ts similarity index 66% rename from x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts rename to x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 61e3dfee420a51..5455cd9f6a4951 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_count_alert_type.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { ESSearchResponse, @@ -17,11 +17,11 @@ import { import { PROCESSOR_EVENT, SERVICE_NAME, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -29,21 +29,21 @@ interface RegisterAlertParams { } const paramsSchema = schema.object({ - serviceName: schema.string(), windowSize: schema.number(), windowUnit: schema.string(), threshold: schema.number(), + serviceName: schema.string(), environment: schema.string(), }); -const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorRate]; +const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorCount]; -export function registerErrorRateAlertType({ +export function registerErrorCountAlertType({ alerts, config$, }: RegisterAlertParams) { alerts.registerType({ - id: AlertType.ErrorRate, + id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, defaultActionGroupId: alertTypeConfig.defaultActionGroupId, @@ -52,37 +52,26 @@ export function registerErrorRateAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerErrorRateAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, + apmActionVariables.serviceName, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, ], }, producer: 'apm', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); - - const alertParams = params as TypeOf; - + const alertParams = params; const indices = await getApmIndices({ config, savedObjectsClient: services.savedObjectsClient, }); - const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL.value - ? [] - : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; - const searchParams = { index: indices['apm_oss.errorIndices'], size: 0, body: { + track_total_hits: true, query: { bool: { filter: [ @@ -93,21 +82,12 @@ export function registerErrorRateAlertType({ }, }, }, - { - term: { - [PROCESSOR_EVENT]: 'error', - }, - }, - { - term: { - [SERVICE_NAME]: alertParams.serviceName, - }, - }, - ...environmentTerm, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + ...getEnvironmentUiFilterES(alertParams.environment), ], }, }, - track_total_hits: true, }, }; @@ -116,18 +96,19 @@ export function registerErrorRateAlertType({ ESSearchRequest > = await services.callCluster('search', searchParams); - const value = response.hits.total.value; + const errorCount = response.hits.total.value; - if (value && value > alertParams.threshold) { + if (errorCount > alertParams.threshold) { const alertInstance = services.alertInstanceFactory( - AlertType.ErrorRate + AlertType.ErrorCount ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { serviceName: alertParams.serviceName, + environment: alertParams.environment, + threshold: alertParams.threshold, + triggerValue: errorCount, }); } - - return {}; }, }); } 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 ead28c325692d6..373d4bd4da832f 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 @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { ProcessorEvent } from '../../../common/processor_event'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { ESSearchResponse } from '../../../typings/elasticsearch'; import { @@ -16,11 +15,12 @@ import { SERVICE_NAME, TRANSACTION_TYPE, TRANSACTION_DURATION, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -57,42 +57,22 @@ export function registerTransactionDurationAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAlertType.variables.transactionType', - { - defaultMessage: 'Transaction type', - } - ), - name: 'transactionType', - }, + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, ], }, producer: 'apm', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); - - const alertParams = params as TypeOf; - + const alertParams = params; const indices = await getApmIndices({ config, savedObjectsClient: services.savedObjectsClient, }); - const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL.value - ? [] - : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; - const searchParams = { index: indices['apm_oss.transactionIndices'], size: 0, @@ -107,33 +87,17 @@ export function registerTransactionDurationAlertType({ }, }, }, - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - }, - { - term: { - [SERVICE_NAME]: alertParams.serviceName, - }, - }, - { - term: { - [TRANSACTION_TYPE]: alertParams.transactionType, - }, - }, - ...environmentTerm, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, + ...getEnvironmentUiFilterES(alertParams.environment), ], }, }, aggs: { agg: alertParams.aggregationType === 'avg' - ? { - avg: { - field: TRANSACTION_DURATION, - }, - } + ? { avg: { field: TRANSACTION_DURATION } } : { percentiles: { field: TRANSACTION_DURATION, @@ -157,19 +121,23 @@ export function registerTransactionDurationAlertType({ const { agg } = response.aggregations; - const value = 'values' in agg ? Object.values(agg.values)[0] : agg?.value; + const transactionDuration = + 'values' in agg ? Object.values(agg.values)[0] : agg?.value; - if (value && value > alertParams.threshold * 1000) { + const threshold = alertParams.threshold * 1000; + + if (transactionDuration && transactionDuration > threshold) { const alertInstance = services.alertInstanceFactory( AlertType.TransactionDuration ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { transactionType: alertParams.transactionType, serviceName: alertParams.serviceName, + environment: alertParams.environment, + threshold, + triggerValue: transactionDuration, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 93af51b572aa57..b3526b6a97ad9f 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; -import { i18n } from '@kbn/i18n'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { AlertingPlugin } from '../../../../alerts/server'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; import { getMLJobIds } from '../service_map/get_service_anomalies'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -47,24 +47,9 @@ export function registerTransactionDurationAnomalyAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.transactionType', - { - defaultMessage: 'Transaction type', - } - ), - name: 'transactionType', - }, + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, ], }, producer: 'apm', @@ -72,7 +57,7 @@ export function registerTransactionDurationAnomalyAlertType({ if (!ml) { return; } - const alertParams = params as TypeOf; + const alertParams = params; const request = {} as KibanaRequest; const { mlAnomalySearch } = ml.mlSystemProvider(request); const anomalyDetectors = ml.anomalyDetectorsProvider(request); @@ -88,6 +73,7 @@ export function registerTransactionDurationAnomalyAlertType({ const anomalySearchParams = { body: { + terminateAfter: 1, size: 0, query: { bool: { @@ -131,10 +117,10 @@ export function registerTransactionDurationAnomalyAlertType({ ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { serviceName: alertParams.serviceName, + transactionType: alertParams.transactionType, + environment: alertParams.environment, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts new file mode 100644 index 00000000000000..a6ed40fc15ec63 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -0,0 +1,131 @@ +/* + * 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 { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { EventOutcome } from '../../../common/event_outcome'; +import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { ESSearchResponse } from '../../../typings/elasticsearch'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE, + EVENT_OUTCOME, +} from '../../../common/elasticsearch_fieldnames'; +import { AlertingPlugin } from '../../../../alerts/server'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { APMConfig } from '../..'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { apmActionVariables } from './action_variables'; + +interface RegisterAlertParams { + alerts: AlertingPlugin['setup']; + config$: Observable; +} + +const paramsSchema = schema.object({ + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + transactionType: schema.string(), + serviceName: schema.string(), + environment: schema.string(), +}); + +const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate]; + +export function registerTransactionErrorRateAlertType({ + alerts, + config$, +}: RegisterAlertParams) { + alerts.registerType({ + id: AlertType.TransactionErrorRate, + name: alertTypeConfig.name, + actionGroups: alertTypeConfig.actionGroups, + defaultActionGroupId: alertTypeConfig.defaultActionGroupId, + validate: { + params: paramsSchema, + }, + actionVariables: { + context: [ + apmActionVariables.transactionType, + apmActionVariables.serviceName, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, + ], + }, + producer: 'apm', + executor: async ({ services, params: alertParams }) => { + const config = await config$.pipe(take(1)).toPromise(); + const indices = await getApmIndices({ + config, + savedObjectsClient: services.savedObjectsClient, + }); + + const searchParams = { + index: indices['apm_oss.transactionIndices'], + size: 0, + body: { + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, + }, + }, + }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, + ...getEnvironmentUiFilterES(alertParams.environment), + ], + }, + }, + aggs: { + erroneous_transactions: { + filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + }, + }, + }, + }; + + const response: ESSearchResponse< + unknown, + typeof searchParams + > = await services.callCluster('search', searchParams); + + if (!response.aggregations) { + return; + } + + const errornousTransactionsCount = + response.aggregations.erroneous_transactions.doc_count; + const totalTransactionCount = response.hits.total.value; + const transactionErrorRate = + (errornousTransactionsCount / totalTransactionCount) * 100; + + if (transactionErrorRate > alertParams.threshold) { + const alertInstance = services.alertInstanceFactory( + AlertType.TransactionErrorRate + ); + + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { + serviceName: alertParams.serviceName, + transactionType: alertParams.transactionType, + environment: alertParams.environment, + threshold: alertParams.threshold, + triggerValue: transactionErrorRate, + }); + } + }, + }); +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 7bcd945d890ad3..d0673335387c64 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -8,6 +8,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; import Boom from 'boom'; +import { ProcessorEvent } from '../../../common/processor_event'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; @@ -79,7 +80,7 @@ async function createAnomalyDetectionJob({ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { exists: { field: TRANSACTION_DURATION } }, ...getEnvironmentUiFilterES(environment), ], diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index a53068d152d03a..fcd4f468d4367f 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -85,7 +85,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: { '@timestamp': { gte: start, lt: end } } }, ], }, @@ -606,7 +606,10 @@ export const tasks: TelemetryTask[] = [ timeout, query: { bool: { - filter: [{ term: { [PROCESSOR_EVENT]: 'error' } }, range1d], + filter: [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + range1d, + ], }, }, aggs: { @@ -640,7 +643,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, range1d, ], }, @@ -674,7 +677,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, range1d, ], must_not: { diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts index 6ff98a9be75f90..ea8d02eb833cfd 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts @@ -5,11 +5,14 @@ */ import { ESFilter } from '../../../../typings/elasticsearch'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; +import { + ENVIRONMENT_NOT_DEFINED, + ENVIRONMENT_ALL, +} from '../../../../common/environment_filter_values'; import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; export function getEnvironmentUiFilterES(environment?: string): ESFilter[] { - if (!environment) { + if (!environment || environment === ENVIRONMENT_ALL.value) { return []; } if (environment === ENVIRONMENT_NOT_DEFINED.value) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 1a7d6028823953..f25062c67f87ad 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -67,7 +67,7 @@ export async function getPageViewTrends({ x: xVal, y: bCount, }; - if (breakdownItem) { + if ('breakdown' in bucket) { const categoryBuckets = bucket.breakdown.buckets; categoryBuckets.forEach(({ key, doc_count: docCount }) => { if (key === 'Other') { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 82666bfaf45196..916dab88d8b735 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4616,7 +4616,6 @@ "xpack.apm.agentMetrics.java.threadCount": "平均カウント", "xpack.apm.agentMetrics.java.threadCountChartTitle": "スレッド数", "xpack.apm.agentMetrics.java.threadCountMax": "最高カウント", - "xpack.apm.alertTypes.errorRate": "エラー率", "xpack.apm.alertTypes.transactionDuration": "トランザクション期間", "xpack.apm.anomaly_detection.error.invalid_license": "異常検知を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。このライセンスがあれば、機械学習を活用して、サービスを監視できます。", "xpack.apm.anomaly_detection.error.missing_read_privileges": "異常検知ジョブを表示するには、機械学習およびAPMの「読み取り」権限が必要です", @@ -4675,11 +4674,6 @@ "xpack.apm.errorGroupDetails.relatedTransactionSample": "関連トランザクションサンプル", "xpack.apm.errorGroupDetails.unhandledLabel": "未対応", "xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "ディスカバリで {occurrencesCount} 件の{occurrencesCount, plural, one {ドキュメント} other {ドキュメント}}を表示。", - "xpack.apm.errorRateAlert.name": "エラー率", - "xpack.apm.errorRateAlert.thresholdMet": "しきい値一致", - "xpack.apm.errorRateAlertTrigger.environment": "環境", - "xpack.apm.errorRateAlertTrigger.errors": "エラー", - "xpack.apm.errorRateAlertTrigger.isAbove": "の下限は", "xpack.apm.errorRateChart.avgLabel": "平均", "xpack.apm.errorRateChart.rateLabel": "レート", "xpack.apm.errorRateChart.title": "トランザクションエラー率", @@ -4781,9 +4775,6 @@ "xpack.apm.propertiesTable.tabs.logStacktraceLabel": "スタックトレース", "xpack.apm.propertiesTable.tabs.metadataLabel": "メタデータ", "xpack.apm.propertiesTable.tabs.timelineLabel": "タイムライン", - "xpack.apm.registerErrorRateAlertType.variables.serviceName": "サービス名", - "xpack.apm.registerTransactionDurationAlertType.variables.serviceName": "サービス名", - "xpack.apm.registerTransactionDurationAlertType.variables.transactionType": "トランザクションタイプ", "xpack.apm.rum.dashboard.backend": "バックエンド", "xpack.apm.rum.dashboard.frontend": "フロントエンド", "xpack.apm.rum.dashboard.overall.label": "全体", @@ -4801,7 +4792,6 @@ "xpack.apm.selectPlaceholder": "オプションを選択:", "xpack.apm.serviceDetails.alertsMenu.alerts": "アラート", "xpack.apm.serviceDetails.alertsMenu.createThresholdAlert": "しきい値アラートを作成", - "xpack.apm.serviceDetails.alertsMenu.errorRate": "エラー率", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "トランザクション期間", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "アクティブアラートを表示", "xpack.apm.serviceDetails.errorsTabLabel": "エラー", @@ -5031,11 +5021,7 @@ "xpack.apm.transactionDurationAlert.aggregationType.99th": "99 パーセンタイル", "xpack.apm.transactionDurationAlert.aggregationType.avg": "平均", "xpack.apm.transactionDurationAlert.name": "トランザクション期間", - "xpack.apm.transactionDurationAlert.thresholdMet": "しきい値一致", - "xpack.apm.transactionDurationAlertTrigger.environment": "環境", - "xpack.apm.transactionDurationAlertTrigger.isAbove": "の下限は", "xpack.apm.transactionDurationAlertTrigger.ms": "ms", - "xpack.apm.transactionDurationAlertTrigger.type": "タイプ", "xpack.apm.transactionDurationAlertTrigger.when": "タイミング", "xpack.apm.transactionDurationLabel": "期間", "xpack.apm.transactions.chart.95thPercentileLabel": "95 パーセンタイル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2c0653a3714a62..ffaf281487fd0c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4617,7 +4617,6 @@ "xpack.apm.agentMetrics.java.threadCount": "平均计数", "xpack.apm.agentMetrics.java.threadCountChartTitle": "线程计数", "xpack.apm.agentMetrics.java.threadCountMax": "最大计数", - "xpack.apm.alertTypes.errorRate": "错误率", "xpack.apm.alertTypes.transactionDuration": "事务持续时间", "xpack.apm.anomaly_detection.error.invalid_license": "要使用异常检测,必须订阅 Elastic 白金级许可证。有了该许可证,您便可借助 Machine Learning 监测服务。", "xpack.apm.anomaly_detection.error.missing_read_privileges": "必须对 Machine Learning 和 APM 具有“读”权限,才能查看“异常检测”作业", @@ -4676,11 +4675,6 @@ "xpack.apm.errorGroupDetails.relatedTransactionSample": "相关的事务样本", "xpack.apm.errorGroupDetails.unhandledLabel": "未处理", "xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "在 Discover 查看 {occurrencesCount} 个 {occurrencesCount, plural, one {匹配项} other {匹配项}}。", - "xpack.apm.errorRateAlert.name": "错误率", - "xpack.apm.errorRateAlert.thresholdMet": "阈值已达到", - "xpack.apm.errorRateAlertTrigger.environment": "环境", - "xpack.apm.errorRateAlertTrigger.errors": "错误", - "xpack.apm.errorRateAlertTrigger.isAbove": "高于", "xpack.apm.errorRateChart.avgLabel": "平均", "xpack.apm.errorRateChart.rateLabel": "比率", "xpack.apm.errorRateChart.title": "事务错误率", @@ -4784,9 +4778,6 @@ "xpack.apm.propertiesTable.tabs.logStacktraceLabel": "日志堆栈跟踪", "xpack.apm.propertiesTable.tabs.metadataLabel": "元数据", "xpack.apm.propertiesTable.tabs.timelineLabel": "时间线", - "xpack.apm.registerErrorRateAlertType.variables.serviceName": "服务名称", - "xpack.apm.registerTransactionDurationAlertType.variables.serviceName": "服务名称", - "xpack.apm.registerTransactionDurationAlertType.variables.transactionType": "事务类型", "xpack.apm.rum.dashboard.backend": "后端", "xpack.apm.rum.dashboard.frontend": "前端", "xpack.apm.rum.dashboard.overall.label": "总体", @@ -4804,7 +4795,6 @@ "xpack.apm.selectPlaceholder": "选择选项:", "xpack.apm.serviceDetails.alertsMenu.alerts": "告警", "xpack.apm.serviceDetails.alertsMenu.createThresholdAlert": "创建阈值告警", - "xpack.apm.serviceDetails.alertsMenu.errorRate": "错误率", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "事务持续时间", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "查看活动的告警", "xpack.apm.serviceDetails.errorsTabLabel": "错误", @@ -5034,11 +5024,7 @@ "xpack.apm.transactionDurationAlert.aggregationType.99th": "第 99 个百分位", "xpack.apm.transactionDurationAlert.aggregationType.avg": "平均值", "xpack.apm.transactionDurationAlert.name": "事务持续时间", - "xpack.apm.transactionDurationAlert.thresholdMet": "阈值已达到", - "xpack.apm.transactionDurationAlertTrigger.environment": "环境", - "xpack.apm.transactionDurationAlertTrigger.isAbove": "高于", "xpack.apm.transactionDurationAlertTrigger.ms": "ms", - "xpack.apm.transactionDurationAlertTrigger.type": "类型", "xpack.apm.transactionDurationAlertTrigger.when": "当", "xpack.apm.transactionDurationLabel": "持续时间", "xpack.apm.transactions.chart.95thPercentileLabel": "第 95 个百分位", From 54a2d90aac1506eb23c62dc5ddf79e4785556d25 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 17 Sep 2020 13:53:10 -0700 Subject: [PATCH 21/30] [build] Use Elastic hosted UBI minimal base image (#77776) Signed-off-by: Tyler Smalley --- src/dev/build/tasks/os_packages/docker_generator/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 362c34d416743a..19487efe1366c9 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -40,7 +40,7 @@ export async function runDockerGenerator( ubi: boolean = false ) { // UBI var config - const baseOSImage = ubi ? 'registry.access.redhat.com/ubi8/ubi-minimal:latest' : 'centos:8'; + const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; const ubiVersionTag = 'ubi8'; const ubiImageFlavor = ubi ? `-${ubiVersionTag}` : ''; From aba1494b37734dcee5137f6f8a93bbcd3f1543e1 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 17 Sep 2020 15:54:37 -0500 Subject: [PATCH 22/30] management/update trusted_apps jest snapshot --- .../view/__snapshots__/trusted_apps_page.test.tsx.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap index 642e86059ed6e6..c8d9b46d5a0d28 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -297,13 +297,13 @@ Object { class="sc-fzoyAV jWxvlI siemWrapperPage" >

Beta @@ -330,7 +330,7 @@ Object {