From 11fcb942ca8967ca07fd871481abfe64b62a98d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Mon, 16 Mar 2026 18:27:09 +0100 Subject: [PATCH 01/15] add client side list_id parser utility --- .../utils/exception_list_items/index.ts | 2 ++ .../exception_list_items/list_id_parser.ts | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/list_id_parser.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts index 1dc32749bc6d5..b032b6982eee9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts @@ -10,3 +10,5 @@ export { entriesToConditionEntriesMap, entriesToConditionEntries, } from './mappers'; + +export { parseListIdsFromImportedFile } from './list_id_parser'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/list_id_parser.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/list_id_parser.ts new file mode 100644 index 0000000000000..6c6b9ef4b8497 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/list_id_parser.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Helper function to extract list ids from an imported file. Does not check whether + * the file is valid, just tries to find list_id fields, so it can be used on UI side + * as a pre-check to ensure only the correct artifact type is being imported. + * + * @param file {File} file to extract list ids from + * @returns {Promise>} set of list ids found in the file + */ +export const parseListIdsFromImportedFile = async (file: File): Promise> => + (await file.text()) + .split('\n') + .filter((x) => x.trim() !== '') + .reduce((acc, line) => { + try { + const parsedItem = JSON.parse(line); + + if (parsedItem.list_id) { + acc.add(parsedItem.list_id); + } + } catch (e) { + // ignore parsing errors, the API will handle them and return an error for the line + } + + return acc; + }, new Set()); From 922df3e4496732139d47a60ca1e37337d207f0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Mon, 16 Mar 2026 18:29:09 +0100 Subject: [PATCH 02/15] do not allow endpoint artifact import on shared list page --- .../import_exceptions_list_flyout/index.tsx | 36 +++++++++++++++++-- .../exceptions/translations/shared_list.ts | 8 +++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx index 3cb6c51e16394..66413f550d39a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx @@ -34,10 +34,15 @@ import type { BulkErrorSchema, ImportExceptionsResponseSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { + ENDPOINT_ARTIFACT_LIST_IDS, + ENDPOINT_ARTIFACT_LISTS, +} from '@kbn/securitysolution-list-constants'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { ToastInput, Toast, ErrorToastOptions } from '@kbn/core-notifications-browser'; +import { parseListIdsFromImportedFile } from '../../../common/utils/exception_list_items'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useImportExceptionList } from '../../hooks/use_import_exception_list'; import * as i18n from '../../translations'; @@ -65,6 +70,9 @@ export const ImportExceptionListFlyout = React.memo( const [asNewList, setAsNewList] = useState(false); const [alreadyExistingItem, setAlreadyExistingItem] = useState(false); const [endpointListImporting, setEndpointListImporting] = useState(false); + const isEndpointExceptionsMovedFFEnabled = useIsExperimentalFeatureEnabled( + 'endpointExceptionsMovedUnderManagement' + ); const resetForm = useCallback(() => { if (filePickerRef.current?.fileInput) { @@ -80,8 +88,21 @@ export const ImportExceptionListFlyout = React.memo( const { start: importExceptionList, ...importExceptionListState } = useImportExceptionList(); const ctrl = useRef(new AbortController()); - const handleImportExceptionList = useCallback(() => { + const handleImportExceptionList = useCallback(async () => { if (!importExceptionListState.loading && files) { + if (isEndpointExceptionsMovedFFEnabled) { + for (const file of Array.from(files)) { + const listIds = await parseListIdsFromImportedFile(file); + + if (ENDPOINT_ARTIFACT_LIST_IDS.some((id) => listIds.has(id))) { + addError(new Error(i18n.IMPORT_ENDPOINT_ARTIFACTS_ERROR_TEXT), { + title: i18n.UPLOAD_ERROR, + }); + return; + } + } + } + ctrl.current = new AbortController(); Array.from(files).forEach((file) => @@ -95,7 +116,16 @@ export const ImportExceptionListFlyout = React.memo( }) ); } - }, [asNewList, files, http, importExceptionList, importExceptionListState.loading, overwrite]); + }, [ + importExceptionListState.loading, + files, + isEndpointExceptionsMovedFFEnabled, + addError, + importExceptionList, + http, + overwrite, + asNewList, + ]); const handleImportSuccess = useCallback( (response: ImportExceptionsResponseSchema) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts b/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts index 5d7c8e7163964..0d66f0f656910 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts @@ -213,6 +213,14 @@ export const IMPORT_EXCEPTION_LIST_AS_NEW_LIST = i18n.translate( } ); +export const IMPORT_ENDPOINT_ARTIFACTS_ERROR_TEXT = i18n.translate( + 'xpack.securitySolution.exceptionsTable.importEndpointArtifactsErrorText', + { + defaultMessage: + 'On this page only shared exception lists can be imported, but at least one file contains Endpoint artifacts. Endpoint artifacts can be imported on their respective pages.', + } +); + export const IMPORT_EXCEPTION_ENDPOINT_LIST_WARNING = i18n.translate( 'xpack.securitySolution.exceptionsTable.importExceptionEndpointListWarning', { From b6383db29d9f1d636a32e553d426b212dee23bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Mon, 16 Mar 2026 18:29:56 +0100 Subject: [PATCH 03/15] disable unused code with feature flag for easier cleanup when we remove the FF --- .../import_exceptions_list_flyout/index.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx index 66413f550d39a..ab66e3d30d9b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx @@ -167,6 +167,7 @@ export const ImportExceptionListFlyout = React.memo( if (err.error.message.includes('already exists')) { setAlreadyExistingItem(true); if ( + !isEndpointExceptionsMovedFFEnabled && err.error.message.includes( `Found that list_id: "${ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id}" already exists` ) @@ -187,7 +188,9 @@ export const ImportExceptionListFlyout = React.memo( importExceptionListState.loading, importExceptionListState?.result, importExceptionListState?.result?.errors, + isEndpointExceptionsMovedFFEnabled, ]); + const handleFileChange = useCallback((inputFiles: FileList | null) => { setFiles(inputFiles ?? null); }, []); @@ -235,22 +238,35 @@ export const ImportExceptionListFlyout = React.memo( setAsNewList(false); }} /> - + {isEndpointExceptionsMovedFFEnabled ? ( { setAsNewList(!asNewList); setOverwrite(false); }} /> - + ) : ( + + { + setAsNewList(!asNewList); + setOverwrite(false); + }} + /> + + )} )} From 108c06f06e39085c9b3ae83ceb4de6b9ae40c829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Mon, 16 Mar 2026 18:31:33 +0100 Subject: [PATCH 04/15] allow only given artifact import on endpoint artifact flyout --- .../artifact_import_flyout.test.tsx | 44 ++++++++++++++++--- .../components/artifact_import_flyout.tsx | 14 +++++- .../components/artifact_list_page/mocks.tsx | 8 +++- .../artifact_list_page/translations.ts | 9 +++- .../pages/blocklist/view/blocklist.tsx | 6 +++ .../endpoint_exceptions/translations.tsx | 6 +++ .../event_filters/view/event_filters_list.tsx | 6 +++ .../view/host_isolation_exceptions_list.tsx | 6 +++ .../trusted_apps/view/trusted_apps_list.tsx | 6 +++ .../view/trusted_devices_list.tsx | 6 +++ 10 files changed, 100 insertions(+), 11 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx index 83d265050c53e..427f6e1b1f853 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx @@ -31,6 +31,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { let mockedTrustedAppApi: ReturnType; let props: ArtifactImportFlyoutProps; let ui: ReturnType; + let currentListId: string; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); @@ -38,9 +39,12 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { mockedTrustedAppApi = trustedAppsAllHttpMocks(coreStart.http); + const apiClient = new TrustedAppsApiClient(coreStart.http); + currentListId = apiClient.listId; + props = { labels: artifactListPageLabels, - apiClient: new TrustedAppsApiClient(coreStart.http), + apiClient, onCancel: jest.fn(), onSuccess: jest.fn(), }; @@ -80,7 +84,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { it('should enable `Import` button when a file is selected', async () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); expect(ui.getImportButton()).toBeEnabled(); }); @@ -88,7 +92,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { it('should call the import API when `Import` button is clicked', async () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); expect(mockedTrustedAppApi.responseProvider.trustedAppImportList).toHaveBeenCalledWith( @@ -107,7 +111,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); expect(ui.getImportButton()).toBeDisabled(); @@ -116,7 +120,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { it('should show a success toast and call `onSuccess` after a successful import', async () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ @@ -127,6 +131,34 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { expect(props.onSuccess).toHaveBeenCalled(); }); + it('should show an error toast if another list is being imported', async () => { + await render(); + + await ui.uploadFile(['some-other-list-id']); + await userEvent.click(ui.getImportButton()); + + expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( + expect.objectContaining( + new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) + ), + { title: artifactListPageLabels.pageImportErrorToastTitle } + ); + }); + + it('should show an error toast if not only the current artifact type is included in the import file', async () => { + await render(); + + await ui.uploadFile(['some-other-list-id', currentListId]); + await userEvent.click(ui.getImportButton()); + + expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( + expect.objectContaining( + new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) + ), + { title: artifactListPageLabels.pageImportErrorToastTitle } + ); + }); + it('should show an error toast if the import API fails', async () => { mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => { throw new Error('Fail message from server'); @@ -134,7 +166,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx index 4d0cb0edf1624..5981728a70953 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx @@ -21,6 +21,7 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { parseListIdsFromImportedFile } from '../../../../common/utils/exception_list_items'; import { useToasts } from '../../../../common/lib/kibana'; import type { ArtifactListPageLabels } from '../translations'; import { useImportArtifactList } from '../../../hooks/artifacts/use_import_artifact_list'; @@ -49,8 +50,17 @@ export const ArtifactImportFlyout: React.FC = ({ const { isLoading, mutate } = useImportArtifactList(apiClient); - const handleOnSubmit = useCallback(() => { + const handleOnSubmit = useCallback(async () => { if (file !== null) { + const listIds = await parseListIdsFromImportedFile(file); + + if (listIds.size > 1 || !listIds.has(apiClient.listId)) { + toasts.addError(new Error(labels.pageImportOnlyCurrentArtifactCanBeImportedError), { + title: labels.pageImportErrorToastTitle, + }); + return; + } + mutate( { file }, { @@ -82,7 +92,7 @@ export const ArtifactImportFlyout: React.FC = ({ } ); } - }, [file, labels, mutate, onSuccess, toasts]); + }, [apiClient.listId, file, labels, mutate, onSuccess, toasts]); const handleOnFileChange: EuiFilePickerProps['onChange'] = useCallback( (files: FileList | null) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx index a9aefa1e40168..84d2da46e7dd1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx @@ -169,10 +169,14 @@ export const getArtifactImportFlyoutUiMocks = ( const getCancelButton = () => renderResult.getByTestId(`${dataTestSubj}-cancelButton`); const getImportButton = () => renderResult.getByTestId(`${dataTestSubj}-importButton`); - const uploadFile = () => + const uploadFile = (listIds: string[]) => userEvent.upload( renderResult.getByTestId(`${dataTestSubj}-filePicker`), - new File(['random file content'], 'trusted_apps.ndjson') + new File( + // every id is duplicated to simulate multiple lines. plus one invalid line to make sure parsing errors are ignored + [listIds.map((id) => `{"list_id":"${id}"}\n{"list_id":"${id}"}\ninvalid line`).join('\n')], + 'trusted_apps.ndjson' + ) ); return { queryImportFlyout, getCancelButton, getImportButton, uploadFile }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts index 2307fd6f13c42..bff004b3eca68 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts @@ -65,6 +65,12 @@ export const artifactListPageLabels = Object.freeze({ defaultMessage: 'Artifact list import failed', } ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.artifactListPage.importOnlyCurrentArtifactCanBeImportedToastMessage', + { + defaultMessage: 'Only the current artifact type can be imported on this page.', + } + ), importFlyoutDetails: i18n.translate( 'xpack.securitySolution.artifactListPage.importFlyoutDetails', { @@ -141,7 +147,7 @@ export const artifactListPageLabels = Object.freeze({ cardActionDeleteLabel: i18n.translate( 'xpack.securitySolution.artifactListPage.cardActionDeleteLabel', { - defaultMessage: 'Delete event filter', + defaultMessage: 'Delete artifact', } ), @@ -173,6 +179,7 @@ export type ArtifactListPageRequiredLabels = Pick< | 'pageExportErrorToastTitle' | 'pageImportSuccessToastTitle' | 'pageImportErrorToastTitle' + | 'pageImportOnlyCurrentArtifactCanBeImportedError' | 'getShowingCountLabel' | 'cardActionEditLabel' | 'cardActionDeleteLabel' diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index 54269a6a191f7..abfd6410b8656 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -59,6 +59,12 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Blocklist import failed', } ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.blocklist.pageImportOnlyCurrentArtifactCanBeImportedError', + { + defaultMessage: 'Only blocklist can be imported on this page.', + } + ), getShowingCountLabel: (total) => i18n.translate('xpack.securitySolution.blocklist.showingTotal', { defaultMessage: diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx index d58bbb82837b6..0270b621bbd9e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx @@ -61,6 +61,12 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Endpoint exception list import failed', } ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.endpointExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', + { + defaultMessage: 'Only endpoint exception list can be imported on this page.', + } + ), getShowingCountLabel: (total) => i18n.translate('xpack.securitySolution.endpointExceptions.showingTotal', { defaultMessage: diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx index b52010aa45596..91556af0d95b9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx @@ -97,6 +97,12 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Event filter list import failed', } ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.eventFilters.pageImportOnlyCurrentArtifactCanBeImportedError', + { + defaultMessage: 'Only event filter list can be imported on this page.', + } + ), getShowingCountLabel: (total) => i18n.translate('xpack.securitySolution.eventFilters.showingTotal', { defaultMessage: 'Showing {total} {total, plural, one {event filter} other {event filters}}', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 215c3cca1c8ca..748b5934a72e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -65,6 +65,12 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageLabels = Object.freeze({ defaultMessage: 'Host isolation exception list import failed', } ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', + { + defaultMessage: 'Only host isolation exception list can be imported on this page.', + } + ), getShowingCountLabel: (total) => i18n.translate('xpack.securitySolution.hostIsolationExceptions.showingTotal', { defaultMessage: diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index fe6fd2174184e..ce9e00a57c385 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -72,6 +72,12 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Trusted application list import failed', } ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.trustedApps.pageImportOnlyCurrentArtifactCanBeImportedError', + { + defaultMessage: 'Only trusted application list can be imported on this page.', + } + ), getShowingCountLabel: (total) => i18n.translate('xpack.securitySolution.trustedApps.showingTotal', { defaultMessage: diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx index dbcd38a1c58b6..48201b1a11a4a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx @@ -78,6 +78,12 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Trusted device list import failed', } ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.pageImportOnlyCurrentArtifactCanBeImportedError', + { + defaultMessage: 'Only trusted device list can be imported on this page.', + } + ), getShowingCountLabel: (total) => i18n.translate('xpack.securitySolution.trustedDevices.list.showingTotal', { defaultMessage: From 832d5c5f04a5df0b8bba9a29d167d2028a3cc225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Tue, 17 Mar 2026 18:26:18 +0100 Subject: [PATCH 05/15] fix jest integration test --- .../integration_tests/artifact_list_page.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx index 740cf9e422fd4..14d61bd9fabdc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx @@ -20,6 +20,7 @@ import { import { getDeferred } from '../../../mocks/utils'; import { useGetEndpointSpecificPolicies } from '../../../services/policies/hooks'; import type { ArtifactEntryCardDecoratorProps } from '../../artifact_entry_card'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; jest.mock('../../../services/policies/hooks', () => ({ useGetEndpointSpecificPolicies: jest.fn(), @@ -231,7 +232,7 @@ describe('When using the ArtifactListPage component', () => { await userEvent.click(importExportUi.getMenuButton()); await userEvent.click(importExportUi.getImportButton()); - await importFlyoutUi.uploadFile(); + await importFlyoutUi.uploadFile([ENDPOINT_ARTIFACT_LISTS.trustedApps.id]); const currentApiCallCount = mockedApi.responseProvider.trustedAppsList.mock.calls.length; await userEvent.click(importFlyoutUi.getImportButton()); From fc3315faeae06ffd85cb626e18d70030d846d5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Tue, 31 Mar 2026 15:35:46 +0200 Subject: [PATCH 06/15] switch behavior to APPEND instead of OVERWRITE --- .../components/artifact_import_flyout.test.tsx | 2 +- .../hooks/artifacts/use_import_artifact_list.test.tsx | 5 +++-- .../exceptions_list/exceptions_list_api_client.test.ts | 3 ++- .../services/exceptions_list/exceptions_list_api_client.ts | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx index 427f6e1b1f853..b5f2321e44703 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx @@ -98,7 +98,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { expect(mockedTrustedAppApi.responseProvider.trustedAppImportList).toHaveBeenCalledWith( expect.objectContaining({ version: '2023-10-31', - query: { overwrite: true } as HttpFetchOptionsWithPath['query'], + query: { overwrite: false, as_new_list: false } as HttpFetchOptionsWithPath['query'], }) ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx index 8447b5e667723..378b8f2537ccf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx @@ -69,13 +69,14 @@ describe('Import artifact list hook', () => { headers: { 'Content-Type': undefined }, body: expect.any(FormData), query: { - overwrite: true, + overwrite: false, + as_new_list: false, }, }) ); }); - it('should throw when importing an artifact list', async () => { + it('should throw when server responds with error', async () => { const expectedError = { response: { status: 500, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts index b5a5a75294fdf..4e19f54035930 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -275,7 +275,8 @@ describe('Exceptions List Api Client', () => { headers: { 'Content-Type': undefined }, body: expect.any(FormData), query: { - overwrite: true, + overwrite: false, + as_new_list: false, }, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts index f8cb56e4e8544..728f971742787 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -319,7 +319,8 @@ export class ExceptionsListApiClient { body: formData, headers: { 'Content-Type': undefined }, query: { - overwrite: true, + overwrite: false, + as_new_list: false, } as ImportExceptionListRequestQuery, }); } From d2ae717b6cb71218fc6436701dc2eb22753e6b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Tue, 31 Mar 2026 16:33:57 +0200 Subject: [PATCH 07/15] add positive friction (confirm modal) to import process --- .../artifact_import_confirm_modal.tsx | 83 +++++++++++++++++++ .../artifact_import_flyout.test.tsx | 36 +++++++- .../components/artifact_import_flyout.tsx | 25 +++++- .../components/artifact_list_page/mocks.tsx | 15 +++- 4 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx new file mode 100644 index 0000000000000..815e82087aa50 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; + +interface ArtifactImportConfirmModalProps { + onCancel: () => void; + onConfirm: () => void; + isLoading: boolean; + 'data-test-subj'?: string; +} + +export const ArtifactImportConfirmModal: React.FC = ({ + onCancel, + onConfirm, + isLoading, + 'data-test-subj': dataTestSubj = 'artifactImportConfirmModal', +}) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const modalTitleId = useGeneratedHtmlId(); + + return ( + + {' '} + + + {i18n.translate('xpack.securitySolution.artifactListPage.importConfirmModal.title', { + defaultMessage: 'Import artifacts?', + })} + + + + +

+ {i18n.translate('xpack.securitySolution.artifactListPage.importConfirmModal.info', { + defaultMessage: + 'This will add new artifacts to your list. If an imported artifact already exists, the current version will be kept and the duplicate will be skipped.', + })} +

+
+
+ + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importConfirmModal.cancelButtonTitle', + { defaultMessage: 'Cancel' } + )} + + + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importConfirmModal.confirmButtonTitle', + { defaultMessage: 'Import' } + )} + + +
+ ); +}; + +ArtifactImportConfirmModal.displayName = 'ArtifactImportConfirmModal'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx index b5f2321e44703..0947d0a7a4f43 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx @@ -89,12 +89,33 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { expect(ui.getImportButton()).toBeEnabled(); }); - it('should call the import API when `Import` button is clicked', async () => { + it('should show a confirmation modal when `Import` button is clicked', async () => { await render(); await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); + expect(ui.queryConfirmModal()).toBeInTheDocument(); + }); + + it("should close the confirmation modal but keep the flyout open when the modal's cancel button is clicked", async () => { + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalCancelButton()); + + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + expect(ui.queryImportFlyout()).toBeInTheDocument(); + }); + + it('should call the import API when the modal is confirmed', async () => { + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + expect(mockedTrustedAppApi.responseProvider.trustedAppImportList).toHaveBeenCalledWith( expect.objectContaining({ version: '2023-10-31', @@ -103,7 +124,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { ); }); - it('should disable `Import` button while the import is in progress', async () => { + it('should disable `Import` button on the modal and the flyout while the import is in progress', async () => { const deferrable = getDeferred(); mockedTrustedAppApi.responseProvider.trustedAppImportList.mockDelay.mockReturnValue( deferrable.promise @@ -113,8 +134,10 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); expect(ui.getImportButton()).toBeDisabled(); + expect(ui.getConfirmModalConfirmButton()).toBeDisabled(); }); it('should show a success toast and call `onSuccess` after a successful import', async () => { @@ -122,6 +145,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ text: '2 artifacts imported.', @@ -131,11 +155,12 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { expect(props.onSuccess).toHaveBeenCalled(); }); - it('should show an error toast if another list is being imported', async () => { + it('should show an error toast and close the modal if another list is being imported', async () => { await render(); await ui.uploadFile(['some-other-list-id']); await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( expect.objectContaining( @@ -143,6 +168,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { ), { title: artifactListPageLabels.pageImportErrorToastTitle } ); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); }); it('should show an error toast if not only the current artifact type is included in the import file', async () => { @@ -150,6 +176,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await ui.uploadFile(['some-other-list-id', currentListId]); await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( expect.objectContaining( @@ -157,6 +184,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { ), { title: artifactListPageLabels.pageImportErrorToastTitle } ); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); }); it('should show an error toast if the import API fails', async () => { @@ -168,10 +196,12 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( expect.objectContaining(new Error('Fail message from server')), { title: 'Artifact list import failed', toastMessage: 'Fail message from server' } ); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx index 5981728a70953..7c8104b419788 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx @@ -27,6 +27,7 @@ import type { ArtifactListPageLabels } from '../translations'; import { useImportArtifactList } from '../../../hooks/artifacts/use_import_artifact_list'; import type { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; +import { ArtifactImportConfirmModal } from './artifact_import_confirm_modal'; export interface ArtifactImportFlyoutProps { onCancel: () => void; @@ -47,10 +48,19 @@ export const ArtifactImportFlyout: React.FC = ({ const getTestId = useTestIdGenerator(dataTestSubj); const [file, setFile] = React.useState(null); + const [showConfirmModal, setShowConfirmModal] = React.useState(false); const { isLoading, mutate } = useImportArtifactList(apiClient); - const handleOnSubmit = useCallback(async () => { + const handleOnSubmit = useCallback(() => { + setShowConfirmModal(true); + }, []); + + const handleOnCancelModal = useCallback(() => { + setShowConfirmModal(false); + }, []); + + const handleOnConfirmModal = useCallback(async () => { if (file !== null) { const listIds = await parseListIdsFromImportedFile(file); @@ -58,6 +68,8 @@ export const ArtifactImportFlyout: React.FC = ({ toasts.addError(new Error(labels.pageImportOnlyCurrentArtifactCanBeImportedError), { title: labels.pageImportErrorToastTitle, }); + setShowConfirmModal(false); + return; } @@ -69,6 +81,7 @@ export const ArtifactImportFlyout: React.FC = ({ title: labels.pageImportErrorToastTitle, toastMessage: error.body?.message || error.message, }); + setShowConfirmModal(false); }, onSuccess: (response) => { toasts.addSuccess({ @@ -87,6 +100,7 @@ export const ArtifactImportFlyout: React.FC = ({ }`, toastLifeTimeMs: 60_000, }); + setShowConfirmModal(false); onSuccess(); }, } @@ -117,6 +131,15 @@ export const ArtifactImportFlyout: React.FC = ({ aria-labelledby={importEndpointArtifactListFlyoutTitleId} data-test-subj={getTestId()} > + {showConfirmModal && ( + + )} +

{labels.pageImportButtonTitle}

diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx index 84d2da46e7dd1..3b7144eae3065 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx @@ -168,6 +168,11 @@ export const getArtifactImportFlyoutUiMocks = ( const queryImportFlyout = () => renderResult.queryByTestId(dataTestSubj); const getCancelButton = () => renderResult.getByTestId(`${dataTestSubj}-cancelButton`); const getImportButton = () => renderResult.getByTestId(`${dataTestSubj}-importButton`); + const queryConfirmModal = () => renderResult.queryByTestId(`${dataTestSubj}-confirmModal`); + const getConfirmModalConfirmButton = () => + renderResult.getByTestId(`${dataTestSubj}-confirmModal-confirmButton`); + const getConfirmModalCancelButton = () => + renderResult.getByTestId(`${dataTestSubj}-confirmModal-cancelButton`); const uploadFile = (listIds: string[]) => userEvent.upload( @@ -179,5 +184,13 @@ export const getArtifactImportFlyoutUiMocks = ( ) ); - return { queryImportFlyout, getCancelButton, getImportButton, uploadFile }; + return { + queryImportFlyout, + getCancelButton, + getImportButton, + queryConfirmModal, + getConfirmModalConfirmButton, + getConfirmModalCancelButton, + uploadFile, + }; }; From 1f53c55d9af52bd09ebf1565243aed218fc34f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Wed, 1 Apr 2026 11:00:07 +0200 Subject: [PATCH 08/15] update error message texts --- .../validators/base_validator.test.ts | 30 +++++++++++++-- .../endpoint/validators/base_validator.ts | 38 +++++++++++++------ .../validators/blocklist_validator.ts | 2 +- .../endpoint_exceptions_validator.ts | 2 +- .../validators/event_filter_validator.ts | 2 +- .../host_isolation_exceptions_validator.ts | 2 +- .../endpoint/validators/mocks.ts | 4 ++ .../validators/trusted_app_validator.ts | 2 +- .../validators/trusted_device_validator.ts | 2 +- 9 files changed, 63 insertions(+), 21 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts index 5a12b473efe89..b948b8d84369e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts @@ -654,6 +654,28 @@ describe('When using Artifacts Exceptions BaseValidator', () => { }); }); + describe('#validateCanImportGlobalArtifacts()', () => { + beforeEach(() => { + exceptionLikeItem.tags = [GLOBAL_ARTIFACT_TAG]; + }); + + it('should error is user does not have new global artifact management privilege', async () => { + authzMock.canManageGlobalArtifacts = false; + + await expect( + validator._validateCanImportGlobalArtifacts(exceptionLikeItem) + ).rejects.toThrow( + /This artifact can't be imported because you don't have permission to manage global artifacts./ + ); + }); + + it('should allow import of global artifacts when user has privilege', async () => { + await expect( + validator._validateCanImportGlobalArtifacts(exceptionLikeItem) + ).resolves.toBeUndefined(); + }); + }); + describe('#validateImportOwnerSpaceIds()', () => { it('should do nothing when item has no tags', async () => { exceptionLikeItem.tags = []; @@ -676,7 +698,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { setArtifactOwnerSpaceId(exceptionLikeItem, 'inaccessible-space'); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /invalid owner space IDs/ + /This artifact can\'t be imported because it belongs to a space you don\'t have access to/ ); }); @@ -689,7 +711,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { ]); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /Importing artifacts that are not visible in the current space/ + /This artifact can't be imported because it isn't visible in the current space/ ); }); }); @@ -709,7 +731,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { setArtifactOwnerSpaceId(exceptionLikeItem, 'other-space'); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /Importing artifacts to a different space requires global artifact management privilege/ + /This artifact can't be imported because you don't have permission to manage artifacts in other spaces/ ); }); @@ -719,7 +741,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { setArtifactOwnerSpaceId(exceptionLikeItem, 'other-space'); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /Importing artifacts to a different space requires global artifact management privilege/ + /This artifact can't be imported because you don't have permission to manage artifacts in other spaces/ ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts index 472fc2853ce8e..9bea0be5b111d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts @@ -47,7 +47,15 @@ const IMPORTING_TO_OTHER_SPACE_NOT_ALLOWED_MESSAGE = i18n.translate( 'xpack.securitySolution.baseValidator.importingToOtherSpaceNotAllowedMessage', { defaultMessage: - 'Importing artifacts to a different space requires global artifact management privilege', + "This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", + } +); + +const GLOBAL_ARTIFACT_IMPORT_NOT_ALLOWED_MESSAGE = i18n.translate( + 'xpack.securitySolution.baseValidator.noGlobalArtifactImportMessage', + { + defaultMessage: + "This artifact can't be imported because you don't have permission to manage global artifacts. Contact your administrator for access.", } ); @@ -62,19 +70,18 @@ export const GLOBAL_ARTIFACT_MANAGEMENT_NOT_ALLOWED_MESSAGE = i18n.translate( const IMPORTING_ARTIFACT_NOT_VISIBLE_IN_CURRENT_SPACE_NOT_ALLOWED_MESSAGE = i18n.translate( 'xpack.securitySolution.baseValidator.importingArtifactNotVisibleInCurrentSpace', { - defaultMessage: 'Importing artifacts that are not visible in the current space is not allowed', + defaultMessage: + "This artifact can't be imported because it isn't visible in the current space. Try importing it from a matching space or a space with access to the related policy.", } ); -const IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID = (spaceIds: string[]): string => - i18n.translate('xpack.securitySolution.baseValidator.invalidOwnerSpaceId', { +const IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID = i18n.translate( + 'xpack.securitySolution.baseValidator.invalidOwnerSpaceId', + { defaultMessage: - 'Importing artifacts with invalid owner space IDs is not allowed. The following space {numberOfSpaces, plural, one {ID is} other {IDs are} } invalid or unaccessible by current user: {invalidSpaceIds}', - values: { - invalidSpaceIds: spaceIds.join(', '), - numberOfSpaces: spaceIds.length, - }, - }); + "This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", + } +); const ITEM_CANNOT_BE_MANAGED_IN_CURRENT_SPACE_MESSAGE = (spaceIds: string[]): string => i18n.translate('xpack.securitySolution.baseValidator.cannotManageItemInCurrentSpace', { @@ -342,7 +349,7 @@ export class BaseValidator { if (invalidSpaceIds.length > 0) { throw new EndpointArtifactExceptionValidationError( - IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID(invalidSpaceIds), + IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID, 403 ); } @@ -422,6 +429,15 @@ export class BaseValidator { } } + protected async validateCanImportGlobalArtifacts(item: ExceptionItemLikeOptions): Promise { + if (!this.isItemByPolicy(item) && !(await this.endpointAuthzPromise).canManageGlobalArtifacts) { + throw new EndpointArtifactExceptionValidationError( + `${ENDPOINT_AUTHZ_ERROR_MESSAGE}. ${GLOBAL_ARTIFACT_IMPORT_NOT_ALLOWED_MESSAGE}`, + 403 + ); + } + } + protected async validateCanUpdateItemInActiveSpace( updatedItem: Partial>, currentSavedItem: ExceptionListItemSchema diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts index 9e634b46f7c5e..607c48e0257b0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts @@ -237,7 +237,7 @@ export class BlocklistValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts index 76248351499e7..6f4c113f48bc2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts @@ -49,7 +49,7 @@ export class EndpointExceptionsValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts index 271209f458d37..e98c7055dba35 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts @@ -56,7 +56,7 @@ export class EventFilterValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts index c2a272400d8db..25bd178bc85d7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts @@ -80,7 +80,7 @@ export class HostIsolationExceptionsValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts index 7025f7b478fa4..28c6ca11c539a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts @@ -70,6 +70,10 @@ export class BaseValidatorMock extends BaseValidator { return this.validateCanCreateGlobalArtifacts(item); } + _validateCanImportGlobalArtifacts(item: ExceptionItemLikeOptions): Promise { + return this.validateCanImportGlobalArtifacts(item); + } + _validateCanUpdateItemInActiveSpace( updatedItem: Partial>, currentSavedItem: ExceptionListItemSchema diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index 00d72ec85d5ef..42ec751483c27 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -227,7 +227,7 @@ export class TrustedAppValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts index 7fe07fe66ce11..c651a7a8158c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts @@ -127,7 +127,7 @@ export class TrustedDeviceValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create From ee8d864fc195ce4eff61032bfef4ba6e177411b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Wed, 1 Apr 2026 14:47:29 +0200 Subject: [PATCH 09/15] show success/warning/danger modal based on import result --- .../artifact_import_flyout.test.tsx | 209 ++++++++++++++---- .../components/artifact_import_flyout.tsx | 46 ++-- .../artifact_list_page/translations.ts | 22 +- 3 files changed, 204 insertions(+), 73 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx index 0947d0a7a4f43..0f3650f144826 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx @@ -140,68 +140,179 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { expect(ui.getConfirmModalConfirmButton()).toBeDisabled(); }); - it('should show a success toast and call `onSuccess` after a successful import', async () => { - await render(); + describe('when handling server response', () => { + const LIST_CONFLICT_ERROR = { + error: { + status_code: 409, + message: + 'Found that list_id: "endpoint_list" already exists. Import of list_id: "endpoint_list" skipped.', + }, + list_id: 'endpoint_list', + }; - await ui.uploadFile([currentListId]); - await userEvent.click(ui.getImportButton()); - await userEvent.click(ui.getConfirmModalConfirmButton()); + const ITEM_CONFLICT_ERROR = { + error: { + status_code: 409, + message: + 'Found that item_id: "0d82595f-f79d-48c8-8522-7715e1640884" already exists. Import of item_id: "0d82595f-f79d-48c8-8522-7715e1640884" skipped.', + }, + list_id: 'endpoint_list', + item_id: '0d82595f-f79d-48c8-8522-7715e1640884', + }; - expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: '2 artifacts imported.', - title: 'Artifact list imported successfully', - toastLifeTimeMs: 60_000, + const ITEM_ENDPOINT_ARTIFACT_ERROR = { + error: { + status_code: 403, + message: + "EndpointArtifactError: This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", + }, + list_id: 'endpoint_list', + item_id: '1a0200db-3dd7-46f4-bb4d-f23904559c32', + }; + + it('should show a success toast and call `onSuccess` after a successful import', async () => { + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Artifacts imported', + text: 'All artifacts were imported successfully.', + toastLifeTimeMs: 60_000, + }); + expect(props.onSuccess).toHaveBeenCalled(); }); - expect(props.onSuccess).toHaveBeenCalled(); - }); - it('should show an error toast and close the modal if another list is being imported', async () => { - await render(); + it('should not care about list conflict in response', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => ({ + errors: [LIST_CONFLICT_ERROR], + success: false, + success_count: 2, + success_exception_lists: false, + success_count_exception_lists: 0, + success_exception_list_items: true, + success_count_exception_list_items: 2, + })); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Artifacts imported', + text: 'All artifacts were imported successfully.', + toastLifeTimeMs: 60_000, + }); + expect(props.onSuccess).toHaveBeenCalled(); + }); - await ui.uploadFile(['some-other-list-id']); - await userEvent.click(ui.getImportButton()); - await userEvent.click(ui.getConfirmModalConfirmButton()); + it('should show a warning toast when some of the items were not imported', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => ({ + errors: [LIST_CONFLICT_ERROR, ITEM_CONFLICT_ERROR, ITEM_ENDPOINT_ARTIFACT_ERROR], + success: false, + success_count: 3, + success_exception_lists: false, + success_count_exception_lists: 0, + success_exception_list_items: false, + success_count_exception_list_items: 3, // there are some successful imports + })); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledWith({ + title: 'Import completed with errors', + toastLifeTimeMs: 60_000, + }); + expect(props.onSuccess).toHaveBeenCalled(); + }); - expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( - expect.objectContaining( - new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) - ), - { title: artifactListPageLabels.pageImportErrorToastTitle } - ); - expect(ui.queryConfirmModal()).not.toBeInTheDocument(); - }); + it('should call `showErrorList` when the `View errors` button is pressed in the warning toast', async () => {}); + + it('should show a danger toast when none of the items were imported', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => ({ + errors: [LIST_CONFLICT_ERROR, ITEM_CONFLICT_ERROR, ITEM_ENDPOINT_ARTIFACT_ERROR], + success: false, + success_count: 3, + success_exception_lists: false, + success_count_exception_lists: 0, + success_exception_list_items: false, + success_count_exception_list_items: 0, // there are no successful imports + })); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: "Artifacts weren't imported", + toastLifeTimeMs: 60_000, + }); + expect(props.onSuccess).toHaveBeenCalled(); + }); - it('should show an error toast if not only the current artifact type is included in the import file', async () => { - await render(); + it('should call `showErrorList` when the `View errors` button is pressed in the error toast', async () => {}); - await ui.uploadFile(['some-other-list-id', currentListId]); - await userEvent.click(ui.getImportButton()); - await userEvent.click(ui.getConfirmModalConfirmButton()); + it('should show an error toast and close the modal if another list is being imported', async () => { + await render(); - expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( - expect.objectContaining( - new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) - ), - { title: artifactListPageLabels.pageImportErrorToastTitle } - ); - expect(ui.queryConfirmModal()).not.toBeInTheDocument(); - }); + await ui.uploadFile(['some-other-list-id']); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); - it('should show an error toast if the import API fails', async () => { - mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => { - throw new Error('Fail message from server'); + expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( + expect.objectContaining( + new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) + ), + { title: artifactListPageLabels.pageImportErrorToastTitle } + ); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); }); - await render(); + it('should show an error toast if not only the current artifact type is included in the import file', async () => { + await render(); - await ui.uploadFile([currentListId]); - await userEvent.click(ui.getImportButton()); - await userEvent.click(ui.getConfirmModalConfirmButton()); + await ui.uploadFile(['some-other-list-id', currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); - expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( - expect.objectContaining(new Error('Fail message from server')), - { title: 'Artifact list import failed', toastMessage: 'Fail message from server' } - ); - expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( + expect.objectContaining( + new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) + ), + { title: artifactListPageLabels.pageImportErrorToastTitle } + ); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + }); + + it('should show an error toast if the import API fails', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => { + throw new Error('Fail message from server'); + }); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( + expect.objectContaining(new Error('Fail message from server')), + { + title: artifactListPageLabels.pageImportErrorToastTitle, + toastMessage: 'Fail message from server', + } + ); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx index 7c8104b419788..51fefe1a6f92c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx @@ -84,22 +84,36 @@ export const ArtifactImportFlyout: React.FC = ({ setShowConfirmModal(false); }, onSuccess: (response) => { - toasts.addSuccess({ - title: labels.pageImportSuccessToastTitle, - text: `${labels.getPageImportSuccessToastText?.( - response.success_count_exception_list_items - )}${ - response.errors.length - ? ` ${response.errors.length} errors happened: ${response.errors - .map( - (item, index) => - `(${index + 1}) item (${item.item_id}): ${item.error.message}.` - ) - .join(' -- ')}` - : '' - }`, - toastLifeTimeMs: 60_000, - }); + if (response.success_exception_list_items === true) { + toasts.addSuccess({ + title: labels.pageImportSuccessToastTitle, + text: labels.pageImportSuccessToastText, + toastLifeTimeMs: 60_000, + }); + } else { + const itemErrors = response.errors.filter( + (error) => + !( + error.error.status_code === 409 && + error.error.message.match(/Found that list_id: "\w+" already exists/) + ) + ); + + if (itemErrors.length > 0 && response.success_count_exception_list_items > 0) { + toasts.addWarning({ + title: labels.pageImportCompletedWithErrorsToastTitle, + toastLifeTimeMs: 60_000, + }); + } + + if (itemErrors.length > 0 && response.success_count_exception_list_items === 0) { + toasts.addDanger({ + title: labels.pageImportErrorToastTitle, + toastLifeTimeMs: 60_000, + }); + } + } + setShowConfirmModal(false); onSuccess(); }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts index bff004b3eca68..982077debcad5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts @@ -50,19 +50,25 @@ export const artifactListPageLabels = Object.freeze({ pageImportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.artifactListPage.importSuccessToastTitle', { - defaultMessage: 'Artifact list imported successfully', + defaultMessage: 'Artifacts imported', + } + ), + pageImportSuccessToastText: i18n.translate( + 'xpack.securitySolution.artifactListPage.importSuccessToastText', + { + defaultMessage: 'All artifacts were imported successfully.', + } + ), + pageImportCompletedWithErrorsToastTitle: i18n.translate( + 'xpack.securitySolution.artifactListPage.importCompletedWithErrorsToastTitle', + { + defaultMessage: 'Import completed with errors', } ), - getPageImportSuccessToastText: (successCount: number): string => - i18n.translate('xpack.securitySolution.artifactListPage.importSuccessToastText', { - defaultMessage: - '{successCount} {successCount, plural, one {artifact} other {artifacts}} imported.', - values: { successCount }, - }), pageImportErrorToastTitle: i18n.translate( 'xpack.securitySolution.artifactListPage.importErrorToastTitle', { - defaultMessage: 'Artifact list import failed', + defaultMessage: "Artifacts weren't imported", } ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( From 191862893f607876be9acd3f3a5c3634b3a277f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Wed, 1 Apr 2026 14:55:01 +0200 Subject: [PATCH 10/15] remove artifact specific success/error toast titles --- .../components/artifact_import_flyout.tsx | 4 ++-- .../components/artifact_list_page/translations.ts | 2 -- .../management/pages/blocklist/view/blocklist.tsx | 12 ------------ .../pages/endpoint_exceptions/translations.tsx | 12 ------------ .../pages/event_filters/view/event_filters_list.tsx | 12 ------------ .../view/host_isolation_exceptions_list.tsx | 12 ------------ .../pages/trusted_apps/view/trusted_apps_list.tsx | 12 ------------ .../trusted_devices/view/trusted_devices_list.tsx | 12 ------------ 8 files changed, 2 insertions(+), 76 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx index 51fefe1a6f92c..dad2d3dd3057c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx @@ -23,7 +23,7 @@ import { import React, { useCallback } from 'react'; import { parseListIdsFromImportedFile } from '../../../../common/utils/exception_list_items'; import { useToasts } from '../../../../common/lib/kibana'; -import type { ArtifactListPageLabels } from '../translations'; +import type { artifactListPageLabels } from '../translations'; import { useImportArtifactList } from '../../../hooks/artifacts/use_import_artifact_list'; import type { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; @@ -33,7 +33,7 @@ export interface ArtifactImportFlyoutProps { onCancel: () => void; onSuccess: () => void; apiClient: ExceptionsListApiClient; - labels: ArtifactListPageLabels; + labels: typeof artifactListPageLabels; 'data-test-subj'?: string; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts index 982077debcad5..a64ea99474527 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts @@ -183,8 +183,6 @@ export type ArtifactListPageRequiredLabels = Pick< | 'pageExportButtonTitle' | 'pageExportSuccessToastTitle' | 'pageExportErrorToastTitle' - | 'pageImportSuccessToastTitle' - | 'pageImportErrorToastTitle' | 'pageImportOnlyCurrentArtifactCanBeImportedError' | 'getShowingCountLabel' | 'cardActionEditLabel' diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index abfd6410b8656..11e8224efdba4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -47,18 +47,6 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Blocklist export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.blocklist.pageImportSuccessToastTitle', - { - defaultMessage: 'Blocklist imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.blocklist.pageImportErrorToastTitle', - { - defaultMessage: 'Blocklist import failed', - } - ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.blocklist.pageImportOnlyCurrentArtifactCanBeImportedError', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx index 0270b621bbd9e..91cabbfb91c7a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx @@ -49,18 +49,6 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Endpoint exception list export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.endpointExceptions.pageImportSuccessToastTitle', - { - defaultMessage: 'Endpoint exception list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.endpointExceptions.pageImportErrorToastTitle', - { - defaultMessage: 'Endpoint exception list import failed', - } - ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx index 91556af0d95b9..08a74be61d231 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx @@ -85,18 +85,6 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Event filter list export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.eventFilters.pageImportSuccessToastTitle', - { - defaultMessage: 'Event filter list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.eventFilters.pageImportErrorToastTitle', - { - defaultMessage: 'Event filter list import failed', - } - ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.eventFilters.pageImportOnlyCurrentArtifactCanBeImportedError', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 748b5934a72e4..41dce1a79c335 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -53,18 +53,6 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageLabels = Object.freeze({ defaultMessage: 'Host isolation exception list export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.hostIsolationExceptions.pageImportSuccessToastTitle', - { - defaultMessage: 'Host isolation exception list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.hostIsolationExceptions.pageImportErrorToastTitle', - { - defaultMessage: 'Host isolation exception list import failed', - } - ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index ce9e00a57c385..6b023437c5517 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -60,18 +60,6 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Trusted application list export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.trustedApps.pageImportSuccessToastTitle', - { - defaultMessage: 'Trusted application list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.trustedApps.pageImportErrorToastTitle', - { - defaultMessage: 'Trusted application list import failed', - } - ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.trustedApps.pageImportOnlyCurrentArtifactCanBeImportedError', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx index 48201b1a11a4a..ecefa220073a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx @@ -66,18 +66,6 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Trusted device list export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.trustedDevices.list.pageImportSuccessToastTitle', - { - defaultMessage: 'Trusted device list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.trustedDevices.list.pageImportErrorToastTitle', - { - defaultMessage: 'Trusted device list import failed', - } - ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageImportOnlyCurrentArtifactCanBeImportedError', { From 20b0f1d83d01e6cb213a2fe06585825235bfb664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Wed, 1 Apr 2026 15:00:58 +0200 Subject: [PATCH 11/15] remove 'list' from texts' wording --- .../components/artifact_list_page/translations.ts | 9 ++++----- .../management/pages/blocklist/view/blocklist.tsx | 12 ++++++------ .../pages/endpoint_exceptions/translations.tsx | 12 ++++++------ .../pages/event_filters/view/event_filters_list.tsx | 12 ++++++------ .../view/host_isolation_exceptions_list.tsx | 12 ++++++------ .../policy/view/artifacts/empty/translations.ts | 2 +- .../policy/view/tabs/blocklists_translations.ts | 2 +- .../view/tabs/endpoint_exceptions_translations.ts | 2 +- .../policy/view/tabs/event_filters_translations.ts | 2 +- .../tabs/host_isolation_exceptions_translations.ts | 2 +- .../policy/view/tabs/trusted_apps_translations.ts | 2 +- .../policy/view/tabs/trusted_devices_translations.ts | 2 +- .../pages/trusted_apps/view/trusted_apps_list.tsx | 12 ++++++------ .../trusted_devices/view/trusted_devices_list.tsx | 12 ++++++------ 14 files changed, 47 insertions(+), 48 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts index a64ea99474527..5b3f2004add54 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts @@ -26,7 +26,7 @@ export const artifactListPageLabels = Object.freeze({ pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.artifactListPage.importButtonTitle', { - defaultMessage: 'Import artifact list', + defaultMessage: 'Import artifacts', } ), pageExportButtonTitle: i18n.translate( @@ -80,14 +80,13 @@ export const artifactListPageLabels = Object.freeze({ importFlyoutDetails: i18n.translate( 'xpack.securitySolution.artifactListPage.importFlyoutDetails', { - defaultMessage: - 'Attention: importing your artifacts will overwrite the existing list, which results in losing all existing artifacts that can be edited by the current user.', + defaultMessage: 'You can import artifacts to your artifact list.', } ), importFlyoutImportSubmitButtonLabel: i18n.translate( 'xpack.securitySolution.artifactListPage.importFlyoutImportSubmitButtonLabel', { - defaultMessage: 'Import list', + defaultMessage: 'Import artifacts', } ), @@ -112,7 +111,7 @@ export const artifactListPageLabels = Object.freeze({ ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.artifactListPage.emptyStateImportButtonLabel', - { defaultMessage: 'Import list' } + { defaultMessage: 'Import artifacts' } ), // ------------------------------ diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index 11e8224efdba4..b85dd416e280d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -30,27 +30,27 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Add blocklist entry', }), pageImportButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageImportButtonTitle', { - defaultMessage: 'Import blocklist', + defaultMessage: 'Import blocklist entries', }), pageExportButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageExportButtonTitle', { - defaultMessage: 'Export blocklist', + defaultMessage: 'Export blocklist entries', }), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.blocklist.pageExportSuccessToastTitle', { - defaultMessage: 'Blocklist exported successfully', + defaultMessage: 'Blocklist entries exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.blocklist.pageExportErrorToastTitle', { - defaultMessage: 'Blocklist export failed', + defaultMessage: 'Blocklist entries export failed', } ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.blocklist.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only blocklist can be imported on this page.', + defaultMessage: 'Only blocklist entries can be imported on this page.', } ), getShowingCountLabel: (total) => @@ -127,7 +127,7 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.blocklist.emptyStateImportButtonLabel', - { defaultMessage: 'Import blocklist' } + { defaultMessage: 'Import blocklist entries' } ), searchPlaceholderInfo: i18n.translate('xpack.securitySolution.blocklist.searchPlaceholderInfo', { defaultMessage: 'Search on the fields below: name, description, value', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx index 91cabbfb91c7a..2a38296655765 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx @@ -28,31 +28,31 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageImportButtonTitle', { - defaultMessage: 'Import endpoint exception list', + defaultMessage: 'Import endpoint exceptions', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageExportButtonTitle', { - defaultMessage: 'Export endpoint exception list', + defaultMessage: 'Export endpoint exceptions', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.exportSuccessToastTitle', { - defaultMessage: 'Endpoint exception list exported successfully', + defaultMessage: 'Endpoint exceptions exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.exportErrorToastTitle', { - defaultMessage: 'Endpoint exception list export failed', + defaultMessage: 'Endpoint exceptions export failed', } ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only endpoint exception list can be imported on this page.', + defaultMessage: 'Only endpoint exceptions can be imported on this page.', } ), getShowingCountLabel: (total) => @@ -135,7 +135,7 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.endpointExceptions.emptyStateImportButtonLabel', - { defaultMessage: 'Import endpoint exception list' } + { defaultMessage: 'Import endpoint exceptions' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.endpointExceptions.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx index 08a74be61d231..3c21881187bff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx @@ -64,31 +64,31 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.eventFilters.pageImportButtonTitle', { - defaultMessage: 'Import event filter list', + defaultMessage: 'Import event filters', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.eventFilters.pageExportButtonTitle', { - defaultMessage: 'Export event filter list', + defaultMessage: 'Export event filters', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.eventFilters.exportSuccessToastTitle', { - defaultMessage: 'Event filter list exported successfully', + defaultMessage: 'Event filters exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.eventFilters.exportErrorToastTitle', { - defaultMessage: 'Event filter list export failed', + defaultMessage: 'Event filters export failed', } ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.eventFilters.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only event filter list can be imported on this page.', + defaultMessage: 'Only event filters can be imported on this page.', } ), getShowingCountLabel: (total) => @@ -167,7 +167,7 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.eventFilters.emptyStateImportButtonLabel', - { defaultMessage: 'Import event filter list' } + { defaultMessage: 'Import event filters' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.eventFilters.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 41dce1a79c335..55504da8bf5bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -32,31 +32,31 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageLabels = Object.freeze({ pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.pageImportButtonTitle', { - defaultMessage: 'Import host isolation exception list', + defaultMessage: 'Import host isolation exceptions', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.pageExportButtonTitle', { - defaultMessage: 'Export host isolation exception list', + defaultMessage: 'Export host isolation exceptions', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.exportSuccessToastTitle', { - defaultMessage: 'Host isolation exception list exported successfully', + defaultMessage: 'Host isolation exceptions exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.exportErrorToastTitle', { - defaultMessage: 'Host isolation exception list export failed', + defaultMessage: 'Host isolation exceptions export failed', } ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only host isolation exception list can be imported on this page.', + defaultMessage: 'Only host isolation exceptions can be imported on this page.', } ), getShowingCountLabel: (total) => @@ -139,7 +139,7 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageLabels = Object.freeze({ ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.emptyStateImportButtonLabel', - { defaultMessage: 'Import host isolation exception list' } + { defaultMessage: 'Import host isolation exceptions' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts index 670984541f837..426146a0c5025 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts @@ -55,6 +55,6 @@ export const POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS = Object.freeze({ ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.artifacts.empty.unexisting.importAction', - { defaultMessage: 'Import artifact list' } + { defaultMessage: 'Import artifacts' } ), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts index fda35e93120a9..85fc316f4c7d5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts @@ -132,7 +132,7 @@ export const POLICY_ARTIFACT_BLOCKLISTS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.blocklist.empty.unexisting.importAction', - { defaultMessage: 'Import blocklist' } + { defaultMessage: 'Import blocklist entries' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.blocklists.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts index f96faf61aadf0..4587c840bc7f3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts @@ -138,7 +138,7 @@ export const POLICY_ARTIFACT_ENDPOINT_EXCEPTIONS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.endpointExceptions.empty.unexisting.importAction', - { defaultMessage: 'Import endpoint exception list' } + { defaultMessage: 'Import endpoint exceptions' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts index be38abfda3a32..c331448b5ad9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts @@ -130,7 +130,7 @@ export const POLICY_ARTIFACT_EVENT_FILTERS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.importAction', - { defaultMessage: 'Import event filter list' } + { defaultMessage: 'Import event filters' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.eventFilters.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts index b59411991bd6d..438096a6ab23a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts @@ -139,7 +139,7 @@ export const POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unexisting.importAction', - { defaultMessage: 'Import host isolation exception list' } + { defaultMessage: 'Import host isolation exceptions' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts index b20dbb42b2256..6fda907fed1e1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts @@ -130,7 +130,7 @@ export const POLICY_ARTIFACT_TRUSTED_APPS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.importAction', - { defaultMessage: 'Import trusted application list' } + { defaultMessage: 'Import trusted applications' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts index 6916725de9741..5b0fe484e35dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts @@ -133,7 +133,7 @@ export const POLICY_ARTIFACT_TRUSTED_DEVICES_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unexisting.importAction', - { defaultMessage: 'Import trusted device list' } + { defaultMessage: 'Import trusted devices' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.trustedDevices.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index 6b023437c5517..62afa4759afbe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -39,31 +39,31 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageImportButtonTitle', { - defaultMessage: 'Import trusted application list', + defaultMessage: 'Import trusted applications', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageExportButtonTitle', { - defaultMessage: 'Export trusted application list', + defaultMessage: 'Export trusted applications', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageExportSuccessToastTitle', { - defaultMessage: 'Trusted application list exported successfully', + defaultMessage: 'Trusted applications exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageExportErrorToastTitle', { - defaultMessage: 'Trusted application list export failed', + defaultMessage: 'Trusted applications export failed', } ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.trustedApps.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only trusted application list can be imported on this page.', + defaultMessage: 'Only trusted applications can be imported on this page.', } ), getShowingCountLabel: (total) => @@ -143,7 +143,7 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.trustedApps.emptyStateImportButtonLabel', - { defaultMessage: 'Import trusted application list' } + { defaultMessage: 'Import trusted applications' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.trustedApps.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx index ecefa220073a6..cd4574a8f9cb9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx @@ -45,31 +45,31 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageImportButtonTitle', { - defaultMessage: 'Import trusted device list', + defaultMessage: 'Import trusted devices', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageExportButtonTitle', { - defaultMessage: 'Export trusted device list', + defaultMessage: 'Export trusted devices', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageExportSuccessToastTitle', { - defaultMessage: 'Trusted device list exported successfully', + defaultMessage: 'Trusted devices exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageExportErrorToastTitle', { - defaultMessage: 'Trusted device list export failed', + defaultMessage: 'Trusted devices export failed', } ), pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only trusted device list can be imported on this page.', + defaultMessage: 'Only trusted devices can be imported on this page.', } ), getShowingCountLabel: (total) => @@ -159,7 +159,7 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.trustedDevices.list.emptyStateImportButtonLabel', - { defaultMessage: 'Import trusted device list' } + { defaultMessage: 'Import trusted devices' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.trustedDevices.list.searchPlaceholderInfo', From 90047a587ed537e284d0d1f31b44c19c8d012efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Wed, 1 Apr 2026 18:39:00 +0200 Subject: [PATCH 12/15] show import error results on a new modal --- .../artifact_list_page/artifact_list_page.tsx | 19 ++- .../artifact_import_confirm_modal.tsx | 1 - .../artifact_import_errors_modal.test.tsx | 111 ++++++++++++++++ .../artifact_import_errors_modal.tsx | 121 ++++++++++++++++++ .../artifact_import_flyout.test.tsx | 35 +++-- .../components/artifact_import_flyout.tsx | 54 +++++++- .../artifact_list_page.test.tsx | 1 + .../artifact_list_page/translations.ts | 25 ++++ 8 files changed, 342 insertions(+), 25 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx index 2a915f5a7b5ff..ec3825c8ead4a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx @@ -7,7 +7,10 @@ import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + BulkErrorSchema, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; import type { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout'; import { useLocation } from 'react-router-dom'; @@ -52,6 +55,7 @@ import { BackToExternalAppButton } from '../back_to_external_app_button'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { ArtifactImportFlyout } from './components/artifact_import_flyout'; import { useIsImportFlyoutOpened } from './hooks/use_is_import_flyout_opened'; +import { ArtifactImportErrorsModal } from './components/artifact_import_errors_modal'; type ArtifactEntryCardType = typeof ArtifactEntryCard; @@ -118,6 +122,8 @@ export const ArtifactListPage = memo( useIsImportFlyoutOpened(allowCardCreateAction) && areEndpointExceptionsMovedUnderManagementFFEnabled; + const [importErrors, setImportErrors] = useState(undefined); + const setUrlParams = useSetUrlParams(); const { urlParams: { filter, includedPolicies }, @@ -298,6 +304,12 @@ export const ArtifactListPage = memo( refetchListData(); }, [closeImportFlyout, refetchListData]); + const handleImportFlyoutOnShowErrors = useCallback((errors: BulkErrorSchema[]) => { + setImportErrors(errors); + }, []); + + const handleCloseImportErrorsModal = useCallback(() => setImportErrors(undefined), []); + const description = useMemo(() => { const subtitleText = labels.pageAboutInfo ? ( {labels.pageAboutInfo} @@ -409,11 +421,16 @@ export const ArtifactListPage = memo( )} + {importErrors && ( + + )} + {selectedItemForDelete && ( - {' '} {i18n.translate('xpack.securitySolution.artifactListPage.importConfirmModal.title', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.test.tsx new file mode 100644 index 0000000000000..2de2282c65aee --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.test.tsx @@ -0,0 +1,111 @@ +/* + * 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 type { ArtifactListPageProps } from '../artifact_list_page'; +import userEvent from '@testing-library/user-event'; +import { + createAppRootMockRenderer, + type AppContextTestRender, +} from '../../../../common/mock/endpoint'; +import type { ArtifactImportErrorsModalProps } from './artifact_import_errors_modal'; +import { ArtifactImportErrorsModal } from './artifact_import_errors_modal'; + +describe('When the flyout is opened in the ArtifactListPage component', () => { + let render: ( + props?: Partial + ) => Promise>; + let renderResult: ReturnType; + let props: ArtifactImportErrorsModalProps; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + + props = { + errors: [], + onClose: jest.fn(), + }; + + render = async () => { + renderResult = mockedContext.render( + + ); + return renderResult; + }; + }); + + it('should display `Close` button', async () => { + await render(); + + expect(renderResult.getByTestId('testModal')).toBeInTheDocument(); + }); + + it('should call `onClose` when the `Close` button is clicked', async () => { + await render(); + + await userEvent.click(renderResult.getByTestId('testModal-closeButton')); + + expect(props.onClose).toHaveBeenCalled(); + }); + + it('should display the list of errors with item ids and messages', async () => { + props.errors = [ + { + item_id: 'item1', + error: { message: 'Error message 1', status_code: 403 }, + }, + { + item_id: 'item2', + error: { message: 'Error message 2', status_code: 403 }, + }, + ]; + + await render(); + + expect(renderResult.getByText('Import errors')).toBeInTheDocument(); + expect(renderResult.getByText('item (item1):')).toBeInTheDocument(); + expect(renderResult.getByText('Error message 1')).toBeInTheDocument(); + expect(renderResult.getByText('item (item2):')).toBeInTheDocument(); + expect(renderResult.getByText('Error message 2')).toBeInTheDocument(); + }); + + it('should handle the case when item_id is undefined', async () => { + props.errors = [ + { + error: { message: 'Error message without item_id', status_code: 403 }, + }, + ]; + + await render(); + + expect(renderResult.getByText('Import errors')).toBeInTheDocument(); + expect(renderResult.getByText('item (undefined):')).toBeInTheDocument(); + expect(renderResult.getByText('Error message without item_id')).toBeInTheDocument(); + expect(renderResult.getByText(/Error message /)).toBeInTheDocument(); + }); + + it('should remove "EndpointArtifactError: " prefix from error messages', async () => { + props.errors = [ + { + item_id: 'item1', + error: { + message: 'EndpointArtifactError: This is an endpoint artifact error message', + status_code: 403, + }, + }, + ]; + + await render(); + + expect(renderResult.getByText('Import errors')).toBeInTheDocument(); + expect(renderResult.getByText('item (item1):')).toBeInTheDocument(); + expect( + renderResult.getByText(/^This is an endpoint artifact error message/) + ).toBeInTheDocument(); + expect(renderResult.queryByText(/EndpointArtifactError/)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.tsx new file mode 100644 index 0000000000000..d95bd5a746742 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.tsx @@ -0,0 +1,121 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { BulkErrorSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; + +export interface ArtifactImportErrorsModalProps { + errors: BulkErrorSchema[]; + onClose: () => void; + 'data-test-subj'?: string; +} + +export const ArtifactImportErrorsModal: React.FC = ({ + errors, + onClose, + 'data-test-subj': dataTestSubj = 'artifactImportErrorsModal', +}) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const modalTitleId = useGeneratedHtmlId(); + + const errorsToDisplay: Array<{ itemId: string; message: string }> = useMemo( + () => + errors.map((error) => ({ + itemId: error.item_id ?? 'undefined', + message: error.error.message.replace('EndpointArtifactError: ', ''), + })), + [errors] + ); + + return ( + + + + {i18n.translate('xpack.securitySolution.artifactListPage.importErrorsModal.title', { + defaultMessage: 'Import errors', + })} + + + + +

+ {i18n.translate('xpack.securitySolution.artifactListPage.importErrorsModal.info', { + defaultMessage: + "Some items couldn't be imported. Review the errors below for details.", + })} +

+
+ + + + ({ + maxHeight: '50vh', + overflowY: 'auto', + border: `1px solid ${euiTheme.euiTheme.colors.borderBasePlain}`, + })} + direction="column" + > + + +
    + {errorsToDisplay.map((error, index) => ( +
  1. ({ + '&::marker': { fontWeight: 'bold' }, + margin: euiTheme.euiTheme.size.m, + })} + key={index} + > + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importErrorsModal.item', + { + defaultMessage: 'item ({itemId}):', + values: { itemId: error.itemId }, + } + )} + {' '} + {error.message} +
  2. + ))} +
+
+
+
+
+ + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importErrorsModal.closeButtonTitle', + { defaultMessage: 'Close' } + )} + + +
+ ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx index 0f3650f144826..3d41673b071e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx @@ -47,6 +47,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { apiClient, onCancel: jest.fn(), onSuccess: jest.fn(), + onShowErrors: jest.fn(), }; render = async () => { @@ -136,8 +137,10 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await userEvent.click(ui.getImportButton()); await userEvent.click(ui.getConfirmModalConfirmButton()); - expect(ui.getImportButton()).toBeDisabled(); - expect(ui.getConfirmModalConfirmButton()).toBeDisabled(); + await waitFor(() => { + expect(ui.getImportButton()).toBeDisabled(); + expect(ui.getConfirmModalConfirmButton()).toBeDisabled(); + }); }); describe('when handling server response', () => { @@ -230,12 +233,11 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledWith({ title: 'Import completed with errors', toastLifeTimeMs: 60_000, + text: expect.any(Function), }); expect(props.onSuccess).toHaveBeenCalled(); }); - it('should call `showErrorList` when the `View errors` button is pressed in the warning toast', async () => {}); - it('should show a danger toast when none of the items were imported', async () => { mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => ({ errors: [LIST_CONFLICT_ERROR, ITEM_CONFLICT_ERROR, ITEM_ENDPOINT_ARTIFACT_ERROR], @@ -256,25 +258,22 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ title: "Artifacts weren't imported", toastLifeTimeMs: 60_000, + text: expect.any(Function), }); expect(props.onSuccess).toHaveBeenCalled(); }); - it('should call `showErrorList` when the `View errors` button is pressed in the error toast', async () => {}); - - it('should show an error toast and close the modal if another list is being imported', async () => { + it('should show an danger toast and close the modal if another list is being imported', async () => { await render(); await ui.uploadFile(['some-other-list-id']); await userEvent.click(ui.getImportButton()); await userEvent.click(ui.getConfirmModalConfirmButton()); - expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( - expect.objectContaining( - new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) - ), - { title: artifactListPageLabels.pageImportErrorToastTitle } - ); + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: artifactListPageLabels.pageImportErrorToastTitle, + text: artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError, + }); expect(ui.queryConfirmModal()).not.toBeInTheDocument(); }); @@ -285,12 +284,10 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await userEvent.click(ui.getImportButton()); await userEvent.click(ui.getConfirmModalConfirmButton()); - expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( - expect.objectContaining( - new Error(artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError) - ), - { title: artifactListPageLabels.pageImportErrorToastTitle } - ); + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: artifactListPageLabels.pageImportErrorToastTitle, + text: artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError, + }); expect(ui.queryConfirmModal()).not.toBeInTheDocument(); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx index dad2d3dd3057c..9b0803324fbc9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx @@ -21,8 +21,10 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { BulkErrorSchema } from '@kbn/securitysolution-io-ts-list-types'; import { parseListIdsFromImportedFile } from '../../../../common/utils/exception_list_items'; -import { useToasts } from '../../../../common/lib/kibana'; +import { useKibana, useToasts } from '../../../../common/lib/kibana'; import type { artifactListPageLabels } from '../translations'; import { useImportArtifactList } from '../../../hooks/artifacts/use_import_artifact_list'; import type { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; @@ -32,19 +34,41 @@ import { ArtifactImportConfirmModal } from './artifact_import_confirm_modal'; export interface ArtifactImportFlyoutProps { onCancel: () => void; onSuccess: () => void; + onShowErrors: (errors: BulkErrorSchema[]) => void; apiClient: ExceptionsListApiClient; labels: typeof artifactListPageLabels; 'data-test-subj'?: string; } +const ArtifactImportErrorToast: React.FC<{ + text: string; + buttonLabel: string; + errors: BulkErrorSchema[]; + onShowErrors: (errors: BulkErrorSchema[]) => void; +}> = ({ text, buttonLabel, errors, onShowErrors }) => { + return ( + <> + {text} + + + onShowErrors(errors)}> + {buttonLabel} + + + + ); +}; + export const ArtifactImportFlyout: React.FC = ({ onCancel, onSuccess, + onShowErrors, apiClient, labels, 'data-test-subj': dataTestSubj = 'artifactImportFlyout', }) => { const toasts = useToasts(); + const services = useKibana().services; const getTestId = useTestIdGenerator(dataTestSubj); const [file, setFile] = React.useState(null); @@ -65,8 +89,9 @@ export const ArtifactImportFlyout: React.FC = ({ const listIds = await parseListIdsFromImportedFile(file); if (listIds.size > 1 || !listIds.has(apiClient.listId)) { - toasts.addError(new Error(labels.pageImportOnlyCurrentArtifactCanBeImportedError), { + toasts.addDanger({ title: labels.pageImportErrorToastTitle, + text: labels.pageImportOnlyCurrentArtifactCanBeImportedError, }); setShowConfirmModal(false); @@ -91,7 +116,7 @@ export const ArtifactImportFlyout: React.FC = ({ toastLifeTimeMs: 60_000, }); } else { - const itemErrors = response.errors.filter( + const itemErrors: BulkErrorSchema[] = response.errors.filter( (error) => !( error.error.status_code === 409 && @@ -103,6 +128,18 @@ export const ArtifactImportFlyout: React.FC = ({ toasts.addWarning({ title: labels.pageImportCompletedWithErrorsToastTitle, toastLifeTimeMs: 60_000, + text: toMountPoint( + , + services + ), }); } @@ -110,6 +147,15 @@ export const ArtifactImportFlyout: React.FC = ({ toasts.addDanger({ title: labels.pageImportErrorToastTitle, toastLifeTimeMs: 60_000, + text: toMountPoint( + , + services + ), }); } } @@ -120,7 +166,7 @@ export const ArtifactImportFlyout: React.FC = ({ } ); } - }, [apiClient.listId, file, labels, mutate, onSuccess, toasts]); + }, [apiClient.listId, file, labels, mutate, onShowErrors, onSuccess, services, toasts]); const handleOnFileChange: EuiFilePickerProps['onChange'] = useCallback( (files: FileList | null) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx index 14d61bd9fabdc..0a3575481c6c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx @@ -236,6 +236,7 @@ describe('When using the ArtifactListPage component', () => { const currentApiCallCount = mockedApi.responseProvider.trustedAppsList.mock.calls.length; await userEvent.click(importFlyoutUi.getImportButton()); + await userEvent.click(importFlyoutUi.getConfirmModalConfirmButton()); await waitFor(() => { expect(mockedApi.responseProvider.trustedAppsList).toHaveBeenCalledTimes( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts index 5b3f2004add54..fdb95667947fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts @@ -65,6 +65,31 @@ export const artifactListPageLabels = Object.freeze({ defaultMessage: 'Import completed with errors', } ), + getPageImportCompletedWithErrorsToastText: ( + importedCount: number, + failedCount: number + ): string => { + return i18n.translate( + 'xpack.securitySolution.artifactListPage.importCompletedWithErrorsToastText', + { + defaultMessage: + '{importedCount} imported, {failedCount} failed. Review the errors for details.', + values: { importedCount, failedCount }, + } + ); + }, + pageImportAllItemsFailedToastText: i18n.translate( + 'xpack.securitySolution.artifactListPage.importAllItemsFailedToastText', + { + defaultMessage: "The artifacts couldn't be imported. Review the errors and try again.", + } + ), + pageImportViewErrorsButton: i18n.translate( + 'xpack.securitySolution.artifactListPage.importViewErrorsButton', + { + defaultMessage: 'View errors', + } + ), pageImportErrorToastTitle: i18n.translate( 'xpack.securitySolution.artifactListPage.importErrorToastTitle', { From e4be5cefa6189050f54fc8ddfdeaa48a2e53e53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Thu, 2 Apr 2026 10:56:38 +0200 Subject: [PATCH 13/15] adapt API tests to the new error messages --- .../artifact_import.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts index bf36e7c5b2593..b3168b026fde8 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts @@ -327,7 +327,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts to a different space requires global artifact management privilege', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", status_code: 403, }, item_id: 'wrong-item', @@ -373,7 +373,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Management of global artifacts requires additional privilege (global artifact management)', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage global artifacts. Contact your administrator for access.", status_code: 403, }, item_id: 'wrong-item', @@ -498,7 +498,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts that are not visible in the current space is not allowed', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because it isn't visible in the current space. Try importing it from a matching space or a space with access to the related policy.", status_code: 403, }, item_id: 'not-visible-in-current-space-because-unassigned', @@ -507,7 +507,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts that are not visible in the current space is not allowed', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because it isn't visible in the current space. Try importing it from a matching space or a space with access to the related policy.", status_code: 403, }, item_id: 'not-visible-in-current-space-because-assigned-only-there', @@ -555,7 +555,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts to a different space requires global artifact management privilege', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", status_code: 403, }, item_id: 'global-artifact-with-invalid-space-id', @@ -564,7 +564,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts to a different space requires global artifact management privilege', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", status_code: 403, }, item_id: 'per-policy-artifact-with-invalid-space-id', @@ -611,7 +611,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Importing artifacts with invalid owner space IDs is not allowed. The following space ID is invalid or unaccessible by current user: i-dont-exist-1', + "EndpointArtifactError: This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", status_code: 403, }, item_id: 'global-artifact-with-invalid-space-id', @@ -620,7 +620,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Importing artifacts with invalid owner space IDs is not allowed. The following space IDs are invalid or unaccessible by current user: i-dont-exist-2, i-dont-exist-3', + "EndpointArtifactError: This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", status_code: 403, }, item_id: 'per-policy-artifact-with-invalid-space-id', @@ -1350,7 +1350,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Management of global artifacts requires additional privilege (global artifact management)', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage global artifacts. Contact your administrator for access.", status_code: 403, }, item_id: 'imported-artifact', From 365d6b9d4106577fae86e5393eae402ed5ecbbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Thu, 2 Apr 2026 15:41:46 +0200 Subject: [PATCH 14/15] update texts --- .../public/exceptions/translations/shared_list.ts | 2 +- .../components/artifact_import_confirm_modal.tsx | 2 +- .../components/artifact_list_page/translations.ts | 4 ++-- .../public/management/pages/blocklist/view/blocklist.tsx | 2 +- .../management/pages/endpoint_exceptions/translations.tsx | 8 ++++---- .../pages/event_filters/view/event_filters_list.tsx | 2 +- .../view/host_isolation_exceptions_list.tsx | 2 +- .../policy/view/tabs/endpoint_exceptions_translations.ts | 2 +- .../pages/trusted_apps/view/trusted_apps_list.tsx | 2 +- .../pages/trusted_devices/view/trusted_devices_list.tsx | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts b/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts index 0d66f0f656910..550e90a640137 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts @@ -217,7 +217,7 @@ export const IMPORT_ENDPOINT_ARTIFACTS_ERROR_TEXT = i18n.translate( 'xpack.securitySolution.exceptionsTable.importEndpointArtifactsErrorText', { defaultMessage: - 'On this page only shared exception lists can be imported, but at least one file contains Endpoint artifacts. Endpoint artifacts can be imported on their respective pages.', + 'You can only import shared exception lists here, but at least one of the imported files contains endpoint artifacts. Import endpoint artifacts from their dedicated pages instead.', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx index 323bc556a610c..d21c7a292ca9d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_confirm_modal.tsx @@ -49,7 +49,7 @@ export const ArtifactImportConfirmModal: React.FC {i18n.translate('xpack.securitySolution.artifactListPage.importConfirmModal.info', { defaultMessage: - 'This will add new artifacts to your list. If an imported artifact already exists, the current version will be kept and the duplicate will be skipped.', + "This will add new artifacts to your list. If an artifact you're importing already exists, the existing version will be kept, and the import of that artifact will be skipped.", })}

diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts index fdb95667947fe..0c7ba43bd9c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts @@ -99,13 +99,13 @@ export const artifactListPageLabels = Object.freeze({ pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.artifactListPage.importOnlyCurrentArtifactCanBeImportedToastMessage', { - defaultMessage: 'Only the current artifact type can be imported on this page.', + defaultMessage: 'You can only import the current artifact type here.', } ), importFlyoutDetails: i18n.translate( 'xpack.securitySolution.artifactListPage.importFlyoutDetails', { - defaultMessage: 'You can import artifacts to your artifact list.', + defaultMessage: 'Import artifacts to your artifact list.', } ), importFlyoutImportSubmitButtonLabel: i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index b85dd416e280d..39667dbcb58e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -50,7 +50,7 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = { pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.blocklist.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only blocklist entries can be imported on this page.', + defaultMessage: 'You can only import blocklist entries here.', } ), getShowingCountLabel: (total) => diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx index 2a38296655765..fd43d985d5a0b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx @@ -28,13 +28,13 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageImportButtonTitle', { - defaultMessage: 'Import endpoint exceptions', + defaultMessage: 'Import Endpoint exceptions', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageExportButtonTitle', { - defaultMessage: 'Export endpoint exceptions', + defaultMessage: 'Export Endpoint exceptions', } ), pageExportSuccessToastTitle: i18n.translate( @@ -52,7 +52,7 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only endpoint exceptions can be imported on this page.', + defaultMessage: 'You can only import Endpoint exceptions here.', } ), getShowingCountLabel: (total) => @@ -135,7 +135,7 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.endpointExceptions.emptyStateImportButtonLabel', - { defaultMessage: 'Import endpoint exceptions' } + { defaultMessage: 'Import Endpoint exceptions' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.endpointExceptions.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx index 3c21881187bff..18c6e0639e601 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx @@ -88,7 +88,7 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = { pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.eventFilters.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only event filters can be imported on this page.', + defaultMessage: 'You can only import event filters here.', } ), getShowingCountLabel: (total) => diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 55504da8bf5bf..2575c733735f9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -56,7 +56,7 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageLabels = Object.freeze({ pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only host isolation exceptions can be imported on this page.', + defaultMessage: 'You can only import host isolation exceptions here.', } ), getShowingCountLabel: (total) => diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts index 4587c840bc7f3..eacae23120474 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts @@ -138,7 +138,7 @@ export const POLICY_ARTIFACT_ENDPOINT_EXCEPTIONS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.endpointExceptions.empty.unexisting.importAction', - { defaultMessage: 'Import endpoint exceptions' } + { defaultMessage: 'Import Endpoint exceptions' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index 62afa4759afbe..9f03a146c4490 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -63,7 +63,7 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageLabels = { pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.trustedApps.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only trusted applications can be imported on this page.', + defaultMessage: 'You can only import trusted applications here.', } ), getShowingCountLabel: (total) => diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx index cd4574a8f9cb9..dfb087159d0af 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx @@ -69,7 +69,7 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageLabels = { pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Only trusted devices can be imported on this page.', + defaultMessage: 'You can only import trusted devices here.', } ), getShowingCountLabel: (total) => From 2d116edab2ed125782d2855df4870d05b192c86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Thu, 2 Apr 2026 16:51:30 +0200 Subject: [PATCH 15/15] memoize handler functions --- .../import_exceptions_list_flyout/index.tsx | 25 ++++++++++--------- .../components/artifact_import_flyout.tsx | 6 ++++- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx index ab66e3d30d9b8..93806adad554a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx @@ -195,6 +195,16 @@ export const ImportExceptionListFlyout = React.memo( setFiles(inputFiles ?? null); }, []); + const handleNewListCheckboxChange = useCallback(() => { + setAsNewList((prev) => !prev); + setOverwrite(false); + }, []); + + const handleOverwriteCheckboxChange = useCallback((): void => { + setOverwrite((prev) => !prev); + setAsNewList(false); + }, []); + const importExceptionListFlyoutTitleId = useGeneratedHtmlId({ prefix: 'importExceptionListFlyoutTitle', }); @@ -233,10 +243,7 @@ export const ImportExceptionListFlyout = React.memo( label={i18n.IMPORT_EXCEPTION_LIST_OVERWRITE} checked={overwrite} data-test-subj="importExceptionListOverwriteExistingCheckbox" - onChange={(e) => { - setOverwrite(!overwrite); - setAsNewList(false); - }} + onChange={handleOverwriteCheckboxChange} /> {isEndpointExceptionsMovedFFEnabled ? ( { - setAsNewList(!asNewList); - setOverwrite(false); - }} + onChange={handleNewListCheckboxChange} /> ) : ( { - setAsNewList(!asNewList); - setOverwrite(false); - }} + onChange={handleNewListCheckboxChange} /> )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx index 9b0803324fbc9..b9d897326d927 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx @@ -46,12 +46,16 @@ const ArtifactImportErrorToast: React.FC<{ errors: BulkErrorSchema[]; onShowErrors: (errors: BulkErrorSchema[]) => void; }> = ({ text, buttonLabel, errors, onShowErrors }) => { + const handleOnClick = useCallback(() => { + onShowErrors(errors); + }, [errors, onShowErrors]); + return ( <> {text} - onShowErrors(errors)}> + {buttonLabel}