diff --git a/package.json b/package.json index 808c068cb878a..67a78a92d9582 100644 --- a/package.json +++ b/package.json @@ -481,7 +481,7 @@ "compare-versions": "3.5.1", "constate": "^3.3.2", "copy-to-clipboard": "^3.0.8", - "core-js": "^3.26.0", + "core-js": "^3.26.1", "cronstrue": "^1.51.0", "cuid": "^2.1.8", "cytoscape": "^3.10.0", @@ -688,7 +688,7 @@ "@babel/core": "^7.20.2", "@babel/eslint-parser": "^7.19.1", "@babel/eslint-plugin": "^7.19.1", - "@babel/generator": "^7.20.3", + "@babel/generator": "^7.20.4", "@babel/helper-plugin-utils": "^7.20.2", "@babel/parser": "^7.20.3", "@babel/plugin-proposal-class-properties": "^7.18.6", @@ -809,7 +809,7 @@ "@types/apidoc": "^0.22.3", "@types/archiver": "^5.3.1", "@types/async": "^3.2.3", - "@types/babel__core": "^7.1.19", + "@types/babel__core": "^7.1.20", "@types/babel__generator": "^7.6.4", "@types/babel__helper-plugin-utils": "^7.10.0", "@types/base64-js": "^1.2.5", @@ -922,7 +922,7 @@ "@types/redux-logger": "^3.0.8", "@types/resolve": "^1.20.1", "@types/seedrandom": ">=2.0.0 <4.0.0", - "@types/selenium-webdriver": "^4.1.6", + "@types/selenium-webdriver": "^4.1.9", "@types/semver": "^7", "@types/set-value": "^2.0.0", "@types/sharp": "^0.30.4", @@ -978,7 +978,7 @@ "callsites": "^3.1.0", "chance": "1.0.18", "chokidar": "^3.5.3", - "chromedriver": "^107.0.2", + "chromedriver": "^107.0.3", "clean-webpack-plugin": "^3.0.0", "compression-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^6.0.2", @@ -1100,7 +1100,7 @@ "resolve": "^1.22.0", "rxjs-marbles": "^7.0.1", "sass-loader": "^10.3.1", - "selenium-webdriver": "^4.5.0", + "selenium-webdriver": "^4.6.0", "simple-git": "^3.10.0", "sinon": "^7.4.2", "sort-package-json": "^1.53.1", diff --git a/packages/kbn-babel-preset/node_preset.js b/packages/kbn-babel-preset/node_preset.js index dfbca5a364f59..aa413a05013fc 100644 --- a/packages/kbn-babel-preset/node_preset.js +++ b/packages/kbn-babel-preset/node_preset.js @@ -31,7 +31,7 @@ module.exports = (_, options = {}) => { // Because of that we should use for that value the same version we install // in the package.json in order to have the same polyfills between the environment // and the tests - corejs: '3.26.0', + corejs: '3.26.1', bugfixes: true, ...(options['@babel/preset-env'] || {}), diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index d7359345bf22e..75ceab91d8af5 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -18,7 +18,7 @@ module.exports = (_, options = {}) => { modules: false, // Please read the explanation for this // in node_preset.js - corejs: '3.26.0', + corejs: '3.26.1', bugfixes: true, }, ], diff --git a/packages/kbn-securitysolution-exception-list-components/src/translations.ts b/packages/kbn-securitysolution-exception-list-components/src/translations.ts index 38cb15f4b742e..7dfebe523c226 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/translations.ts +++ b/packages/kbn-securitysolution-exception-list-components/src/translations.ts @@ -11,14 +11,14 @@ import { i18n } from '@kbn/i18n'; export const EMPTY_VIEWER_STATE_EMPTY_TITLE = i18n.translate( 'exceptionList-components.empty.viewer.state.empty.title', { - defaultMessage: 'Add exceptions to this rule', + defaultMessage: 'Add exceptions to this list', } ); export const EMPTY_VIEWER_STATE_EMPTY_BODY = i18n.translate( 'exceptionList-components.empty.viewer.state.empty.body', { - defaultMessage: 'There is no exception in your rule. Create your first rule exception.', + defaultMessage: 'There is no exception in your list. Create your first exception.', } ); export const EMPTY_VIEWER_STATE_EMPTY_SEARCH_TITLE = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx index 564cddd1af89c..a2a4b756d4c5b 100644 --- a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx @@ -70,7 +70,12 @@ export function GeneralSettings() { return ( <> +

{i18n.translate('xpack.apm.apmSettings.kibanaLink.label', { - defaultMessage: 'Kibana advanced settings', + defaultMessage: 'Kibana advanced settings.', })} ), }} /> - } - iconType="iInCircle" - /> +

