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==