+
{apmSettingsKeys.map((settingKey) => { const editableConfig = settingsEditableConfig[settingKey]; diff --git a/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx b/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx index cb49cd3372340..a9ba3ed9cc4f5 100644 --- a/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.test.tsx @@ -193,4 +193,154 @@ describe('useTagsAction', () => { ); }); }); + + it('do not update cases with no changes', async () => { + const updateSpy = jest.spyOn(api, 'updateCases'); + + const { result, waitFor } = renderHook( + () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + const action = result.current.getAction([{ ...basicCase, tags: [] }]); + + act(() => { + action.onClick(); + }); + + expect(onAction).toHaveBeenCalled(); + expect(result.current.isFlyoutOpen).toBe(true); + + act(() => { + result.current.onSaveTags({ selectedTags: [], unSelectedTags: ['pepsi'] }); + }); + + await waitFor(() => { + expect(result.current.isFlyoutOpen).toBe(false); + expect(onActionSuccess).not.toHaveBeenCalled(); + expect(updateSpy).not.toHaveBeenCalled(); + }); + }); + + it('do not update if the selected tags are the same but with different order', async () => { + const updateSpy = jest.spyOn(api, 'updateCases'); + + const { result, waitFor } = renderHook( + () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + const action = result.current.getAction([{ ...basicCase, tags: ['1', '2'] }]); + + act(() => { + action.onClick(); + }); + + expect(onAction).toHaveBeenCalled(); + expect(result.current.isFlyoutOpen).toBe(true); + + act(() => { + result.current.onSaveTags({ selectedTags: ['2', '1'], unSelectedTags: [] }); + }); + + await waitFor(() => { + expect(result.current.isFlyoutOpen).toBe(false); + expect(onActionSuccess).not.toHaveBeenCalled(); + expect(updateSpy).not.toHaveBeenCalled(); + }); + }); + + it('do not update if the selected tags are the same', async () => { + const updateSpy = jest.spyOn(api, 'updateCases'); + + const { result, waitFor } = renderHook( + () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + const action = result.current.getAction([{ ...basicCase, tags: ['1'] }]); + + act(() => { + action.onClick(); + }); + + expect(onAction).toHaveBeenCalled(); + expect(result.current.isFlyoutOpen).toBe(true); + + act(() => { + result.current.onSaveTags({ selectedTags: ['1'], unSelectedTags: [] }); + }); + + await waitFor(() => { + expect(result.current.isFlyoutOpen).toBe(false); + expect(onActionSuccess).not.toHaveBeenCalled(); + expect(updateSpy).not.toHaveBeenCalled(); + }); + }); + + it('do not update if selecting and unselecting the same tag', async () => { + const updateSpy = jest.spyOn(api, 'updateCases'); + + const { result, waitFor } = renderHook( + () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + const action = result.current.getAction([{ ...basicCase, tags: ['1'] }]); + + act(() => { + action.onClick(); + }); + + expect(onAction).toHaveBeenCalled(); + expect(result.current.isFlyoutOpen).toBe(true); + + act(() => { + result.current.onSaveTags({ selectedTags: ['1'], unSelectedTags: ['1'] }); + }); + + await waitFor(() => { + expect(result.current.isFlyoutOpen).toBe(false); + expect(onActionSuccess).not.toHaveBeenCalled(); + expect(updateSpy).not.toHaveBeenCalled(); + }); + }); + + it('do not update with empty tags and no selection', async () => { + const updateSpy = jest.spyOn(api, 'updateCases'); + + const { result, waitFor } = renderHook( + () => useTagsAction({ onAction, onActionSuccess, isDisabled: false }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + const action = result.current.getAction([{ ...basicCase, tags: [] }]); + + act(() => { + action.onClick(); + }); + + expect(onAction).toHaveBeenCalled(); + expect(result.current.isFlyoutOpen).toBe(true); + + act(() => { + result.current.onSaveTags({ selectedTags: [], unSelectedTags: [] }); + }); + + await waitFor(() => { + expect(result.current.isFlyoutOpen).toBe(false); + expect(onActionSuccess).not.toHaveBeenCalled(); + expect(updateSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.tsx b/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.tsx index b8c2506cdb99c..4711eb31830ec 100644 --- a/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.tsx +++ b/x-pack/plugins/cases/public/components/actions/tags/use_tags_action.tsx @@ -7,7 +7,8 @@ import { EuiIcon } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; -import { difference } from 'lodash'; +import { difference, isEqual } from 'lodash'; +import type { CaseUpdateRequest } from '../../../../common/ui'; import { useUpdateCases } from '../../../containers/use_bulk_update_case'; import type { Case } from '../../../../common'; import { useCasesContext } from '../../cases_context/use_cases_context'; @@ -33,20 +34,32 @@ export const useTagsAction = ({ onAction, onActionSuccess, isDisabled }: UseActi [onAction] ); + const areTagsEqual = (originalTags: Set, tagsToUpdate: Set): boolean => { + return isEqual(originalTags, tagsToUpdate); + }; + const onSaveTags = useCallback( (tagsSelection: TagsSelectionState) => { onAction(); onFlyoutClosed(); - const casesToUpdate = selectedCasesToEditTags.map((theCase) => { - const tags = difference(theCase.tags, tagsSelection.unSelectedTags); - const uniqueTags = new Set([...tags, ...tagsSelection.selectedTags]); - return { - tags: Array.from(uniqueTags.values()), - id: theCase.id, - version: theCase.version, - }; - }); + const casesToUpdate = selectedCasesToEditTags.reduce((acc, theCase) => { + const tagsWithoutUnselectedTags = difference(theCase.tags, tagsSelection.unSelectedTags); + const uniqueTags = new Set([...tagsWithoutUnselectedTags, ...tagsSelection.selectedTags]); + + if (areTagsEqual(new Set([...theCase.tags]), uniqueTags)) { + return acc; + } + + return [ + ...acc, + { + tags: Array.from(uniqueTags.values()), + id: theCase.id, + version: theCase.version, + }, + ]; + }, [] as CaseUpdateRequest[]); updateCases( { diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 65a350679a8c5..3c0599613cdd1 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -138,7 +138,8 @@ const waitForFormToRender = async (renderer: Screen) => { }); }; -describe('Create case', () => { +// FLAKY: https://github.com/elastic/kibana/issues/142284 +describe.skip('Create case', () => { const refetch = jest.fn(); const onFormSubmitSuccess = jest.fn(); const afterCaseCreated = jest.fn(); @@ -446,7 +447,9 @@ describe('Create case', () => { }); }); - describe('Step 2 - Connector Fields', () => { + // FLAKY: https://github.com/elastic/kibana/issues/143407 + // FLAKY: https://github.com/elastic/kibana/issues/142282 + describe.skip('Step 2 - Connector Fields', () => { it(`should submit and push to Jira connector`, async () => { useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index d7f522358e5bd..b06befcc54929 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -75,7 +75,7 @@ describe('Cases API', () => { }); const data = ['1', '2']; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await deleteCases(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'DELETE', @@ -84,7 +84,7 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await deleteCases(data, abortCtrl.signal); expect(resp).toEqual(''); }); @@ -96,7 +96,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue(actionLicenses); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await getActionLicense(abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/actions/connector_types`, { method: 'GET', @@ -104,7 +104,7 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await getActionLicense(abortCtrl.signal); expect(resp).toEqual(actionLicenses); }); @@ -117,7 +117,7 @@ describe('Cases API', () => { }); const data = basicCase.id; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await getCase(data, true, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, { method: 'GET', @@ -126,12 +126,12 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await getCase(data, true, abortCtrl.signal); expect(resp).toEqual(basicCase); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(caseWithRegisteredAttachmentsSnake); const resp = await getCase(data, true, abortCtrl.signal); expect(resp).toEqual(caseWithRegisteredAttachments); @@ -151,7 +151,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue({ ...basicResolveCase, target_alias_id: targetAliasId }); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await resolveCase(caseId, true, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${caseId}/resolve`, { method: 'GET', @@ -160,12 +160,12 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await resolveCase(caseId, true, abortCtrl.signal); expect(resp).toEqual({ ...basicResolveCase, case: basicCase, targetAliasId }); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue({ ...basicResolveCase, case: caseWithRegisteredAttachmentsSnake, @@ -187,7 +187,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue(allCasesSnake); }); - test('should be called with correct check url, method, signal with empty defaults', async () => { + it('should be called with correct check url, method, signal with empty defaults', async () => { await getCases({ filterOptions: DEFAULT_FILTER_OPTIONS, queryParams: DEFAULT_QUERY_PARAMS, @@ -204,7 +204,7 @@ describe('Cases API', () => { }); }); - test('should applies correct all filters', async () => { + it('should applies correct all filters', async () => { await getCases({ filterOptions: { searchFields: DEFAULT_FILTER_OPTIONS.searchFields, @@ -237,7 +237,7 @@ describe('Cases API', () => { }); }); - test('should apply the severity field correctly (with severity value)', async () => { + it('should apply the severity field correctly (with severity value)', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -258,7 +258,7 @@ describe('Cases API', () => { }); }); - test('should not send the severity field with "all" severity value', async () => { + it('should not send the severity field with "all" severity value', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -278,7 +278,7 @@ describe('Cases API', () => { }); }); - test('should apply the severity field correctly (with status value)', async () => { + it('should apply the severity field correctly (with status value)', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -299,7 +299,7 @@ describe('Cases API', () => { }); }); - test('should not send the severity field with "all" status value', async () => { + it('should not send the severity field with "all" status value', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -319,7 +319,7 @@ describe('Cases API', () => { }); }); - test('should not send the assignees field if it an empty array', async () => { + it('should not send the assignees field if it an empty array', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -339,7 +339,7 @@ describe('Cases API', () => { }); }); - test('should convert a single null value to none', async () => { + it('should convert a single null value to none', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -360,7 +360,7 @@ describe('Cases API', () => { }); }); - test('should converts null value in the array to none', async () => { + it('should converts null value in the array to none', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -381,7 +381,7 @@ describe('Cases API', () => { }); }); - test('should handle tags with weird chars', async () => { + it('should handle tags with weird chars', async () => { const weirdTags: string[] = ['(', '"double"']; await getCases({ @@ -414,7 +414,7 @@ describe('Cases API', () => { }); }); - test('should return correct response and not covert to camel case registered attachments', async () => { + it('should return correct response and not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(allCasesSnake); const resp = await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, @@ -433,7 +433,7 @@ describe('Cases API', () => { fetchMock.mockClear(); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await getCasesStatus({ http, signal: abortCtrl.signal, @@ -446,7 +446,7 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await getCasesStatus({ http, signal: abortCtrl.signal, @@ -463,7 +463,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue(caseUserActionsSnake); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, { method: 'GET', @@ -471,12 +471,12 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(resp).toEqual(caseUserActions); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(caseUserActionsWithRegisteredAttachmentsSnake); const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(resp).toEqual(caseUserActionsWithRegisteredAttachments); @@ -489,7 +489,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue(tags); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { method: 'GET', @@ -500,7 +500,7 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(tags); }); @@ -514,7 +514,7 @@ describe('Cases API', () => { const data = { description: 'updated description' }; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { @@ -526,7 +526,7 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await patchCase( basicCase.id, { description: 'updated description' }, @@ -537,7 +537,7 @@ describe('Cases API', () => { expect(resp).toEqual([basicCase]); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue([caseWithRegisteredAttachmentsSnake]); const resp = await patchCase( basicCase.id, @@ -564,7 +564,7 @@ describe('Cases API', () => { }, ]; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await updateCases(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', @@ -573,10 +573,15 @@ describe('Cases API', () => { }); }); - test('should return correct response should not covert to camel case registered attachments', async () => { + it('should return correct response should not covert to camel case registered attachments', async () => { const resp = await updateCases(data, abortCtrl.signal); expect(resp).toEqual(cases); }); + + it('returns an empty array if the cases are empty', async () => { + const resp = await updateCases([], abortCtrl.signal); + expect(resp).toEqual([]); + }); }); describe('patchComment', () => { @@ -585,7 +590,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue(basicCaseSnake); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -608,7 +613,7 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -620,7 +625,7 @@ describe('Cases API', () => { expect(resp).toEqual(basicCase); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(caseWithRegisteredAttachmentsSnake); const resp = await patchComment({ @@ -657,7 +662,7 @@ describe('Cases API', () => { owner: SECURITY_SOLUTION_OWNER, }; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await postCase(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'POST', @@ -666,12 +671,12 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await postCase(data, abortCtrl.signal); expect(resp).toEqual(basicCase); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(caseWithRegisteredAttachmentsSnake); const resp = await postCase(data, abortCtrl.signal); expect(resp).toEqual(caseWithRegisteredAttachments); @@ -701,7 +706,7 @@ describe('Cases API', () => { }, ]; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await createAttachments(data, basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith( INTERNAL_BULK_CREATE_ATTACHMENTS_URL.replace('{case_id}', basicCase.id), @@ -713,12 +718,12 @@ describe('Cases API', () => { ); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await createAttachments(data, basicCase.id, abortCtrl.signal); expect(resp).toEqual(basicCase); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(caseWithRegisteredAttachmentsSnake); const resp = await createAttachments(data, basicCase.id, abortCtrl.signal); expect(resp).toEqual(caseWithRegisteredAttachments); @@ -733,7 +738,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue(pushedCaseSnake); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith( `${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`, @@ -745,12 +750,12 @@ describe('Cases API', () => { ); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(resp).toEqual(pushedCase); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(caseWithRegisteredAttachmentsSnake); const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(resp).toEqual(caseWithRegisteredAttachments); @@ -764,7 +769,7 @@ describe('Cases API', () => { }); const commentId = 'ab1234'; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { const resp = await deleteComment({ caseId: basicCaseId, commentId, @@ -784,7 +789,7 @@ describe('Cases API', () => { fetchMock.mockResolvedValue(['siem', 'observability']); }); - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { const resp = await getFeatureIds( { registrationContext: ['security', 'observability.logs'] }, abortCtrl.signal @@ -811,7 +816,7 @@ describe('Cases API', () => { owner: SECURITY_SOLUTION_OWNER, }; - test('should be called with correct check url, method, signal', async () => { + it('should be called with correct check url, method, signal', async () => { await postComment(data, basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { @@ -821,12 +826,12 @@ describe('Cases API', () => { }); }); - test('should return correct response', async () => { + it('should return correct response', async () => { const resp = await postComment(data, basicCase.id, abortCtrl.signal); expect(resp).toEqual(basicCase); }); - test('should not covert to camel case registered attachments', async () => { + it('should not covert to camel case registered attachments', async () => { fetchMock.mockResolvedValue(caseWithRegisteredAttachmentsSnake); const resp = await postComment(data, basicCase.id, abortCtrl.signal); expect(resp).toEqual(caseWithRegisteredAttachments); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 651de220ded2b..b31bc0e65446c 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -231,6 +231,10 @@ export const updateCases = async ( cases: CaseUpdateRequest[], signal: AbortSignal ): Promise => { + if (cases.length === 0) { + return []; + } + const response = await KibanaServices.get().http.fetch(CASES_URL, { method: 'PATCH', body: JSON.stringify({ cases }), diff --git a/x-pack/plugins/cases/public/containers/translations.ts b/x-pack/plugins/cases/public/containers/translations.ts index 5bf4acf385fce..892af5864cdc3 100644 --- a/x-pack/plugins/cases/public/containers/translations.ts +++ b/x-pack/plugins/cases/public/containers/translations.ts @@ -17,6 +17,10 @@ export const ERROR_DELETING = i18n.translate('xpack.cases.containers.errorDeleti defaultMessage: 'Error deleting data', }); +export const ERROR_UPDATING = i18n.translate('xpack.cases.containers.errorUpdatingTitle', { + defaultMessage: 'Error updating data', +}); + export const UPDATED_CASE = (caseTitle: string) => i18n.translate('xpack.cases.containers.updatedCase', { values: { caseTitle }, diff --git a/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx b/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx index 85a5a743d16d3..81af102cac652 100644 --- a/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx @@ -37,7 +37,7 @@ export const useUpdateCases = () => { showSuccessToast(successToasterTitle); }, onError: (error: ServerError) => { - showErrorToast(error, { title: i18n.ERROR_DELETING }); + showErrorToast(error, { title: i18n.ERROR_UPDATING }); }, } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_errors.tsx index 0d11185a48705..0b380e003f48d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_errors.tsx @@ -31,7 +31,7 @@ export const InferenceErrors: React.FC = () => { dataType: 'date', field: 'timestamp', name: i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineLogs.tableColumn.timestamp', + 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineInferenceLogs.tableColumn.timestamp', { defaultMessage: 'Timestamp' } ), }, @@ -39,8 +39,8 @@ export const InferenceErrors: React.FC = () => { dataType: 'string', field: 'message', name: i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineLogs.tableColumn.message', - { defaultMessage: 'Inference error' } + 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineInferenceLogs.tableColumn.message', + { defaultMessage: 'Error message' } ), textOnly: true, }, @@ -48,7 +48,7 @@ export const InferenceErrors: React.FC = () => { dataType: 'number', field: 'doc_count', name: i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineLogs.tableColumn.docCount', + 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineInferenceLogs.tableColumn.docCount', { defaultMessage: 'Approx. document count' } ), }, @@ -63,15 +63,11 @@ export const InferenceErrors: React.FC = () => { title={

{i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineLogs.title', - { defaultMessage: 'Ingestion logs' } + 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineInferenceLogs.title', + { defaultMessage: 'Inference errors' } )}

} - subtitle={i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineLogs.subtitle', - { defaultMessage: 'Errors and dropped data failures' } - )} > {isLoading ? ( @@ -82,7 +78,7 @@ export const InferenceErrors: React.FC = () => { items={inferenceErrors} rowHeader="message" noItemsMessage={i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineLogs.emptyMessage', + 'xpack.enterpriseSearch.content.indices.pipelines.tabs.pipelineInferenceLogs.emptyMessage', { defaultMessage: 'This index has no inference errors' } )} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_history.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_history.tsx index 5f0de19f064ed..9b33a1a0df0e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_history.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_history.tsx @@ -9,13 +9,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { - EuiBasicTable, - EuiBasicTableColumn, - EuiLink, - EuiSpacer, - EuiLoadingSpinner, -} from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -71,17 +65,6 @@ export const InferenceHistory: React.FC = () => { 'The following inference processors were found in the _ingest.processors field of documents on this index.', } )} - footerDocLink={ - // TODO: insert real doc link - - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.tabs.inferenceHistory.docLink', - { - defaultMessage: 'Learn more about inference history', - } - )} - - } > {isLoading ? ( @@ -90,6 +73,10 @@ export const InferenceHistory: React.FC = () => { columns={historyColumns} items={inferenceHistory ?? []} rowHeader="pipeline" + noItemsMessage={i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.tabs.inferenceHistory.emptyMessage', + { defaultMessage: 'This index has no inference history' } + )} /> )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss index cd3c318635932..30554c94d5cd5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss @@ -15,4 +15,11 @@ height: 100%; } } + + .enterpriseSearchInferencePipelineModalFooter { + .euiButtonEmpty__content { + padding-left: $euiSizeM; + padding-right: $euiSizeM; + } + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx index bc8d8d7962ca3..96e942718444e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx @@ -215,7 +215,7 @@ export const ModalFooter: React.FC< break; } return ( - + {previousStep !== undefined ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx index 14e3f06c97a05..feb4ca8c87a4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx @@ -33,6 +33,7 @@ import { IndexViewLogic } from '../../index_view_logic'; import { EMPTY_PIPELINE_CONFIGURATION, MLInferenceLogic } from './ml_inference_logic'; import { MlModelSelectOption } from './model_select_option'; import { PipelineSelectOption } from './pipeline_select_option'; +import { TargetFieldHelpText } from './target_field_help_text'; import { MODEL_REDACTED_VALUE, MODEL_SELECT_PLACEHOLDER } from './utils'; const MODEL_SELECT_PLACEHOLDER_VALUE = 'model_placeholder$$'; @@ -117,6 +118,7 @@ export const ConfigurePipeline: React.FC = () => { ]; const inputsDisabled = configuration.existingPipeline !== false; + const selectedModel = supportedMLModels.find((model) => model.model_id === modelID); return ( <> @@ -173,6 +175,7 @@ export const ConfigurePipeline: React.FC = () => { existingPipeline: e.target.value === 'true', }) } + value={configuration.existingPipeline?.toString() ?? ''} /> @@ -209,12 +212,21 @@ export const ConfigurePipeline: React.FC = () => { )} helpText={ !nameError && - i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText', - { - defaultMessage: - 'Pipeline names are unique within a deployment and can only contain letters, numbers, underscores, and hyphens.', - } + configuration.existingPipeline === false && ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText', + { + defaultMessage: + 'Pipeline names are unique within a deployment and can only contain letters, numbers, underscores, and hyphens. This will create a pipeline named {pipelineName}.', + values: { + pipelineName: `ml-inference-${ + pipelineName.length > 0 ? pipelineName : '' + }`, + }, + } + )} + ) } error={nameError && formErrors.pipelineName} @@ -312,29 +324,26 @@ export const ConfigurePipeline: React.FC = () => { ) } error={formErrors.destinationField} isInvalid={formErrors.destinationField !== undefined} > = ({ pipeline }) => { - const modelIdDisplay = pipeline.modelId.length > 0 ? pipeline.modelId : REDACTED_MODE_ID_DISPLAY; + const modelIdDisplay = pipeline.modelId.length > 0 ? pipeline.modelId : MODEL_REDACTED_VALUE; return ( {pipeline.disabled && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/target_field_help_text.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/target_field_help_text.tsx new file mode 100644 index 0000000000000..2332f4c7222de --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/target_field_help_text.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; + +import { + getMlModelTypesForModelConfig, + SUPPORTED_PYTORCH_TASKS, +} from '../../../../../../../common/ml_inference_pipeline'; +import { TrainedModel } from '../../../../api/ml_models/ml_trained_models_logic'; +import { getMLType } from '../../../shared/ml_inference/utils'; + +export interface TargetFieldHelpTextProps { + model?: TrainedModel; + pipelineName: string; + targetField: string; +} + +export const TargetFieldHelpText: React.FC = ({ + pipelineName, + targetField, + model, +}) => { + const baseText = targetField + ? i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.targetField.helpText.userProvided', + { + defaultMessage: + 'This names the field that holds the inference result. It will be prefixed with "ml.inference", ml.inference.{targetField}', + values: { + targetField, + }, + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.targetField.helpText.default', + { + defaultMessage: + 'This names the field that holds the inference result. It will be prefixed with "ml.inference", if not set it will be defaulted to "ml.inference.{pipelineName}"', + values: { + pipelineName: pipelineName || '', + }, + } + ); + const fieldName = targetField || pipelineName || ''; + const modelType = model ? getMLType(getMlModelTypesForModelConfig(model)) : ''; + if (modelType === SUPPORTED_PYTORCH_TASKS.TEXT_CLASSIFICATION) { + return ( + +

{baseText}

+

+ , + }} + /> +

+
+ ); + } + if (modelType === SUPPORTED_PYTORCH_TASKS.TEXT_EMBEDDING) { + return ( + +

{baseText}

+

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.targetField.helpText.textEmbeddingModel', + { + defaultMessage: 'Additionally the predicted_value will be copied to "{fieldName}"', + values: { + fieldName, + }, + } + )} +

+
+ ); + } + return {baseText}; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts index 537a3bc43699c..e83cd35992f77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts @@ -74,5 +74,5 @@ export const MODEL_SELECT_PLACEHOLDER = i18n.translate( export const MODEL_REDACTED_VALUE = i18n.translate( 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.model.redactedValue', - { defaultMessage: 'Model is unavailable' } + { defaultMessage: "This model isn't available in the Kibana space" } ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/confirm_fleet_server_connection.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/confirm_fleet_server_connection.tsx index f09d529b7f531..de6ff3f95fbf9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/confirm_fleet_server_connection.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/confirm_fleet_server_connection.tsx @@ -47,6 +47,7 @@ const ConfirmFleetServerConnectionStepContent: React.FunctionComponent<{ const handleContinueClick = () => { fleetStatus.forceDisplayInstructions = false; + flyoutContext.closeFleetServerFlyout(); flyoutContext.openEnrollmentFlyout(); }; @@ -61,7 +62,11 @@ const ConfirmFleetServerConnectionStepContent: React.FunctionComponent<{ - + = submit, }) => { const { getHref } = useLink(); + const flyoutContext = useFlyoutContext(); if (status === 'success') { return ( @@ -71,12 +73,16 @@ const GettingStartedStepContent: React.FunctionComponent = values={{ hostUrl: {selectedFleetServerHost?.host_urls[0]}, fleetSettingsLink: ( - + flyoutContext.closeFleetServerFlyout()} + flush="left" + > - + ), }} /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 179f61db9a5b7..a4c2fb335e9a5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -18,6 +18,7 @@ import { EuiFieldNumber, EuiFieldText, EuiSuperSelect, + EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -71,6 +72,10 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = // agent monitoring checkbox group can appear multiple times in the DOM, ids have to be unique to work correctly const monitoringCheckboxIdSuffix = Date.now(); + const hasManagedPackagePolicy = + 'package_policies' in agentPolicy && + agentPolicy?.package_policies?.some((packagePolicy) => packagePolicy.is_managed); + return ( <> = > {(deleteAgentPolicyPrompt) => { return ( - deleteAgentPolicyPrompt(agentPolicy.id!, onDelete)} + + ) : undefined + } > - - + deleteAgentPolicyPrompt(agentPolicy.id!, onDelete)} + isDisabled={hasManagedPackagePolicy} + > + + + ); }} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_proxies_table/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_proxies_table/index.tsx index f4bfb93f80e5e..9482078607094 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_proxies_table/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_proxies_table/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { EuiBasicTable, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiBasicTable, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -43,6 +43,19 @@ export const FleetProxiesTable: React.FunctionComponent {fleetProxy.name}

+ {fleetProxy.is_preconfigured && ( + + + + )}
), width: '288px', @@ -60,7 +73,7 @@ export const FleetProxiesTable: React.FunctionComponent { width: '68px', render: (fleetProxy: FleetProxy) => { - const isDeleteVisible = true; + const isDeleteVisible = !fleetProxy.is_preconfigured; return ( diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index 0e685e8b45135..3c982ef9b516b 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -22,6 +22,7 @@ import { PreconfiguredAgentPoliciesSchema, PreconfiguredOutputsSchema, PreconfiguredFleetServerHostsSchema, + PreconfiguredFleetProxiesSchema, } from './types'; const DEFAULT_BUNDLED_PACKAGE_LOCATION = path.join(__dirname, '../target/bundled_packages'); @@ -117,6 +118,7 @@ export const config: PluginConfigDescriptor = { agentPolicies: PreconfiguredAgentPoliciesSchema, outputs: PreconfiguredOutputsSchema, fleetServerHosts: PreconfiguredFleetServerHostsSchema, + proxies: PreconfiguredFleetProxiesSchema, agentIdVerificationEnabled: schema.boolean({ defaultValue: true }), developer: schema.object({ disableRegistryVersionCheck: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 5ed4b0a290c93..3571612cae4d4 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -9,6 +9,8 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/serv import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { PackagePolicyRestrictionRelatedError } from '../errors'; + import type { AgentPolicy, FullAgentPolicy, @@ -173,6 +175,30 @@ describe('agent policy', () => { { id: 'package-1' }, ]); }); + + it('should throw error for agent policy which has managed package poolicy', async () => { + mockedPackagePolicyService.findAllForAgentPolicy.mockReturnValue([ + { + id: 'package-1', + is_managed: true, + }, + ] as any); + try { + await agentPolicyService.delete(soClient, esClient, 'mocked'); + } catch (e) { + expect(e.message).toEqual( + new PackagePolicyRestrictionRelatedError( + `Cannot delete agent policy mocked that contains managed package policies` + ).message + ); + } + + await agentPolicyService.delete(soClient, esClient, 'mocked', { force: true }); + + expect(packagePolicyService.runDeleteExternalCallbacks).toHaveBeenCalledWith([ + { id: 'package-1' }, + ]); + }); }); describe('bumpRevision', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 44696a7f1a997..48b0209e4359d 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -58,6 +58,7 @@ import { AgentPolicyNameExistsError, HostedAgentPolicyRestrictionRelatedError, AgentPolicyNotFoundError, + PackagePolicyRestrictionRelatedError, } from '../errors'; import type { FullAgentConfigMap } from '../../common/types/models/agent_cm'; @@ -671,6 +672,14 @@ class AgentPolicyService { const packagePolicies = await packagePolicyService.findAllForAgentPolicy(soClient, id); if (packagePolicies.length) { + const hasManagedPackagePolicies = packagePolicies.some( + (packagePolicy) => packagePolicy.is_managed + ); + if (hasManagedPackagePolicies && !options?.force) { + throw new PackagePolicyRestrictionRelatedError( + `Cannot delete agent policy ${id} that contains managed package policies` + ); + } const deletedPackagePolicies: DeletePackagePoliciesResponse = await packagePolicyService.delete( soClient, diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/fleet_proxies.ts b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_proxies.ts new file mode 100644 index 0000000000000..cfc0beb1cddd2 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_proxies.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import { isEqual } from 'lodash'; +import pMap from 'p-map'; + +import type { FleetConfigType } from '../../config'; +import type { FleetProxy } from '../../types'; +import { + bulkGetFleetProxies, + createFleetProxy, + deleteFleetProxy, + listFleetProxies, + updateFleetProxy, +} from '../fleet_proxies'; +import { listFleetServerHostsForProxyId } from '../fleet_server_host'; +import { agentPolicyService } from '../agent_policy'; +import { outputService } from '../output'; + +export function getPreconfiguredFleetProxiesFromConfig(config?: FleetConfigType) { + const { proxies: fleetProxiesFromConfig } = config; + + return fleetProxiesFromConfig.map((proxyConfig: any) => ({ + ...proxyConfig, + is_preconfigured: true, + })); +} + +function hasChanged(existingProxy: FleetProxy, preconfiguredFleetProxy: FleetProxy) { + return ( + (!existingProxy.is_preconfigured || + existingProxy.name !== existingProxy.name || + existingProxy.url !== preconfiguredFleetProxy.name || + !isEqual( + existingProxy.proxy_headers ?? null, + preconfiguredFleetProxy.proxy_headers ?? null + ) || + existingProxy.certificate_authorities) ?? + null !== preconfiguredFleetProxy.certificate_authorities ?? + (null || existingProxy.certificate) ?? + null !== preconfiguredFleetProxy.certificate ?? + (null || existingProxy.certificate_key) ?? + null !== preconfiguredFleetProxy.certificate_key ?? + null + ); +} + +async function createOrUpdatePreconfiguredFleetProxies( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + preconfiguredFleetProxies: FleetProxy[] +) { + const existingFleetProxies = await bulkGetFleetProxies( + soClient, + preconfiguredFleetProxies.map(({ id }) => id), + { ignoreNotFound: true } + ); + await Promise.all( + preconfiguredFleetProxies.map(async (preconfiguredFleetProxy) => { + const existingProxy = existingFleetProxies.find( + (fleetProxy) => fleetProxy.id === preconfiguredFleetProxy.id + ); + + const { id, ...data } = preconfiguredFleetProxy; + + const isCreate = !existingProxy; + const isUpdateWithNewData = existingProxy + ? hasChanged(existingProxy, preconfiguredFleetProxy) + : false; + + if (isCreate) { + await createFleetProxy( + soClient, + { + ...data, + is_preconfigured: true, + }, + { id, overwrite: true, fromPreconfiguration: true } + ); + } else if (isUpdateWithNewData) { + await updateFleetProxy( + soClient, + id, + { + ...data, + is_preconfigured: true, + }, + { fromPreconfiguration: true } + ); + // Bump all the agent policy that use that proxy + const [{ items: fleetServerHosts }, { items: outputs }] = await Promise.all([ + listFleetServerHostsForProxyId(soClient, id), + outputService.listAllForProxyId(soClient, id), + ]); + if ( + fleetServerHosts.some((host) => host.is_default) || + outputs.some((output) => output.is_default || output.is_default_monitoring) + ) { + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient); + } else { + await pMap( + outputs, + (output) => + agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, output.id), + { + concurrency: 20, + } + ); + await pMap( + fleetServerHosts, + (fleetServerHost) => + agentPolicyService.bumpAllAgentPoliciesForFleetServerHosts( + soClient, + esClient, + fleetServerHost.id + ), + { + concurrency: 20, + } + ); + } + } + }) + ); +} + +async function cleanPreconfiguredFleetProxies( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + preconfiguredFleetProxies: FleetProxy[] +) { + const existingFleetProxies = await listFleetProxies(soClient); + const existingPreconfiguredFleetProxies = existingFleetProxies.items.filter( + (o) => o.is_preconfigured === true + ); + + for (const existingFleetProxy of existingPreconfiguredFleetProxies) { + const hasBeenDelete = !preconfiguredFleetProxies.find(({ id }) => existingFleetProxy.id === id); + if (!hasBeenDelete) { + continue; + } + + const [{ items: fleetServerHosts }, { items: outputs }] = await Promise.all([ + listFleetServerHostsForProxyId(soClient, existingFleetProxy.id), + outputService.listAllForProxyId(soClient, existingFleetProxy.id), + ]); + const isUsed = fleetServerHosts.length > 0 || outputs.length > 0; + if (isUsed) { + await updateFleetProxy( + soClient, + existingFleetProxy.id, + { is_preconfigured: false }, + { + fromPreconfiguration: true, + } + ); + } else { + await deleteFleetProxy(soClient, existingFleetProxy.id, { + fromPreconfiguration: true, + }); + } + } +} + +export async function ensurePreconfiguredFleetProxies( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + preconfiguredFleetProxies: FleetProxy[] +) { + await createOrUpdatePreconfiguredFleetProxies(soClient, esClient, preconfiguredFleetProxies); + await cleanPreconfiguredFleetProxies(soClient, esClient, preconfiguredFleetProxies); +} diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 8837ae3522ac1..996701c920387 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -19,6 +19,7 @@ import { setupFleet } from './setup'; jest.mock('./preconfiguration'); jest.mock('./preconfiguration/outputs'); +jest.mock('./preconfiguration/fleet_proxies'); jest.mock('./settings'); jest.mock('./output'); jest.mock('./download_source'); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index e43efb44adb5f..2802fd34bc001 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -29,6 +29,10 @@ import { ensurePreconfiguredOutputs, getPreconfiguredOutputFromConfig, } from './preconfiguration/outputs'; +import { + ensurePreconfiguredFleetProxies, + getPreconfiguredFleetProxiesFromConfig, +} from './preconfiguration/fleet_proxies'; import { outputService } from './output'; import { downloadSourceService } from './download_source'; @@ -86,6 +90,13 @@ async function createSetupSideEffects( await migrateSettingsToFleetServerHost(soClient); logger.debug('Setting up Fleet download source'); const defaultDownloadSource = await downloadSourceService.ensureDefault(soClient); + // Need to be done before outputs and fleet server hosts as these object can reference a proxy + logger.debug('Setting up Proxy'); + await ensurePreconfiguredFleetProxies( + soClient, + esClient, + getPreconfiguredFleetProxiesFromConfig(appContextService.getConfig()) + ); logger.debug('Setting up Fleet Sever Hosts'); await ensurePreconfiguredFleetServerHosts( diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 3100fb04a46f1..13ba525ee420b 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -95,6 +95,24 @@ export const PreconfiguredFleetServerHostsSchema = schema.arrayOf( { defaultValue: [] } ); +export const PreconfiguredFleetProxiesSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + name: schema.string(), + url: schema.string(), + proxy_headers: schema.maybe( + schema.recordOf( + schema.string(), + schema.oneOf([schema.string(), schema.boolean(), schema.number()]) + ) + ), + certificate_authorities: schema.maybe(schema.string()), + certificate: schema.maybe(schema.string()), + certificate_key: schema.maybe(schema.string()), + }), + { defaultValue: [] } +); + export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( schema.object({ ...AgentPolicyBaseSchema, diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 8807de5b0f173..abaac9bd05f9f 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useEffect, FC } from 'react'; -import { useHistory, useLocation, Router, RouteProps } from 'react-router-dom'; -import { Location } from 'history'; +import React, { FC } from 'react'; +import { Router, type RouteProps } from 'react-router-dom'; +import { type Location } from 'history'; import type { AppMountParameters, @@ -74,26 +74,6 @@ export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children ); }; -/** - * This component provides compatibility with the previous hash based - * URL format used by HashRouter. Even if we migrate all internal URLs - * to one without hashes, we should keep this redirect in place to - * support legacy bookmarks and as a fallback for unmigrated URLs - * from other plugins. - */ -const LegacyHashUrlRedirect: FC = ({ children }) => { - const history = useHistory(); - const location = useLocation(); - - useEffect(() => { - if (location.hash.startsWith('#/')) { - history.push(location.hash.replace('#', '')); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.hash]); - - return <>{children}; -}; /** * `MlRouter` is based on `BrowserRouter` and takes in `ScopedHistory` provided * by Kibana. `LegacyHashUrlRedirect` provides compatibility with legacy hash based URLs. @@ -104,12 +84,10 @@ export const MlRouter: FC<{ pageDeps: PageDependencies; }> = ({ pageDeps }) => ( - - - - - - - + + + + + ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx index 09e17398220d8..c32717f7f9118 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import type { CoreStart } from '@kbn/core/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; @@ -14,6 +14,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import styled from 'styled-components'; import { DataView } from '@kbn/data-views-plugin/common'; import { FormulaPublicApi } from '@kbn/lens-plugin/public'; +import { i18n } from '@kbn/i18n'; import { useAppDataView } from './use_app_data_view'; import { ObservabilityPublicPluginsStart, useFetcher } from '../../../..'; import type { ExploratoryEmbeddableProps, ExploratoryEmbeddableComponentProps } from './embeddable'; @@ -70,6 +71,10 @@ export function getExploratoryViewEmbeddable( ); } + if (!dataViews[series?.dataType]) { + return ; + } + return ( @@ -103,3 +108,17 @@ const LoadingWrapper = styled.div<{ align-items: center; justify-content: center; `; + +function EmptyState({ height }: { height?: string }) { + return ( + + + {NO_DATA_LABEL} + + + ); +} + +const NO_DATA_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.noData', { + defaultMessage: 'No data', +}); diff --git a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts index d43c6f27e97ea..4b911c383d831 100644 --- a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts +++ b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts @@ -113,13 +113,17 @@ export class ObservabilityDataViews { const { runtimeFields } = getFieldFormatsForApp(app); - const dataView = await this.dataViews.create({ - title: appIndicesPattern, - id: getAppDataViewId(app, indices), - timeFieldName: '@timestamp', - fieldFormats: this.getFieldFormats(app), - name: DataTypesLabels[app], - }); + const dataView = await this.dataViews.create( + { + title: appIndicesPattern, + id: getAppDataViewId(app, indices), + timeFieldName: '@timestamp', + fieldFormats: this.getFieldFormats(app), + name: DataTypesLabels[app], + }, + false, + false + ); if (runtimeFields !== null) { runtimeFields.forEach(({ name, field }) => { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 860214a0dc1ac..9a8be961fa881 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -96,7 +96,6 @@ export enum SecurityPageName { endpoints = 'endpoints', eventFilters = 'event_filters', exceptions = 'exceptions', - sharedExceptionListDetails = 'shared-exception-list-details', exploreLanding = 'explore', hostIsolationExceptions = 'host_isolation_exceptions', hosts = 'hosts', @@ -149,6 +148,7 @@ export const ALERTS_PATH = '/alerts' as const; export const RULES_PATH = '/rules' as const; export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const; export const EXCEPTIONS_PATH = '/exceptions' as const; +export const EXCEPTION_LIST_DETAIL_PATH = `${EXCEPTIONS_PATH}/details/:detailName` as const; export const HOSTS_PATH = '/hosts' as const; export const USERS_PATH = '/users' as const; export const KUBERNETES_PATH = '/kubernetes' as const; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts index 8305d2aa08ae1..6bd6bbdbda02b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -140,6 +140,7 @@ describe('Endpoint Authz service', () => { ['canReadPolicyManagement', 'readPolicyManagement'], ['canWriteActionsLogManagement', 'writeActionsLogManagement'], ['canReadActionsLogManagement', 'readActionsLogManagement'], + ['canAccessEndpointActionsLogManagement', 'readActionsLogManagement'], ['canIsolateHost', 'writeHostIsolation'], ['canUnIsolateHost', 'writeHostIsolation'], ['canKillProcess', 'writeProcessOperations'], @@ -166,6 +167,10 @@ describe('Endpoint Authz service', () => { ['canReadPolicyManagement', ['writePolicyManagement', 'readPolicyManagement']], ['canWriteActionsLogManagement', ['writeActionsLogManagement']], ['canReadActionsLogManagement', ['writeActionsLogManagement', 'readActionsLogManagement']], + [ + 'canAccessEndpointActionsLogManagement', + ['writeActionsLogManagement', 'readActionsLogManagement'], + ], ['canIsolateHost', ['writeHostIsolation']], ['canUnIsolateHost', ['writeHostIsolation']], ['canKillProcess', ['writeProcessOperations']], @@ -218,6 +223,7 @@ describe('Endpoint Authz service', () => { canWriteSecuritySolution: false, canReadSecuritySolution: false, canAccessFleet: false, + canAccessEndpointActionsLogManagement: false, canAccessEndpointManagement: false, canCreateArtifactsByPolicy: false, canDeleteHostIsolationExceptions: false, diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts index f4a2c9894108d..5c83571b6373e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -63,6 +63,8 @@ export function hasKibanaPrivilege( * @param hasHostIsolationExceptionsItems if set to `true`, then Host Isolation Exceptions related authz properties * may be adjusted to account for a license downgrade scenario */ + +// eslint-disable-next-line complexity export const calculateEndpointAuthz = ( licenseService: LicenseService, fleetAuthz: FleetAuthz, @@ -223,6 +225,7 @@ export const calculateEndpointAuthz = ( canReadPolicyManagement, canWriteActionsLogManagement, canReadActionsLogManagement: canReadActionsLogManagement && isEnterpriseLicense, + canAccessEndpointActionsLogManagement: canReadActionsLogManagement && isPlatinumPlusLicense, // Response Actions canIsolateHost: canIsolateHost && isPlatinumPlusLicense, canUnIsolateHost: canIsolateHost, @@ -250,6 +253,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => { return { ...defaultEndpointPermissions(), canAccessFleet: false, + canAccessEndpointActionsLogManagement: false, canAccessEndpointManagement: false, canCreateArtifactsByPolicy: false, canWriteEndpointList: false, diff --git a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts index fbfa97ad73328..e693e6d0e4cff 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts @@ -24,6 +24,8 @@ export interface EndpointAuthz extends EndpointPermissions { canAccessFleet: boolean; /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ canAccessEndpointManagement: boolean; + /** If user has permissions to access Actions Log management and also has a platinum license (used for endpoint details flyout) */ + canAccessEndpointActionsLogManagement: boolean; /** if user has permissions to create Artifacts by Policy */ canCreateArtifactsByPolicy: boolean; /** if user has write permissions to endpoint list */ diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index a654eeb7d3e14..db8cf4f92ccaf 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -234,15 +234,6 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ defaultMessage: 'Exception lists', }), ], - deepLinks: [ - { - id: SecurityPageName.sharedExceptionListDetails, - title: 'List Details', - path: '/exceptions/shared/:exceptionListId', - navLinkStatus: AppNavLinkStatus.hidden, - searchable: false, - }, - ], }, ], }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index 84aec14891328..0cb13b5bcc4a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -133,6 +133,11 @@ const rulesBReadcrumb = { href: 'securitySolutionUI/rules', }; +const exceptionsBReadcrumb = { + text: 'Rule Exceptions', + href: 'securitySolutionUI/exceptions', +}; + const manageBreadcrumbs = { text: 'Manage', href: 'securitySolutionUI/administration', @@ -433,6 +438,32 @@ describe('Navigation Breadcrumbs', () => { }, ]); }); + + test('should return Exceptions breadcrumbs when supplied exception Details pageName', () => { + const mockListName = 'new shared list'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject( + SecurityPageName.exceptions, + `/exceptions/details/${mockListName}`, + undefined + ), + state: { + listName: mockListName, + }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + securityBreadCrumb, + exceptionsBReadcrumb, + { + text: mockListName, + href: ``, + }, + ]); + }); }); describe('setBreadcrumbs()', () => { @@ -773,6 +804,31 @@ describe('Navigation Breadcrumbs', () => { }, ]); }); + test('should return Exceptions breadcrumbs when supplied exception Details pageName', () => { + const mockListName = 'new shared list'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject( + SecurityPageName.exceptions, + `/exceptions/details/${mockListName}`, + undefined + ), + state: { + listName: mockListName, + }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + securityBreadCrumb, + exceptionsBReadcrumb, + { + text: mockListName, + href: ``, + }, + ]); + }); }); describe('setBreadcrumbs()', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 98b5cc2a01d22..287238f57a11c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -13,6 +13,7 @@ import type { StartServices } from '../../../../types'; import { getTrailingBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; import { getTrailingBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/details'; import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; +import { getTrailingBreadcrumbs as geExceptionsBreadcrumbs } from '../../../../exceptions/utils/pages.utils'; import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_security_posture/breadcrumbs'; import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../users/pages/details/utils'; import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs'; @@ -126,6 +127,8 @@ const getTrailingBreadcrumbsForRoutes = ( return getDetectionRulesBreadcrumbs(spyState, getSecuritySolutionUrl); } + if (isExceptionRoutes(spyState)) return geExceptionsBreadcrumbs(spyState, getSecuritySolutionUrl); + if (isKubernetesRoutes(spyState)) { return getKubernetesBreadcrumbs(spyState, getSecuritySolutionUrl); } @@ -161,6 +164,9 @@ const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute spyState.pageName === SecurityPageName.rules || spyState.pageName === SecurityPageName.rulesCreate; +const isExceptionRoutes = (spyState: RouteSpyState) => + spyState.pageName === SecurityPageName.exceptions; + const isCloudSecurityPostureBenchmarksRoutes = (spyState: RouteSpyState) => spyState.pageName === SecurityPageName.cloudSecurityPostureBenchmarks; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index ba3bb55afedc0..d5712bee73746 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -143,6 +143,7 @@ describe('When using useEndpointPrivileges hook', () => { getEndpointPrivilegesInitialStateMock({ canCreateArtifactsByPolicy: false, canIsolateHost: false, + canAccessEndpointActionsLogManagement: false, canWriteHostIsolationExceptions: false, canReadHostIsolationExceptions: hasHIE, canDeleteHostIsolationExceptions: hasHIE, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx index c1f11b17cbbd9..372839eba9e40 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx @@ -24,7 +24,7 @@ import type { ExceptionsBuilderReturnExceptionItem, } from '@kbn/securitysolution-list-utils'; import type { DataViewBase } from '@kbn/es-query'; -import styled, { css } from 'styled-components'; +import styled, { css, createGlobalStyle } from 'styled-components'; import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { hasEqlSequenceQuery, isEqlRule } from '../../../../../../common/detection_engine/utils'; import type { Rule } from '../../../../rule_management/logic/types'; @@ -56,6 +56,15 @@ const SectionHeader = styled(EuiTitle)` font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; `} `; +// EuiCombox doesn't support change of z-index, or providing any class to portal +// This fix ovveride z-index for EuiFlyout, which conflict with EuiComboBox on this flyout +// fix x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx#L429 +// TODO: should be fixed on Component level +const EuiComboboxZIndexGlobalStyle = createGlobalStyle` + [data-test-subj="comboBoxOptionsList osSelectionDropdown-optionsList"] { + z-index: 6000 !important; + } +`; interface ExceptionsFlyoutConditionsComponentProps { /* Exception list item field value for "name" */ @@ -233,6 +242,7 @@ const ExceptionsConditionsComponent: React.FC
+ )} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_meta_form/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_meta_form/translations.ts index db3519ed98616..8a80694ded7dd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_meta_form/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_meta_form/translations.ts @@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n'; export const RULE_EXCEPTION_NAME_LABEL = i18n.translate( 'xpack.securitySolution.rule_exceptions.itemMeta.nameLabel', { - defaultMessage: 'Rule exception name', + defaultMessage: 'Exception name', } ); export const RULE_EXCEPTION_NAME_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.rule_exceptions.itemMeta.namePlaceholder', { - defaultMessage: 'Name your rule exception', + defaultMessage: 'Name your exception', } ); diff --git a/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx index 8031de23ef4c4..bb8b855740ae4 100644 --- a/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx @@ -97,6 +97,9 @@ export const ExceptionsListCard = memo( handleConfirmExceptionFlyout, handleCancelExceptionItemFlyout, goToExceptionDetail, + emptyViewerTitle, + emptyViewerBody, + emptyViewerButtonText, } = useExceptionsListCard({ exceptionsList, handleExport, @@ -187,6 +190,9 @@ export const ExceptionsListCard = memo( onPaginationChange={onPaginationChange} onCreateExceptionListItem={onAddExceptionClick} lastUpdated={null} + emptyViewerTitle={emptyViewerTitle} + emptyViewerBody={emptyViewerBody} + emptyViewerButtonText={emptyViewerButtonText} /> diff --git a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx index 68bd02dd9f542..492ba500de894 100644 --- a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx @@ -33,6 +33,7 @@ interface ListExceptionItemsProps { pagination: Pagination; emptyViewerTitle?: string; emptyViewerBody?: string; + emptyViewerButtonText?: string; viewerStatus: ViewerStatus | ''; ruleReferences: RuleReferences; hideUtility?: boolean; @@ -50,6 +51,7 @@ const ListExceptionItemsComponent: FC = ({ pagination, emptyViewerTitle, emptyViewerBody, + emptyViewerButtonText, viewerStatus, ruleReferences, hideUtility = false, @@ -68,6 +70,7 @@ const ListExceptionItemsComponent: FC = ({ exceptions={exceptions} emptyViewerTitle={emptyViewerTitle} emptyViewerBody={emptyViewerBody} + emptyViewerButtonText={emptyViewerButtonText} pagination={pagination} lastUpdated={lastUpdated} editActionLabel={i18n.EXCEPTION_ITEM_CARD_EDIT_LABEL} diff --git a/x-pack/plugins/security_solution/public/exceptions/components/list_with_search/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/list_with_search/index.tsx index 9deda24248fc8..27324a53e5cb8 100644 --- a/x-pack/plugins/security_solution/public/exceptions/components/list_with_search/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/components/list_with_search/index.tsx @@ -42,6 +42,7 @@ const ListWithSearchComponent: FC = ({ pagination, emptyViewerTitle, emptyViewerBody, + emptyViewerButtonText, viewerStatus, ruleReferences, showAddExceptionFlyout, @@ -94,7 +95,11 @@ const ListWithSearchComponent: FC = ({ /> )} = ({ exceptions={exceptions} emptyViewerTitle={emptyViewerTitle} emptyViewerBody={emptyViewerBody} + emptyViewerButtonText={emptyViewerButtonText} pagination={pagination} lastUpdated={lastUpdated} onPaginationChange={onPaginationChange} diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx b/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx index bdef9b8dd4e22..3d7a0c500edac 100644 --- a/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx @@ -10,6 +10,8 @@ import type { ExceptionListItemSchema, NamespaceType, } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + import { ViewerStatus } from '@kbn/securitysolution-exception-list-components'; import { useGeneratedHtmlId } from '@elastic/eui'; import { useGetSecuritySolutionLinkProps } from '../../../common/components/links'; @@ -85,6 +87,22 @@ export const useExceptionsListCard = ({ const listCannotBeEdited = checkIfListCannotBeEdited(exceptionsList); + const emptyViewerTitle = useMemo(() => { + return viewerStatus === ViewerStatus.EMPTY ? i18n.EXCEPTION_LIST_EMPTY_VIEWER_TITLE : ''; + }, [viewerStatus]); + + const emptyViewerBody = useMemo(() => { + return viewerStatus === ViewerStatus.EMPTY + ? i18n.EXCEPTION_LIST_EMPTY_VIEWER_BODY(exceptionsList.name) + : ''; + }, [exceptionsList.name, viewerStatus]); + + const emptyViewerButtonText = useMemo(() => { + return exceptionsList.type === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_LIST_EMPTY_VIEWER_BUTTON_ENDPOINT + : i18n.EXCEPTION_LIST_EMPTY_VIEWER_BUTTON; + }, [exceptionsList.type]); + const menuActionItems = useMemo( () => [ { @@ -145,8 +163,8 @@ export const useExceptionsListCard = ({ // routes to x-pack/plugins/security_solution/public/exceptions/routes.tsx const { onClick: goToExceptionDetail } = useGetSecuritySolutionLinkProps()({ - deepLinkId: SecurityPageName.sharedExceptionListDetails, - path: `/exceptions/shared/${exceptionsList.list_id}`, + deepLinkId: SecurityPageName.exceptions, + path: `/details/${exceptionsList.list_id}`, }); return { listId, @@ -177,5 +195,8 @@ export const useExceptionsListCard = ({ handleConfirmExceptionFlyout, handleCancelExceptionItemFlyout, goToExceptionDetail, + emptyViewerTitle, + emptyViewerBody, + emptyViewerButtonText, }; }; diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts index 66a3e10d914c1..0e574c8b19039 100644 --- a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts +++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts @@ -53,8 +53,8 @@ export const useListDetailsView = () => { const { exportExceptionList, deleteExceptionList } = useApi(http); - const { exceptionListId } = useParams<{ - exceptionListId: string; + const { detailName: exceptionListId } = useParams<{ + detailName: string; }>(); const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData(); @@ -147,6 +147,7 @@ export const useListDetailsView = () => { type: list.type, name: listDetails.name, description: listDetails.description || list.description, + namespace_type: list.namespace_type, }, }); } catch (error) { diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_with_search/index.ts b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_with_search/index.ts index 14a51deedb493..5539fadf02d2c 100644 --- a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_with_search/index.ts +++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_with_search/index.ts @@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionListItemSchema, ExceptionListSchema, @@ -68,6 +69,12 @@ export const useListWithSearchComponent = ( : ''; }, [list.name, viewerStatus]); + const emptyViewerButtonText = useMemo(() => { + return list.type === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_LIST_EMPTY_VIEWER_BUTTON_ENDPOINT + : i18n.EXCEPTION_LIST_EMPTY_VIEWER_BUTTON; + }, [list.type]); + // #region Callbacks const onSearch = useCallback( @@ -108,6 +115,7 @@ export const useListWithSearchComponent = ( viewerStatus, emptyViewerTitle, emptyViewerBody, + emptyViewerButtonText, ruleReferences: exceptionListReferences, showAddExceptionFlyout, showEditExceptionFlyout, diff --git a/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx b/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx index 7697ba01427d3..cd140a1160a44 100644 --- a/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import type { FC } from 'react'; import { @@ -13,6 +13,8 @@ import { ViewerStatus, } from '@kbn/securitysolution-exception-list-components'; import { EuiLoadingContent } from '@elastic/eui'; +import { SecurityPageName } from '../../../../common/constants'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { ReferenceErrorModal } from '../../../detections/components/value_lists_management_flyout/reference_error_modal'; import type { Rule } from '../../../detection_engine/rule_management/logic/types'; import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout'; @@ -53,53 +55,90 @@ export const ListsDetailViewComponent: FC = () => { handleReferenceDelete, } = useListDetailsView(); - if (viewerStatus === ViewerStatus.ERROR) - return ; + const detailsViewContent = useMemo(() => { + if (viewerStatus === ViewerStatus.ERROR) + return ; - if (isLoading) return ; + if (isLoading) return ; - if (invalidListId || !listName || !list) return ; - return ( - <> - - + if (invalidListId || !listName || !list) return ; + return ( + <> + + - - - - {showManageRulesFlyout ? ( - + + - ) : null} + {showManageRulesFlyout ? ( + + ) : null} + + ); + }, [ + canUserEditList, + disableManageButton, + exportedList, + headerBackOptions, + invalidListId, + isLoading, + isReadOnly, + linkedRules, + list, + listDescription, + listId, + listName, + referenceModalState.contentText, + referenceModalState.rulesReferences, + refreshExceptions, + showManageButtonLoader, + showManageRulesFlyout, + showReferenceErrorModal, + viewerStatus, + onCancelManageRules, + onEditListDetails, + onExportList, + onManageRules, + onRuleSelectionChange, + onSaveManageRules, + handleCloseReferenceErrorModal, + handleDelete, + handleReferenceDelete, + ]); + return ( + <> + + {detailsViewContent} ); }; diff --git a/x-pack/plugins/security_solution/public/exceptions/routes.tsx b/x-pack/plugins/security_solution/public/exceptions/routes.tsx index 13cc29411f97c..dd94e51f6c33b 100644 --- a/x-pack/plugins/security_solution/public/exceptions/routes.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/routes.tsx @@ -10,7 +10,11 @@ import { Route } from '@kbn/kibana-react-plugin/public'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import * as i18n from './translations'; -import { EXCEPTIONS_PATH, SecurityPageName } from '../../common/constants'; +import { + EXCEPTIONS_PATH, + SecurityPageName, + EXCEPTION_LIST_DETAIL_PATH, +} from '../../common/constants'; import { SharedLists, ListsDetailView } from './pages'; import { SpyRoute } from '../common/utils/route/spy_routes'; @@ -29,9 +33,8 @@ const ExceptionsRoutes = () => ( const ExceptionsListDetailRoute = () => ( - + - ); @@ -42,7 +45,7 @@ const ExceptionsContainerComponent: React.FC = () => { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts b/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts index 45600807749ff..d606bc7b47b21 100644 --- a/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts +++ b/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts @@ -21,6 +21,13 @@ export const EXCEPTION_LIST_EMPTY_VIEWER_BODY = (listName: string) => 'There is no exception in your [{listName}]. Create rule exceptions to this list.', }); +export const EXCEPTION_LIST_EMPTY_VIEWER_BUTTON_ENDPOINT = i18n.translate( + 'xpack.securitySolution.exception.list.empty.viewer_button_endpoint', + { + defaultMessage: 'Create endpoint exception', + } +); + export const EXCEPTION_LIST_EMPTY_VIEWER_BUTTON = i18n.translate( 'xpack.securitySolution.exception.list.empty.viewer_button', { @@ -28,6 +35,13 @@ export const EXCEPTION_LIST_EMPTY_VIEWER_BUTTON = i18n.translate( } ); +export const EXCEPTION_LIST_EMPTY_SEARCH_BAR_BUTTON_ENDPOINT = i18n.translate( + 'xpack.securitySolution.exception.list.search_bar_button_enpoint', + { + defaultMessage: 'Add endpoint exception to list', + } +); + export const EXCEPTION_LIST_EMPTY_SEARCH_BAR_BUTTON = i18n.translate( 'xpack.securitySolution.exception.list.search_bar_button', { diff --git a/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts b/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts index 038cdc7ee1b4d..bb3fbd5663dae 100644 --- a/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts +++ b/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts @@ -183,7 +183,7 @@ export const UPLOAD_BUTTON = i18n.translate( export const uploadSuccessMessage = (fileName: string) => i18n.translate('xpack.securitySolution.lists.exceptionListImportSuccess', { - defaultMessage: "Exception list '{fileName}' was imported", + defaultMessage: 'Exception list {fileName} was imported', values: { fileName }, }); diff --git a/x-pack/plugins/security_solution/public/exceptions/utils/pages.utils.ts b/x-pack/plugins/security_solution/public/exceptions/utils/pages.utils.ts new file mode 100644 index 0000000000000..9c1a3289aca6f --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/utils/pages.utils.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ChromeBreadcrumb } from '@kbn/core/public'; +import { EXCEPTIONS_PATH } from '../../../common/constants'; +import type { GetSecuritySolutionUrl } from '../../common/components/link_to'; +import type { RouteSpyState } from '../../common/utils/route/types'; + +const isListDetailPage = (pathname: string) => + pathname.includes(EXCEPTIONS_PATH) && pathname.includes('/details'); + +export const getTrailingBreadcrumbs = ( + params: RouteSpyState, + getSecuritySolutionUrl: GetSecuritySolutionUrl +): ChromeBreadcrumb[] => { + let breadcrumb: ChromeBreadcrumb[] = []; + + if (isListDetailPage(params.pathName) && params.state?.listName) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state.listName, + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx index dc7b9aef5c81f..ffba2f85bbf7c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx @@ -27,6 +27,7 @@ import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../../../../common/endpoint/ import { useUserPrivileges as _useUserPrivileges } from '../../../common/components/user_privileges'; import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks'; import { waitFor } from '@testing-library/react'; +import { getUserPrivilegesMockDefaultValue } from '../../../common/components/user_privileges/__mocks__'; let mockUseGetEndpointActionList: { isFetched?: boolean; @@ -138,8 +139,7 @@ jest.mock('../../hooks/response_actions/use_get_file_info', () => { const mockUseGetEndpointsList = useGetEndpointsList as jest.Mock; -// FLAKY https://github.com/elastic/kibana/issues/145635 -describe.skip('Response actions history', () => { +describe('Response actions history', () => { const useUserPrivilegesMock = _useUserPrivileges as jest.Mock< ReturnType >; @@ -195,6 +195,7 @@ describe.skip('Response actions history', () => { ...baseMockedActionList, }; jest.clearAllMocks(); + useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue); }); describe('When index does not exist yet', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index 1d7c18016fc06..d5babbd8c6cc2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -43,7 +43,7 @@ export const EndpointDetails = memo(() => { const policyInfo = useEndpointSelector(policyVersionInfo); const hostStatus = useEndpointSelector(hostStatusInfo); const show = useEndpointSelector(showView); - const { canReadActionsLogManagement } = useUserPrivileges().endpointPrivileges; + const { canAccessEndpointActionsLogManagement } = useUserPrivileges().endpointPrivileges; const ContentLoadingMarkup = useMemo( () => ( @@ -82,7 +82,7 @@ export const EndpointDetails = memo(() => { // show the response actions history tab // only when the user has the required permission - if (canReadActionsLogManagement) { + if (canAccessEndpointActionsLogManagement) { tabs.push({ id: EndpointDetailsTabsTypes.activityLog, name: i18.ACTIVITY_LOG.tabTitle, @@ -97,7 +97,7 @@ export const EndpointDetails = memo(() => { return tabs; }, [ - canReadActionsLogManagement, + canAccessEndpointActionsLogManagement, ContentLoadingMarkup, hostDetails, policyInfo, @@ -142,7 +142,7 @@ export const EndpointDetails = memo(() => { hostname={hostDetails.host.hostname} // show overview tab if forcing response actions history // tab via URL without permission - show={!canReadActionsLogManagement ? 'details' : show} + show={!canAccessEndpointActionsLogManagement ? 'details' : show} tabs={getTabs(hostDetails.agent.id)} /> )} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 4f0f7437ddc42..fab0facfb6551 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -44,7 +44,7 @@ export const useEndpointActionItems = ( canAccessResponseConsole, canIsolateHost, canUnIsolateHost, - canReadActionsLogManagement, + canAccessEndpointActionsLogManagement, } = useUserPrivileges().endpointPrivileges; return useMemo(() => { @@ -141,7 +141,7 @@ export const useEndpointActionItems = ( }, ] : []), - ...(options?.isEndpointList && canReadActionsLogManagement + ...(options?.isEndpointList && canAccessEndpointActionsLogManagement ? [ { 'data-test-subj': 'actionsLink', @@ -253,7 +253,7 @@ export const useEndpointActionItems = ( }, [ allCurrentUrlParams, canAccessResponseConsole, - canReadActionsLogManagement, + canAccessEndpointActionsLogManagement, endpointMetadata, fleetAgentPolicies, getAppUrl, diff --git a/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx index 5a28c41b6ab4f..4f0a675c3f540 100644 --- a/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx @@ -13,6 +13,7 @@ import type { AppContextTestRender } from '../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; import { useUserPrivileges } from '../../../common/components/user_privileges'; import { endpointPageHttpMock } from '../endpoint_hosts/mocks'; +import { getUserPrivilegesMockDefaultValue } from '../../../common/components/user_privileges/__mocks__'; jest.mock('../../../common/components/user_privileges'); @@ -29,7 +30,7 @@ describe('when in the Administration tab', () => { }); afterEach(() => { - useUserPrivilegesMock.mockReset(); + useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue); }); describe('when the user has no permissions', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts index bd80df0c73413..6c6098e2c97f5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts @@ -40,7 +40,13 @@ interface CallApiRouteInterface { authz?: Partial; } -const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); +const Enterprise = licenseMock.createLicense({ + license: { type: 'enterprise', mode: 'enterprise' }, +}); + +const Platinum = licenseMock.createLicense({ + license: { type: 'platinum', mode: 'platinum' }, +}); const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); describe('Action List Route', () => { @@ -94,7 +100,7 @@ describe('Action List Route', () => { const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); - const withLicense = license ? license : Platinum; + const withLicense = license ? license : Enterprise; licenseEmitter.next(withLicense); ctx.securitySolution.getEndpointAuthz.mockResolvedValue({ @@ -102,7 +108,8 @@ describe('Action List Route', () => { // mimicking the behavior of the EndpointAuthz class // just so we can test the license check here // since getEndpointAuthzInitialStateMock sets all keys to true - canReadActionsLogManagement: licenseService.isPlatinumPlus(), + canReadActionsLogManagement: licenseService.isEnterprise(), + canAccessEndpointActionsLogManagement: licenseService.isPlatinumPlus(), }), ...authz, }); @@ -135,13 +142,27 @@ describe('Action List Route', () => { expect(mockResponse.ok).toBeCalled(); }); - it('does not allow user without `canReadActionsLogManagement` access for API requests', async () => { + it('allows user with `canAccessEndpointActionsLogManagement` access for API requests', async () => { await callApiRoute(ENDPOINTS_ACTION_LIST_ROUTE, { - authz: { canReadActionsLogManagement: false }, + authz: { canAccessEndpointActionsLogManagement: true }, + }); + expect(mockResponse.ok).toBeCalled(); + }); + + it('does not allow user without `canReadActionsLogManagement` or `canAccessEndpointActionsLogManagement` access for API requests', async () => { + await callApiRoute(ENDPOINTS_ACTION_LIST_ROUTE, { + authz: { canReadActionsLogManagement: false, canAccessEndpointActionsLogManagement: false }, }); expect(mockResponse.forbidden).toBeCalled(); }); + it('does allow user access to API requests if license is at least platinum', async () => { + await callApiRoute(ENDPOINTS_ACTION_LIST_ROUTE, { + license: Platinum, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it('does not allow user access to API requests if license is below platinum', async () => { await callApiRoute(ENDPOINTS_ACTION_LIST_ROUTE, { license: Gold, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts index 6a932f9bb8af8..5423ca8866155 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts @@ -33,7 +33,7 @@ export function registerActionListRoutes( options: { authRequired: true, tags: ['access:securitySolution'] }, }, withEndpointAuthz( - { all: ['canReadActionsLogManagement'] }, + { any: ['canReadActionsLogManagement', 'canAccessEndpointActionsLogManagement'] }, endpointContext.logFactory.get('endpointActionList'), actionListHandler(endpointContext) ) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts index 51326c8adbd12..b8f6166818807 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts @@ -38,7 +38,7 @@ jest.mock('../../services'); const mockGetActionList = getActionList as jest.Mock; const mockGetActionListByStatus = getActionListByStatus as jest.Mock; -describe(' Action List Handler', () => { +describe('Action List Handler', () => { let endpointAppContextService: EndpointAppContextService; let mockResponse: jest.Mocked; diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts index 7d85b1c2278df..7f4b29a436a37 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts @@ -19,6 +19,7 @@ export const OverviewStatusType = t.type({ disabledCount: t.number, upConfigs: t.array(OverviewStatusMetaDataCodec), downConfigs: t.array(OverviewStatusMetaDataCodec), + enabledIds: t.array(t.string), }); export type OverviewStatus = t.TypeOf; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/links/view_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/links/view_errors.tsx new file mode 100644 index 0000000000000..0ca0b19a4f58e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/links/view_errors.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useSyntheticsSettingsContext } from '../../../contexts'; + +export const ErrorsLink = ({ disabled }: { disabled?: boolean }) => { + const { basePath } = useSyntheticsSettingsContext(); + + return ( + + + + ); +}; + +const VIEW_ERRORS = i18n.translate('xpack.synthetics.monitorSummary.viewErrors', { + defaultMessage: 'View errors', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx index e2f0b2396364d..f6a2dcfdd2b41 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx @@ -14,6 +14,7 @@ import { } from '@elastic/eui'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; import { FailedTestsCount } from './failed_tests_count'; import { useGetUrlParams } from '../../../hooks'; import { SyntheticsDatePicker } from '../../common/date_picker/synthetics_date_picker'; @@ -31,6 +32,8 @@ export const MonitorErrors = () => { [dateRangeEnd, dateRangeStart] ); + const monitorId = useMonitorQueryId(); + return ( <> @@ -43,7 +46,13 @@ export const MonitorErrors = () => { - + {monitorId && ( + + )} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx index be6d3b4d47e1f..845c8def36356 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx @@ -22,6 +22,7 @@ import { AvailabilitySparklines } from '../monitor_summary/availability_sparklin import { DurationSparklines } from '../monitor_summary/duration_sparklines'; import { MonitorCompleteSparklines } from '../monitor_summary/monitor_complete_sparklines'; import { MonitorStatusPanel } from '../monitor_status/monitor_status_panel'; +import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; const STATS_WIDTH_SINGLE_COLUMN_THRESHOLD = 360; // ✨ determined by trial and error @@ -39,6 +40,8 @@ export const MonitorHistory = () => { [updateUrlParams] ); + const monitorId = useMonitorQueryId(); + return ( @@ -76,10 +79,22 @@ export const MonitorHistory = () => { - + {monitorId && ( + + )} - + {monitorId && ( + + )} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx index 6d96e8e39e966..6f2781a53d37e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx @@ -6,8 +6,7 @@ */ import { useKibana } from '@kbn/kibana-react-plugin/public'; -import React from 'react'; -import { useParams } from 'react-router-dom'; +import React, { useMemo } from 'react'; import { useEuiTheme } from '@elastic/eui'; import { ClientPluginsStart } from '../../../../../plugin'; import { useSelectedLocation } from '../hooks/use_selected_location'; @@ -15,18 +14,19 @@ import { useSelectedLocation } from '../hooks/use_selected_location'; interface Props { from: string; to: string; + monitorId: string[]; } -export const MonitorErrorSparklines = (props: Props) => { +export const MonitorErrorSparklines = ({ from, to, monitorId }: Props) => { const { observability } = useKibana().services; const { ExploratoryViewEmbeddable } = observability; - const { monitorId } = useParams<{ monitorId: string }>(); - const { euiTheme } = useEuiTheme(); const selectedLocation = useSelectedLocation(); + const time = useMemo(() => ({ from, to }), [from, to]); + if (!selectedLocation) { return null; } @@ -39,10 +39,10 @@ export const MonitorErrorSparklines = (props: Props) => { hideTicks={true} attributes={[ { + time, seriesType: 'area', - time: props, reportDefinitions: { - 'monitor.id': [monitorId], + 'monitor.id': monitorId, 'observer.geo.name': [selectedLocation?.label], }, dataType: 'synthetics', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx index 8ebc48ea38da9..d8cf1dc057084 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx @@ -6,26 +6,26 @@ */ import { useKibana } from '@kbn/kibana-react-plugin/public'; -import React from 'react'; +import React, { useMemo } from 'react'; import { ReportTypes } from '@kbn/observability-plugin/public'; import { ClientPluginsStart } from '../../../../../plugin'; -import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; import { useSelectedLocation } from '../hooks/use_selected_location'; interface MonitorErrorsCountProps { from: string; to: string; + monitorId: string[]; } -export const MonitorErrorsCount = (props: MonitorErrorsCountProps) => { +export const MonitorErrorsCount = ({ monitorId, from, to }: MonitorErrorsCountProps) => { const { observability } = useKibana().services; const { ExploratoryViewEmbeddable } = observability; - const monitorId = useMonitorQueryId(); - const selectedLocation = useSelectedLocation(); + const time = useMemo(() => ({ from, to }), [from, to]); + if (!selectedLocation || !monitorId) { return null; } @@ -37,9 +37,9 @@ export const MonitorErrorsCount = (props: MonitorErrorsCountProps) => { reportType={ReportTypes.SINGLE_METRIC} attributes={[ { - time: props, + time, reportDefinitions: { - 'monitor.id': [monitorId], + 'monitor.id': monitorId, 'observer.geo.name': [selectedLocation?.label], }, dataType: 'synthetics', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx index 32d4a17d0fb26..ba655148047b9 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx @@ -18,6 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import { LoadWhenInView } from '@kbn/observability-plugin/public'; +import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; import { useEarliestStartDate } from '../hooks/use_earliest_start_data'; import { MonitorErrorSparklines } from './monitor_error_sparklines'; import { MonitorStatusPanel } from '../monitor_status/monitor_status_panel'; @@ -36,6 +37,8 @@ export const MonitorSummary = () => { const { from, loading } = useEarliestStartDate(); const to = 'now'; + const monitorId = useMonitorQueryId(); + if (loading) { return ; } @@ -77,10 +80,12 @@ export const MonitorSummary = () => { - + {monitorId && } - + {monitorId && ( + + )} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx index 6e68ad4008764..68db7152d220a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item.tsx @@ -41,7 +41,7 @@ export const MetricItem = ({ const [isMouseOver, setIsMouseOver] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const locationName = useLocationName({ locationId: monitor.location?.id }); - const { locations } = useStatusByLocation(monitor.id); + const { locations } = useStatusByLocation(monitor.configId); const ping = locations.find((loc) => loc.observer?.geo?.name === locationName); const theme = useTheme(); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx new file mode 100644 index 0000000000000..a4ae1fc95bcb8 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { ErrorsLink } from '../../../../common/links/view_errors'; +import { MonitorErrorSparklines } from '../../../../monitor_details/monitor_summary/monitor_error_sparklines'; +import { MonitorErrorsCount } from '../../../../monitor_details/monitor_summary/monitor_errors_count'; +import { selectOverviewStatus } from '../../../../../state'; + +export function OverviewErrors() { + const { status } = useSelector(selectOverviewStatus); + + return ( + + +

{headingText}

+
+ + + + + + + + + + + + +
+ ); +} + +const headingText = i18n.translate('xpack.synthetics.overview.errors.headingText', { + defaultMessage: 'Last 6 hours', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx index 743a95018e86b..0b7e2e0716440 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx @@ -94,11 +94,11 @@ export function OverviewStatus() { }, [status, statusFilter]); return ( - +

{headingText}

- + { useTrackPageview({ app: 'synthetics', path: 'overview' }); @@ -115,10 +116,13 @@ export const OverviewPage: React.FC = () => { {Boolean(!monitorsLoaded || syntheticsMonitors?.length > 0) && ( <> - + + + + diff --git a/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts b/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts index 3a03d96f14db7..09e985ec0ad70 100644 --- a/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts +++ b/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts @@ -166,6 +166,7 @@ describe('current status route', () => { ); expect(await queryMonitorStatus(uptimeEsClient, 3, 140000, ['id1', 'id2'])).toEqual({ down: 1, + enabledIds: ['id1', 'id2'], up: 2, upConfigs: [ { @@ -302,6 +303,7 @@ describe('current status route', () => { */ expect(await queryMonitorStatus(uptimeEsClient, 10000, 2500, ['id1', 'id2'])).toEqual({ down: 1, + enabledIds: ['id1', 'id2'], up: 2, upConfigs: [ { diff --git a/x-pack/plugins/synthetics/server/routes/status/current_status.ts b/x-pack/plugins/synthetics/server/routes/status/current_status.ts index 637990a017f88..b62377930bace 100644 --- a/x-pack/plugins/synthetics/server/routes/status/current_status.ts +++ b/x-pack/plugins/synthetics/server/routes/status/current_status.ts @@ -35,7 +35,7 @@ export async function queryMonitorStatus( esClient: UptimeEsClient, maxLocations: number, maxPeriod: number, - ids: Array + ids: string[] ): Promise> { const idSize = Math.trunc(DEFAULT_MAX_ES_BUCKET_SIZE / maxLocations); const pageCount = Math.ceil(ids.length / idSize); @@ -135,7 +135,7 @@ export async function queryMonitorStatus( }); }); } - return { up, down, upConfigs, downConfigs }; + return { up, down, upConfigs, downConfigs, enabledIds: ids }; } /** @@ -150,9 +150,9 @@ export async function getStatus( syntheticsMonitorClient: SyntheticsMonitorClient, params: MonitorsQuery ) { - const enabledIds: Array = []; const { query } = params; let monitors; + const enabledIds: string[] = []; let disabledCount = 0; let page = 1; let maxPeriod = 0; @@ -195,6 +195,7 @@ export async function getStatus( ); return { + enabledIds, disabledCount, up, down, diff --git a/x-pack/test/functional/apps/lens/group1/index.ts b/x-pack/test/functional/apps/lens/group1/index.ts index 302289319adbf..622953098b725 100644 --- a/x-pack/test/functional/apps/lens/group1/index.ts +++ b/x-pack/test/functional/apps/lens/group1/index.ts @@ -63,7 +63,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext }); after(async () => { - await esArchiver.unload(esArchive); + await esNode.unload(esArchive); await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.importExport.unload(fixtureDirs.lensBasic); await kibanaServer.importExport.unload(fixtureDirs.lensDefault); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 10477c5a4797a..6d32e399493fe 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -106,7 +106,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should redirect to the Overview page from the unrecognized routes', async () => { - await PageObjects.common.navigateToUrl('ml', 'magic-ai'); + await PageObjects.common.navigateToUrl('ml', 'magic-ai', { + shouldUseHashForSubUrl: false, + insertTimestamp: false, + }); await ml.testExecution.logTestStep('should display a warning banner'); await ml.overviewPage.assertPageNotFoundBannerText('magic-ai'); diff --git a/x-pack/test/functional/services/ml/overview_page.ts b/x-pack/test/functional/services/ml/overview_page.ts index 9fd536b24e760..ac860382f7b67 100644 --- a/x-pack/test/functional/services/ml/overview_page.ts +++ b/x-pack/test/functional/services/ml/overview_page.ts @@ -87,7 +87,7 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC await this.assertPageNotFoundBannerExists(); const text = await testSubjects.getVisibleText('mlPageNotFoundBannerText'); expect(text).to.eql( - `The Machine Learning application doesn't recognize this route: /ml/${pathname}. You've been redirected to the Overview page.` + `The Machine Learning application doesn't recognize this route: /${pathname}. You've been redirected to the Overview page.` ); }, }; diff --git a/yarn.lock b/yarn.lock index 1348827b2539c..9052863b64a9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -150,7 +150,7 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.20.1", "@babel/generator@^7.20.2", "@babel/generator@^7.20.3", "@babel/generator@^7.7.2": +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.20.1", "@babel/generator@^7.20.2", "@babel/generator@^7.20.4", "@babel/generator@^7.7.2": version "7.20.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== @@ -6234,10 +6234,10 @@ resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.15.tgz#26d4768fdda0e466f18d6c9918ca28cc89a4e1fe" integrity sha512-PAmPfzvFA31mRoqZyTVsgJMsvbynR429UTTxhmfsUCrWGh3/fxOrzqBtaTPJsn4UtzTv4Vb0+/O7CARWb69N4g== -"@types/babel__core@*", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.19": - version "7.1.19" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" - integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== +"@types/babel__core@*", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.20": + version "7.1.20" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.20.tgz#e168cdd612c92a2d335029ed62ac94c95b362359" + integrity sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -7476,10 +7476,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.1.6": - version "4.1.8" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.8.tgz#ec20feef480574c0c343b1ae179dc4f4b002155e" - integrity sha512-k5F++V1mGDxRVxVsZauiBwIMDR9rt1LSOa+nN/1ZghSDED2L6c7EWtzo9jnJDet73LVsOyW6CtCAmZBgMNnnuQ== +"@types/selenium-webdriver@^4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.9.tgz#90b24668bf1ec0a049fbc7aeebd4f7ab672440fa" + integrity sha512-QNCYI3Rgjf3bZ2GZwXnUpOJUPR8p7+c9Um9MEKggLaUaF8UfjitH5aPCV1PF0DHVEiPYsXayGVS6+67DO3VILw== dependencies: "@types/ws" "*" @@ -10313,7 +10313,7 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^107.0.2: +chromedriver@^107.0.3: version "107.0.3" resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-107.0.3.tgz#330c0808bb14a53f13ab7e2b0c78adf3cdb4c14b" integrity sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg== @@ -10983,10 +10983,10 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.9: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.4, core-js@^3.26.0, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3: - version "3.26.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.0.tgz#a516db0ed0811be10eac5d94f3b8463d03faccfe" - integrity sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw== +core-js@^3.0.4, core-js@^3.26.1, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3: + version "3.26.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.1.tgz#7a9816dabd9ee846c1c0fe0e8fcad68f3709134e" + integrity sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA== core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -24067,7 +24067,7 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selenium-webdriver@^4.5.0: +selenium-webdriver@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.6.0.tgz#b4e27959e94618b398358b26efe2e64debb22dfd" integrity sha512-HIH/+J+V7l/lbSRSOwyLcpjezg9CV4DLo1pBhP9aphuMlf/PJXEDwC/A/Ht2bFc1AqQppFBGvClYcuMzyO6tRw==