From 776fe46e4334192f5590914067664b6d9c5acf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Thu, 10 Mar 2022 11:50:49 +0100 Subject: [PATCH] [Security Solution] [Endpoint] Creates generic policy tab artifact component to be used for all of our artifacts (#126685) * Initial commit adding a generic component for policy artifact tabs * Adds labels and translations. Also fixes loading state using isRefetching * Fix checks and wrong count update when removing/adding items * Fixes translations and adds extra privileges checks * Use hook to retrieve url params instead of redux * Fixes wrong loading state in flyout * Extracts conditional artifact logic from generic components and adds an artifact_layout unit test * Include new changes in policy tabs component * Adds policy artifact flyout unit test * Adds policy artifacts list unit test * Adds policy artifacts delete modal unit test * Adds external privileges checks on unit tests * Uses FormattedMessage to include EuiLink inside the translation * Uses FormattedMessage * Generate new ExceptionsListApiClient instance when http changes * Removes existing policy tab artifacts code in favor of generic component * Update translation files * Include pr suggestions * Uses useUrlPagination hook for pagination * Fix checks * Fixes uni test * Reorder use_list_artifact hook params and move custom getInstance functions inside a useCallback * Fix typos and added pr feedback * Fix typo in searchableFields props and vars Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../artifacts/use_list_artifact.test.tsx | 4 +- .../hooks/artifacts/use_list_artifact.tsx | 34 +- .../hooks/artifacts/use_summary_artifact.tsx | 8 +- .../host_isolation_exceptions/constants.ts | 7 + .../host_isolation_exceptions/view/hooks.ts | 3 +- .../store/policy_details/action/index.ts | 3 +- .../action/policy_trusted_apps_action.ts | 91 --- .../store/policy_details/middleware/index.ts | 6 - .../policy_trusted_apps_middleware.ts | 443 ------------- .../store/policy_details/reducer/index.ts | 3 +- .../reducer/initial_policy_details_state.ts | 9 - .../reducer/trusted_apps_reducer.test.ts | 272 -------- .../reducer/trusted_apps_reducer.ts | 124 ---- .../store/policy_details/selectors/index.ts | 1 - .../selectors/trusted_apps_selectors.test.ts | 589 ------------------ .../selectors/trusted_apps_selectors.ts | 265 -------- .../public/management/pages/policy/types.ts | 20 - .../delete_modal}/index.ts | 3 +- .../policy_artifacts_delete_modal.test.tsx} | 23 +- .../policy_artifacts_delete_modal.tsx | 88 +++ .../artifacts/delete_modal/translations.ts | 64 ++ .../policy/view/artifacts/empty/index.ts | 13 + .../policy_artifacts_empty_unassigned.tsx | 80 +++ .../policy_artifacts_empty_unexisting.tsx | 59 ++ .../view/artifacts/empty/translations.ts | 48 ++ .../use_policy_artifacts_empty_hooks.ts} | 39 +- .../list => artifacts/flyout}/index.ts | 3 +- .../flyout/policy_artifacts_flyout.test.tsx} | 44 +- .../flyout/policy_artifacts_flyout.tsx | 236 +++++++ .../view/artifacts/flyout/translations.ts | 95 +++ .../flyout => artifacts/layout}/index.ts | 3 +- .../layout/policy_artifacts_layout.test.tsx | 191 ++++++ .../layout/policy_artifacts_layout.tsx | 247 ++++++++ .../view/artifacts/layout/translations.ts | 34 + .../delete_modal => artifacts/list}/index.ts | 3 +- .../list/policy_artifacts_list.test.tsx} | 64 +- .../artifacts/list/policy_artifacts_list.tsx | 207 ++++++ .../view/artifacts/list/translations.ts | 36 ++ .../policy/view/artifacts/translations.ts | 88 +++ .../policy_event_filters_delete_modal.tsx | 118 ---- .../policy/view/event_filters/empty/index.ts | 9 - .../policy_event_filters_empty_unassigned.tsx | 81 --- .../policy_event_filters_empty_unexisting.tsx | 53 -- .../use_policy_event_filters_empty_hooks.ts | 65 -- .../flyout/policy_event_filters_flyout.tsx | 281 --------- .../pages/policy/view/event_filters/hooks.ts | 161 ----- .../policy_event_filters_layout.test.tsx | 128 ---- .../layout/policy_event_filters_layout.tsx | 180 ------ .../list/policy_event_filters_list.tsx | 198 ------ .../components/assign_flyout.test.tsx | 289 --------- .../components/assign_flyout.tsx | 323 ---------- .../components/delete_modal.test.tsx | 121 ---- .../components/delete_modal.tsx | 131 ---- .../components/empty_unassigned.tsx | 76 --- .../components/empty_unexisting.tsx | 55 -- .../components/list.test.tsx | 220 ------- .../components/list.tsx | 225 ------- .../host_isolation_exceptions_tab.test.tsx | 165 ----- .../host_isolation_exceptions_tab.tsx | 191 ------ .../pages/policy/view/policy_hooks.ts | 106 +--- .../view/tabs/event_filters_translations.ts | 152 +++++ .../host_isolation_exceptions_translations.ts | 166 +++++ .../pages/policy/view/tabs/policy_tabs.tsx | 106 +++- .../view/tabs/trusted_apps_translations.ts | 152 +++++ .../policy/view/trusted_apps/empty/index.ts | 9 - .../policy_trusted_apps_empty_unassigned.tsx | 80 --- .../policy_trusted_apps_empty_unexisting.tsx | 53 -- .../use_policy_trusted_apps_empty_hooks.ts | 65 -- .../policy/view/trusted_apps/flyout/index.ts | 8 - .../policy_trusted_apps_flyout.test.tsx | 192 ------ .../flyout/policy_trusted_apps_flyout.tsx | 264 -------- .../pages/policy/view/trusted_apps/index.ts | 8 - .../policy/view/trusted_apps/layout/index.ts | 8 - .../policy_trusted_apps_layout.test.tsx | 198 ------ .../layout/policy_trusted_apps_layout.tsx | 179 ------ .../list/policy_trusted_apps_list.test.tsx | 346 ---------- .../list/policy_trusted_apps_list.tsx | 285 --------- ...ove_trusted_app_from_policy_modal.test.tsx | 258 -------- .../remove_trusted_app_from_policy_modal.tsx | 156 ----- .../exceptions_list_api_client.test.ts | 19 + .../exceptions_list_api_client.ts | 9 +- .../translations/translations/ja-JP.json | 96 +-- .../translations/translations/zh-CN.json | 97 +-- 83 files changed, 2289 insertions(+), 7343 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts rename x-pack/plugins/security_solution/public/management/pages/policy/view/{event_filters/layout => artifacts/delete_modal}/index.ts (63%) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx => artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx} (84%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/translations.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/index.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts rename x-pack/plugins/security_solution/public/management/pages/policy/view/{host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts => artifacts/empty/use_policy_artifacts_empty_hooks.ts} (60%) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{event_filters/list => artifacts/flyout}/index.ts (65%) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{event_filters/flyout/policy_event_filters_flyout.test.tsx => artifacts/flyout/policy_artifacts_flyout.test.tsx} (85%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/translations.ts rename x-pack/plugins/security_solution/public/management/pages/policy/view/{event_filters/flyout => artifacts/layout}/index.ts (65%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/translations.ts rename x-pack/plugins/security_solution/public/management/pages/policy/view/{event_filters/delete_modal => artifacts/list}/index.ts (67%) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{event_filters/list/policy_event_filters_list.test.tsx => artifacts/list/policy_artifacts_list.test.tsx} (64%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/translations.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/use_policy_event_filters_empty_hooks.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/use_policy_trusted_apps_empty_hooks.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.tsx diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.test.tsx index e7f389735056e..e41660daa5c59 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.test.tsx @@ -56,7 +56,7 @@ describe('List artifact hook', () => { result = await renderQuery( () => - useListArtifact(instance, searchableFields, options, { + useListArtifact(instance, options, searchableFields, { onSuccess: onSuccessMock, retry: false, }), @@ -92,7 +92,7 @@ describe('List artifact hook', () => { result = await renderQuery( () => - useListArtifact(instance, searchableFields, options, { + useListArtifact(instance, options, searchableFields, { onError: onErrorMock, retry: false, }), diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.tsx index 9ac894649d602..32aa0b26daa1d 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_list_artifact.tsx @@ -7,35 +7,45 @@ import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { HttpFetchError } from 'kibana/public'; import { QueryObserverResult, useQuery, UseQueryOptions } from 'react-query'; +import { DEFAULT_EXCEPTION_LIST_ITEM_SEARCHABLE_FIELDS } from '../../../../common/endpoint/service/artifacts/constants'; +import { MaybeImmutable } from '../../../../common/endpoint/types'; import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; import { parsePoliciesAndFilterToKql, parseQueryFilterToKQL } from '../../common/utils'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useListArtifact( exceptionListApiClient: ExceptionsListApiClient, - searcheableFields: string[], - options: { - filter: string; - page: number; - perPage: number; - policies: string[]; - } = { + options: Partial<{ + filter?: string; + page?: number; + perPage?: number; + policies?: string[]; + excludedPolicies?: string[]; + }> = { filter: '', - page: MANAGEMENT_DEFAULT_PAGE, + page: MANAGEMENT_DEFAULT_PAGE + 1, perPage: MANAGEMENT_DEFAULT_PAGE_SIZE, policies: [], + excludedPolicies: [], }, - customQueryOptions: UseQueryOptions + searchableFields: MaybeImmutable = DEFAULT_EXCEPTION_LIST_ITEM_SEARCHABLE_FIELDS, + customQueryOptions: Partial< + UseQueryOptions + > = DEFAULT_OPTIONS, + customQueryIds: string[] = [] ): QueryObserverResult { - const { filter, page, perPage, policies } = options; + const { filter, page, perPage, policies, excludedPolicies } = options; return useQuery( - ['list', exceptionListApiClient, options], + [...customQueryIds, 'list', exceptionListApiClient, options], () => { return exceptionListApiClient.find({ filter: parsePoliciesAndFilterToKql({ policies, - kuery: parseQueryFilterToKQL(filter, searcheableFields), + excludedPolicies, + kuery: parseQueryFilterToKQL(filter, searchableFields), }), perPage, page, diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_summary_artifact.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_summary_artifact.tsx index e068ab650d391..9e4ca1682f022 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_summary_artifact.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_summary_artifact.tsx @@ -10,9 +10,11 @@ import { QueryObserverResult, useQuery, UseQueryOptions } from 'react-query'; import { parsePoliciesAndFilterToKql, parseQueryFilterToKQL } from '../../common/utils'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +const DEFAULT_OPTIONS = Object.freeze({}); + export function useSummaryArtifact( exceptionListApiClient: ExceptionsListApiClient, - searcheableFields: string[], + searchableFields: string[], options: { filter: string; policies: string[]; @@ -20,7 +22,7 @@ export function useSummaryArtifact( filter: '', policies: [], }, - customQueryOptions: UseQueryOptions + customQueryOptions: UseQueryOptions = DEFAULT_OPTIONS ): QueryObserverResult { const { filter, policies } = options; @@ -30,7 +32,7 @@ export function useSummaryArtifact( return exceptionListApiClient.summary( parsePoliciesAndFilterToKql({ policies, - kuery: parseQueryFilterToKQL(filter, searcheableFields), + kuery: parseQueryFilterToKQL(filter, searchableFields), }) ); }, diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts index 8ba18f3df976c..9fe60fe6508be 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts @@ -15,6 +15,13 @@ import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME, } from '@kbn/securitysolution-list-constants'; +export const SEARCHABLE_FIELDS: Readonly = [ + `item_id`, + `name`, + `description`, + `entries.value`, +]; + export const HOST_ISOLATION_EXCEPTIONS_LIST_TYPE: ExceptionListType = ExceptionListTypeEnum.ENDPOINT_HOST_ISOLATION_EXCEPTIONS; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts index 7ab03f9eaa68f..6b043822968f5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts @@ -23,6 +23,7 @@ import { } from '../../../common/constants'; import { getHostIsolationExceptionsListPath } from '../../../common/routing'; import { parsePoliciesAndFilterToKql, parseQueryFilterToKQL } from '../../../common/utils'; +import { SEARCHABLE_FIELDS } from '../constants'; import { getHostIsolationExceptionItems, getHostIsolationExceptionSummary, @@ -85,8 +86,6 @@ export function useCanSeeHostIsolationExceptionsMenu(): boolean { return canSeeMenu; } -const SEARCHABLE_FIELDS: Readonly = [`item_id`, `name`, `description`, `entries.value`]; - export function useFetchHostIsolationExceptionsList({ filter, page, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/index.ts index ab84bb4f253ea..2be4735685454 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/index.ts @@ -6,6 +6,5 @@ */ import { PolicySettingsAction } from './policy_settings_action'; -import { PolicyTrustedAppsAction } from './policy_trusted_apps_action'; -export type PolicyDetailsAction = PolicySettingsAction | PolicyTrustedAppsAction; +export type PolicyDetailsAction = PolicySettingsAction; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts deleted file mode 100644 index 3b27c7cd1b27c..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Action } from 'redux'; -import { AsyncResourceState } from '../../../../../state'; -import { - PutTrustedAppUpdateResponse, - GetTrustedAppsListResponse, - TrustedApp, - MaybeImmutable, -} from '../../../../../../../common/endpoint/types'; -import { PolicyArtifactsState } from '../../../types'; - -export interface PolicyArtifactsAssignableListPageDataChanged { - type: 'policyArtifactsAssignableListPageDataChanged'; - payload: AsyncResourceState; -} - -export interface PolicyArtifactsUpdateTrustedApps { - type: 'policyArtifactsUpdateTrustedApps'; - payload: { - action: 'assign' | 'remove'; - artifacts: MaybeImmutable; - }; -} - -export interface PolicyArtifactsUpdateTrustedAppsChanged { - type: 'policyArtifactsUpdateTrustedAppsChanged'; - payload: AsyncResourceState; -} - -export interface PolicyArtifactsAssignableListExistDataChanged { - type: 'policyArtifactsAssignableListExistDataChanged'; - payload: AsyncResourceState; -} - -export interface PolicyArtifactsAssignableListPageDataFilter { - type: 'policyArtifactsAssignableListPageDataFilter'; - payload: { filter: string }; -} - -export interface PolicyArtifactsDeosAnyTrustedAppExists { - type: 'policyArtifactsDeosAnyTrustedAppExists'; - payload: AsyncResourceState; -} - -export interface PolicyArtifactsHasTrustedApps { - type: 'policyArtifactsHasTrustedApps'; - payload: AsyncResourceState; -} - -export interface AssignedTrustedAppsListStateChanged - extends Action<'assignedTrustedAppsListStateChanged'> { - payload: PolicyArtifactsState['assignedList']; -} - -export interface PolicyDetailsListOfAllPoliciesStateChanged - extends Action<'policyDetailsListOfAllPoliciesStateChanged'> { - payload: PolicyArtifactsState['policies']; -} - -export type PolicyDetailsTrustedAppsForceListDataRefresh = - Action<'policyDetailsTrustedAppsForceListDataRefresh'>; - -export type PolicyDetailsArtifactsResetRemove = Action<'policyDetailsArtifactsResetRemove'>; - -export interface PolicyDetailsTrustedAppsRemoveListStateChanged - extends Action<'policyDetailsTrustedAppsRemoveListStateChanged'> { - payload: PolicyArtifactsState['removeList']; -} - -/** - * All of the possible actions for Trusted Apps under the Policy Details store - */ -export type PolicyTrustedAppsAction = - | PolicyArtifactsAssignableListPageDataChanged - | PolicyArtifactsUpdateTrustedApps - | PolicyArtifactsUpdateTrustedAppsChanged - | PolicyArtifactsAssignableListExistDataChanged - | PolicyArtifactsAssignableListPageDataFilter - | PolicyArtifactsDeosAnyTrustedAppExists - | PolicyArtifactsHasTrustedApps - | AssignedTrustedAppsListStateChanged - | PolicyDetailsListOfAllPoliciesStateChanged - | PolicyDetailsTrustedAppsForceListDataRefresh - | PolicyDetailsTrustedAppsRemoveListStateChanged - | PolicyDetailsArtifactsResetRemove; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts index a5752ed1658d3..2409193d63819 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts @@ -7,24 +7,18 @@ import { ImmutableMiddlewareFactory } from '../../../../../../common/store'; import { MiddlewareRunnerContext, PolicyDetailsState } from '../../../types'; -import { policyTrustedAppsMiddlewareRunner } from './policy_trusted_apps_middleware'; import { policySettingsMiddlewareRunner } from './policy_settings_middleware'; -import { TrustedAppsHttpService } from '../../../../trusted_apps/service'; export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = ( coreStart ) => { - // Initialize services needed by Policy middleware - const trustedAppsService = new TrustedAppsHttpService(coreStart.http); const middlewareContext: MiddlewareRunnerContext = { coreStart, - trustedAppsService, }; return (store) => (next) => async (action) => { next(action); policySettingsMiddlewareRunner(middlewareContext, store, action); - policyTrustedAppsMiddlewareRunner(middlewareContext, store, action); }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts deleted file mode 100644 index e8a647c257b01..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts +++ /dev/null @@ -1,443 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty } from 'lodash/fp'; -import { - GetPolicyListResponse, - MiddlewareRunner, - MiddlewareRunnerContext, - PolicyAssignedTrustedApps, - PolicyDetailsState, - PolicyDetailsStore, - PolicyRemoveTrustedApps, -} from '../../../types'; -import { - doesPolicyTrustedAppsListNeedUpdate, - getCurrentArtifactsLocation, - getCurrentPolicyAssignedTrustedAppsState, - getCurrentTrustedAppsRemoveListState, - getCurrentUrlLocationPaginationParams, - getLatestLoadedPolicyAssignedTrustedAppsState, - getTrustedAppsIsRemoving, - getTrustedAppsPolicyListState, - isOnPolicyTrustedAppsView, - isPolicyTrustedAppListLoading, - licensedPolicy, - policyIdFromParams, - getDoesAnyTrustedAppExistsIsLoading, -} from '../selectors'; -import { - GetTrustedAppsListResponse, - Immutable, - MaybeImmutable, - PutTrustedAppUpdateResponse, - TrustedApp, -} from '../../../../../../../common/endpoint/types'; -import { ImmutableMiddlewareAPI } from '../../../../../../common/store'; -import { TrustedAppsService } from '../../../../trusted_apps/service'; -import { - asStaleResourceState, - createFailedResourceState, - createLoadedResourceState, - createLoadingResourceState, - isLoadingResourceState, - isUninitialisedResourceState, - isLoadedResourceState, -} from '../../../../../state'; -import { parseQueryFilterToKQL } from '../../../../../common/utils'; -import { SEARCHABLE_FIELDS } from '../../../../trusted_apps/constants'; -import { PolicyDetailsAction } from '../action'; -import { ServerApiError } from '../../../../../../common/types'; - -/** Runs all middleware actions associated with the Trusted Apps view in Policy Details */ -export const policyTrustedAppsMiddlewareRunner: MiddlewareRunner = async ( - context, - store, - action -) => { - const state = store.getState(); - - /* ----------------------------------------------------------- - If not on the Trusted Apps Policy view, then just return - ----------------------------------------------------------- */ - if (!isOnPolicyTrustedAppsView(state)) { - return; - } - - const { trustedAppsService } = context; - - switch (action.type) { - case 'userChangedUrl': - fetchPolicyTrustedAppsIfNeeded(context, store); - fetchAllPoliciesIfNeeded(context, store); - - if (action.type === 'userChangedUrl' && getCurrentArtifactsLocation(state).show === 'list') { - await searchTrustedApps(store, trustedAppsService); - } - - break; - - case 'policyDetailsTrustedAppsForceListDataRefresh': - fetchPolicyTrustedAppsIfNeeded(context, store, true); - break; - - case 'policyArtifactsUpdateTrustedApps': - if ( - getCurrentArtifactsLocation(state).show === 'list' && - action.payload.action === 'assign' - ) { - await updateTrustedApps(store, trustedAppsService, action.payload.artifacts); - } else if (action.payload.action === 'remove') { - removeTrustedAppsFromPolicy(context, store, action.payload.artifacts); - } - - break; - - case 'policyArtifactsAssignableListPageDataFilter': - if (getCurrentArtifactsLocation(state).show === 'list') { - await searchTrustedApps(store, trustedAppsService, action.payload.filter); - } - - break; - } -}; - -const checkIfThereAreAssignableTrustedApps = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const state = store.getState(); - const policyId = policyIdFromParams(state); - - store.dispatch({ - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createLoadingResourceState(), - }); - try { - const trustedApps = await trustedAppsService.getTrustedAppsList({ - page: 1, - per_page: 100, - kuery: `(not exception-list-agnostic.attributes.tags:"policy:${policyId}") AND (not exception-list-agnostic.attributes.tags:"policy:all")`, - }); - - store.dispatch({ - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createLoadedResourceState(!isEmpty(trustedApps.data)), - }); - } catch (err) { - store.dispatch({ - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createFailedResourceState(err.body ?? err), - }); - } -}; - -const checkIfPolicyHasTrustedAppsAssigned = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const state = store.getState(); - if (isLoadingResourceState(state.artifacts.hasTrustedApps)) { - return; - } - if (isLoadedResourceState(state.artifacts.hasTrustedApps)) { - store.dispatch({ - type: 'policyArtifactsHasTrustedApps', - payload: createLoadingResourceState(state.artifacts.hasTrustedApps), - }); - } else { - store.dispatch({ - type: 'policyArtifactsHasTrustedApps', - payload: createLoadingResourceState(), - }); - } - try { - const policyId = policyIdFromParams(state); - const kuery = `(exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all")`; - const trustedApps = await trustedAppsService.getTrustedAppsList({ - page: 1, - per_page: 100, - kuery, - }); - - if ( - !trustedApps.total && - isUninitialisedResourceState(state.artifacts.doesAnyTrustedAppExists) - ) { - await checkIfAnyTrustedApp(store, trustedAppsService); - } - - store.dispatch({ - type: 'policyArtifactsHasTrustedApps', - payload: createLoadedResourceState(trustedApps), - }); - } catch (err) { - store.dispatch({ - type: 'policyArtifactsHasTrustedApps', - payload: createFailedResourceState(err.body ?? err), - }); - } -}; - -const checkIfAnyTrustedApp = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const state = store.getState(); - if (getDoesAnyTrustedAppExistsIsLoading(state)) { - return; - } - store.dispatch({ - type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadingResourceState(), - }); - try { - const trustedApps = await trustedAppsService.getTrustedAppsList({ - page: 1, - per_page: 100, - }); - - store.dispatch({ - type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadedResourceState(trustedApps), - }); - } catch (err) { - store.dispatch({ - type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createFailedResourceState(err.body ?? err), - }); - } -}; - -const searchTrustedApps = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService, - filter?: string -) => { - const state = store.getState(); - const policyId = policyIdFromParams(state); - - store.dispatch({ - type: 'policyArtifactsAssignableListPageDataChanged', - payload: createLoadingResourceState(), - }); - - try { - const kuery = [ - `(not exception-list-agnostic.attributes.tags:"policy:${policyId}") AND (not exception-list-agnostic.attributes.tags:"policy:all")`, - ]; - - if (filter) { - const filterKuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined; - if (filterKuery) { - kuery.push(filterKuery); - } - } - - const trustedApps = await trustedAppsService.getTrustedAppsList({ - page: 1, - per_page: 100, - kuery: kuery.join(' AND '), - }); - - store.dispatch({ - type: 'policyArtifactsAssignableListPageDataChanged', - payload: createLoadedResourceState(trustedApps), - }); - - if (isEmpty(trustedApps.data)) { - checkIfThereAreAssignableTrustedApps(store, trustedAppsService); - } - } catch (err) { - store.dispatch({ - type: 'policyArtifactsAssignableListPageDataChanged', - payload: createFailedResourceState(err.body ?? err), - }); - } -}; - -const updateTrustedApps = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService, - trustedApps: MaybeImmutable -) => { - const state = store.getState(); - const policyId = policyIdFromParams(state); - - store.dispatch({ - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: createLoadingResourceState(), - }); - - try { - const updatedTrustedApps = await trustedAppsService.assignPolicyToTrustedApps( - policyId, - trustedApps - ); - await checkIfPolicyHasTrustedAppsAssigned(store, trustedAppsService); - - store.dispatch({ - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: createLoadedResourceState(updatedTrustedApps), - }); - - store.dispatch({ type: 'policyDetailsTrustedAppsForceListDataRefresh' }); - } catch (err) { - store.dispatch({ - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: createFailedResourceState(err.body ?? err), - }); - } -}; - -const fetchPolicyTrustedAppsIfNeeded = async ( - { trustedAppsService }: MiddlewareRunnerContext, - { getState, dispatch }: PolicyDetailsStore, - forceFetch: boolean = false -) => { - const state = getState(); - - if (isPolicyTrustedAppListLoading(state)) { - return; - } - - if (forceFetch || doesPolicyTrustedAppsListNeedUpdate(state)) { - dispatch({ - type: 'assignedTrustedAppsListStateChanged', - payload: createLoadingResourceState( - asStaleResourceState(getCurrentPolicyAssignedTrustedAppsState(state)) - ), - }); - - try { - const urlLocationData = getCurrentUrlLocationPaginationParams(state); - const policyId = policyIdFromParams(state); - const kuery = [ - `((exception-list-agnostic.attributes.tags:"policy:${policyId}") OR (exception-list-agnostic.attributes.tags:"policy:all"))`, - ]; - - if (urlLocationData.filter) { - const filterKuery = - parseQueryFilterToKQL(urlLocationData.filter, SEARCHABLE_FIELDS) || undefined; - if (filterKuery) { - kuery.push(filterKuery); - } - } - const fetchResponse = await trustedAppsService.getTrustedAppsList({ - page: urlLocationData.page_index + 1, - per_page: urlLocationData.page_size, - kuery: kuery.join(' AND '), - }); - - dispatch({ - type: 'assignedTrustedAppsListStateChanged', - payload: createLoadedResourceState>({ - location: urlLocationData, - artifacts: fetchResponse, - }), - }); - - if (isUninitialisedResourceState(state.artifacts.hasTrustedApps)) { - await checkIfPolicyHasTrustedAppsAssigned({ getState, dispatch }, trustedAppsService); - } - } catch (error) { - dispatch({ - type: 'assignedTrustedAppsListStateChanged', - payload: createFailedResourceState>( - error as ServerApiError, - getLatestLoadedPolicyAssignedTrustedAppsState(getState()) - ), - }); - } - } -}; - -const fetchAllPoliciesIfNeeded = async ( - { trustedAppsService }: MiddlewareRunnerContext, - { getState, dispatch }: PolicyDetailsStore -) => { - const state = getState(); - const currentPoliciesState = getTrustedAppsPolicyListState(state); - const isLoading = isLoadingResourceState(currentPoliciesState); - const hasBeenLoaded = !isUninitialisedResourceState(currentPoliciesState); - - if (isLoading || hasBeenLoaded) { - return; - } - - dispatch({ - type: 'policyDetailsListOfAllPoliciesStateChanged', - // @ts-expect-error ts 4.5 upgrade - payload: createLoadingResourceState(asStaleResourceState(currentPoliciesState)), - }); - - try { - const policyList = await trustedAppsService.getPolicyList({ - query: { - page: 1, - perPage: 1000, - }, - }); - - dispatch({ - type: 'policyDetailsListOfAllPoliciesStateChanged', - payload: createLoadedResourceState(policyList), - }); - } catch (error) { - dispatch({ - type: 'policyDetailsListOfAllPoliciesStateChanged', - payload: createFailedResourceState(error.body || error), - }); - } -}; - -const removeTrustedAppsFromPolicy = async ( - { trustedAppsService }: MiddlewareRunnerContext, - { getState, dispatch }: PolicyDetailsStore, - trustedApps: MaybeImmutable -): Promise => { - const state = getState(); - - if (getTrustedAppsIsRemoving(state)) { - return; - } - - dispatch({ - type: 'policyDetailsTrustedAppsRemoveListStateChanged', - payload: createLoadingResourceState( - asStaleResourceState(getCurrentTrustedAppsRemoveListState(state)) - ), - }); - - try { - const currentPolicyId = licensedPolicy(state)?.id; - - if (!currentPolicyId) { - throw new Error('current policy id not found'); - } - - const response = await trustedAppsService.removePolicyFromTrustedApps( - currentPolicyId, - trustedApps - ); - await checkIfPolicyHasTrustedAppsAssigned({ getState, dispatch }, trustedAppsService); - - dispatch({ - type: 'policyDetailsTrustedAppsRemoveListStateChanged', - payload: createLoadedResourceState({ artifacts: trustedApps, response }), - }); - - dispatch({ - type: 'policyDetailsTrustedAppsForceListDataRefresh', - }); - } catch (error) { - dispatch({ - type: 'policyDetailsTrustedAppsRemoveListStateChanged', - payload: createFailedResourceState(error.body || error), - }); - } -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/index.ts index a577c1ca85ef0..264f315be1898 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/index.ts @@ -10,7 +10,6 @@ import { PolicyDetailsState } from '../../../types'; import { AppAction } from '../../../../../../common/store/actions'; import { policySettingsReducer } from './policy_settings_reducer'; import { initialPolicyDetailsState } from './initial_policy_details_state'; -import { policyTrustedAppsReducer } from './trusted_apps_reducer'; export * from './initial_policy_details_state'; @@ -18,7 +17,7 @@ export const policyDetailsReducer: ImmutableReducer { - return [policySettingsReducer, policyTrustedAppsReducer].reduce( + return [policySettingsReducer].reduce( (updatedState, runReducer) => runReducer(updatedState, action), state ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts index a1e63bc889dd6..23bd0f9a56d3e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts @@ -11,7 +11,6 @@ import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE, } from '../../../../../common/constants'; -import { createUninitialisedResourceState } from '../../../../../state'; /** * Return a fresh copy of initial state, since we mutate state in the reducer. @@ -34,13 +33,5 @@ export const initialPolicyDetailsState: () => Immutable = () show: undefined, filter: '', }, - assignableList: createUninitialisedResourceState(), - trustedAppsToUpdate: createUninitialisedResourceState(), - assignableListEntriesExist: createUninitialisedResourceState(), - doesAnyTrustedAppExists: createUninitialisedResourceState(), - hasTrustedApps: createUninitialisedResourceState(), - assignedList: createUninitialisedResourceState(), - policies: createUninitialisedResourceState(), - removeList: createUninitialisedResourceState(), }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts deleted file mode 100644 index e1d2fab6dcdb6..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { PolicyDetailsState } from '../../../types'; -import { initialPolicyDetailsState } from './initial_policy_details_state'; -import { policyTrustedAppsReducer } from './trusted_apps_reducer'; - -import { ImmutableObject } from '../../../../../../../common/endpoint/types'; -import { - createLoadedResourceState, - createUninitialisedResourceState, - createLoadingResourceState, - createFailedResourceState, -} from '../../../../../state'; -import { getMockListResponse, getAPIError, getMockCreateResponse } from '../../../test_utils'; -import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; - -describe('policy trusted apps reducer', () => { - let initialState: ImmutableObject; - - beforeEach(() => { - initialState = { - ...initialPolicyDetailsState(), - location: { - pathname: getPolicyDetailsArtifactsListPath('abc'), - search: '', - hash: '', - }, - }; - }); - - describe('PolicyTrustedApps', () => { - describe('policyArtifactsAssignableListPageDataChanged', () => { - it('sets assignable list uninitialised', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListPageDataChanged', - payload: createUninitialisedResourceState(), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: { - type: 'UninitialisedResourceState', - }, - }, - }); - }); - it('sets assignable list loading', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListPageDataChanged', - payload: createLoadingResourceState(createUninitialisedResourceState()), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: { - previousState: { - type: 'UninitialisedResourceState', - }, - type: 'LoadingResourceState', - }, - }, - }); - }); - it('sets assignable list loaded', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListPageDataChanged', - payload: createLoadedResourceState(getMockListResponse()), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: { - data: getMockListResponse(), - type: 'LoadedResourceState', - }, - }, - }); - }); - it('sets assignable list failed', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListPageDataChanged', - payload: createFailedResourceState(getAPIError()), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: { - type: 'FailedResourceState', - error: getAPIError(), - lastLoadedState: undefined, - }, - }, - }); - }); - }); - }); - - describe('policyArtifactsUpdateTrustedAppsChanged', () => { - it('sets update trusted app uninitialised', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: createUninitialisedResourceState(), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: { - type: 'UninitialisedResourceState', - }, - }, - }); - }); - it('sets update trusted app loading', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: createLoadingResourceState(createUninitialisedResourceState()), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: { - previousState: { - type: 'UninitialisedResourceState', - }, - type: 'LoadingResourceState', - }, - }, - }); - }); - it('sets update trusted app loaded', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: createLoadedResourceState([getMockCreateResponse()]), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: { - data: [getMockCreateResponse()], - type: 'LoadedResourceState', - }, - }, - }); - }); - it('sets update trusted app failed', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: createFailedResourceState(getAPIError()), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: { - type: 'FailedResourceState', - error: getAPIError(), - lastLoadedState: undefined, - }, - }, - }); - }); - }); - - describe('policyArtifactsAssignableListExistDataChanged', () => { - it('sets exists trusted app uninitialised', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createUninitialisedResourceState(), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: { - type: 'UninitialisedResourceState', - }, - }, - }); - }); - it('sets exists trusted app loading', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createLoadingResourceState(createUninitialisedResourceState()), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: { - previousState: { - type: 'UninitialisedResourceState', - }, - type: 'LoadingResourceState', - }, - }, - }); - }); - it('sets exists trusted app loaded negative', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createLoadedResourceState(false), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: { - data: false, - type: 'LoadedResourceState', - }, - }, - }); - }); - it('sets exists trusted app loaded positive', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createLoadedResourceState(true), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: { - data: true, - type: 'LoadedResourceState', - }, - }, - }); - }); - it('sets exists trusted app failed', () => { - const result = policyTrustedAppsReducer(initialState, { - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createFailedResourceState(getAPIError()), - }); - - expect(result).toStrictEqual({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: { - type: 'FailedResourceState', - error: getAPIError(), - lastLoadedState: undefined, - }, - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts deleted file mode 100644 index f601e3ef0afb4..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ImmutableReducer } from '../../../../../../common/store'; -import { PolicyDetailsState } from '../../../types'; -import { AppAction } from '../../../../../../common/store/actions'; -import { initialPolicyDetailsState } from './initial_policy_details_state'; -import { isUninitialisedResourceState } from '../../../../../state'; -import { getCurrentPolicyAssignedTrustedAppsState, isOnPolicyTrustedAppsView } from '../selectors'; - -export const policyTrustedAppsReducer: ImmutableReducer = ( - state = initialPolicyDetailsState(), - action -) => { - /* ---------------------------------------------------------- - If not on the Trusted Apps Policy view, then just return - ---------------------------------------------------------- */ - if (!isOnPolicyTrustedAppsView(state)) { - // If the artifacts state namespace needs resetting, then do it now - if (!isUninitialisedResourceState(getCurrentPolicyAssignedTrustedAppsState(state))) { - return { - ...state, - artifacts: initialPolicyDetailsState().artifacts, - }; - } - - return state; - } - - if (action.type === 'policyArtifactsAssignableListPageDataChanged') { - return { - ...state, - artifacts: { - ...state.artifacts, - assignableList: action.payload, - }, - }; - } - - if (action.type === 'policyArtifactsUpdateTrustedAppsChanged') { - return { - ...state, - artifacts: { - ...state.artifacts, - trustedAppsToUpdate: action.payload, - }, - }; - } - - if (action.type === 'policyArtifactsAssignableListExistDataChanged') { - return { - ...state, - artifacts: { - ...state.artifacts, - assignableListEntriesExist: action.payload, - }, - }; - } - - if (action.type === 'policyArtifactsDeosAnyTrustedAppExists') { - return { - ...state, - artifacts: { - ...state?.artifacts, - doesAnyTrustedAppExists: action.payload, - }, - }; - } - - if (action.type === 'policyArtifactsHasTrustedApps') { - return { - ...state, - artifacts: { - ...state?.artifacts, - hasTrustedApps: action.payload, - }, - }; - } - if (action.type === 'assignedTrustedAppsListStateChanged') { - return { - ...state, - artifacts: { - ...state?.artifacts, - assignedList: action.payload, - }, - }; - } - - if (action.type === 'policyDetailsListOfAllPoliciesStateChanged') { - return { - ...state, - artifacts: { - ...state.artifacts, - policies: action.payload, - }, - }; - } - - if (action.type === 'policyDetailsTrustedAppsRemoveListStateChanged') { - return { - ...state, - artifacts: { - ...state.artifacts, - removeList: action.payload, - }, - }; - } - - if (action.type === 'policyDetailsArtifactsResetRemove') { - return { - ...state, - artifacts: { - ...state.artifacts, - removeList: initialPolicyDetailsState().artifacts.removeList, - }, - }; - } - - return state; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/index.ts index d9c167d4a801c..808791338ca7f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/index.ts @@ -6,5 +6,4 @@ */ export * from './policy_settings_selectors'; -export * from './trusted_apps_selectors'; export * from './policy_common_selectors'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts deleted file mode 100644 index 0fbd674b265b0..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts +++ /dev/null @@ -1,589 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PolicyArtifactsState, PolicyDetailsState } from '../../../types'; -import { initialPolicyDetailsState } from '../reducer'; -import { - getAssignableArtifactsList, - getAssignableArtifactsListIsLoading, - getUpdateArtifactsIsLoading, - getUpdateArtifactsIsFailed, - getUpdateArtifactsLoaded, - getAssignableArtifactsListExist, - getAssignableArtifactsListExistIsLoading, - getUpdateArtifacts, - doesPolicyTrustedAppsListNeedUpdate, - isPolicyTrustedAppListLoading, - getPolicyTrustedAppList, - getPolicyTrustedAppsListPagination, - getTrustedAppsListOfAllPolicies, - getTrustedAppsAllPoliciesById, -} from './trusted_apps_selectors'; -import { getCurrentArtifactsLocation, isOnPolicyTrustedAppsView } from './policy_common_selectors'; - -import { ImmutableObject } from '../../../../../../../common/endpoint/types'; -import { - createLoadedResourceState, - createUninitialisedResourceState, - createLoadingResourceState, - createFailedResourceState, -} from '../../../../../state'; -import { MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH } from '../../../../../common/constants'; -import { - getMockListResponse, - getAPIError, - getMockCreateResponse, - getMockPolicyDetailsArtifactListUrlParams, - getMockPolicyDetailsArtifactsPageLocationUrlParams, -} from '../../../test_utils'; -import { getGeneratedPolicyResponse } from '../../../../trusted_apps/store/mocks'; - -describe('policy trusted apps selectors', () => { - let initialState: ImmutableObject; - - const createArtifactsState = ( - artifacts: Partial = {} - ): ImmutableObject => { - return { - ...initialState, - artifacts: { - ...initialState.artifacts, - ...artifacts, - }, - }; - }; - - beforeEach(() => { - initialState = initialPolicyDetailsState(); - }); - - describe('doesPolicyTrustedAppsListNeedUpdate()', () => { - it('should return true if state is not loaded', () => { - expect(doesPolicyTrustedAppsListNeedUpdate(initialState)).toBe(true); - }); - - it('should return true if it is loaded, but URL params were changed', () => { - expect( - doesPolicyTrustedAppsListNeedUpdate( - createArtifactsState({ - location: getMockPolicyDetailsArtifactsPageLocationUrlParams({ page_index: 4 }), - assignedList: createLoadedResourceState({ - location: getMockPolicyDetailsArtifactListUrlParams(), - artifacts: getMockListResponse(), - }), - }) - ) - ).toBe(true); - }); - - it('should return false if state is loaded adn URL params are the same', () => { - expect( - doesPolicyTrustedAppsListNeedUpdate( - createArtifactsState({ - location: getMockPolicyDetailsArtifactsPageLocationUrlParams(), - assignedList: createLoadedResourceState({ - location: getMockPolicyDetailsArtifactListUrlParams(), - artifacts: getMockListResponse(), - }), - }) - ) - ).toBe(false); - }); - }); - - describe('isPolicyTrustedAppListLoading()', () => { - it('should return true when loading data', () => { - expect( - isPolicyTrustedAppListLoading( - createArtifactsState({ - assignedList: createLoadingResourceState(createUninitialisedResourceState()), - }) - ) - ).toBe(true); - }); - - it.each([ - ['uninitialized', createUninitialisedResourceState() as PolicyArtifactsState['assignedList']], - ['loaded', createLoadedResourceState({}) as PolicyArtifactsState['assignedList']], - ['failed', createFailedResourceState({}) as PolicyArtifactsState['assignedList']], - ])('should return false when state is %s', (__, assignedListState) => { - expect( - isPolicyTrustedAppListLoading(createArtifactsState({ assignedList: assignedListState })) - ).toBe(false); - }); - }); - - describe('getPolicyTrustedAppList()', () => { - it('should return the list of trusted apps', () => { - const listResponse = getMockListResponse(); - - expect( - getPolicyTrustedAppList( - createArtifactsState({ - location: getMockPolicyDetailsArtifactsPageLocationUrlParams(), - assignedList: createLoadedResourceState({ - location: getMockPolicyDetailsArtifactListUrlParams(), - artifacts: listResponse, - }), - }) - ) - ).toEqual(listResponse.data); - }); - - it('should return empty array if no data is loaded', () => { - expect(getPolicyTrustedAppList(initialState)).toEqual([]); - }); - }); - - describe('getPolicyTrustedAppsListPagination()', () => { - it('should return default pagination data even if no api data is available', () => { - expect(getPolicyTrustedAppsListPagination(initialState)).toEqual({ - pageIndex: 0, - pageSize: 10, - pageSizeOptions: [10, 20, 50], - totalItemCount: 0, - }); - }); - - it('should return pagination data based on api response data', () => { - const listResponse = getMockListResponse(); - - listResponse.page = 6; - listResponse.per_page = 100; - listResponse.total = 1000; - - expect( - getPolicyTrustedAppsListPagination( - createArtifactsState({ - location: getMockPolicyDetailsArtifactsPageLocationUrlParams({ - page_index: 5, - page_size: 100, - }), - assignedList: createLoadedResourceState({ - location: getMockPolicyDetailsArtifactListUrlParams({ - page_index: 5, - page_size: 100, - }), - artifacts: listResponse, - }), - }) - ) - ).toEqual({ - pageIndex: 5, - pageSize: 100, - pageSizeOptions: [10, 20, 50], - totalItemCount: 1000, - }); - }); - }); - - describe('getTrustedAppsListOfAllPolicies()', () => { - it('should return the loaded list of policies', () => { - const policiesApiResponse = getGeneratedPolicyResponse(); - - expect( - getTrustedAppsListOfAllPolicies( - createArtifactsState({ - policies: createLoadedResourceState(policiesApiResponse), - }) - ) - ).toEqual(policiesApiResponse.items); - }); - - it('should return an empty array of no policy data was loaded yet', () => { - expect(getTrustedAppsListOfAllPolicies(initialState)).toEqual([]); - }); - }); - - describe('getTrustedAppsAllPoliciesById()', () => { - it('should return an empty object if no polices', () => { - expect(getTrustedAppsAllPoliciesById(initialState)).toEqual({}); - }); - - it('should return an object with policy id and policy data', () => { - const policiesApiResponse = getGeneratedPolicyResponse(); - - expect( - getTrustedAppsAllPoliciesById( - createArtifactsState({ - policies: createLoadedResourceState(policiesApiResponse), - }) - ) - ).toEqual({ [policiesApiResponse.items[0].id]: policiesApiResponse.items[0] }); - }); - }); - - describe('isOnPolicyTrustedAppsPage()', () => { - it('when location is on policy trusted apps page', () => { - const isOnPage = isOnPolicyTrustedAppsView({ - ...initialState, - location: { - pathname: MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, - search: '', - hash: '', - }, - }); - expect(isOnPage).toBeFalsy(); - }); - it('when location is not on policy trusted apps page', () => { - const isOnPage = isOnPolicyTrustedAppsView({ - ...initialState, - location: { pathname: '', search: '', hash: '' }, - }); - expect(isOnPage).toBeFalsy(); - }); - }); - - describe('getCurrentArtifactsLocation()', () => { - it('when location is defined', () => { - const location = getCurrentArtifactsLocation(initialState); - expect(location).toEqual({ filter: '', page_index: 0, page_size: 10, show: undefined }); - }); - it('when location has show param to list', () => { - const location = getCurrentArtifactsLocation({ - ...initialState, - artifacts: { - ...initialState.artifacts, - location: { ...initialState.artifacts.location, show: 'list' }, - }, - }); - expect(location).toEqual({ filter: '', page_index: 0, page_size: 10, show: 'list' }); - }); - }); - - describe('getAssignableArtifactsList()', () => { - it('when assignable list is uninitialised', () => { - const assignableList = getAssignableArtifactsList(initialState); - expect(assignableList).toBeUndefined(); - }); - it('when assignable list is loading', () => { - const assignableList = getAssignableArtifactsList({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: createLoadingResourceState(createUninitialisedResourceState()), - }, - }); - expect(assignableList).toBeUndefined(); - }); - it('when assignable list is loaded', () => { - const assignableList = getAssignableArtifactsList({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: createLoadedResourceState(getMockListResponse()), - }, - }); - expect(assignableList).toEqual(getMockListResponse()); - }); - }); - - describe('getAssignableArtifactsListIsLoading()', () => { - it('when assignable list is loading', () => { - const isLoading = getAssignableArtifactsListIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: createLoadingResourceState(createUninitialisedResourceState()), - }, - }); - expect(isLoading).toBeTruthy(); - }); - it('when assignable list is uninitialised', () => { - const isLoading = getAssignableArtifactsListIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: createUninitialisedResourceState(), - }, - }); - expect(isLoading).toBeFalsy(); - }); - it('when assignable list is loaded', () => { - const isLoading = getAssignableArtifactsListIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableList: createLoadedResourceState(getMockListResponse()), - }, - }); - expect(isLoading).toBeFalsy(); - }); - }); - - describe('getUpdateArtifactsIsLoading()', () => { - it('when update artifacts is loading', () => { - const isLoading = getUpdateArtifactsIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadingResourceState(createUninitialisedResourceState()), - }, - }); - expect(isLoading).toBeTruthy(); - }); - it('when update artifacts is uninitialised', () => { - const isLoading = getUpdateArtifactsIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createUninitialisedResourceState(), - }, - }); - expect(isLoading).toBeFalsy(); - }); - it('when update artifacts is loaded', () => { - const isLoading = getUpdateArtifactsIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadedResourceState([getMockCreateResponse()]), - }, - }); - expect(isLoading).toBeFalsy(); - }); - }); - - describe('getUpdateArtifactsIsFailed()', () => { - it('when update artifacts is loading', () => { - const hasFailed = getUpdateArtifactsIsFailed({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadingResourceState(createUninitialisedResourceState()), - }, - }); - expect(hasFailed).toBeFalsy(); - }); - it('when update artifacts is uninitialised', () => { - const hasFailed = getUpdateArtifactsIsFailed({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createUninitialisedResourceState(), - }, - }); - expect(hasFailed).toBeFalsy(); - }); - it('when update artifacts is loaded', () => { - const hasFailed = getUpdateArtifactsIsFailed({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadedResourceState([getMockCreateResponse()]), - }, - }); - expect(hasFailed).toBeFalsy(); - }); - it('when update artifacts has failed', () => { - const hasFailed = getUpdateArtifactsIsFailed({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createFailedResourceState(getAPIError()), - }, - }); - expect(hasFailed).toBeTruthy(); - }); - }); - - describe('getUpdateArtifactsLoaded()', () => { - it('when update artifacts is loading', () => { - const isLoaded = getUpdateArtifactsLoaded({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadingResourceState(createUninitialisedResourceState()), - }, - }); - expect(isLoaded).toBeFalsy(); - }); - it('when update artifacts is uninitialised', () => { - const isLoaded = getUpdateArtifactsLoaded({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createUninitialisedResourceState(), - }, - }); - expect(isLoaded).toBeFalsy(); - }); - it('when update artifacts is loaded', () => { - const isLoaded = getUpdateArtifactsLoaded({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadedResourceState([getMockCreateResponse()]), - }, - }); - expect(isLoaded).toBeTruthy(); - }); - it('when update artifacts has failed', () => { - const isLoaded = getUpdateArtifactsLoaded({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createFailedResourceState(getAPIError()), - }, - }); - expect(isLoaded).toBeFalsy(); - }); - }); - - describe('getUpdateArtifacts()', () => { - it('when update artifacts is loading', () => { - const isLoading = getUpdateArtifacts({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadingResourceState(createUninitialisedResourceState()), - }, - }); - expect(isLoading).toBeUndefined(); - }); - it('when update artifacts is uninitialised', () => { - const isLoading = getUpdateArtifacts({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createUninitialisedResourceState(), - }, - }); - expect(isLoading).toBeUndefined(); - }); - it('when update artifacts is loaded', () => { - const isLoading = getUpdateArtifacts({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createLoadedResourceState([getMockCreateResponse()]), - }, - }); - expect(isLoading).toEqual([getMockCreateResponse()]); - }); - it('when update artifacts has failed', () => { - const isLoading = getUpdateArtifacts({ - ...initialState, - artifacts: { - ...initialState.artifacts, - trustedAppsToUpdate: createFailedResourceState(getAPIError()), - }, - }); - expect(isLoading).toBeUndefined(); - }); - }); - - describe('getAssignableArtifactsListExist()', () => { - it('when check artifacts exists is loading', () => { - const exists = getAssignableArtifactsListExist({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createLoadingResourceState( - createUninitialisedResourceState() - ), - }, - }); - expect(exists).toBeFalsy(); - }); - it('when check artifacts exists is uninitialised', () => { - const exists = getAssignableArtifactsListExist({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createUninitialisedResourceState(), - }, - }); - expect(exists).toBeFalsy(); - }); - it('when check artifacts exists is loaded with negative result', () => { - const exists = getAssignableArtifactsListExist({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createLoadedResourceState(false), - }, - }); - expect(exists).toBeFalsy(); - }); - it('when check artifacts exists is loaded with positive result', () => { - const exists = getAssignableArtifactsListExist({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createLoadedResourceState(true), - }, - }); - expect(exists).toBeTruthy(); - }); - it('when check artifacts exists has failed', () => { - const exists = getAssignableArtifactsListExist({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createFailedResourceState(getAPIError()), - }, - }); - expect(exists).toBeFalsy(); - }); - }); - - describe('getAssignableArtifactsListExistIsLoading()', () => { - it('when check artifacts exists is loading', () => { - const isLoading = getAssignableArtifactsListExistIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createLoadingResourceState( - createUninitialisedResourceState() - ), - }, - }); - expect(isLoading).toBeTruthy(); - }); - it('when check artifacts exists is uninitialised', () => { - const isLoading = getAssignableArtifactsListExistIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createUninitialisedResourceState(), - }, - }); - expect(isLoading).toBeFalsy(); - }); - it('when check artifacts exists is loaded with negative result', () => { - const isLoading = getAssignableArtifactsListExistIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createLoadedResourceState(false), - }, - }); - expect(isLoading).toBeFalsy(); - }); - it('when check artifacts exists is loaded with positive result', () => { - const isLoading = getAssignableArtifactsListExistIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createLoadedResourceState(true), - }, - }); - expect(isLoading).toBeFalsy(); - }); - it('when check artifacts exists has failed', () => { - const isLoading = getAssignableArtifactsListExistIsLoading({ - ...initialState, - artifacts: { - ...initialState.artifacts, - assignableListEntriesExist: createFailedResourceState(getAPIError()), - }, - }); - expect(isLoading).toBeFalsy(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts deleted file mode 100644 index d341b8ae7a180..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createSelector } from 'reselect'; -import { Pagination } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import { - PolicyArtifactsState, - PolicyAssignedTrustedApps, - PolicyDetailsArtifactsPageListLocationParams, - PolicyDetailsSelector, - PolicyDetailsState, -} from '../../../types'; -import { - Immutable, - ImmutableArray, - PostTrustedAppCreateResponse, - GetTrustedAppsListResponse, - PolicyData, -} from '../../../../../../../common/endpoint/types'; -import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../../../common/constants'; -import { - getLastLoadedResourceState, - isFailedResourceState, - isLoadedResourceState, - isLoadingResourceState, - LoadedResourceState, -} from '../../../../../state'; -import { getCurrentArtifactsLocation } from './policy_common_selectors'; -import { ServerApiError } from '../../../../../../common/types'; - -export const doesPolicyHaveTrustedAppsAssignedList = ( - state: PolicyDetailsState -): { loading: boolean; hasTrustedApps: boolean } => { - return { - loading: isLoadingResourceState(state.artifacts.assignedList), - hasTrustedApps: isLoadedResourceState(state.artifacts.assignedList) - ? !isEmpty(state.artifacts.assignedList.data.artifacts.data) - : false, - }; -}; - -/** - * Returns current assignable artifacts list - */ -export const getAssignableArtifactsList = ( - state: Immutable -): Immutable | undefined => - getLastLoadedResourceState(state.artifacts.assignableList)?.data; - -/** - * Returns if assignable list is loading - */ -export const getAssignableArtifactsListIsLoading = ( - state: Immutable -): boolean => isLoadingResourceState(state.artifacts.assignableList); - -/** - * Returns if update action is loading - */ -export const getUpdateArtifactsIsLoading = (state: Immutable): boolean => - isLoadingResourceState(state.artifacts.trustedAppsToUpdate); - -/** - * Returns if update action is loading - */ -export const getUpdateArtifactsIsFailed = (state: Immutable): boolean => - isFailedResourceState(state.artifacts.trustedAppsToUpdate); - -/** - * Returns if update action is done successfully - */ -export const getUpdateArtifactsLoaded = (state: Immutable): boolean => { - return isLoadedResourceState(state.artifacts.trustedAppsToUpdate); -}; - -/** - * Returns true if there is data assignable even if the search didn't returned it. - */ -export const getAssignableArtifactsListExist = (state: Immutable): boolean => { - return ( - isLoadedResourceState(state.artifacts.assignableListEntriesExist) && - state.artifacts.assignableListEntriesExist.data - ); -}; - -/** - * Returns true if there is data assignable even if the search didn't returned it. - */ -export const getAssignableArtifactsListExistIsLoading = ( - state: Immutable -): boolean => { - return isLoadingResourceState(state.artifacts.assignableListEntriesExist); -}; - -/** - * Returns artifacts to be updated - */ -export const getUpdateArtifacts = ( - state: Immutable -): ImmutableArray | undefined => { - return state.artifacts.trustedAppsToUpdate.type === 'LoadedResourceState' - ? state.artifacts.trustedAppsToUpdate.data - : undefined; -}; - -/** - * Returns does any TA exists - */ -export const getDoesTrustedAppExists = (state: Immutable): boolean => { - return ( - isLoadedResourceState(state.artifacts.doesAnyTrustedAppExists) && - !!state.artifacts.doesAnyTrustedAppExists.data.total - ); -}; - -/** - * Returns does any TA exists loading - */ -export const doesTrustedAppExistsLoading = (state: Immutable): boolean => { - return isLoadingResourceState(state.artifacts.doesAnyTrustedAppExists); -}; - -/** Returns a boolean of whether the user is on the policy details page or not */ -export const getCurrentPolicyAssignedTrustedAppsState: PolicyDetailsSelector< - PolicyArtifactsState['assignedList'] -> = (state) => { - return state.artifacts.assignedList; -}; - -/** Returns current filter value */ -export const getCurrentPolicyArtifactsFilter: PolicyDetailsSelector = (state) => { - return state.artifacts.location.filter; -}; - -export const getLatestLoadedPolicyAssignedTrustedAppsState: PolicyDetailsSelector< - undefined | LoadedResourceState -> = createSelector(getCurrentPolicyAssignedTrustedAppsState, (currentAssignedTrustedAppsState) => { - return getLastLoadedResourceState(currentAssignedTrustedAppsState); -}); - -export const getCurrentUrlLocationPaginationParams: PolicyDetailsSelector = - // eslint-disable-next-line @typescript-eslint/naming-convention - createSelector(getCurrentArtifactsLocation, ({ filter, page_index, page_size }) => { - return { filter, page_index, page_size }; - }); - -export const doesPolicyTrustedAppsListNeedUpdate: PolicyDetailsSelector = createSelector( - getCurrentPolicyAssignedTrustedAppsState, - getCurrentUrlLocationPaginationParams, - (assignedListState, locationData) => { - return ( - !isLoadedResourceState(assignedListState) || - (isLoadedResourceState(assignedListState) && - ( - Object.keys(locationData) as Array - ).some((key) => assignedListState.data.location[key] !== locationData[key])) - ); - } -); - -export const isPolicyTrustedAppListLoading: PolicyDetailsSelector = createSelector( - getCurrentPolicyAssignedTrustedAppsState, - (assignedState) => isLoadingResourceState(assignedState) -); - -export const getPolicyTrustedAppList: PolicyDetailsSelector = - createSelector(getLatestLoadedPolicyAssignedTrustedAppsState, (assignedState) => { - return assignedState?.data.artifacts.data ?? []; - }); - -export const getPolicyTrustedAppsListPagination: PolicyDetailsSelector = createSelector( - getLatestLoadedPolicyAssignedTrustedAppsState, - (currentAssignedTrustedAppsState) => { - const trustedAppsApiResponse = currentAssignedTrustedAppsState?.data.artifacts; - - return { - // Trusted apps api is `1` based for page - need to subtract here for `Pagination` component - pageIndex: trustedAppsApiResponse?.page ? trustedAppsApiResponse.page - 1 : 0, - pageSize: trustedAppsApiResponse?.per_page ?? MANAGEMENT_PAGE_SIZE_OPTIONS[0], - totalItemCount: trustedAppsApiResponse?.total || 0, - pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], - }; - } -); - -export const getTotalPolicyTrustedAppsListPagination = ( - state: Immutable -): number => { - return getLastLoadedResourceState(state.artifacts.hasTrustedApps)?.data.total || 0; -}; - -export const getTrustedAppsPolicyListState: PolicyDetailsSelector< - PolicyDetailsState['artifacts']['policies'] -> = (state) => state.artifacts.policies; - -export const getTrustedAppsListOfAllPolicies: PolicyDetailsSelector = createSelector( - getTrustedAppsPolicyListState, - (policyListState) => { - return getLastLoadedResourceState(policyListState)?.data.items ?? []; - } -); - -export const getTrustedAppsAllPoliciesById: PolicyDetailsSelector< - Record> -> = createSelector(getTrustedAppsListOfAllPolicies, (allPolicies) => { - return allPolicies.reduce>>((mapById, policy) => { - mapById[policy.id] = policy; - return mapById; - }, {}) as Immutable>>; -}); - -export const getHasTrustedApps: PolicyDetailsSelector = (state) => { - return !!getLastLoadedResourceState(state.artifacts.hasTrustedApps)?.data.total; -}; - -export const getIsLoadedHasTrustedApps: PolicyDetailsSelector = (state) => - !!getLastLoadedResourceState(state.artifacts.hasTrustedApps); - -export const getHasTrustedAppsIsLoading: PolicyDetailsSelector = (state) => - isLoadingResourceState(state.artifacts.hasTrustedApps); - -export const getDoesAnyTrustedAppExists: PolicyDetailsSelector< - PolicyDetailsState['artifacts']['doesAnyTrustedAppExists'] -> = (state) => state.artifacts.doesAnyTrustedAppExists; - -export const getDoesAnyTrustedAppExistsIsLoading: PolicyDetailsSelector = createSelector( - getDoesAnyTrustedAppExists, - (doesAnyTrustedAppExists) => { - return isLoadingResourceState(doesAnyTrustedAppExists); - } -); - -export const getPolicyTrustedAppListError: PolicyDetailsSelector< - Immutable | undefined -> = createSelector(getCurrentPolicyAssignedTrustedAppsState, (currentAssignedTrustedAppsState) => { - if (isFailedResourceState(currentAssignedTrustedAppsState)) { - return currentAssignedTrustedAppsState.error; - } -}); - -export const getCurrentTrustedAppsRemoveListState: PolicyDetailsSelector< - PolicyArtifactsState['removeList'] -> = (state) => state.artifacts.removeList; - -export const getTrustedAppsIsRemoving: PolicyDetailsSelector = createSelector( - getCurrentTrustedAppsRemoveListState, - (removeListState) => isLoadingResourceState(removeListState) -); - -export const getTrustedAppsRemovalError: PolicyDetailsSelector = - createSelector(getCurrentTrustedAppsRemoveListState, (removeListState) => { - if (isFailedResourceState(removeListState)) { - return removeListState.error; - } - }); - -export const getTrustedAppsWasRemoveSuccessful: PolicyDetailsSelector = createSelector( - getCurrentTrustedAppsRemoveListState, - (removeListState) => isLoadedResourceState(removeListState) -); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index bb511c886c83c..6057c0545fa63 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -13,7 +13,6 @@ import { ProtectionFields, PolicyData, UIPolicyConfig, - PostTrustedAppCreateResponse, MaybeImmutable, GetTrustedAppsListResponse, TrustedApp, @@ -26,10 +25,8 @@ import { GetPackagePoliciesResponse, UpdatePackagePolicyResponse, } from '../../../../../fleet/common'; -import { AsyncResourceState } from '../../state'; import { ImmutableMiddlewareAPI } from '../../../common/store'; import { AppAction } from '../../../common/store/actions'; -import { TrustedAppsService } from '../trusted_apps/service'; export type PolicyDetailsStore = ImmutableMiddlewareAPI; @@ -44,7 +41,6 @@ export type MiddlewareRunner = ( export interface MiddlewareRunnerContext { coreStart: CoreStart; - trustedAppsService: TrustedAppsService; } export type PolicyDetailsSelector = ( @@ -91,22 +87,6 @@ export interface PolicyRemoveTrustedApps { export interface PolicyArtifactsState { /** artifacts location params */ location: PolicyDetailsArtifactsPageLocation; - /** A list of artifacts can be linked to the policy */ - assignableList: AsyncResourceState; - /** Represents if available trusted apps entries exist, regardless of whether the list is showing results */ - assignableListEntriesExist: AsyncResourceState; - /** A list of trusted apps going to be updated */ - trustedAppsToUpdate: AsyncResourceState; - /** Represents if there is any trusted app existing */ - doesAnyTrustedAppExists: AsyncResourceState; - /** Represents if there is any trusted app existing assigned to the policy (without filters) */ - hasTrustedApps: AsyncResourceState; - /** List of artifacts currently assigned to the policy (body specific and global) */ - assignedList: AsyncResourceState; - /** A list of all available polices */ - policies: AsyncResourceState; - /** list of artifacts to remove. Holds the ids that were removed and the API response */ - removeList: AsyncResourceState; } export interface PolicyDetailsArtifactsPageListLocationParams { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/index.ts similarity index 63% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/index.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/index.ts index c35cb26dfe8fc..4a8db92171414 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { PolicyEventFiltersLayout } from './policy_event_filters_layout'; +export { PolicyArtifactsDeleteModal } from './policy_artifacts_delete_modal'; +export { POLICY_ARTIFACT_DELETE_MODAL_LABELS } from './translations'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx similarity index 84% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx index 2e00dab303007..ed5553337d6ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx @@ -15,33 +15,36 @@ import { AppContextTestRender, createAppRootMockRenderer, } from '../../../../../../common/mock/endpoint'; -import { PolicyEventFiltersDeleteModal } from './policy_event_filters_delete_modal'; +import { PolicyArtifactsDeleteModal } from './policy_artifacts_delete_modal'; import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; -import { cleanEventFilterToUpdate } from '../../../../event_filters/service/service_actions'; +import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { POLICY_ARTIFACT_DELETE_MODAL_LABELS } from './translations'; -describe('Policy details event filter delete modal', () => { +describe('Policy details artifacts delete modal', () => { let policyId: string; let render: () => Promise>; let renderResult: ReturnType; let mockedContext: AppContextTestRender; let exception: ExceptionListItemSchema; let mockedApi: ReturnType; - let onCancel: () => void; + let onCloseMock: () => jest.Mock; beforeEach(() => { policyId = uuid.v4(); mockedContext = createAppRootMockRenderer(); exception = getExceptionListItemSchemaMock(); - onCancel = jest.fn(); + onCloseMock = jest.fn(); mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); render = async () => { await act(async () => { renderResult = mockedContext.render( - ); await waitFor(mockedApi.responseProvider.eventFiltersList); @@ -74,7 +77,7 @@ describe('Policy details event filter delete modal', () => { await waitFor(() => { expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenLastCalledWith({ body: JSON.stringify( - cleanEventFilterToUpdate({ + EventFiltersApiClient.cleanExceptionsBeforeUpdate({ ...exception, tags: ['policy:1234', 'policy:4321', 'not-a-policy-tag'], }) @@ -93,7 +96,7 @@ describe('Policy details event filter delete modal', () => { expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalled(); }); - expect(onCancel).toHaveBeenCalled(); + expect(onCloseMock).toHaveBeenCalled(); expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalled(); }); @@ -112,7 +115,7 @@ describe('Policy details event filter delete modal', () => { }); expect(mockedContext.coreStart.notifications.toasts.addError).toHaveBeenCalledWith(error, { - title: 'Error while attempt to remove event filter', + title: 'Error while attempting to remove artifact', }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.tsx new file mode 100644 index 0000000000000..a92411c22ce2b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.tsx @@ -0,0 +1,88 @@ +/* + * 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 { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui'; +import { useQueryClient } from 'react-query'; +import { HttpFetchError } from 'kibana/public'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import React, { useCallback } from 'react'; +import { useBulkUpdateArtifact } from '../../../../../hooks/artifacts'; +import { useToasts } from '../../../../../../common/lib/kibana'; +import { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client'; +import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../../../../common/endpoint/service/artifacts'; +import { POLICY_ARTIFACT_DELETE_MODAL_LABELS } from './translations'; + +interface PolicyArtifactsDeleteModalProps { + policyId: string; + policyName: string; + apiClient: ExceptionsListApiClient; + exception: ExceptionListItemSchema; + onClose: () => void; + labels: typeof POLICY_ARTIFACT_DELETE_MODAL_LABELS; +} + +export const PolicyArtifactsDeleteModal = React.memo( + ({ policyId, policyName, apiClient, exception, onClose, labels }) => { + const toasts = useToasts(); + const queryClient = useQueryClient(); + + const { mutate: updateArtifact, isLoading: isUpdateArtifactLoading } = useBulkUpdateArtifact( + apiClient, + { + onSuccess: () => { + toasts.addSuccess({ + title: labels.deleteModalSuccessMessageTitle, + text: labels.deleteModalSuccessMessageText(exception, policyName), + }); + queryClient.invalidateQueries(['list', apiClient]); + onClose(); + }, + onError: (error?: HttpFetchError) => { + toasts.addError(error as unknown as Error, { + title: labels.deleteModalErrorMessage, + }); + }, + } + ); + + const handleModalConfirm = useCallback(() => { + const modifiedException = { + ...exception, + tags: exception.tags.filter((tag) => tag !== `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policyId}`), + }; + updateArtifact([modifiedException]); + }, [exception, policyId, updateArtifact]); + + const handleOnClose = useCallback(() => { + if (!isUpdateArtifactLoading) { + onClose(); + } + }, [isUpdateArtifactLoading, onClose]); + + return ( + + +

{labels.deleteModalImpactInfo}

+
+ + + + +

{labels.deleteModalConfirmInfo}

+
+
+ ); + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/translations.ts new file mode 100644 index 0000000000000..bbbe4ed73c295 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/translations.ts @@ -0,0 +1,64 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export const POLICY_ARTIFACT_DELETE_MODAL_LABELS = Object.freeze({ + deleteModalTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.title', + { + defaultMessage: 'Remove artifact from policy', + } + ), + + deleteModalImpactInfo: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.messageCallout', + { + defaultMessage: + 'This artifact will be removed only from this policy and can still be found and managed from the artifact page.', + } + ), + + deleteModalConfirmInfo: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.message', + { + defaultMessage: 'Are you sure you wish to continue?', + } + ), + + deleteModalSubmitButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.confirmLabel', + { + defaultMessage: 'Remove from policy', + } + ), + + deleteModalCancelButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.cancelLabel', + { defaultMessage: 'Cancel' } + ), + + deleteModalSuccessMessageTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.successToastTitle', + { defaultMessage: 'Successfully removed' } + ), + deleteModalSuccessMessageText: (exception: ExceptionListItemSchema, policyName: string): string => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.successToastText', + { + defaultMessage: '"{artifactName}" has been removed from {policyName} policy', + values: { artifactName: exception.name, policyName }, + } + ), + deleteModalErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeDialog.errorToastTitle', + { + defaultMessage: 'Error while attempting to remove artifact', + } + ), +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/index.ts new file mode 100644 index 0000000000000..db833460283cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/index.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export { PolicyArtifactsEmptyUnassigned } from './policy_artifacts_empty_unassigned'; +export { PolicyArtifactsEmptyUnexisting } from './policy_artifacts_empty_unexisting'; +export { + POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS, + POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS, +} from './translations'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx new file mode 100644 index 0000000000000..5a606de45068d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx @@ -0,0 +1,80 @@ +/* + * 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, { memo, useCallback } from 'react'; +import { EuiButton, EuiEmptyPrompt, EuiPageTemplate, EuiLink } from '@elastic/eui'; +import { usePolicyDetailsArtifactsNavigateCallback } from '../../policy_hooks'; +import { useGetLinkTo } from './use_policy_artifacts_empty_hooks'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS } from './translations'; +import { EventFiltersPageLocation } from '../../../../event_filters/types'; +import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; +import { HostIsolationExceptionsPageLocation } from '../../../../host_isolation_exceptions/types'; +interface CommonProps { + policyId: string; + policyName: string; + listId: string; + labels: typeof POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS; + getPolicyArtifactsPath: (policyId: string) => string; + getArtifactPath: ( + location?: + | Partial + | Partial + | Partial + ) => string; +} + +export const PolicyArtifactsEmptyUnassigned = memo( + ({ policyId, policyName, listId, labels, getPolicyArtifactsPath, getArtifactPath }) => { + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; + const { onClickHandler, toRouteUrl } = useGetLinkTo( + policyId, + policyName, + getPolicyArtifactsPath, + getArtifactPath + ); + + const navigateCallback = usePolicyDetailsArtifactsNavigateCallback(listId); + const onClickPrimaryButtonHandler = useCallback( + () => + navigateCallback({ + show: 'list', + }), + [navigateCallback] + ); + return ( + + {labels.emptyUnassignedTitle}} + body={labels.emptyUnassignedMessage(policyName)} + actions={[ + ...(canCreateArtifactsByPolicy + ? [ + + {labels.emptyUnassignedPrimaryActionButtonTitle} + , + ] + : []), + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {labels.emptyUnassignedSecondaryActionButtonTitle} + , + ]} + /> + + ); + } +); + +PolicyArtifactsEmptyUnassigned.displayName = 'PolicyArtifactsEmptyUnassigned'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx new file mode 100644 index 0000000000000..c62db552e9a01 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx @@ -0,0 +1,59 @@ +/* + * 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, { memo } from 'react'; +import { EuiEmptyPrompt, EuiButton, EuiPageTemplate } from '@elastic/eui'; +import { useGetLinkTo } from './use_policy_artifacts_empty_hooks'; +import { POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS } from './translations'; +import { EventFiltersPageLocation } from '../../../../event_filters/types'; +import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; +import { HostIsolationExceptionsPageLocation } from '../../../../host_isolation_exceptions/types'; + +interface CommonProps { + policyId: string; + policyName: string; + labels: typeof POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS; + getPolicyArtifactsPath: (policyId: string) => string; + getArtifactPath: ( + location?: + | Partial + | Partial + | Partial + ) => string; +} + +export const PolicyArtifactsEmptyUnexisting = memo( + ({ policyId, policyName, labels, getPolicyArtifactsPath, getArtifactPath }) => { + const { onClickHandler, toRouteUrl } = useGetLinkTo( + policyId, + policyName, + getPolicyArtifactsPath, + getArtifactPath, + { + show: 'create', + } + ); + return ( + + {labels.emptyUnexistingTitle}} + body={labels.emptyUnexistingMessage} + actions={ + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {labels.emptyUnexistingPrimaryActionButtonTitle} + + } + /> + + ); + } +); + +PolicyArtifactsEmptyUnexisting.displayName = 'PolicyArtifactsEmptyUnexisting'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts new file mode 100644 index 0000000000000..8fdb3f2dbfb5b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS = Object.freeze({ + emptyUnassignedTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.title', + { defaultMessage: 'No assigned artifacts' } + ), + emptyUnassignedMessage: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.content', { + defaultMessage: + 'There are currently no artifacts assigned to {policyName}. Assign artifacts now or add and manage them on the artifacts page.', + values: { policyName }, + }), + emptyUnassignedPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.primaryAction', + { + defaultMessage: 'Assign artifacts', + } + ), + emptyUnassignedSecondaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.secondaryAction', + { + defaultMessage: 'Manage artifacts', + } + ), +}); + +export const POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS = Object.freeze({ + emptyUnexistingTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.empty.unexisting.title', + { defaultMessage: 'No artifacts exist' } + ), + emptyUnexistingMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.empty.unexisting.content', + { defaultMessage: 'There are currently no artifacts applied to your endpoints.' } + ), + emptyUnexistingPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.empty.unexisting.action', + { defaultMessage: 'Add artifacts' } + ), +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/use_policy_artifacts_empty_hooks.ts similarity index 60% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/use_policy_artifacts_empty_hooks.ts index 494dfd9a7ae08..2304cb7dfdd6d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/use_policy_artifacts_empty_hooks.ts @@ -9,35 +9,38 @@ import { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { useAppUrl } from '../../../../../../common/lib/kibana/hooks'; -import { - getPolicyHostIsolationExceptionsPath, - getHostIsolationExceptionsListPath, -} from '../../../../../common/routing'; import { APP_UI_ID } from '../../../../../../../common/constants'; +import { EventFiltersPageLocation } from '../../../../event_filters/types'; +import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; import { HostIsolationExceptionsPageLocation } from '../../../../host_isolation_exceptions/types'; export const useGetLinkTo = ( policyId: string, policyName: string, - location?: Partial + getPolicyArtifactsPath: (policyId: string) => string, + getArtifactPath: ( + location?: + | Partial + | Partial + | Partial + ) => string, + location?: Partial<{ show: 'create' }> ) => { const { getAppUrl } = useAppUrl(); const { toRoutePath, toRouteUrl } = useMemo(() => { - const path = getHostIsolationExceptionsListPath(location); + const path = getArtifactPath(location); return { toRoutePath: path, toRouteUrl: getAppUrl({ path }), }; - }, [getAppUrl, location]); + }, [getAppUrl, getArtifactPath, location]); - const policyHostIsolationExceptionsPath = useMemo( - () => getPolicyHostIsolationExceptionsPath(policyId), - [policyId] - ); - const policyHostIsolationExceptionsRouteState = useMemo(() => { + const policyArtifactsPath = getPolicyArtifactsPath(policyId); + + const policyArtifactRouteState = useMemo(() => { return { backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.backButtonLabel', + 'xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.backButtonLabel', { defaultMessage: 'Back to {policyName} policy', values: { @@ -48,24 +51,24 @@ export const useGetLinkTo = ( onBackButtonNavigateTo: [ APP_UI_ID, { - path: policyHostIsolationExceptionsPath, + path: policyArtifactsPath, }, ], backButtonUrl: getAppUrl({ appId: APP_UI_ID, - path: policyHostIsolationExceptionsPath, + path: policyArtifactsPath, }), }; - }, [getAppUrl, policyName, policyHostIsolationExceptionsPath]); + }, [getAppUrl, policyName, policyArtifactsPath]); const onClickHandler = useNavigateToAppEventHandler(APP_UI_ID, { - state: policyHostIsolationExceptionsRouteState, + state: policyArtifactRouteState, path: toRoutePath, }); return { onClickHandler, toRouteUrl, - state: policyHostIsolationExceptionsRouteState, + state: policyArtifactRouteState, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/index.ts similarity index 65% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/index.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/index.ts index 9b13b6f5741a9..7337ccff2a836 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { PolicyEventFiltersList } from './policy_event_filters_list'; +export { PolicyArtifactsFlyout } from './policy_artifacts_flyout'; +export { POLICY_ARTIFACT_FLYOUT_LABELS } from './translations'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx similarity index 85% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx index 7d984cdb2a382..e9ac077857284 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx @@ -17,8 +17,9 @@ import { } from '../../../../../../common/mock/endpoint'; import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; import { PolicyData } from '../../../../../../../common/endpoint/types'; +import { MANAGEMENT_DEFAULT_PAGE } from '../../../../../common/constants'; import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; -import { PolicyEventFiltersFlyout } from './policy_event_filters_flyout'; +import { MAX_ALLOWED_RESULTS, PolicyArtifactsFlyout } from './policy_artifacts_flyout'; import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; import { @@ -26,6 +27,8 @@ import { UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { cleanEventFilterToUpdate } from '../../../../event_filters/service/service_actions'; +import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { POLICY_ARTIFACT_FLYOUT_LABELS } from './translations'; const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ path: '/api/exception_lists/items/_find', @@ -33,8 +36,8 @@ const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ filter: customFilter, list_id: ['endpoint_event_filters'], namespace_type: ['agnostic'], - page: undefined, - per_page: 100, + page: MANAGEMENT_DEFAULT_PAGE + 1, + per_page: MAX_ALLOWED_RESULTS, sort_field: undefined, sort_order: undefined, }, @@ -59,7 +62,7 @@ const getCleanedExceptionWithNewTags = ( return cleanEventFilterToUpdate(exceptionToUpdateWithNewTags); }; -describe('Policy details event filters flyout', () => { +describe('Policy details artifacts flyout', () => { let render: () => Promise>; let renderResult: ReturnType; let mockedContext: AppContextTestRender; @@ -76,7 +79,13 @@ describe('Policy details event filters flyout', () => { render = async () => { await act(async () => { renderResult = mockedContext.render( - + ); await waitFor(mockedApi.responseProvider.eventFiltersList); }); @@ -89,7 +98,7 @@ describe('Policy details event filters flyout', () => { return getFoundExceptionListItemSchemaMock(1); }); await render(); - expect(mockedApi.responseProvider.eventFiltersList).toHaveBeenLastCalledWith( + expect(mockedApi.responseProvider.eventFiltersList).toHaveBeenCalledWith( getDefaultQueryParameters( parsePoliciesAndFilterToKql({ excludedPolicies: [policy.id, 'all'], @@ -124,7 +133,7 @@ describe('Policy details event filters flyout', () => { }) ) ); - expect(renderResult.getByTestId('eventFilters-no-items-found')).toBeTruthy(); + expect(renderResult.getByTestId('artifacts-no-items-found')).toBeTruthy(); }); }); @@ -132,7 +141,7 @@ describe('Policy details event filters flyout', () => { // both exceptions list requests will return no results mockedApi.responseProvider.eventFiltersList.mockImplementation(() => getEmptyList()); await render(); - expect(await renderResult.findByTestId('eventFilters-no-assignable-items')).toBeTruthy(); + expect(await renderResult.findByTestId('artifacts-no-assignable-items')).toBeTruthy(); }); it('should disable the submit button if no exceptions are selected', async () => { @@ -141,7 +150,7 @@ describe('Policy details event filters flyout', () => { }); await render(); expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - expect(renderResult.getByTestId('eventFilters-assign-confirm-button')).toBeDisabled(); + expect(renderResult.getByTestId('artifacts-assign-confirm-button')).toBeDisabled(); }); it('should enable the submit button if an exception is selected', async () => { @@ -155,7 +164,7 @@ describe('Policy details event filters flyout', () => { // click the first item userEvent.click(renderResult.getByTestId(`${firstOneName}_checkbox`)); - expect(renderResult.getByTestId('eventFilters-assign-confirm-button')).toBeEnabled(); + expect(renderResult.getByTestId('artifacts-assign-confirm-button')).toBeEnabled(); }); it('should warn the user when there are over 100 results in the flyout', async () => { @@ -167,7 +176,7 @@ describe('Policy details event filters flyout', () => { }); await render(); expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - expect(renderResult.getByTestId('eventFilters-too-many-results')).toBeTruthy(); + expect(renderResult.getByTestId('artifacts-too-many-results')).toBeTruthy(); }); describe('when submitting the form', () => { @@ -205,7 +214,7 @@ describe('Policy details event filters flyout', () => { // click the first item userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); // submit the form - userEvent.click(renderResult.getByTestId('eventFilters-assign-confirm-button')); + userEvent.click(renderResult.getByTestId('artifacts-assign-confirm-button')); // verify the request with the new tag await waitFor(() => { @@ -219,7 +228,7 @@ describe('Policy details event filters flyout', () => { await waitFor(() => { expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: `"${FIRST_ONE_NAME}" has been added to your event filters list.`, + text: `"${FIRST_ONE_NAME}" has been added to your artifacts list.`, title: 'Success', }); }); @@ -231,7 +240,7 @@ describe('Policy details event filters flyout', () => { userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); userEvent.click(renderResult.getByTestId(`${SECOND_ONE_NAME}_checkbox`)); // submit the form - userEvent.click(renderResult.getByTestId('eventFilters-assign-confirm-button')); + userEvent.click(renderResult.getByTestId('artifacts-assign-confirm-button')); // verify the request with the new tag await waitFor(() => { @@ -253,27 +262,26 @@ describe('Policy details event filters flyout', () => { await waitFor(() => { expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: '2 event filters have been added to your list.', + text: '2 artifacts have been added to your list.', title: 'Success', }); }); expect(onCloseMock).toHaveBeenCalled(); }); - it('should show a toast error when the request fails and close the flyout', async () => { + it('should show a toast error when the request fails', async () => { mockedApi.responseProvider.eventFiltersUpdateOne.mockImplementation(() => { throw new Error('the server is too far away'); }); // click first item userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); // submit the form - userEvent.click(renderResult.getByTestId('eventFilters-assign-confirm-button')); + userEvent.click(renderResult.getByTestId('artifacts-assign-confirm-button')); await waitFor(() => { expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( 'An error occurred updating artifacts' ); - expect(onCloseMock).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx new file mode 100644 index 0000000000000..1ba31da565304 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.tsx @@ -0,0 +1,236 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import { useQueryClient } from 'react-query'; +import { isEmpty, without } from 'lodash/fp'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + EuiTitle, + EuiFlyout, + EuiSpacer, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiCallOut, + EuiEmptyPrompt, +} from '@elastic/eui'; +import { SearchExceptions } from '../../../../../components/search_exceptions'; +import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; +import { useToasts } from '../../../../../../common/lib/kibana'; +import { PolicyArtifactsAssignableList } from '../../artifacts/assignable'; +import { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client'; +import { useListArtifact, useBulkUpdateArtifact } from '../../../../../hooks/artifacts'; +import { POLICY_ARTIFACT_FLYOUT_LABELS } from './translations'; + +interface PolicyArtifactsFlyoutProps { + policyItem: ImmutableObject; + apiClient: ExceptionsListApiClient; + searchableFields: string[]; + onClose: () => void; + labels: typeof POLICY_ARTIFACT_FLYOUT_LABELS; +} + +export const MAX_ALLOWED_RESULTS = 100; + +export const PolicyArtifactsFlyout = React.memo( + ({ policyItem, apiClient, searchableFields, onClose, labels }) => { + const toasts = useToasts(); + const queryClient = useQueryClient(); + const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); + const [currentFilter, setCurrentFilter] = useState(''); + + const bulkUpdateMutation = useBulkUpdateArtifact(apiClient, { + onSuccess: (updatedExceptions: ExceptionListItemSchema[]) => { + toasts.addSuccess({ + title: labels.flyoutSuccessMessageTitle, + text: labels.flyoutSuccessMessageText(updatedExceptions), + }); + queryClient.invalidateQueries(['list', apiClient]); + onClose(); + }, + onError: () => { + toasts.addDanger(labels.flyoutErrorMessage); + }, + }); + + const { + data: artifacts, + isLoading: isLoadingArtifacts, + isRefetching: isRefetchingArtifacts, + } = useListArtifact( + apiClient, + { + perPage: MAX_ALLOWED_RESULTS, + filter: currentFilter, + excludedPolicies: [policyItem.id, 'all'], + }, + searchableFields + ); + + const { data: allNotAssigned, isLoading: isLoadingAllNotAssigned } = useListArtifact( + apiClient, + + { + excludedPolicies: [policyItem.id, 'all'], + }, + searchableFields + ); + + const handleOnSearch = useCallback((query) => { + setSelectedArtifactIds([]); + setCurrentFilter(query); + }, []); + + const handleOnConfirmAction = useCallback(() => { + if (!artifacts) { + return; + } + const artifactsToUpdate: ExceptionListItemSchema[] = []; + selectedArtifactIds.forEach((selectedId) => { + const artifact = artifacts.data.find((current) => current.id === selectedId); + if (artifact) { + artifact.tags = [...artifact.tags, `policy:${policyItem.id}`]; + artifactsToUpdate.push(artifact); + } + }); + bulkUpdateMutation.mutate(artifactsToUpdate); + }, [bulkUpdateMutation, artifacts, policyItem.id, selectedArtifactIds]); + + const handleSelectArtifacts = (artifactId: string, selected: boolean) => { + setSelectedArtifactIds((currentSelectedArtifactIds) => + selected + ? [...currentSelectedArtifactIds, artifactId] + : without([artifactId], currentSelectedArtifactIds) + ); + }; + + const searchWarningMessage = useMemo( + () => ( + <> + + {labels.flyoutWarningCalloutMessage(MAX_ALLOWED_RESULTS)} + + + + ), + [labels] + ); + + const assignableArtifacts = useMemo( + () => allNotAssigned?.total !== 0 && (artifacts?.total !== 0 || currentFilter !== ''), + [allNotAssigned?.total, artifacts?.total, currentFilter] + ); + + const isGlobalLoading = useMemo( + () => isLoadingArtifacts || isRefetchingArtifacts || isLoadingAllNotAssigned, + [isLoadingAllNotAssigned, isLoadingArtifacts, isRefetchingArtifacts] + ); + + const noItemsMessage = useMemo(() => { + if (isGlobalLoading) { + return null; + } + + // there are no artifacts assignable to this policy + if (!assignableArtifacts) { + return ( + {labels.flyoutNoArtifactsToBeAssignedMessage}

} + /> + ); + } + + // there are no results for the current search + if (artifacts?.total === 0) { + return ( + {labels.flyoutNoSearchResultsMessage}

} + /> + ); + } + }, [ + isGlobalLoading, + assignableArtifacts, + artifacts?.total, + labels.flyoutNoArtifactsToBeAssignedMessage, + labels.flyoutNoSearchResultsMessage, + ]); + + return ( + + + +

{labels.flyoutTitle}

+
+ + {labels.flyoutSubtitle(policyItem.name)} +
+ + {(artifacts?.total || 0) > MAX_ALLOWED_RESULTS ? searchWarningMessage : null} + {!isLoadingAllNotAssigned && assignableArtifacts && ( + + )} + + + + + {noItemsMessage} + + + + + + {labels.flyoutCancelButtonTitle} + + + + + {labels.flyoutSubmitButtonTitle(policyItem.name)} + + + + +
+ ); + } +); + +PolicyArtifactsFlyout.displayName = 'PolicyArtifactsFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/translations.ts new file mode 100644 index 0000000000000..071a6f7334fb7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/translations.ts @@ -0,0 +1,95 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export const POLICY_ARTIFACT_FLYOUT_LABELS = Object.freeze({ + flyoutWarningCalloutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.searchWarning.title', + { + defaultMessage: 'Limited search results', + } + ), + flyoutWarningCalloutMessage: (maxNumber: number) => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.searchWarning.text', + { + defaultMessage: + 'Only the first {maxNumber} artifacts are displayed. Please use the search bar to refine the results.', + values: { maxNumber }, + } + ), + flyoutNoArtifactsToBeAssignedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.noAssignable', + { + defaultMessage: 'There are no artifacts that can be assigned to this policy.', + } + ), + flyoutNoSearchResultsMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.noResults', + { + defaultMessage: 'No items found', + } + ), + flyoutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.title', + { + defaultMessage: 'Assign artifacts', + } + ), + flyoutSubtitle: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.subtitle', { + defaultMessage: 'Select artifacts to add to {policyName}', + values: { policyName }, + }), + flyoutSearchPlaceholder: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.search.label', + { + defaultMessage: 'Search artifacts', + } + ), + flyoutCancelButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.cancel', + { + defaultMessage: 'Cancel', + } + ), + flyoutSubmitButtonTitle: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.confirm', { + defaultMessage: 'Assign to {policyName}', + values: { policyName }, + }), + flyoutErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.toastError.text', + { + defaultMessage: `An error occurred updating artifacts`, + } + ), + flyoutSuccessMessageTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.toastSuccess.title', + { + defaultMessage: 'Success', + } + ), + flyoutSuccessMessageText: (updatedExceptions: ExceptionListItemSchema[]): string => + updatedExceptions.length > 1 + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.toastSuccess.textMultiples', + { + defaultMessage: '{count} artifacts have been added to your list.', + values: { count: updatedExceptions.length }, + } + ) + : i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.flyout.toastSuccess.textSingle', + { + defaultMessage: '"{name}" has been added to your artifacts list.', + values: { name: updatedExceptions[0].name }, + } + ), +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/index.ts similarity index 65% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/index.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/index.ts index ae6861787044d..77db9c5c4fa50 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { PolicyEventFiltersFlyout } from './policy_event_filters_flyout'; +export { PolicyArtifactsLayout } from './policy_artifacts_layout'; +export { POLICY_ARTIFACT_LAYOUT_LABELS } from './translations'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx new file mode 100644 index 0000000000000..62342b4e64fcd --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx @@ -0,0 +1,191 @@ +/* + * 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 { act, waitFor } from '@testing-library/react'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../../common/mock/endpoint'; +import { + getEventFiltersListPath, + getPolicyDetailsArtifactsListPath, + getPolicyEventFiltersPath, +} from '../../../../../common/routing'; +import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; + +import { PolicyArtifactsLayout } from './policy_artifacts_layout'; +import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; +import { parsePoliciesAndFilterToKql } from '../../../../../common/utils'; +import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; +import { POLICY_ARTIFACT_EVENT_FILTERS_LABELS } from '../../tabs/event_filters_translations'; +import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; +import { FormattedMessage } from '@kbn/i18n-react'; + +let render: (externalPrivileges?: boolean) => Promise>; +let mockedContext: AppContextTestRender; +let renderResult: ReturnType; +let policyItem: ImmutableObject; +const generator = new EndpointDocGenerator(); +let mockedApi: ReturnType; +let history: AppContextTestRender['history']; + +const getEventFiltersLabels = () => ({ + ...POLICY_ARTIFACT_EVENT_FILTERS_LABELS, + layoutAboutMessage: (count: number, link: React.ReactElement): React.ReactNode => ( + + ), +}); + +describe('Policy artifacts layout', () => { + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); + mockedApi.responseProvider.eventFiltersList.mockClear(); + policyItem = generator.generatePolicyPackagePolicy(); + ({ history } = mockedContext); + + getEndpointPrivilegesInitialStateMock({ + canCreateArtifactsByPolicy: true, + }); + render = async (externalPrivileges = true) => { + await act(async () => { + renderResult = mockedContext.render( + + EventFiltersApiClient.getInstance(mockedContext.coreStart.http) + } + searchableFields={EVENT_FILTERS_SEARCHABLE_FIELDS} + getArtifactPath={getEventFiltersListPath} + getPolicyArtifactsPath={getPolicyEventFiltersPath} + externalPrivileges={externalPrivileges} + /> + ); + await waitFor(mockedApi.responseProvider.eventFiltersList); + }); + return renderResult; + }; + history.push(getPolicyEventFiltersPath(policyItem.id)); + }); + + it('should render layout with a loader', async () => { + const component = mockedContext.render( + + EventFiltersApiClient.getInstance(mockedContext.coreStart.http) + } + searchableFields={[...EVENT_FILTERS_SEARCHABLE_FIELDS]} + getArtifactPath={getEventFiltersListPath} + getPolicyArtifactsPath={getPolicyEventFiltersPath} + /> + ); + expect(component.getByTestId('policy-artifacts-loading-spinner')).toBeTruthy(); + }); + + it('should render layout with no assigned artifacts data when there are no artifacts', async () => { + mockedApi.responseProvider.eventFiltersList.mockReturnValue( + getFoundExceptionListItemSchemaMock(0) + ); + + await render(); + expect(await renderResult.findByTestId('policy-artifacts-empty-unexisting')).not.toBeNull(); + }); + + it('should render layout with no assigned artifacts data when there are artifacts', async () => { + mockedApi.responseProvider.eventFiltersList.mockImplementation( + (args?: { query: { filter: string } }) => { + if ( + !args || + args.query.filter !== parsePoliciesAndFilterToKql({ policies: [policyItem.id, 'all'] }) + ) { + return getFoundExceptionListItemSchemaMock(1); + } else { + return getFoundExceptionListItemSchemaMock(0); + } + } + ); + + await render(); + + expect(await renderResult.findByTestId('policy-artifacts-empty-unassigned')).not.toBeNull(); + }); + + it('should render layout with data', async () => { + mockedApi.responseProvider.eventFiltersList.mockReturnValue( + getFoundExceptionListItemSchemaMock(3) + ); + await render(); + expect(await renderResult.findByTestId('policy-artifacts-header-section')).not.toBeNull(); + expect(await renderResult.findByTestId('policy-artifacts-layout-about')).not.toBeNull(); + expect((await renderResult.findByTestId('policy-artifacts-layout-about')).textContent).toMatch( + '3 event filters' + ); + }); + + it('should hide `Assign artifacts to policy` on empty state with unassigned policies when downgraded to a gold or below license', async () => { + getEndpointPrivilegesInitialStateMock({ + canCreateArtifactsByPolicy: false, + }); + mockedApi.responseProvider.eventFiltersList.mockReturnValue( + getFoundExceptionListItemSchemaMock(0) + ); + + await render(); + mockedContext.history.push(getPolicyDetailsArtifactsListPath(policyItem.id)); + expect(renderResult.queryByTestId('artifacts-assign-button')).toBeNull(); + }); + + it('should hide the `Assign artifacts to policy` button license is downgraded to gold or below', async () => { + getEndpointPrivilegesInitialStateMock({ + canCreateArtifactsByPolicy: false, + }); + mockedApi.responseProvider.eventFiltersList.mockReturnValue( + getFoundExceptionListItemSchemaMock(5) + ); + + await render(); + mockedContext.history.push(getPolicyDetailsArtifactsListPath(policyItem.id)); + + expect(renderResult.queryByTestId('artifacts-assign-button')).toBeNull(); + }); + + it('should hide the `Assign artifacts` flyout when license is downgraded to gold or below', async () => { + getEndpointPrivilegesInitialStateMock({ + canCreateArtifactsByPolicy: false, + }); + mockedApi.responseProvider.eventFiltersList.mockReturnValue( + getFoundExceptionListItemSchemaMock(2) + ); + + await render(); + mockedContext.history.push( + `${getPolicyDetailsArtifactsListPath(policyItem.id)}/eventFilters?show=list` + ); + + expect(renderResult.queryByTestId('artifacts-assign-flyout')).toBeNull(); + }); + + describe('Without external privileges', () => { + it('should not display the assign policies button', async () => { + mockedApi.responseProvider.eventFiltersList.mockReturnValue( + getFoundExceptionListItemSchemaMock(5) + ); + await render(false); + expect(renderResult.queryByTestId('artifacts-assign-button')).toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx new file mode 100644 index 0000000000000..c1f771fdcccde --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx @@ -0,0 +1,247 @@ +/* + * 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, useCallback, useState } from 'react'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + EuiTitle, + EuiPageHeader, + EuiPageHeaderSection, + EuiText, + EuiSpacer, + EuiLink, + EuiButton, + EuiPageContent, +} from '@elastic/eui'; +import { useAppUrl } from '../../../../../../common/lib/kibana'; +import { APP_UI_ID } from '../../../../../../../common/constants'; +import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; +import { ManagementPageLoader } from '../../../../../components/management_page_loader'; +import { useUrlParams } from '../../../../../components/hooks/use_url_params'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { usePolicyDetailsArtifactsNavigateCallback } from '../../policy_hooks'; +import { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client'; +import { useListArtifact } from '../../../../../hooks/artifacts'; +import { PolicyArtifactsEmptyUnassigned, PolicyArtifactsEmptyUnexisting } from '../empty'; +import { PolicyArtifactsList } from '../list'; +import { PolicyArtifactsFlyout } from '../flyout'; +import { PolicyArtifactsPageLabels, policyArtifactsPageLabels } from '../translations'; +import { PolicyArtifactsDeleteModal } from '../delete_modal'; +import { EventFiltersPageLocation } from '../../../../event_filters/types'; +import { HostIsolationExceptionsPageLocation } from '../../../../host_isolation_exceptions/types'; +import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; + +interface PolicyArtifactsLayoutProps { + policyItem?: ImmutableObject | undefined; + /** A list of labels for the given policy artifact page. Not all have to be defined, only those that should override the defaults */ + labels: PolicyArtifactsPageLabels; + getExceptionsListApiClient: () => ExceptionsListApiClient; + searchableFields: readonly string[]; + getArtifactPath: ( + location?: + | Partial + | Partial + | Partial + ) => string; + getPolicyArtifactsPath: (policyId: string) => string; + /** A boolean to check extra privileges for restricted actions, true when it's allowed, false when not */ + externalPrivileges?: boolean; +} +export const PolicyArtifactsLayout = React.memo( + ({ + policyItem, + labels: _labels = {}, + getExceptionsListApiClient, + searchableFields, + getArtifactPath, + getPolicyArtifactsPath, + externalPrivileges = true, + }) => { + const exceptionsListApiClient = useMemo( + () => getExceptionsListApiClient(), + [getExceptionsListApiClient] + ); + const { getAppUrl } = useAppUrl(); + const navigateCallback = usePolicyDetailsArtifactsNavigateCallback( + exceptionsListApiClient.listId + ); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; + const { urlParams } = useUrlParams(); + const [exceptionItemToDelete, setExceptionItemToDelete] = useState< + ExceptionListItemSchema | undefined + >(); + + const labels = useMemo(() => { + return { + ...policyArtifactsPageLabels, + ..._labels, + }; + }, [_labels]); + + const { data: allAssigned, isLoading: isLoadingAllAssigned } = useListArtifact( + exceptionsListApiClient, + { + policies: policyItem ? [policyItem.id, 'all'] : [], + }, + searchableFields + ); + + const { + data: allArtifacts, + isLoading: isLoadingAllArtifacts, + isRefetching: isRefetchingAllArtifacts, + } = useListArtifact(exceptionsListApiClient, {}, searchableFields, {}, ['allExisting']); + + const handleOnClickAssignButton = useCallback(() => { + navigateCallback({ show: 'list' }); + }, [navigateCallback]); + const handleOnCloseFlyout = useCallback(() => { + navigateCallback({ show: undefined }); + }, [navigateCallback]); + + const handleDeleteModalClose = useCallback(() => { + setExceptionItemToDelete(undefined); + }, [setExceptionItemToDelete]); + + const handleOnDeleteActionCallback = useCallback( + (item) => { + setExceptionItemToDelete(item); + }, + [setExceptionItemToDelete] + ); + + const assignToPolicyButton = useMemo( + () => ( + + {labels.layoutAssignButtonTitle} + + ), + [handleOnClickAssignButton, labels.layoutAssignButtonTitle] + ); + + const aboutInfo = useMemo(() => { + const link = ( + + {labels.layoutViewAllLinkMessage} + + ); + + return labels.layoutAboutMessage(allAssigned?.total || 0, link); + }, [getAppUrl, getArtifactPath, labels, allAssigned?.total]); + + const isGlobalLoading = useMemo( + () => isLoadingAllAssigned || isLoadingAllArtifacts || isRefetchingAllArtifacts, + [isLoadingAllAssigned, isLoadingAllArtifacts, isRefetchingAllArtifacts] + ); + + const isEmptyState = useMemo(() => allAssigned && allAssigned.total === 0, [allAssigned]); + + if (!policyItem || isGlobalLoading) { + return ; + } + + if (isEmptyState) { + return ( + <> + {canCreateArtifactsByPolicy && urlParams.show === 'list' && ( + + )} + {allArtifacts && allArtifacts.total !== 0 ? ( + + ) : ( + + )} + + ); + } + + return ( +
+ + + +

{labels.layoutTitle}

+
+ + + + +

{aboutInfo}

+
+
+ + {canCreateArtifactsByPolicy && externalPrivileges && assignToPolicyButton} + +
+ {canCreateArtifactsByPolicy && externalPrivileges && urlParams.show === 'list' && ( + + )} + {exceptionItemToDelete && ( + + )} + + + + +
+ ); + } +); + +PolicyArtifactsLayout.displayName = 'PolicyArtifactsLayout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/translations.ts new file mode 100644 index 0000000000000..82ffb8b16ca7a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/translations.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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'; + +export const POLICY_ARTIFACT_LAYOUT_LABELS = Object.freeze({ + layoutTitle: i18n.translate('xpack.securitySolution.endpoint.policy.artifacts.layout.title', { + defaultMessage: 'Assigned artifacts', + }), + layoutAssignButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.assignToPolicy', + { + defaultMessage: 'Assign artifact to policy', + } + ), + layoutViewAllLinkMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.layout.about.viewAllLinkLabel', + { + defaultMessage: 'view all artifacts', + } + ), + layoutAboutMessage: (count: number, _: React.ReactElement): React.ReactNode => { + return i18n.translate('xpack.securitySolution.endpoint.policy.artifacts.layout.about', { + defaultMessage: + 'There {count, plural, one {is} other {are}} {count} {count, plural, =1 {artifact} other {artifacts}} associated with this policy. Click here to view all artifacts', + values: { count }, + }); + }, +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/index.ts similarity index 67% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/index.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/index.ts index 4dd64e5c2f938..260b580de7497 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { PolicyEventFiltersDeleteModal } from './policy_event_filters_delete_modal'; +export { PolicyArtifactsList } from './policy_artifacts_list'; +export { POLICY_ARTIFACT_LIST_LABELS } from './translations'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx similarity index 64% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.test.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx index e8425a57b4012..9ce3d56dac472 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx @@ -15,12 +15,14 @@ import { } from '../../../../../../common/mock/endpoint'; import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; import { PolicyData } from '../../../../../../../common/endpoint/types'; -import { getPolicyEventFiltersPath } from '../../../../../common/routing'; +import { getEventFiltersListPath, getPolicyEventFiltersPath } from '../../../../../common/routing'; import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; -import { PolicyEventFiltersList } from './policy_event_filters_list'; +import { PolicyArtifactsList } from './policy_artifacts_list'; import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; +import { POLICY_ARTIFACT_LIST_LABELS } from './translations'; +import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; const endpointGenerator = new EndpointDocGenerator('seed'); const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ @@ -36,22 +38,37 @@ const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ }, }); -describe('Policy details event filters list', () => { - let render: () => Promise>; +describe('Policy details artifacts list', () => { + let render: (externalPrivileges?: boolean) => Promise>; let renderResult: ReturnType; let history: AppContextTestRender['history']; let mockedContext: AppContextTestRender; let mockedApi: ReturnType; let policy: PolicyData; - + let handleOnDeleteActionCallbackMock: jest.Mock; beforeEach(() => { policy = endpointGenerator.generatePolicyPackagePolicy(); mockedContext = createAppRootMockRenderer(); mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); ({ history } = mockedContext); - render = async () => { + handleOnDeleteActionCallbackMock = jest.fn(); + getEndpointPrivilegesInitialStateMock({ + canCreateArtifactsByPolicy: true, + }); + render = async (externalPrivileges = true) => { await act(async () => { - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); await waitFor(mockedApi.responseProvider.eventFiltersList); }); return renderResult; @@ -65,8 +82,8 @@ describe('Policy details event filters list', () => { getFoundExceptionListItemSchemaMock(0) ); await render(); - expect(renderResult.getByTestId('policyDetailsEventFiltersSearchCount')).toHaveTextContent( - 'Showing 0 event filters' + expect(renderResult.getByTestId('policyDetailsArtifactsSearchCount')).toHaveTextContent( + 'Showing 0 artifacts' ); expect(renderResult.getByTestId('searchField')).toBeTruthy(); }); @@ -76,22 +93,22 @@ describe('Policy details event filters list', () => { getFoundExceptionListItemSchemaMock(3) ); await render(); - expect(renderResult.getAllByTestId('eventFilters-collapsed-list-card')).toHaveLength(3); + expect(renderResult.getAllByTestId('artifacts-collapsed-list-card')).toHaveLength(3); expect( - renderResult.queryAllByTestId('eventFilters-collapsed-list-card-criteriaConditions') + renderResult.queryAllByTestId('artifacts-collapsed-list-card-criteriaConditions') ).toHaveLength(0); }); it('should expand an item when expand is clicked', async () => { await render(); - expect(renderResult.getAllByTestId('eventFilters-collapsed-list-card')).toHaveLength(1); + expect(renderResult.getAllByTestId('artifacts-collapsed-list-card')).toHaveLength(1); userEvent.click( - renderResult.getByTestId('eventFilters-collapsed-list-card-header-expandCollapse') + renderResult.getByTestId('artifacts-collapsed-list-card-header-expandCollapse') ); expect( - renderResult.queryAllByTestId('eventFilters-collapsed-list-card-criteriaConditions') + renderResult.queryAllByTestId('artifacts-collapsed-list-card-criteriaConditions') ).toHaveLength(1); }); @@ -130,7 +147,7 @@ describe('Policy details event filters list', () => { await render(); // click the actions button userEvent.click( - renderResult.getByTestId('eventFilters-collapsed-list-card-header-actions-button') + renderResult.getByTestId('artifacts-collapsed-list-card-header-actions-button') ); expect(renderResult.queryByTestId('view-full-details-action')).toBeTruthy(); }); @@ -144,9 +161,24 @@ describe('Policy details event filters list', () => { ); await render(); userEvent.click( - renderResult.getByTestId('eventFilters-collapsed-list-card-header-actions-button') + renderResult.getByTestId('artifacts-collapsed-list-card-header-actions-button') ); expect(renderResult.queryByTestId('remove-from-policy-action')).toBeNull(); }); + + describe('without external privileges', () => { + it('should not display the delete action, do show the full details', async () => { + mockedApi.responseProvider.eventFiltersList.mockReturnValue( + getFoundExceptionListItemSchemaMock(1) + ); + await render(false); + // click the actions button + userEvent.click( + await renderResult.findByTestId('artifacts-collapsed-list-card-header-actions-button') + ); + expect(renderResult.queryByTestId('remove-from-policy-action')).toBeFalsy(); + expect(renderResult.queryByTestId('view-full-details-action')).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx new file mode 100644 index 0000000000000..4c87ba883f39a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx @@ -0,0 +1,207 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import { EuiSpacer, EuiText, Pagination } from '@elastic/eui'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useAppUrl } from '../../../../../../common/lib/kibana'; +import { APP_UI_ID } from '../../../../../../../common/constants'; +import { SearchExceptions } from '../../../../../components/search_exceptions'; +import { useEndpointPoliciesToArtifactPolicies } from '../../../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; +import { useUrlParams } from '../../../../../components/hooks/use_url_params'; +import { useUrlPagination } from '../../../../../components/hooks/use_url_pagination'; +import { useGetEndpointSpecificPolicies } from '../../../../../services/policies/hooks'; +import { + ArtifactCardGrid, + ArtifactCardGridProps, +} from '../../../../../components/artifact_card_grid'; +import { usePolicyDetailsArtifactsNavigateCallback } from '../../policy_hooks'; +import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; +import { isGlobalPolicyEffected } from '../../../../../components/effected_policy_select/utils'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { useGetLinkTo } from '../empty/use_policy_artifacts_empty_hooks'; +import { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client'; +import { useListArtifact } from '../../../../../hooks/artifacts'; +import { POLICY_ARTIFACT_LIST_LABELS } from './translations'; +import { EventFiltersPageLocation } from '../../../../event_filters/types'; +import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; +import { HostIsolationExceptionsPageLocation } from '../../../../host_isolation_exceptions/types'; + +interface PolicyArtifactsListProps { + policy: ImmutableObject; + apiClient: ExceptionsListApiClient; + searchableFields: string[]; + getArtifactPath: ( + location?: + | Partial + | Partial + | Partial + ) => string; + getPolicyArtifactsPath: (policyId: string) => string; + labels: typeof POLICY_ARTIFACT_LIST_LABELS; + onDeleteActionCallback: (item: ExceptionListItemSchema) => void; + externalPrivileges?: boolean; +} + +export const PolicyArtifactsList = React.memo( + ({ + policy, + apiClient, + searchableFields, + getArtifactPath, + getPolicyArtifactsPath, + labels, + onDeleteActionCallback, + externalPrivileges = true, + }) => { + const { getAppUrl } = useAppUrl(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; + const policiesRequest = useGetEndpointSpecificPolicies({ perPage: 1000 }); + const navigateCallback = usePolicyDetailsArtifactsNavigateCallback(apiClient.listId); + const { urlParams } = useUrlParams(); + const [expandedItemsMap, setExpandedItemsMap] = useState>(new Map()); + + const { state } = useGetLinkTo(policy.id, policy.name, getPolicyArtifactsPath, getArtifactPath); + + const { pageSizeOptions, pagination: urlPagination, setPagination } = useUrlPagination(); + + const { + data: artifacts, + isLoading: isLoadingArtifacts, + isRefetching: isRefetchingArtifacts, + } = useListArtifact( + apiClient, + { + page: urlPagination.page, + perPage: urlPagination.pageSize, + filter: urlParams.filter as string, + policies: [policy.id, 'all'], + }, + searchableFields + ); + + const pagination: Pagination = useMemo( + () => ({ + pageSize: urlPagination.pageSize, + pageIndex: urlPagination.page - 1, + pageSizeOptions, + totalItemCount: artifacts?.total || 0, + }), + [artifacts?.total, pageSizeOptions, urlPagination.page, urlPagination.pageSize] + ); + + const handleOnSearch = useCallback( + (filter) => { + navigateCallback({ filter }); + }, + [navigateCallback] + ); + + const handleOnExpandCollapse = useCallback( + ({ expanded, collapsed }) => { + const newExpandedMap = new Map(expandedItemsMap); + for (const item of expanded) { + newExpandedMap.set(item.id, true); + } + for (const item of collapsed) { + newExpandedMap.set(item.id, false); + } + setExpandedItemsMap(newExpandedMap); + }, + [expandedItemsMap] + ); + const handleOnPageChange = useCallback( + ({ pageIndex, pageSize }) => { + if (artifacts?.total) setPagination({ page: pageIndex + 1, pageSize }); + }, + [artifacts?.total, setPagination] + ); + + const totalItemsCountLabel = useMemo(() => { + return labels.listTotalItemCountMessage(artifacts?.data.length || 0); + }, [artifacts?.data.length, labels]); + + const artifactCardPolicies = useEndpointPoliciesToArtifactPolicies(policiesRequest.data?.items); + const provideCardProps = useCallback( + (artifact) => { + const viewUrlPath = getArtifactPath({ + filter: (artifact as ExceptionListItemSchema).item_id, + }); + const fullDetailsAction = { + icon: 'controlsHorizontal', + children: labels.listFullDetailsActionTitle, + href: getAppUrl({ appId: APP_UI_ID, path: viewUrlPath }), + navigateAppId: APP_UI_ID, + navigateOptions: { path: viewUrlPath, state }, + 'data-test-subj': 'view-full-details-action', + }; + const item = artifact as ExceptionListItemSchema; + + const isGlobal = isGlobalPolicyEffected(item.tags); + const deleteAction = { + icon: 'trash', + children: labels.listRemoveActionTitle, + onClick: () => { + onDeleteActionCallback(item); + }, + disabled: isGlobal, + toolTipContent: isGlobal ? labels.listRemoveActionNotAllowedMessage : undefined, + toolTipPosition: 'top' as const, + 'data-test-subj': 'remove-from-policy-action', + }; + return { + expanded: expandedItemsMap.get(item.id) || false, + actions: + canCreateArtifactsByPolicy && externalPrivileges + ? [fullDetailsAction, deleteAction] + : [fullDetailsAction], + policies: artifactCardPolicies, + }; + }, + [ + artifactCardPolicies, + canCreateArtifactsByPolicy, + expandedItemsMap, + externalPrivileges, + getAppUrl, + getArtifactPath, + labels.listFullDetailsActionTitle, + labels.listRemoveActionNotAllowedMessage, + labels.listRemoveActionTitle, + onDeleteActionCallback, + state, + ] + ); + + return ( + <> + + + + {totalItemsCountLabel} + + + + + ); + } +); + +PolicyArtifactsList.displayName = 'PolicyArtifactsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/translations.ts new file mode 100644 index 0000000000000..5d9cd3879d37f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const POLICY_ARTIFACT_LIST_LABELS = Object.freeze({ + listTotalItemCountMessage: (totalItemsCount: number): string => + i18n.translate('xpack.securitySolution.endpoint.policy.artifacts.list.totalItemCount', { + defaultMessage: 'Showing {totalItemsCount, plural, one {# artifact} other {# artifacts}}', + values: { totalItemsCount }, + }), + listFullDetailsActionTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.fullDetailsAction', + { defaultMessage: 'View full details' } + ), + listRemoveActionTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeAction', + { defaultMessage: 'Remove from policy' } + ), + listRemoveActionNotAllowedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.removeActionNotAllowed', + { + defaultMessage: 'Globally applied artifact cannot be removed from policy.', + } + ), + listSearchPlaceholderMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.artifacts.list.search.placeholder', + { + defaultMessage: `Search on the fields below: name, description, value`, + } + ), +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/translations.ts new file mode 100644 index 0000000000000..84331c6cec95c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/translations.ts @@ -0,0 +1,88 @@ +/* + * 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 { POLICY_ARTIFACT_DELETE_MODAL_LABELS } from './delete_modal'; +import { + POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS, + POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS, +} from './empty'; +import { POLICY_ARTIFACT_FLYOUT_LABELS } from './flyout'; +import { POLICY_ARTIFACT_LAYOUT_LABELS } from './layout'; +import { POLICY_ARTIFACT_LIST_LABELS } from './list'; + +export const policyArtifactsPageLabels = Object.freeze({ + // ------------------------------ + // POLICY ARTIFACT LAYOUT + // ------------------------------ + ...POLICY_ARTIFACT_LAYOUT_LABELS, + + // ------------------------------ + // POLICY ARTIFACT DELETE MODAL + // ------------------------------ + ...POLICY_ARTIFACT_DELETE_MODAL_LABELS, + + // ------------------------------ + // POLICY ARTIFACT FLYOUT + // ------------------------------ + ...POLICY_ARTIFACT_FLYOUT_LABELS, + + // ------------------------------ + // POLICY ARTIFACT EMPTY UNASSIGNED + // ------------------------------ + ...POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS, + + // ------------------------------ + // POLICY ARTIFACT EMPTY UNEXISTING + // ------------------------------ + ...POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS, + + // ------------------------------ + // POLICY ARTIFACT LIST + // ------------------------------ + ...POLICY_ARTIFACT_LIST_LABELS, +}); + +type IAllLabels = typeof policyArtifactsPageLabels; + +/** + * The set of labels that normally have the policy artifact specific name in it, thus must be set for every page + */ +export type PolicyArtifactsPageRequiredLabels = Pick< + IAllLabels, + | 'deleteModalTitle' + | 'deleteModalImpactInfo' + | 'deleteModalErrorMessage' + | 'flyoutWarningCalloutMessage' + | 'flyoutNoArtifactsToBeAssignedMessage' + | 'flyoutTitle' + | 'flyoutSubtitle' + | 'flyoutSearchPlaceholder' + | 'flyoutErrorMessage' + | 'flyoutSuccessMessageText' + | 'emptyUnassignedTitle' + | 'emptyUnassignedMessage' + | 'emptyUnassignedPrimaryActionButtonTitle' + | 'emptyUnassignedSecondaryActionButtonTitle' + | 'emptyUnexistingTitle' + | 'emptyUnexistingMessage' + | 'emptyUnexistingPrimaryActionButtonTitle' + | 'listTotalItemCountMessage' + | 'listRemoveActionNotAllowedMessage' + | 'listSearchPlaceholderMessage' + | 'layoutTitle' + | 'layoutAssignButtonTitle' + | 'layoutViewAllLinkMessage' + | 'layoutAboutMessage' +>; + +export type PolicyArtifactPageOptionalLabels = Omit< + IAllLabels, + keyof PolicyArtifactsPageRequiredLabels +>; + +export type PolicyArtifactsPageLabels = PolicyArtifactsPageRequiredLabels & + Partial; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx deleted file mode 100644 index bfa2f09ab9773..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import React, { useCallback } from 'react'; -import { useToasts } from '../../../../../../common/lib/kibana'; -import { ServerApiError } from '../../../../../../common/types'; -import { useBulkUpdateEventFilters } from '../hooks'; - -export const PolicyEventFiltersDeleteModal = ({ - policyId, - policyName, - exception, - onCancel, -}: { - policyId: string; - policyName: string; - exception: ExceptionListItemSchema; - onCancel: () => void; -}) => { - const toasts = useToasts(); - - const { mutate: updateEventFilter, isLoading: isUpdateEventFilterLoading } = - useBulkUpdateEventFilters({ - onUpdateSuccess: () => { - toasts.addSuccess({ - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.successToastTitle', - { defaultMessage: 'Successfully removed' } - ), - text: i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.successToastText', - { - defaultMessage: '"{eventFilterName}" has been removed from {policyName} policy', - values: { eventFilterName: exception.name, policyName }, - } - ), - }); - onCancel(); - }, - onUpdateError: (error?: ServerApiError) => { - toasts.addError(error as unknown as Error, { - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.errorToastTitle', - { - defaultMessage: 'Error while attempt to remove event filter', - } - ), - }); - onCancel(); - }, - onSettledCallback: onCancel, - }); - - const handleModalConfirm = useCallback(() => { - const modifiedException = { - ...exception, - tags: exception.tags.filter((tag) => tag !== `policy:${policyId}`), - }; - updateEventFilter([modifiedException]); - }, [exception, policyId, updateEventFilter]); - - const handleCancel = useCallback(() => { - if (!isUpdateEventFilterLoading) { - onCancel(); - } - }, [isUpdateEventFilterLoading, onCancel]); - - return ( - - -

- -

-
- - - - -

- -

-
-
- ); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/index.ts deleted file mode 100644 index a9c930ed0b935..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { PolicyEventFiltersEmptyUnassigned } from './policy_event_filters_empty_unassigned'; -export { PolicyEventFiltersEmptyUnexisting } from './policy_event_filters_empty_unexisting'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx deleted file mode 100644 index ac944371acdda..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback } from 'react'; -import { EuiButton, EuiEmptyPrompt, EuiPageTemplate, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { usePolicyDetailsEventFiltersNavigateCallback } from '../../policy_hooks'; -import { useGetLinkTo } from './use_policy_event_filters_empty_hooks'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; - -interface CommonProps { - policyId: string; - policyName: string; -} - -export const PolicyEventFiltersEmptyUnassigned = memo(({ policyId, policyName }) => { - const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); - - const navigateCallback = usePolicyDetailsEventFiltersNavigateCallback(); - const onClickPrimaryButtonHandler = useCallback( - () => - navigateCallback({ - show: 'list', - }), - [navigateCallback] - ); - return ( - - - - - } - body={ - - } - actions={[ - ...(canCreateArtifactsByPolicy - ? [ - - - , - ] - : []), - // eslint-disable-next-line @elastic/eui/href-or-on-click - - - , - ]} - /> - - ); -}); - -PolicyEventFiltersEmptyUnassigned.displayName = 'PolicyEventFiltersEmptyUnassigned'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx deleted file mode 100644 index 7976fc8a566da..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiEmptyPrompt, EuiButton, EuiPageTemplate } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useGetLinkTo } from './use_policy_event_filters_empty_hooks'; - -interface CommonProps { - policyId: string; - policyName: string; -} - -export const PolicyEventFiltersEmptyUnexisting = memo(({ policyId, policyName }) => { - const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName, { show: 'create' }); - return ( - - - - - } - body={ - - } - actions={ - // eslint-disable-next-line @elastic/eui/href-or-on-click - - - - } - /> - - ); -}); - -PolicyEventFiltersEmptyUnexisting.displayName = 'PolicyEventFiltersEmptyUnexisting'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/use_policy_event_filters_empty_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/use_policy_event_filters_empty_hooks.ts deleted file mode 100644 index 0a7aa8fbabf6d..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/use_policy_event_filters_empty_hooks.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { useAppUrl } from '../../../../../../common/lib/kibana/hooks'; -import { getPolicyEventFiltersPath, getEventFiltersListPath } from '../../../../../common/routing'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { EventFiltersPageLocation } from '../../../../event_filters/types'; - -export const useGetLinkTo = ( - policyId: string, - policyName: string, - location?: Partial -) => { - const { getAppUrl } = useAppUrl(); - const { toRoutePath, toRouteUrl } = useMemo(() => { - const path = getEventFiltersListPath(location); - return { - toRoutePath: path, - toRouteUrl: getAppUrl({ path }), - }; - }, [getAppUrl, location]); - - const policyEventFiltersPath = useMemo(() => getPolicyEventFiltersPath(policyId), [policyId]); - const policyEventFilterRouteState = useMemo(() => { - return { - backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.backButtonLabel', - { - defaultMessage: 'Back to {policyName} policy', - values: { - policyName, - }, - } - ), - onBackButtonNavigateTo: [ - APP_UI_ID, - { - path: policyEventFiltersPath, - }, - ], - backButtonUrl: getAppUrl({ - appId: APP_UI_ID, - path: policyEventFiltersPath, - }), - }; - }, [getAppUrl, policyName, policyEventFiltersPath]); - - const onClickHandler = useNavigateToAppEventHandler(APP_UI_ID, { - state: policyEventFilterRouteState, - path: toRoutePath, - }); - - return { - onClickHandler, - toRouteUrl, - state: policyEventFilterRouteState, - }; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx deleted file mode 100644 index 5c97b914bb1ca..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useMemo, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { isEmpty, without } from 'lodash/fp'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { - EuiTitle, - EuiFlyout, - EuiSpacer, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, - EuiCallOut, - EuiEmptyPrompt, -} from '@elastic/eui'; -import { SearchExceptions } from '../../../../../components/search_exceptions'; -import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; -import { useToasts } from '../../../../../../common/lib/kibana'; -import { useSearchNotAssignedEventFilters, useBulkUpdateEventFilters } from '../hooks'; -import { PolicyArtifactsAssignableList } from '../../artifacts/assignable'; - -interface PolicyEventFiltersFlyoutProps { - policyItem: ImmutableObject; - onClose: () => void; -} - -const MAX_ALLOWED_RESULTS = 100; - -export const PolicyEventFiltersFlyout = React.memo( - ({ policyItem, onClose }) => { - const toasts = useToasts(); - const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); - const [currentFilter, setCurrentFilter] = useState(''); - - const bulkUpdateMutation = useBulkUpdateEventFilters({ - onUpdateSuccess: (updatedExceptions: ExceptionListItemSchema[]) => { - toasts.addSuccess({ - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.title', - { - defaultMessage: 'Success', - } - ), - text: - updatedExceptions.length > 1 - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textMultiples', - { - defaultMessage: '{count} event filters have been added to your list.', - values: { count: updatedExceptions.length }, - } - ) - : i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textSingle', - { - defaultMessage: '"{name}" has been added to your event filters list.', - values: { name: updatedExceptions[0].name }, - } - ), - }); - }, - onUpdateError: () => { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastError.text', - { - defaultMessage: `An error occurred updating artifacts`, - } - ) - ); - }, - onSettledCallback: onClose, - }); - - const { - data: eventFilters, - isLoading: isLoadingEventFilters, - isRefetching: isRefetchingEventFilters, - } = useSearchNotAssignedEventFilters(policyItem.id, { - perPage: MAX_ALLOWED_RESULTS, - filter: currentFilter, - }); - - const { data: allNotAssigned, isLoading: isLoadingAllNotAssigned } = - useSearchNotAssignedEventFilters(policyItem.id, { - enabled: currentFilter !== '' && eventFilters?.total === 0, - }); - - const handleOnSearch = useCallback((query) => { - setSelectedArtifactIds([]); - setCurrentFilter(query); - }, []); - - const handleOnConfirmAction = useCallback(() => { - if (!eventFilters) { - return; - } - const eventFiltersToUpdate: ExceptionListItemSchema[] = []; - selectedArtifactIds.forEach((selectedId) => { - const eventFilter = eventFilters.data.find((current) => current.id === selectedId); - if (eventFilter) { - eventFilter.tags = [...eventFilter.tags, `policy:${policyItem.id}`]; - eventFiltersToUpdate.push(eventFilter); - } - }); - bulkUpdateMutation.mutate(eventFiltersToUpdate); - }, [bulkUpdateMutation, eventFilters, policyItem.id, selectedArtifactIds]); - - const handleSelectArtifacts = (artifactId: string, selected: boolean) => { - setSelectedArtifactIds((currentSelectedArtifactIds) => - selected - ? [...currentSelectedArtifactIds, artifactId] - : without([artifactId], currentSelectedArtifactIds) - ); - }; - - const searchWarningMessage = useMemo( - () => ( - <> - - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.searchWarning.text', - { - defaultMessage: - 'Only the first 100 event filters are displayed. Please use the search bar to refine the results.', - } - )} - - - - ), - [] - ); - - const noItemsMessage = useMemo(() => { - if (isLoadingEventFilters || isRefetchingEventFilters || isLoadingAllNotAssigned) { - return null; - } - - // there are no event filters assignable to this policy - if (allNotAssigned?.total === 0 || (eventFilters?.total === 0 && currentFilter === '')) { - return ( - - } - /> - ); - } - - // there are no results for the current search - if (eventFilters?.total === 0) { - return ( - - } - /> - ); - } - }, [ - allNotAssigned?.total, - currentFilter, - eventFilters?.total, - isLoadingAllNotAssigned, - isLoadingEventFilters, - isRefetchingEventFilters, - ]); - - return ( - - - -

- -

-
- - -
- - {(eventFilters?.total || 0) > MAX_ALLOWED_RESULTS ? searchWarningMessage : null} - - - - - - {noItemsMessage} - - - - - - - - - - - - - - - -
- ); - } -); - -PolicyEventFiltersFlyout.displayName = 'PolicyEventFiltersFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts deleted file mode 100644 index 24e3cb464d4d3..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import pMap from 'p-map'; -import { - ExceptionListItemSchema, - FoundExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; -import { QueryObserverResult, useMutation, useQuery, useQueryClient } from 'react-query'; -import { ServerApiError } from '../../../../../common/types'; -import { useHttp } from '../../../../../common/lib/kibana'; -import { getList, updateOne } from '../../../event_filters/service/service_actions'; -import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../common/utils'; -import { SEARCHABLE_FIELDS } from '../../../event_filters/constants'; - -export function useGetAllAssignedEventFilters( - policyId: string, - enabled: boolean = true -): QueryObserverResult { - const http = useHttp(); - return useQuery( - ['eventFilters', 'assigned', policyId], - () => { - return getList({ - http, - filter: parsePoliciesAndFilterToKql({ policies: [...(policyId ? [policyId] : []), 'all'] }), - }); - }, - { - refetchIntervalInBackground: false, - refetchOnWindowFocus: false, - refetchOnMount: true, - enabled, - keepPreviousData: true, - } - ); -} - -export function useSearchAssignedEventFilters( - policyId: string, - options: { filter?: string; page?: number; perPage?: number } -): QueryObserverResult { - const http = useHttp(); - const { filter, page, perPage } = options; - - return useQuery( - ['eventFilters', 'assigned', 'search', policyId, options], - () => { - return getList({ - http, - filter: parsePoliciesAndFilterToKql({ - policies: [policyId, 'all'], - kuery: parseQueryFilterToKQL(filter || '', SEARCHABLE_FIELDS), - }), - perPage, - page: (page ?? 0) + 1, - }); - }, - { - refetchIntervalInBackground: false, - refetchOnWindowFocus: false, - refetchOnMount: true, - keepPreviousData: true, - } - ); -} -export function useSearchNotAssignedEventFilters( - policyId: string, - options: { filter?: string; perPage?: number; enabled?: boolean } -): QueryObserverResult { - const http = useHttp(); - return useQuery( - ['eventFilters', 'notAssigned', policyId, options], - () => { - const { filter, perPage } = options; - - return getList({ - http, - filter: parsePoliciesAndFilterToKql({ - excludedPolicies: [policyId, 'all'], - kuery: parseQueryFilterToKQL(filter || '', SEARCHABLE_FIELDS), - }), - perPage, - }); - }, - { - refetchIntervalInBackground: false, - refetchOnWindowFocus: false, - refetchOnMount: true, - keepPreviousData: true, - enabled: options.enabled ?? true, - } - ); -} - -export function useBulkUpdateEventFilters( - callbacks: { - onUpdateSuccess?: (updatedExceptions: ExceptionListItemSchema[]) => void; - onUpdateError?: (error?: ServerApiError) => void; - onSettledCallback?: () => void; - } = {} -) { - const http = useHttp(); - const queryClient = useQueryClient(); - - const { - onUpdateSuccess = () => {}, - onUpdateError = () => {}, - onSettledCallback = () => {}, - } = callbacks; - - return useMutation< - ExceptionListItemSchema[], - ServerApiError, - ExceptionListItemSchema[], - () => void - >( - (eventFilters: ExceptionListItemSchema[]) => { - return pMap( - eventFilters, - (eventFilter) => { - return updateOne(http, eventFilter); - }, - { - concurrency: 5, - } - ); - }, - { - onSuccess: onUpdateSuccess, - onError: onUpdateError, - onSettled: () => { - queryClient.invalidateQueries(['eventFilters', 'notAssigned']); - queryClient.invalidateQueries(['eventFilters', 'assigned']); - onSettledCallback(); - }, - } - ); -} - -export function useGetAllEventFilters(): QueryObserverResult< - FoundExceptionListItemSchema, - ServerApiError -> { - const http = useHttp(); - return useQuery( - ['eventFilters', 'all'], - () => { - return getList({ http }); - }, - { - refetchIntervalInBackground: false, - refetchOnWindowFocus: false, - refetchOnMount: true, - keepPreviousData: true, - } - ); -} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.test.tsx deleted file mode 100644 index fe0668e63774f..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.test.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { PolicyEventFiltersLayout } from './policy_event_filters_layout'; -import * as reactTestingLibrary from '@testing-library/react'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; -import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; - -import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; -import { parsePoliciesAndFilterToKql } from '../../../../../common/utils'; -import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; -import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; - -let render: () => ReturnType; -let mockedContext: AppContextTestRender; -let policyItem: ImmutableObject; -const generator = new EndpointDocGenerator(); -let mockedApi: ReturnType; - -describe('Policy event filters layout', () => { - beforeEach(() => { - mockedContext = createAppRootMockRenderer(); - mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); - policyItem = generator.generatePolicyPackagePolicy(); - render = () => mockedContext.render(); - }); - - afterEach(() => reactTestingLibrary.cleanup()); - - it('should render layout with a loader', async () => { - const component = render(); - expect(component.getByTestId('policy-event-filters-loading-spinner')).toBeTruthy(); - }); - - it('should render layout with no assigned event filters data when there are not event filters', async () => { - mockedApi.responseProvider.eventFiltersList.mockReturnValue( - getFoundExceptionListItemSchemaMock(0) - ); - - const component = render(); - expect(await component.findByTestId('policy-event-filters-empty-unexisting')).not.toBeNull(); - }); - - it('should render layout with no assigned event filters data when there are event filters', async () => { - mockedApi.responseProvider.eventFiltersList.mockImplementation( - // @ts-expect-error - (args) => { - const params = args.query; - if ( - params && - params.filter === parsePoliciesAndFilterToKql({ policies: [policyItem.id, 'all'] }) - ) { - return getFoundExceptionListItemSchemaMock(0); - } else { - return getFoundExceptionListItemSchemaMock(1); - } - } - ); - - const component = render(); - - expect(await component.findByTestId('policy-event-filters-empty-unassigned')).not.toBeNull(); - }); - - it('should render layout with data', async () => { - mockedApi.responseProvider.eventFiltersList.mockReturnValue( - getFoundExceptionListItemSchemaMock(3) - ); - const component = render(); - expect(await component.findByTestId('policy-event-filters-header-section')).not.toBeNull(); - expect(await component.findByTestId('policy-event-filters-layout-about')).not.toBeNull(); - expect((await component.findByTestId('policy-event-filters-layout-about')).textContent).toMatch( - '3 event filters' - ); - }); - - it('should hide `Assign event filters to policy` on empty state with unassigned policies when downgraded to a gold or below license', () => { - getEndpointPrivilegesInitialStateMock({ - canCreateArtifactsByPolicy: false, - }); - mockedApi.responseProvider.eventFiltersList.mockReturnValue( - getFoundExceptionListItemSchemaMock(0) - ); - - const component = render(); - mockedContext.history.push(getPolicyDetailsArtifactsListPath(policyItem.id)); - expect(component.queryByTestId('assign-event-filter-button')).toBeNull(); - }); - - it('should hide the `Assign event filters to policy` button license is downgraded to gold or below', () => { - getEndpointPrivilegesInitialStateMock({ - canCreateArtifactsByPolicy: false, - }); - mockedApi.responseProvider.eventFiltersList.mockReturnValue( - getFoundExceptionListItemSchemaMock(5) - ); - - const component = render(); - mockedContext.history.push(getPolicyDetailsArtifactsListPath(policyItem.id)); - - expect(component.queryByTestId('eventFilters-assign-button')).toBeNull(); - }); - - it('should hide the `Assign event filters` flyout when license is downgraded to gold or below', () => { - getEndpointPrivilegesInitialStateMock({ - canCreateArtifactsByPolicy: false, - }); - mockedApi.responseProvider.eventFiltersList.mockReturnValue( - getFoundExceptionListItemSchemaMock(2) - ); - - const component = render(); - mockedContext.history.push( - `${getPolicyDetailsArtifactsListPath(policyItem.id)}/eventFilters?show=list` - ); - - expect(component.queryByTestId('eventFilters-assign-flyout')).toBeNull(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx deleted file mode 100644 index 850a303654c52..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiTitle, - EuiPageHeader, - EuiPageHeaderSection, - EuiText, - EuiSpacer, - EuiLink, - EuiButton, - EuiPageContent, -} from '@elastic/eui'; -import { useAppUrl } from '../../../../../../common/lib/kibana'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; -import { getEventFiltersListPath } from '../../../../../common/routing'; -import { useGetAllAssignedEventFilters, useGetAllEventFilters } from '../hooks'; -import { ManagementPageLoader } from '../../../../../components/management_page_loader'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { PolicyEventFiltersEmptyUnassigned, PolicyEventFiltersEmptyUnexisting } from '../empty'; -import { - usePolicyDetailsSelector, - usePolicyDetailsEventFiltersNavigateCallback, -} from '../../policy_hooks'; -import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors'; -import { PolicyEventFiltersFlyout } from '../flyout'; -import { PolicyEventFiltersList } from '../list'; - -interface PolicyEventFiltersLayoutProps { - policyItem?: ImmutableObject | undefined; -} -export const PolicyEventFiltersLayout = React.memo( - ({ policyItem }) => { - const { getAppUrl } = useAppUrl(); - const navigateCallback = usePolicyDetailsEventFiltersNavigateCallback(); - const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); - - const { data: allAssigned, isLoading: isLoadingAllAssigned } = useGetAllAssignedEventFilters( - policyItem?.id || '', - !!policyItem?.id - ); - - const { data: allEventFilters, isLoading: isLoadingAllEventFilters } = useGetAllEventFilters(); - - const handleOnClickAssignButton = useCallback(() => { - navigateCallback({ show: 'list' }); - }, [navigateCallback]); - const handleOnCloseFlyout = useCallback(() => { - navigateCallback({ show: undefined }); - }, [navigateCallback]); - - const assignToPolicyButton = useMemo( - () => ( - - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.layout.assignToPolicy', - { - defaultMessage: 'Assign event filters to policy', - } - )} - - ), - [handleOnClickAssignButton] - ); - - const aboutInfo = useMemo(() => { - const link = ( - - - - ); - - return ( - - ); - }, [getAppUrl, allAssigned]); - - const isGlobalLoading = useMemo( - () => isLoadingAllAssigned || isLoadingAllEventFilters, - [isLoadingAllAssigned, isLoadingAllEventFilters] - ); - - const isEmptyState = useMemo(() => allAssigned && allAssigned.total === 0, [allAssigned]); - - if (!policyItem || isGlobalLoading) { - return ; - } - - if (isEmptyState) { - return ( - <> - {canCreateArtifactsByPolicy && urlParams.show === 'list' && ( - - )} - {allEventFilters && allEventFilters.total !== 0 ? ( - - ) : ( - - )} - - ); - } - - return ( -
- - - -

- {i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.layout.title', - { - defaultMessage: 'Assigned event filters', - } - )} -

-
- - - - -

{aboutInfo}

-
-
- - {canCreateArtifactsByPolicy && assignToPolicyButton} - -
- {canCreateArtifactsByPolicy && urlParams.show === 'list' && ( - - )} - - - - -
- ); - } -); - -PolicyEventFiltersLayout.displayName = 'PolicyEventFiltersLayout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx deleted file mode 100644 index 2c6d160d174e7..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useMemo, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, Pagination } from '@elastic/eui'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { useAppUrl } from '../../../../../../common/lib/kibana'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { useSearchAssignedEventFilters } from '../hooks'; -import { SearchExceptions } from '../../../../../components/search_exceptions'; -import { useEndpointPoliciesToArtifactPolicies } from '../../../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; -import { - MANAGEMENT_PAGE_SIZE_OPTIONS, - MANAGEMENT_DEFAULT_PAGE_SIZE, -} from '../../../../../common/constants'; -import { useGetEndpointSpecificPolicies } from '../../../../../services/policies/hooks'; -import { - ArtifactCardGrid, - ArtifactCardGridProps, -} from '../../../../../components/artifact_card_grid'; -import { - usePolicyDetailsEventFiltersNavigateCallback, - usePolicyDetailsSelector, -} from '../../policy_hooks'; -import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors'; -import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; -import { PolicyEventFiltersDeleteModal } from '../delete_modal'; -import { isGlobalPolicyEffected } from '../../../../../components/effected_policy_select/utils'; -import { getEventFiltersListPath } from '../../../../../common/routing'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { useGetLinkTo } from '../empty/use_policy_event_filters_empty_hooks'; - -interface PolicyEventFiltersListProps { - policy: ImmutableObject; -} -export const PolicyEventFiltersList = React.memo(({ policy }) => { - const { getAppUrl } = useAppUrl(); - const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const policiesRequest = useGetEndpointSpecificPolicies({ perPage: 1000 }); - const navigateCallback = usePolicyDetailsEventFiltersNavigateCallback(); - const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); - const [expandedItemsMap, setExpandedItemsMap] = useState>(new Map()); - const [exceptionItemToDelete, setExceptionItemToDelete] = useState< - ExceptionListItemSchema | undefined - >(); - const { state } = useGetLinkTo(policy.id, policy.name); - - const { - data: eventFilters, - isLoading: isLoadingEventFilters, - isRefetching: isRefetchingEventFilters, - } = useSearchAssignedEventFilters(policy.id, { - page: urlParams.page_index || 0, - perPage: urlParams.page_size || MANAGEMENT_DEFAULT_PAGE_SIZE, - filter: urlParams.filter, - }); - - const pagination: Pagination = { - pageSize: urlParams.page_size || MANAGEMENT_DEFAULT_PAGE_SIZE, - pageIndex: urlParams.page_index || 0, - pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], - totalItemCount: eventFilters?.total || 0, - }; - - const handleOnSearch = useCallback( - (filter) => { - navigateCallback({ filter }); - }, - [navigateCallback] - ); - - const handleOnExpandCollapse: ArtifactCardGridProps['onExpandCollapse'] = ({ - expanded, - collapsed, - }) => { - const newExpandedMap = new Map(expandedItemsMap); - for (const item of expanded) { - newExpandedMap.set(item.id, true); - } - for (const item of collapsed) { - newExpandedMap.set(item.id, false); - } - setExpandedItemsMap(newExpandedMap); - }; - const handleOnPageChange = useCallback( - ({ pageIndex, pageSize }) => { - if (eventFilters?.total) navigateCallback({ page_index: pageIndex, page_size: pageSize }); - }, - [eventFilters?.total, navigateCallback] - ); - - const totalItemsCountLabel = useMemo(() => { - return i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.list.totalItemCount', - { - defaultMessage: - 'Showing {totalItemsCount, plural, one {# event filter} other {# event filters}}', - values: { totalItemsCount: eventFilters?.data.length || 0 }, - } - ); - }, [eventFilters?.data.length]); - - const artifactCardPolicies = useEndpointPoliciesToArtifactPolicies(policiesRequest.data?.items); - const provideCardProps: ArtifactCardGridProps['cardComponentProps'] = (artifact) => { - const viewUrlPath = getEventFiltersListPath({ - filter: (artifact as ExceptionListItemSchema).item_id, - }); - const fullDetailsAction = { - icon: 'controlsHorizontal', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.list.fullDetailsAction', - { defaultMessage: 'View full details' } - ), - href: getAppUrl({ appId: APP_UI_ID, path: viewUrlPath }), - navigateAppId: APP_UI_ID, - navigateOptions: { path: viewUrlPath, state }, - 'data-test-subj': 'view-full-details-action', - }; - const item = artifact as ExceptionListItemSchema; - - const isGlobal = isGlobalPolicyEffected(item.tags); - const deleteAction = { - icon: 'trash', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeAction', - { defaultMessage: 'Remove from policy' } - ), - onClick: () => { - setExceptionItemToDelete(item); - }, - disabled: isGlobal, - toolTipContent: isGlobal - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeActionNotAllowed', - { - defaultMessage: 'Globally applied event filters cannot be removed from policy.', - } - ) - : undefined, - toolTipPosition: 'top' as const, - 'data-test-subj': 'remove-from-policy-action', - }; - return { - expanded: expandedItemsMap.get(item.id) || false, - actions: canCreateArtifactsByPolicy ? [fullDetailsAction, deleteAction] : [fullDetailsAction], - policies: artifactCardPolicies, - }; - }; - - const handleDeleteModalClose = useCallback(() => { - setExceptionItemToDelete(undefined); - }, [setExceptionItemToDelete]); - - return ( - <> - {exceptionItemToDelete && ( - - )} - - - - {totalItemsCountLabel} - - - - - ); -}); - -PolicyEventFiltersList.displayName = 'PolicyEventFiltersList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.test.tsx deleted file mode 100644 index f295509b101b8..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.test.tsx +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import uuid from 'uuid'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { getExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; -import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; -import { PolicyData } from '../../../../../../../common/endpoint/types'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing'; -import { - getHostIsolationExceptionItems, - updateOneHostIsolationExceptionItem, -} from '../../../../host_isolation_exceptions/service'; -import { PolicyHostIsolationExceptionsAssignFlyout } from './assign_flyout'; - -jest.mock('../../../../host_isolation_exceptions/service'); -jest.mock('../../../../../../common/components/user_privileges'); - -const useUserPrivilegesMock = useUserPrivileges as jest.Mock; -const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; -const updateOneHostIsolationExceptionItemMock = updateOneHostIsolationExceptionItem as jest.Mock; -const endpointGenerator = new EndpointDocGenerator('seed'); -const emptyList = { - data: [], - page: 1, - per_page: 10, - total: 0, -}; - -describe('Policy details host isolation exceptions assign flyout', () => { - let policyId: string; - let policy: PolicyData; - let render: () => ReturnType; - let renderResult: ReturnType; - let history: AppContextTestRender['history']; - let mockedContext: AppContextTestRender; - let onClose: () => void; - - beforeEach(() => { - getHostIsolationExceptionItemsMock.mockClear(); - updateOneHostIsolationExceptionItemMock.mockClear(); - useUserPrivilegesMock.mockReturnValue({ - endpointPrivileges: { - canIsolateHost: true, - }, - }); - policy = endpointGenerator.generatePolicyPackagePolicy(); - policyId = policy.id; - mockedContext = createAppRootMockRenderer(); - onClose = jest.fn(); - ({ history } = mockedContext); - render = () => - (renderResult = mockedContext.render( - - )); - - history.push(getPolicyHostIsolationExceptionsPath(policyId, { show: 'list' })); - }); - - it('should render a list of assignable policies and searchbar', async () => { - getHostIsolationExceptionItemsMock.mockImplementation(() => { - return getFoundExceptionListItemSchemaMock(1); - }); - render(); - await waitFor(() => { - expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith( - expect.objectContaining({ - filter: `((not exception-list-agnostic.attributes.tags:"policy:${policyId}" AND not exception-list-agnostic.attributes.tags:"policy:all"))`, - }) - ); - }); - expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - expect(renderResult.getByTestId('searchField')).toBeTruthy(); - }); - - it('should render "no items found" when searched for a term without data', async () => { - // first render - getHostIsolationExceptionItemsMock.mockImplementationOnce(() => { - return getFoundExceptionListItemSchemaMock(1); - }); - render(); - expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - - // results for search - getHostIsolationExceptionItemsMock.mockResolvedValueOnce(emptyList); - - // do a search - userEvent.type(renderResult.getByTestId('searchField'), 'no results with this{enter}'); - - await waitFor(() => { - expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith( - expect.objectContaining({ - filter: `((not exception-list-agnostic.attributes.tags:"policy:${policyId}" AND not exception-list-agnostic.attributes.tags:"policy:all")) AND ((exception-list-agnostic.attributes.item_id:(*no*results*with*this*) OR exception-list-agnostic.attributes.name:(*no*results*with*this*) OR exception-list-agnostic.attributes.description:(*no*results*with*this*) OR exception-list-agnostic.attributes.entries.value:(*no*results*with*this*)))`, - }) - ); - expect(renderResult.getByTestId('hostIsolationExceptions-no-items-found')).toBeTruthy(); - }); - }); - - it('should render "not assignable items" when no possible exceptions can be assigned', async () => { - // both exceptions list requests will return no results - getHostIsolationExceptionItemsMock.mockResolvedValue(emptyList); - render(); - expect( - await renderResult.findByTestId('hostIsolationExceptions-no-assignable-items') - ).toBeTruthy(); - }); - - it('should disable the submit button if no exceptions are selected', async () => { - getHostIsolationExceptionItemsMock.mockImplementationOnce(() => { - return getFoundExceptionListItemSchemaMock(1); - }); - render(); - expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - expect( - renderResult.getByTestId('hostIsolationExceptions-assign-confirm-button') - ).toBeDisabled(); - }); - - it('should enable the submit button if an exeption is selected', async () => { - const exceptions = getFoundExceptionListItemSchemaMock(1); - const firstOneName = exceptions.data[0].name; - getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions); - - render(); - expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - - // click the first item - userEvent.click(renderResult.getByTestId(`${firstOneName}_checkbox`)); - - expect(renderResult.getByTestId('hostIsolationExceptions-assign-confirm-button')).toBeEnabled(); - }); - - it('should warn the user when there are over 100 results in the flyout', async () => { - getHostIsolationExceptionItemsMock.mockImplementation(() => { - return { - ...getFoundExceptionListItemSchemaMock(1), - total: 120, - }; - }); - render(); - expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - expect(renderResult.getByTestId('hostIsolationExceptions-too-many-results')).toBeTruthy(); - }); - - describe('without privileges', () => { - beforeEach(() => { - useUserPrivilegesMock.mockReturnValue({ endpointPrivileges: { canIsolateHost: false } }); - }); - it('should not render if invoked without privileges', () => { - render(); - expect(renderResult.queryByTestId('hostIsolationExceptions-assign-flyout')).toBeNull(); - }); - - it('should call onClose if accessed without privileges', () => { - render(); - expect(onClose).toHaveBeenCalled(); - }); - }); - - describe('when submitting the form', () => { - const FIRST_ONE_NAME = uuid.v4(); - const SECOND_ONE_NAME = uuid.v4(); - const testTags = ['policy:1234', 'non-policy-tag', 'policy:4321']; - let exceptions: FoundExceptionListItemSchema; - - beforeEach(async () => { - exceptions = { - ...emptyList, - total: 2, - data: [ - getExceptionListItemSchemaMock({ - name: FIRST_ONE_NAME, - id: uuid.v4(), - tags: testTags, - }), - getExceptionListItemSchemaMock({ - name: SECOND_ONE_NAME, - id: uuid.v4(), - tags: testTags, - }), - ], - }; - getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions); - - render(); - // wait fo the list to render - expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); - }); - - it('should submit the exception when submit is pressed (1 exception), display a toast and close the flyout', async () => { - updateOneHostIsolationExceptionItemMock.mockImplementation(async (_http, exception) => { - return exception; - }); - // click the first item - userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); - // submit the form - userEvent.click(renderResult.getByTestId('hostIsolationExceptions-assign-confirm-button')); - - // verify the request with the new tag - await waitFor(() => { - expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalledWith( - mockedContext.coreStart.http, - { - ...exceptions.data[0], - tags: [...testTags, `policy:${policyId}`], - } - ); - }); - - await waitFor(() => { - expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: `"${FIRST_ONE_NAME}" has been added to your host isolation exceptions list.`, - title: 'Success', - }); - }); - expect(onClose).toHaveBeenCalled(); - }); - - it('should submit the exception when submit is pressed (2 exceptions), display a toast and close the flyout', async () => { - updateOneHostIsolationExceptionItemMock.mockImplementation(async (_http, exception) => { - return exception; - }); - // click the first two items - userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); - userEvent.click(renderResult.getByTestId(`${SECOND_ONE_NAME}_checkbox`)); - // submit the form - userEvent.click(renderResult.getByTestId('hostIsolationExceptions-assign-confirm-button')); - - // verify the request with the new tag - await waitFor(() => { - // first exception - expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalledWith( - mockedContext.coreStart.http, - { - ...exceptions.data[0], - tags: [...testTags, `policy:${policyId}`], - } - ); - // second exception - expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalledWith( - mockedContext.coreStart.http, - { - ...exceptions.data[1], - tags: [...testTags, `policy:${policyId}`], - } - ); - }); - - await waitFor(() => { - expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: '2 host isolation exceptions have been added to your list.', - title: 'Success', - }); - }); - expect(onClose).toHaveBeenCalled(); - }); - - it('should show a toast error when the request fails and close the flyout', async () => { - updateOneHostIsolationExceptionItemMock.mockRejectedValue( - new Error('the server is too far away') - ); - // click first item - userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); - // submit the form - userEvent.click(renderResult.getByTestId('hostIsolationExceptions-assign-confirm-button')); - - await waitFor(() => { - expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( - 'An error occurred updating artifacts' - ); - expect(onClose).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx deleted file mode 100644 index 8e27fea173816..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { isEmpty, without } from 'lodash/fp'; -import pMap from 'p-map'; -import React, { useEffect, useMemo, useState } from 'react'; -import { useMutation, useQueryClient } from 'react-query'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { PolicyData } from '../../../../../../../common/endpoint/types'; -import { useHttp, useToasts } from '../../../../../../common/lib/kibana'; -import { SearchExceptions } from '../../../../../components/search_exceptions'; -import { updateOneHostIsolationExceptionItem } from '../../../../host_isolation_exceptions/service'; -import { useFetchHostIsolationExceptionsList } from '../../../../host_isolation_exceptions/view/hooks'; -import { PolicyArtifactsAssignableList } from '../../artifacts/assignable'; - -const MAX_ALLOWED_RESULTS = 100; - -export const PolicyHostIsolationExceptionsAssignFlyout = ({ - policy, - onClose, -}: { - policy: PolicyData; - onClose: () => void; -}) => { - const http = useHttp(); - const toasts = useToasts(); - const queryClient = useQueryClient(); - const privileges = useUserPrivileges().endpointPrivileges; - - useEffect(() => { - if (!privileges.canIsolateHost) { - onClose(); - } - }, [onClose, privileges.canIsolateHost]); - - const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); - const [currentFilter, setCurrentFilter] = useState(''); - - const onUpdateSuccesss = (updatedExceptions: ExceptionListItemSchema[]) => { - if (updatedExceptions.length > 0) { - toasts.addSuccess({ - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.title', - { - defaultMessage: 'Success', - } - ), - text: - updatedExceptions.length > 1 - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.textMultiples', - { - defaultMessage: '{count} host isolation exceptions have been added to your list.', - values: { count: updatedExceptions.length }, - } - ) - : i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.textSingle', - { - defaultMessage: '"{name}" has been added to your host isolation exceptions list.', - values: { name: updatedExceptions[0].name }, - } - ), - }); - } - }; - - const onUpdateError = () => { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastError.text', - { - defaultMessage: `An error occurred updating artifacts`, - } - ) - ); - }; - - const exceptionsRequest = useFetchHostIsolationExceptionsList({ - excludedPolicies: [policy.id, 'all'], - page: 0, - filter: currentFilter, - perPage: MAX_ALLOWED_RESULTS, - }); - - const allPossibleExceptionsRequest = useFetchHostIsolationExceptionsList({ - excludedPolicies: [policy.id, 'all'], - page: 0, - perPage: MAX_ALLOWED_RESULTS, - // only request if there's a filter and no results from the regular request - enabled: currentFilter !== '' && exceptionsRequest.data?.total === 0, - }); - - const mutation = useMutation( - () => { - const toMutate = exceptionsRequest.data?.data.filter((exception) => { - return selectedArtifactIds.includes(exception.id); - }); - - if (toMutate === undefined) { - return Promise.reject(new Error('no exceptions selected')); - } - - return pMap( - toMutate, - (exception) => { - exception.tags = [...exception.tags, `policy:${policy.id}`]; - return updateOneHostIsolationExceptionItem(http, exception); - }, - { - concurrency: 10, - } - ); - }, - { - onSuccess: onUpdateSuccesss, - onError: onUpdateError, - onSettled: () => { - queryClient.invalidateQueries(['endpointSpecificPolicies']); - queryClient.invalidateQueries(['hostIsolationExceptions']); - onClose(); - }, - } - ); - - const handleOnConfirmAction = () => { - mutation.mutate(); - }; - - const handleOnSearch = (term: string) => { - // reset existing selection - setSelectedArtifactIds([]); - setCurrentFilter(term); - }; - - const handleSelectArtifact = (artifactId: string, selected: boolean) => { - setSelectedArtifactIds((currentSelectedArtifactIds) => - selected - ? [...currentSelectedArtifactIds, artifactId] - : without([artifactId], currentSelectedArtifactIds) - ); - }; - - const searchWarningMessage = useMemo( - () => ( - <> - - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.searchWarning.text', - { - defaultMessage: - 'Only the first 100 host isolation exceptions are displayed. Please use the search bar to refine the results.', - } - )} - - - - ), - [] - ); - - const noItemsMessage = useMemo(() => { - if (exceptionsRequest.isLoading || allPossibleExceptionsRequest.isLoading) { - return null; - } - - // there are no host isolation exceptions assignable to this policy - if ( - allPossibleExceptionsRequest.data?.total === 0 || - (exceptionsRequest.data?.total === 0 && currentFilter === '') - ) { - return ( - - } - /> - ); - } - - // there are no results for the current search - if (exceptionsRequest.data?.total === 0) { - return ( - - } - /> - ); - } - }, [ - allPossibleExceptionsRequest.data?.total, - allPossibleExceptionsRequest.isLoading, - currentFilter, - exceptionsRequest.data?.total, - exceptionsRequest.isLoading, - ]); - - // do not render if doesn't have adecuate privleges - if (!privileges.loading && !privileges.canIsolateHost) { - return null; - } - - return ( - - - -

- -

-
- - -
- - {(exceptionsRequest.data?.total || 0) > MAX_ALLOWED_RESULTS ? searchWarningMessage : null} - - - - - - {noItemsMessage} - - - - - - - - - - - - - - - -
- ); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx deleted file mode 100644 index 5e750b5599d71..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { act, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import uuid from 'uuid'; -import { getExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing'; -import { updateOneHostIsolationExceptionItem } from '../../../../host_isolation_exceptions/service'; -import { PolicyHostIsolationExceptionsDeleteModal } from './delete_modal'; - -jest.mock('../../../../host_isolation_exceptions/service'); - -const updateOneHostIsolationExceptionItemMock = updateOneHostIsolationExceptionItem as jest.Mock; - -describe('Policy details host isolation exceptions delete modal', () => { - let policyId: string; - let render: () => ReturnType; - let renderResult: ReturnType; - let history: AppContextTestRender['history']; - let mockedContext: AppContextTestRender; - let exception: ExceptionListItemSchema; - let onCancel: () => void; - - beforeEach(() => { - policyId = uuid.v4(); - mockedContext = createAppRootMockRenderer(); - exception = getExceptionListItemSchemaMock(); - onCancel = jest.fn(); - updateOneHostIsolationExceptionItemMock.mockClear(); - ({ history } = mockedContext); - render = () => - (renderResult = mockedContext.render( - - )); - - act(() => { - history.push(getPolicyHostIsolationExceptionsPath(policyId)); - }); - }); - - it('should render with enabled buttons', () => { - render(); - expect(renderResult.getByTestId('confirmModalCancelButton')).toBeEnabled(); - expect(renderResult.getByTestId('confirmModalConfirmButton')).toBeEnabled(); - }); - - it('should disable the submit button while deleting ', async () => { - updateOneHostIsolationExceptionItemMock.mockImplementation(() => { - return new Promise((resolve) => setImmediate(resolve)); - }); - render(); - const confirmButton = renderResult.getByTestId('confirmModalConfirmButton'); - userEvent.click(confirmButton); - - await waitFor(() => { - expect(confirmButton).toBeDisabled(); - }); - }); - - it('should call the API with the removed policy from the exception tags', async () => { - exception.tags = ['policy:1234', 'policy:4321', `policy:${policyId}`, 'not-a-policy-tag']; - render(); - const confirmButton = renderResult.getByTestId('confirmModalConfirmButton'); - userEvent.click(confirmButton); - - await waitFor(() => { - expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalledWith( - mockedContext.coreStart.http, - expect.objectContaining({ - id: exception.id, - tags: ['policy:1234', 'policy:4321', 'not-a-policy-tag'], - }) - ); - }); - }); - - it('should show a success toast if the operation was success', async () => { - updateOneHostIsolationExceptionItemMock.mockReturnValue('all good'); - render(); - const confirmButton = renderResult.getByTestId('confirmModalConfirmButton'); - userEvent.click(confirmButton); - - await waitFor(() => { - expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalled(); - }); - - expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalled(); - }); - - it('should show an error toast if the operation failed', async () => { - const error = new Error('the server is too far away'); - updateOneHostIsolationExceptionItemMock.mockRejectedValue(error); - render(); - const confirmButton = renderResult.getByTestId('confirmModalConfirmButton'); - userEvent.click(confirmButton); - - await waitFor(() => { - expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalled(); - }); - - expect(mockedContext.coreStart.notifications.toasts.addError).toHaveBeenCalledWith(error, { - title: 'Error while attempt to remove host isolation exception', - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx deleted file mode 100644 index ad8868bf68346..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import React from 'react'; -import { useMutation, useQueryClient } from 'react-query'; -import { useHttp, useToasts } from '../../../../../../common/lib/kibana'; -import { ServerApiError } from '../../../../../../common/types'; -import { updateOneHostIsolationExceptionItem } from '../../../../host_isolation_exceptions/service'; - -export const PolicyHostIsolationExceptionsDeleteModal = ({ - policyId, - policyName, - exception, - onCancel, -}: { - policyId: string; - policyName: string; - exception: ExceptionListItemSchema; - onCancel: () => void; -}) => { - const toasts = useToasts(); - const http = useHttp(); - const queryClient = useQueryClient(); - - const onDeleteError = (error: ServerApiError) => { - toasts.addError(error as unknown as Error, { - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.errorToastTitle', - { - defaultMessage: 'Error while attempt to remove host isolation exception', - } - ), - }); - onCancel(); - }; - - const onDeleteSuccess = () => { - queryClient.invalidateQueries(['endpointSpecificPolicies']); - queryClient.invalidateQueries(['hostIsolationExceptions']); - toasts.addSuccess({ - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.successToastTitle', - { defaultMessage: 'Successfully removed' } - ), - text: i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.successToastText', - { - defaultMessage: - '"{hostIsolationExceptionName}" has been removed from {policyName} policy', - values: { hostIsolationExceptionName: exception.name, policyName }, - } - ), - }); - onCancel(); - }; - - const mutation = useMutation( - async () => { - const modifiedException = { - ...exception, - tags: exception.tags.filter((tag) => tag !== `policy:${policyId}`), - }; - return updateOneHostIsolationExceptionItem(http, modifiedException); - }, - { - onSuccess: onDeleteSuccess, - onError: onDeleteError, - } - ); - - const handleModalConfirm = () => { - mutation.mutate(); - }; - - const handleCancel = () => { - if (!mutation.isLoading) { - onCancel(); - } - }; - - return ( - - -

- -

-
- - - - -

- -

-
-
- ); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx deleted file mode 100644 index 40438338c5216..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiEmptyPrompt, EuiLink, EuiPageTemplate } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback } from 'react'; -import { PolicyData } from '../../../../../../../common/endpoint/types'; -import { usePolicyDetailsHostIsolationExceptionsNavigateCallback } from '../../policy_hooks'; -import { useGetLinkTo } from './use_policy_host_isolation_exceptions_empty_hooks'; - -export const PolicyHostIsolationExceptionsEmptyUnassigned = ({ - policy, -}: { - policy: PolicyData; -}) => { - const { onClickHandler, toRouteUrl } = useGetLinkTo(policy.id, policy.name); - const navigateCallback = usePolicyDetailsHostIsolationExceptionsNavigateCallback(); - const onClickPrimaryButtonHandler = useCallback( - () => - navigateCallback({ - show: 'list', - }), - [navigateCallback] - ); - - return ( - - - - - } - body={ - - } - actions={[ - - - , - // eslint-disable-next-line @elastic/eui/href-or-on-click - - - , - ]} - /> - - ); -}; - -PolicyHostIsolationExceptionsEmptyUnassigned.displayName = - 'PolicyHostIsolationExceptionsEmptyUnassigned'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx deleted file mode 100644 index 94185904ce6cc..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; -import { PolicyData } from '../../../../../../../common/endpoint/types'; -import { useGetLinkTo } from './use_policy_host_isolation_exceptions_empty_hooks'; - -export const PolicyHostIsolationExceptionsEmptyUnexisting = ({ - policy, -}: { - policy: PolicyData; -}) => { - const { onClickHandler, toRouteUrl } = useGetLinkTo(policy.id, policy.name, { show: 'create' }); - - return ( - - - - - } - body={ - - } - actions={ - // eslint-disable-next-line @elastic/eui/href-or-on-click - - - - } - /> - - ); -}; - -PolicyHostIsolationExceptionsEmptyUnexisting.displayName = - 'PolicyHostIsolationExceptionsEmptyUnexisting'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx deleted file mode 100644 index 17e3ace9a6410..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import uuid from 'uuid'; -import { getExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing'; -import { PolicyHostIsolationExceptionsList } from './list'; -import { getHostIsolationExceptionItems } from '../../../../host_isolation_exceptions/service'; - -jest.mock('../../../../../../common/components/user_privileges'); -jest.mock('../../../../host_isolation_exceptions/service'); - -const useUserPrivilegesMock = useUserPrivileges as jest.Mock; -const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; - -const emptyList = { - data: [], - page: 1, - per_page: 10, - total: 0, -}; - -describe('Policy details host isolation exceptions tab', () => { - let policyId: string; - let render: () => ReturnType; - let renderResult: ReturnType; - let history: AppContextTestRender['history']; - let mockedContext: AppContextTestRender; - - beforeEach(() => { - policyId = uuid.v4(); - getHostIsolationExceptionItemsMock.mockClear(); - useUserPrivilegesMock.mockReturnValue({ - endpointPrivileges: { - canIsolateHost: true, - }, - }); - mockedContext = createAppRootMockRenderer(); - ({ history } = mockedContext); - render = () => - (renderResult = mockedContext.render( - - )); - - act(() => { - history.push(getPolicyHostIsolationExceptionsPath(policyId)); - }); - }); - - it('should display a searchbar and count even with no exceptions', async () => { - getHostIsolationExceptionItemsMock.mockResolvedValue(emptyList); - render(); - expect( - await renderResult.findByTestId('policyDetailsHostIsolationExceptionsSearchCount') - ).toHaveTextContent('Showing 0 host isolation exceptions'); - expect(renderResult.getByTestId('searchField')).toBeTruthy(); - }); - - it('should render the list of exceptions collapsed and expand it when clicked', async () => { - // render 3 - getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(3)); - render(); - expect( - await renderResult.findAllByTestId('hostIsolationExceptions-collapsed-list-card') - ).toHaveLength(3); - expect( - renderResult.queryAllByTestId( - 'hostIsolationExceptions-collapsed-list-card-criteriaConditions' - ) - ).toHaveLength(0); - }); - - it('should expand an item when expand is clicked', async () => { - getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1)); - render(); - expect( - await renderResult.findAllByTestId('hostIsolationExceptions-collapsed-list-card') - ).toHaveLength(1); - - userEvent.click( - renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-expandCollapse') - ); - - expect( - renderResult.queryAllByTestId( - 'hostIsolationExceptions-collapsed-list-card-criteriaConditions' - ) - ).toHaveLength(1); - }); - - it('should change the address location when a filter is applied', async () => { - getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1)); - render(); - userEvent.type(await renderResult.findByTestId('searchField'), 'search me{enter}'); - expect(history.location.search).toBe('?filter=search%20me'); - }); - - it('should apply a filter when requested from location search params', async () => { - history.push(getPolicyHostIsolationExceptionsPath(policyId, { filter: 'my filter' })); - getHostIsolationExceptionItemsMock.mockResolvedValue(() => - getFoundExceptionListItemSchemaMock(4) - ); - render(); - expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith({ - filter: `((exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all")) AND ((exception-list-agnostic.attributes.item_id:(*my*filter*) OR exception-list-agnostic.attributes.name:(*my*filter*) OR exception-list-agnostic.attributes.description:(*my*filter*) OR exception-list-agnostic.attributes.entries.value:(*my*filter*)))`, - http: mockedContext.coreStart.http, - page: 1, - perPage: 10, - }); - }); - - it('should disable the "remove from policy" option to global exceptions', async () => { - const testException = getExceptionListItemSchemaMock({ tags: ['policy:all'] }); - const exceptions = { - ...emptyList, - data: [testException], - total: 1, - }; - getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions); - render(); - // click the actions button - userEvent.click( - await renderResult.findByTestId( - 'hostIsolationExceptions-collapsed-list-card-header-actions-button' - ) - ); - expect(renderResult.getByTestId('remove-from-policy-action')).toBeDisabled(); - }); - - it('should enable the "remove from policy" option to policy-specific exceptions ', async () => { - const testException = getExceptionListItemSchemaMock({ - tags: [`policy:${policyId}`, 'policy:1234', 'not-a-policy-tag'], - }); - const exceptions = { - ...emptyList, - data: [testException], - total: 1, - }; - getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions); - render(); - // click the actions button - userEvent.click( - await renderResult.findByTestId( - 'hostIsolationExceptions-collapsed-list-card-header-actions-button' - ) - ); - expect(renderResult.getByTestId('remove-from-policy-action')).toBeEnabled(); - }); - - it('should enable the "view full details" action', async () => { - getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1)); - render(); - // click the actions button - userEvent.click( - await renderResult.findByTestId( - 'hostIsolationExceptions-collapsed-list-card-header-actions-button' - ) - ); - expect(renderResult.queryByTestId('view-full-details-action')).toBeTruthy(); - }); - - it('should render the delete dialog when the "remove from policy" button is clicked', async () => { - const testException = getExceptionListItemSchemaMock({ - tags: [`policy:${policyId}`, 'policy:1234', 'not-a-policy-tag'], - }); - const exceptions = { - ...emptyList, - data: [testException], - total: 1, - }; - getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions); - render(); - // click the actions button - userEvent.click( - await renderResult.findByTestId( - 'hostIsolationExceptions-collapsed-list-card-header-actions-button' - ) - ); - userEvent.click(renderResult.getByTestId('remove-from-policy-action')); - - // check the dialog is there - expect(renderResult.getByTestId('remove-from-policy-dialog')).toBeTruthy(); - }); - - describe('without privileges', () => { - beforeEach(() => { - useUserPrivilegesMock.mockReturnValue({ - endpointPrivileges: { - canIsolateHost: false, - }, - }); - }); - - it('should not display the delete action, do show the full details', async () => { - getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1)); - render(); - // click the actions button - userEvent.click( - await renderResult.findByTestId( - 'hostIsolationExceptions-collapsed-list-card-header-actions-button' - ) - ); - expect(renderResult.queryByTestId('remove-from-policy-action')).toBeFalsy(); - expect(renderResult.queryByTestId('view-full-details-action')).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx deleted file mode 100644 index 840d2c9f9809d..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import React, { useCallback, useMemo, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { useAppUrl } from '../../../../../../common/lib/kibana'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { - MANAGEMENT_DEFAULT_PAGE_SIZE, - MANAGEMENT_PAGE_SIZE_OPTIONS, -} from '../../../../../common/constants'; -import { - getHostIsolationExceptionsListPath, - getPolicyHostIsolationExceptionsPath, -} from '../../../../../common/routing'; -import { - ArtifactCardGrid, - ArtifactCardGridProps, -} from '../../../../../components/artifact_card_grid'; -import { useEndpointPoliciesToArtifactPolicies } from '../../../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; -import { isGlobalPolicyEffected } from '../../../../../components/effected_policy_select/utils'; -import { SearchExceptions } from '../../../../../components/search_exceptions'; -import { useGetEndpointSpecificPolicies } from '../../../../../services/policies/hooks'; -import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { PolicyHostIsolationExceptionsDeleteModal } from './delete_modal'; -import { useFetchHostIsolationExceptionsList } from '../../../../host_isolation_exceptions/view/hooks'; -import { useGetLinkTo } from './use_policy_host_isolation_exceptions_empty_hooks'; - -export const PolicyHostIsolationExceptionsList = ({ - policyId, - policyName, -}: { - policyId: string; - policyName: string; -}) => { - const history = useHistory(); - const { getAppUrl } = useAppUrl(); - - const privileges = useUserPrivileges().endpointPrivileges; - const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); - - const { state } = useGetLinkTo(policyId, policyName); - - // load the list of policies> - const policiesRequest = useGetEndpointSpecificPolicies({ perPage: 1000 }); - const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); - - const [exceptionItemToDelete, setExceptionItemToDelete] = useState< - ExceptionListItemSchema | undefined - >(); - - const [expandedItemsMap, setExpandedItemsMap] = useState>(new Map()); - - const policySearchedExceptionsListRequest = useFetchHostIsolationExceptionsList({ - filter: location.filter, - page: location.page_index, - perPage: location.page_size, - policies: [policyId, 'all'], - }); - - const pagination = { - totalItemCount: policySearchedExceptionsListRequest?.data?.total ?? 0, - pageSize: policySearchedExceptionsListRequest?.data?.per_page ?? MANAGEMENT_DEFAULT_PAGE_SIZE, - pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], - pageIndex: (policySearchedExceptionsListRequest?.data?.page ?? 1) - 1, - }; - - const handlePageChange = useCallback( - ({ pageIndex, pageSize }) => { - history.push( - getPolicyHostIsolationExceptionsPath(policyId, { - ...urlParams, - // If user changed page size, then reset page index back to the first page - page_index: pageIndex, - page_size: pageSize, - }) - ); - }, - [history, policyId, urlParams] - ); - - const handleSearchInput = useCallback( - (filter: string) => { - history.push( - getPolicyHostIsolationExceptionsPath(policyId, { - ...urlParams, - filter, - }) - ); - }, - [history, policyId, urlParams] - ); - - const artifactCardPolicies = useEndpointPoliciesToArtifactPolicies(policiesRequest.data?.items); - const provideCardProps: ArtifactCardGridProps['cardComponentProps'] = (artifact) => { - const item = artifact as ExceptionListItemSchema; - const isGlobal = isGlobalPolicyEffected(item.tags); - const deleteAction = { - icon: 'trash', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeAction', - { defaultMessage: 'Remove from policy' } - ), - onClick: () => { - setExceptionItemToDelete(item); - }, - disabled: isGlobal, - toolTipContent: isGlobal - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeActionNotAllowed', - { - defaultMessage: - 'Globally applied host isolation exceptions cannot be removed from policy.', - } - ) - : undefined, - toolTipPosition: 'top' as const, - 'data-test-subj': 'remove-from-policy-action', - }; - const viewUrlPath = getHostIsolationExceptionsListPath({ filter: item.item_id }); - - const fullDetailsAction = { - icon: 'controlsHorizontal', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.fullDetailsAction', - { defaultMessage: 'View full details' } - ), - href: getAppUrl({ appId: APP_UI_ID, path: viewUrlPath }), - navigateAppId: APP_UI_ID, - navigateOptions: { path: viewUrlPath, state }, - 'data-test-subj': 'view-full-details-action', - }; - - return { - expanded: expandedItemsMap.get(item.id) || false, - actions: privileges.canIsolateHost ? [fullDetailsAction, deleteAction] : [fullDetailsAction], - policies: artifactCardPolicies, - }; - }; - - const handleExpandCollapse: ArtifactCardGridProps['onExpandCollapse'] = ({ - expanded, - collapsed, - }) => { - const newExpandedMap = new Map(expandedItemsMap); - for (const item of expanded) { - newExpandedMap.set(item.id, true); - } - for (const item of collapsed) { - newExpandedMap.set(item.id, false); - } - setExpandedItemsMap(newExpandedMap); - }; - - const handleDeleteModalClose = useCallback(() => { - setExceptionItemToDelete(undefined); - }, [setExceptionItemToDelete]); - - const totalItemsCountLabel = useMemo(() => { - return i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.totalItemCount', - { - defaultMessage: - 'Showing {totalItemsCount, plural, one {# host isolation exception} other {# host isolation exceptions}}', - values: { totalItemsCount: pagination.totalItemCount }, - } - ); - }, [pagination.totalItemCount]); - - return ( - <> - {exceptionItemToDelete ? ( - - ) : null} - - - - {totalItemsCountLabel} - - - - - ); -}; -PolicyHostIsolationExceptionsList.displayName = 'PolicyHostIsolationExceptionsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx deleted file mode 100644 index 2cebae47ed69f..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { waitFor } from '@testing-library/react'; -import React from 'react'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; -import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; -import { PolicyData } from '../../../../../../common/endpoint/types'; -import { useUserPrivileges } from '../../../../../common/components/user_privileges'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../common/mock/endpoint'; -import { getPolicyHostIsolationExceptionsPath } from '../../../../common/routing'; -import { getHostIsolationExceptionItems } from '../../../host_isolation_exceptions/service'; -import { PolicyHostIsolationExceptionsTab } from './host_isolation_exceptions_tab'; - -jest.mock('../../../host_isolation_exceptions/service'); -jest.mock('../../../../../common/components/user_privileges'); - -const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; -const useUserPrivilegesMock = useUserPrivileges as jest.Mock; - -const endpointGenerator = new EndpointDocGenerator('seed'); - -const emptyList = { - data: [], - page: 1, - per_page: 10, - total: 0, -}; - -describe('Policy details host isolation exceptions tab', () => { - let policyId: string; - let policy: PolicyData; - let render: () => ReturnType; - let renderResult: ReturnType; - let history: AppContextTestRender['history']; - let mockedContext: AppContextTestRender; - - beforeEach(() => { - getHostIsolationExceptionItemsMock.mockClear(); - policy = endpointGenerator.generatePolicyPackagePolicy(); - policyId = policy.id; - useUserPrivilegesMock.mockReturnValue({ - endpointPrivileges: { - canIsolateHost: true, - }, - }); - mockedContext = createAppRootMockRenderer(); - ({ history } = mockedContext); - render = () => - (renderResult = mockedContext.render()); - - history.push(getPolicyHostIsolationExceptionsPath(policyId)); - }); - - it('should display display a "loading" state while requests happen', async () => { - const promises: Array<() => void> = []; - getHostIsolationExceptionItemsMock.mockImplementation(() => { - return new Promise((resolve) => promises.push(resolve)); - }); - render(); - expect(await renderResult.findByTestId('policyHostIsolationExceptionsTabLoading')).toBeTruthy(); - // prevent memory leaks - promises.forEach((resolve) => resolve()); - }); - - it("should display an 'unexistent' empty state if there are no host isolation exceptions at all", async () => { - // mock no data for all requests - getHostIsolationExceptionItemsMock.mockResolvedValue({ - ...emptyList, - }); - render(); - expect( - await renderResult.findByTestId('policy-host-isolation-exceptions-empty-unexisting') - ).toBeTruthy(); - }); - - it("should display an 'unassigned' empty state and 'add' button if there are no host isolation exceptions assigned", async () => { - // mock no data for all requests - getHostIsolationExceptionItemsMock.mockImplementation((params) => { - // no filter = fetch all exceptions - if (!params.filter) { - return { - ...emptyList, - total: 1, - }; - } - return { - ...emptyList, - }; - }); - render(); - expect( - await renderResult.findByTestId('policy-host-isolation-exceptions-empty-unassigned') - ).toBeTruthy(); - expect(renderResult.getByTestId('empty-assign-host-isolation-exceptions-button')).toBeTruthy(); - }); - - it('Should display the count of total assigned policies', async () => { - getHostIsolationExceptionItemsMock.mockImplementation(() => { - return getFoundExceptionListItemSchemaMock(4); - }); - render(); - expect( - await renderResult.findByTestId('policyHostIsolationExceptionsTabSubtitle') - ).toHaveTextContent('There are 4 host isolation exceptions associated with this policy'); - }); - - describe('and the user is trying to assign policies', () => { - it('should not render the assign button if there are not existing exceptions', async () => { - getHostIsolationExceptionItemsMock.mockReturnValue(emptyList); - render(); - await waitFor(() => { - expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledTimes(2); - }); - expect(renderResult.queryByTestId('hostIsolationExceptions-assign-button')).toBeFalsy(); - }); - - it('should not open the assign flyout if there are not existing exceptions', async () => { - history.push(getPolicyHostIsolationExceptionsPath(policyId, { show: 'list' })); - getHostIsolationExceptionItemsMock.mockReturnValue(emptyList); - render(); - await waitFor(() => { - expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledTimes(2); - }); - expect(renderResult.queryByTestId('hostIsolationExceptions-assign-flyout')).toBeFalsy(); - }); - - it('should open the assign flyout if there are existing exceptions', async () => { - history.push(getPolicyHostIsolationExceptionsPath(policyId, { show: 'list' })); - getHostIsolationExceptionItemsMock.mockImplementation(() => { - return getFoundExceptionListItemSchemaMock(1); - }); - render(); - await waitFor(() => { - expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledTimes(3); - }); - expect(await renderResult.findByTestId('hostIsolationExceptions-assign-flyout')).toBeTruthy(); - }); - }); - - describe('Without can isolate privileges', () => { - beforeEach(() => { - useUserPrivilegesMock.mockReturnValue({ - endpointPrivileges: { - canIsolateHost: false, - }, - }); - }); - it('should not display the assign policies button', async () => { - getHostIsolationExceptionItemsMock.mockImplementation(() => { - return getFoundExceptionListItemSchemaMock(5); - }); - render(); - expect(await renderResult.findByTestId('policyHostIsolationExceptionsTab')).toBeTruthy(); - expect(renderResult.queryByTestId('hostIsolationExceptions-assign-button')).toBeFalsy(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx deleted file mode 100644 index f9ec756a8be76..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { - EuiButton, - EuiLink, - EuiPageContent, - EuiPageHeader, - EuiPageHeaderSection, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; -import { useUserPrivileges } from '../../../../../common/components/user_privileges'; -import { APP_UI_ID } from '../../../../../../common/constants'; -import { PolicyData } from '../../../../../../common/endpoint/types'; -import { useAppUrl } from '../../../../../common/lib/kibana'; -import { - MANAGEMENT_DEFAULT_PAGE, - MANAGEMENT_DEFAULT_PAGE_SIZE, -} from '../../../../common/constants'; -import { - getHostIsolationExceptionsListPath, - getPolicyHostIsolationExceptionsPath, -} from '../../../../common/routing'; -import { ManagementPageLoader } from '../../../../components/management_page_loader'; -import { useFetchHostIsolationExceptionsList } from '../../../host_isolation_exceptions/view/hooks'; -import { getCurrentArtifactsLocation } from '../../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from '../policy_hooks'; -import { PolicyHostIsolationExceptionsAssignFlyout } from './components/assign_flyout'; -import { PolicyHostIsolationExceptionsEmptyUnassigned } from './components/empty_unassigned'; -import { PolicyHostIsolationExceptionsEmptyUnexisting } from './components/empty_unexisting'; -import { PolicyHostIsolationExceptionsList } from './components/list'; - -export const PolicyHostIsolationExceptionsTab = ({ policy }: { policy: PolicyData }) => { - const { getAppUrl } = useAppUrl(); - const privileges = useUserPrivileges().endpointPrivileges; - - const policyId = policy.id; - - const history = useHistory(); - const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); - - const toHostIsolationList = getAppUrl({ - appId: APP_UI_ID, - path: getHostIsolationExceptionsListPath(), - }); - - const allPolicyExceptionsListRequest = useFetchHostIsolationExceptionsList({ - page: MANAGEMENT_DEFAULT_PAGE, - perPage: MANAGEMENT_DEFAULT_PAGE_SIZE, - policies: [policyId, 'all'], - }); - - const allExceptionsListRequest = useFetchHostIsolationExceptionsList({ - page: MANAGEMENT_DEFAULT_PAGE, - perPage: MANAGEMENT_DEFAULT_PAGE_SIZE, - // only do this request if no assigned policies found - enabled: allPolicyExceptionsListRequest.data?.total === 0, - }); - - const hasNoAssignedOrExistingExceptions = allPolicyExceptionsListRequest.data?.total === 0; - const hasNoExistingExceptions = allExceptionsListRequest.data?.total === 0; - - const subTitle = useMemo(() => { - const link = ( - - - - ); - - return allPolicyExceptionsListRequest.data ? ( - - ) : null; - }, [allPolicyExceptionsListRequest.data, toHostIsolationList]); - - const handleAssignButton = () => { - history.push( - getPolicyHostIsolationExceptionsPath(policyId, { - ...location, - show: 'list', - }) - ); - }; - - const handleFlyoutOnClose = () => { - history.push( - getPolicyHostIsolationExceptionsPath(policyId, { - ...location, - show: undefined, - }) - ); - }; - - const assignFlyout = - location.show === 'list' ? ( - - ) : null; - - const isLoading = - allPolicyExceptionsListRequest.isLoading || allExceptionsListRequest.isLoading || !policy; - - // render non-existent or non-assigned messages - if (!isLoading && (hasNoAssignedOrExistingExceptions || hasNoExistingExceptions)) { - if (hasNoExistingExceptions) { - return ; - } else { - return ( - <> - {assignFlyout} - - - ); - } - } - - // render header and list - return !isLoading ? ( -
- {assignFlyout} - - - -

- {i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.title', - { - defaultMessage: 'Assigned host isolation exceptions', - } - )} -

-
- - - - -

{subTitle}

-
-
- {privileges.canIsolateHost ? ( - - - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.assignToPolicy', - { - defaultMessage: 'Assign host isolation exceptions to policy', - } - )} - - - ) : null} -
- - - - - -
- ) : ( - - ); -}; -PolicyHostIsolationExceptionsTab.displayName = 'PolicyHostIsolationExceptionsTab'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts index 2ea41063c2b73..e8f3f97c6e0c1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts @@ -5,10 +5,13 @@ * 2.0. */ -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; -import { i18n } from '@kbn/i18n'; +import { + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; import { PolicyDetailsArtifactsPageLocation, PolicyDetailsState } from '../types'; import { State } from '../../../../common/store'; import { @@ -20,14 +23,7 @@ import { getPolicyEventFiltersPath, getPolicyHostIsolationExceptionsPath, } from '../../../common/routing'; -import { - getCurrentArtifactsLocation, - getUpdateArtifacts, - getUpdateArtifactsLoaded, - getUpdateArtifactsIsFailed, - policyIdFromParams, -} from '../store/policy_details/selectors'; -import { useToasts } from '../../../../common/lib/kibana'; +import { getCurrentArtifactsLocation, policyIdFromParams } from '../store/policy_details/selectors'; /** * Narrows global state down to the PolicyDetailsState before calling the provided Policy Details Selector @@ -66,83 +62,35 @@ export function usePolicyDetailsNavigateCallback() { ); } -export function usePolicyDetailsEventFiltersNavigateCallback() { +export function usePolicyDetailsArtifactsNavigateCallback(listId: string) { const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); const history = useHistory(); const policyId = usePolicyDetailsSelector(policyIdFromParams); - return useCallback( - (args: Partial) => - history.push( - getPolicyEventFiltersPath(policyId, { + const getPath = useCallback( + (args: Partial) => { + if (listId === ENDPOINT_TRUSTED_APPS_LIST_ID) { + return getPolicyDetailsArtifactsListPath(policyId, { ...location, ...args, - }) - ), - [history, location, policyId] + }); + } else if (listId === ENDPOINT_EVENT_FILTERS_LIST_ID) { + return getPolicyEventFiltersPath(policyId, { + ...location, + ...args, + }); + } else { + return getPolicyHostIsolationExceptionsPath(policyId, { + ...location, + ...args, + }); + } + }, + [listId, location, policyId] ); -} - -export function usePolicyDetailsHostIsolationExceptionsNavigateCallback() { - const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); - const history = useHistory(); - const policyId = usePolicyDetailsSelector(policyIdFromParams); return useCallback( - (args: Partial) => - history.push( - getPolicyHostIsolationExceptionsPath(policyId, { - ...location, - ...args, - }) - ), - [history, location, policyId] + (args: Partial) => history.push(getPath(args)), + [getPath, history] ); } - -export const usePolicyTrustedAppsNotification = () => { - const updateSuccessfull = usePolicyDetailsSelector(getUpdateArtifactsLoaded); - const updateFailed = usePolicyDetailsSelector(getUpdateArtifactsIsFailed); - const updatedArtifacts = usePolicyDetailsSelector(getUpdateArtifacts); - const toasts = useToasts(); - const [wasAlreadyHandled] = useState(new WeakSet()); - - if (updateSuccessfull && updatedArtifacts && !wasAlreadyHandled.has(updatedArtifacts)) { - wasAlreadyHandled.add(updatedArtifacts); - const updateCount = updatedArtifacts.length; - - toasts.addSuccess({ - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.title', - { - defaultMessage: 'Success', - } - ), - text: - updateCount > 1 - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textMultiples', - { - defaultMessage: '{count} trusted applications have been added to your list.', - values: { count: updateCount }, - } - ) - : i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textSingle', - { - defaultMessage: '"{name}" has been added to your trusted applications list.', - values: { name: updatedArtifacts[0].data.name }, - } - ), - }); - } else if (updateFailed) { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastError.text', - { - defaultMessage: `An error occurred updating artifacts`, - } - ) - ); - } -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts new file mode 100644 index 0000000000000..29b731a1eee56 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts @@ -0,0 +1,152 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export const POLICY_ARTIFACT_EVENT_FILTERS_LABELS = Object.freeze({ + deleteModalTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.title', + { + defaultMessage: 'Remove event filter from policy', + } + ), + deleteModalImpactInfo: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.messageCallout', + { + defaultMessage: + 'This event filter will be removed only from this policy and can still be found and managed from the artifact page.', + } + ), + deleteModalErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.errorToastTitle', + { + defaultMessage: 'Error while attempting to remove event filter', + } + ), + flyoutWarningCalloutMessage: (maxNumber: number) => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.searchWarning.text', + { + defaultMessage: + 'Only the first {maxNumber} event filters are displayed. Please use the search bar to refine the results.', + values: { maxNumber }, + } + ), + flyoutNoArtifactsToBeAssignedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.noAssignable', + { + defaultMessage: 'There are no event filters that can be assigned to this policy.', + } + ), + flyoutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.title', + { + defaultMessage: 'Assign event filters', + } + ), + flyoutSubtitle: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.subtitle', { + defaultMessage: 'Select event filters to add to {policyName}', + values: { policyName }, + }), + flyoutSearchPlaceholder: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.search.label', + { + defaultMessage: 'Search event filters', + } + ), + flyoutErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastError.text', + { + defaultMessage: `An error occurred updating event filters`, + } + ), + flyoutSuccessMessageText: (updatedExceptions: ExceptionListItemSchema[]): string => + updatedExceptions.length > 1 + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textMultiples', + { + defaultMessage: '{count} event filters have been added to your list.', + values: { count: updatedExceptions.length }, + } + ) + : i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textSingle', + { + defaultMessage: '"{name}" has been added to your event filter list.', + values: { name: updatedExceptions[0].name }, + } + ), + emptyUnassignedTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.title', + { defaultMessage: 'No assigned event filters' } + ), + emptyUnassignedMessage: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.content', { + defaultMessage: + 'There are currently no event filters assigned to {policyName}. Assign event filters now or add and manage them on the event filters page.', + values: { policyName }, + }), + emptyUnassignedPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.primaryAction', + { + defaultMessage: 'Assign event filters', + } + ), + emptyUnassignedSecondaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.secondaryAction', + { + defaultMessage: 'Manage event filters', + } + ), + emptyUnexistingTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.title', + { defaultMessage: 'No event filters exist' } + ), + emptyUnexistingMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.content', + { defaultMessage: 'There are currently no event filters applied to your endpoints.' } + ), + emptyUnexistingPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.action', + { defaultMessage: 'Add event filters' } + ), + listTotalItemCountMessage: (totalItemsCount: number): string => + i18n.translate('xpack.securitySolution.endpoint.policy.eventFilters.list.totalItemCount', { + defaultMessage: + 'Showing {totalItemsCount, plural, one {# event filter} other {# event filters}}', + values: { totalItemsCount }, + }), + listRemoveActionNotAllowedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeActionNotAllowed', + { + defaultMessage: 'Globally applied event filter cannot be removed from policy.', + } + ), + listSearchPlaceholderMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.list.search.placeholder', + { + defaultMessage: `Search on the fields below: name, description, comments, value`, + } + ), + layoutTitle: i18n.translate('xpack.securitySolution.endpoint.policy.eventFilters.layout.title', { + defaultMessage: 'Assigned event filters', + }), + layoutAssignButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.assignToPolicy', + { + defaultMessage: 'Assign event filters to policy', + } + ), + layoutViewAllLinkMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.about.viewAllLinkLabel', + { + defaultMessage: 'view all event filters', + } + ), +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts new file mode 100644 index 0000000000000..2df0d18ac03d4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts @@ -0,0 +1,166 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export const POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS = Object.freeze({ + deleteModalTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.list.removeDialog.title', + { + defaultMessage: 'Remove host isolation exception from policy', + } + ), + deleteModalImpactInfo: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.list.removeDialog.messageCallout', + { + defaultMessage: + 'This host isolation exception will be removed only from this policy and can still be found and managed from the artifact page.', + } + ), + deleteModalErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.list.removeDialog.errorToastTitle', + { + defaultMessage: 'Error while attempting to remove host isolation exception', + } + ), + flyoutWarningCalloutMessage: (maxNumber: number) => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.flyout.searchWarning.text', + { + defaultMessage: + 'Only the first {maxNumber} host isolation exceptions are displayed. Please use the search bar to refine the results.', + values: { maxNumber }, + } + ), + flyoutNoArtifactsToBeAssignedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.flyout.noAssignable', + { + defaultMessage: 'There are no host isolation exceptions that can be assigned to this policy.', + } + ), + flyoutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.flyout.title', + { + defaultMessage: 'Assign host isolation exceptions', + } + ), + flyoutSubtitle: (policyName: string): string => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.flyout.subtitle', + { + defaultMessage: 'Select host isolation exceptions to add to {policyName}', + values: { policyName }, + } + ), + flyoutSearchPlaceholder: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.search.label', + { + defaultMessage: 'Search host isolation exceptions', + } + ), + flyoutErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.flyout.toastError.text', + { + defaultMessage: `An error occurred updating host isolation exceptions`, + } + ), + flyoutSuccessMessageText: (updatedExceptions: ExceptionListItemSchema[]): string => + updatedExceptions.length > 1 + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.flyout.toastSuccess.textMultiples', + { + defaultMessage: '{count} host isolation exceptions have been added to your list.', + values: { count: updatedExceptions.length }, + } + ) + : i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.flyout.toastSuccess.textSingle', + { + defaultMessage: '"{name}" has been added to your host isolation exception list.', + values: { name: updatedExceptions[0].name }, + } + ), + emptyUnassignedTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unassigned.title', + { defaultMessage: 'No assigned host isolation exceptions' } + ), + emptyUnassignedMessage: (policyName: string): string => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unassigned.content', + { + defaultMessage: + 'There are currently no host isolation exceptions assigned to {policyName}. Assign host isolation exceptions now or add and manage them on the host isolation exceptions page.', + values: { policyName }, + } + ), + emptyUnassignedPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unassigned.primaryAction', + { + defaultMessage: 'Assign host isolation exceptions', + } + ), + emptyUnassignedSecondaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unassigned.secondaryAction', + { + defaultMessage: 'Manage host isolation exceptions', + } + ), + emptyUnexistingTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unexisting.title', + { defaultMessage: 'No host isolation exceptions exist' } + ), + emptyUnexistingMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unexisting.content', + { + defaultMessage: 'There are currently no host isolation exceptions applied to your endpoints.', + } + ), + emptyUnexistingPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unexisting.action', + { defaultMessage: 'Add host isolation exceptions' } + ), + listTotalItemCountMessage: (totalItemsCount: number): string => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.list.totalItemCount', + { + defaultMessage: + 'Showing {totalItemsCount, plural, one {# host isolation exception} other {# host isolation exceptions}}', + values: { totalItemsCount }, + } + ), + listRemoveActionNotAllowedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.list.removeActionNotAllowed', + { + defaultMessage: 'Globally applied host isolation exception cannot be removed from policy.', + } + ), + listSearchPlaceholderMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.list.search.placeholder', + { + defaultMessage: `Search on the fields below: name, description, IP`, + } + ), + layoutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.title', + { + defaultMessage: 'Assigned host isolation exceptions', + } + ), + layoutAssignButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.assignToPolicy', + { + defaultMessage: 'Assign host isolation exceptions to policy', + } + ), + layoutViewAllLinkMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationException.layout.about.viewAllLinkLabel', + { + defaultMessage: 'view all host isolation exceptions', + } + ), +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index 02e0da1d0b915..706995974fcbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -7,16 +7,21 @@ import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; -import { PolicyData } from '../../../../../../common/endpoint/types'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { getPolicyDetailPath, getPolicyEventFiltersPath, getPolicyHostIsolationExceptionsPath, getPolicyTrustedAppsPath, + getEventFiltersListPath, + getHostIsolationExceptionsListPath, + getTrustedAppsListPath, + getPolicyDetailsArtifactsListPath, } from '../../../../common/routing'; +import { useHttp } from '../../../../../common/lib/kibana'; import { ManagementPageLoader } from '../../../../components/management_page_loader'; import { useFetchHostIsolationExceptionsList } from '../../../host_isolation_exceptions/view/hooks'; import { @@ -27,11 +32,18 @@ import { policyDetails, policyIdFromParams, } from '../../store/policy_details/selectors'; -import { PolicyEventFiltersLayout } from '../event_filters/layout'; -import { PolicyHostIsolationExceptionsTab } from '../host_isolation_exceptions/host_isolation_exceptions_tab'; +import { PolicyArtifactsLayout } from '../artifacts/layout/policy_artifacts_layout'; import { PolicyFormLayout } from '../policy_forms/components'; import { usePolicyDetailsSelector } from '../policy_hooks'; -import { PolicyTrustedAppsLayout } from '../trusted_apps/layout'; +import { POLICY_ARTIFACT_EVENT_FILTERS_LABELS } from './event_filters_translations'; +import { POLICY_ARTIFACT_TRUSTED_APPS_LABELS } from './trusted_apps_translations'; +import { POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS } from './host_isolation_exceptions_translations'; +import { TrustedAppsApiClient } from '../../../trusted_apps/service/trusted_apps_api_client'; +import { EventFiltersApiClient } from '../../../event_filters/service/event_filters_api_client'; +import { HostIsolationExceptionsApiClient } from '../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; +import { SEARCHABLE_FIELDS as TRUSTED_APPS_SEARCHABLE_FIELDS } from '../../../trusted_apps/constants'; +import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../event_filters/constants'; +import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants'; const enum PolicyTabKeys { SETTINGS = 'settings', @@ -48,6 +60,7 @@ interface PolicyTab { export const PolicyTabs = React.memo(() => { const history = useHistory(); + const http = useHttp(); const isInSettingsTab = usePolicyDetailsSelector(isOnPolicyFormView); const isInTrustedAppsTab = usePolicyDetailsSelector(isOnPolicyTrustedAppsView); const isInEventFilters = usePolicyDetailsSelector(isOnPolicyEventFiltersView); @@ -76,7 +89,55 @@ export const PolicyTabs = React.memo(() => { } }, [canSeeHostIsolationExceptions, history, isInHostIsolationExceptionsTab, policyId]); + const getTrustedAppsApiClientInstance = useCallback( + () => TrustedAppsApiClient.getInstance(http), + [http] + ); + + const getEventFiltersApiClientInstance = useCallback( + () => EventFiltersApiClient.getInstance(http), + [http] + ); + + const getHostIsolationExceptionsApiClientInstance = useCallback( + () => HostIsolationExceptionsApiClient.getInstance(http), + [http] + ); + const tabs: Record = useMemo(() => { + const trustedAppsLabels = { + ...POLICY_ARTIFACT_TRUSTED_APPS_LABELS, + layoutAboutMessage: (count: number, link: React.ReactElement): React.ReactNode => ( + + ), + }; + + const eventFiltersLabels = { + ...POLICY_ARTIFACT_EVENT_FILTERS_LABELS, + layoutAboutMessage: (count: number, link: React.ReactElement): React.ReactNode => ( + + ), + }; + + const hostIsolationExceptionsLabels = { + ...POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS, + layoutAboutMessage: (count: number, link: React.ReactElement): React.ReactNode => ( + + ), + }; + return { [PolicyTabKeys.SETTINGS]: { id: PolicyTabKeys.SETTINGS, @@ -98,7 +159,14 @@ export const PolicyTabs = React.memo(() => { content: ( <> - + ), }, @@ -110,7 +178,14 @@ export const PolicyTabs = React.memo(() => { content: ( <> - + ), }, @@ -126,13 +201,28 @@ export const PolicyTabs = React.memo(() => { content: ( <> - + ), } : undefined, }; - }, [canSeeHostIsolationExceptions, policyItem]); + }, [ + canSeeHostIsolationExceptions, + getEventFiltersApiClientInstance, + getHostIsolationExceptionsApiClientInstance, + getTrustedAppsApiClientInstance, + policyItem, + privileges.canIsolateHost, + ]); // convert tabs object into an array EuiTabbedContent can understand const tabsList: PolicyTab[] = useMemo( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts new file mode 100644 index 0000000000000..f83568498df25 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts @@ -0,0 +1,152 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export const POLICY_ARTIFACT_TRUSTED_APPS_LABELS = Object.freeze({ + deleteModalTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.title', + { + defaultMessage: 'Remove trusted app from policy', + } + ), + deleteModalImpactInfo: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.messageCallout', + { + defaultMessage: + 'This trusted app will be removed only from this policy and can still be found and managed from the artifact page.', + } + ), + deleteModalErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.errorToastTitle', + { + defaultMessage: 'Error while attempting to remove trusted app', + } + ), + flyoutWarningCalloutMessage: (maxNumber: number) => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.searchWarning.text', + { + defaultMessage: + 'Only the first {maxNumber} trusted apps are displayed. Please use the search bar to refine the results.', + values: { maxNumber }, + } + ), + flyoutNoArtifactsToBeAssignedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.noAssignable', + { + defaultMessage: 'There are no trusted apps that can be assigned to this policy.', + } + ), + flyoutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.title', + { + defaultMessage: 'Assign trusted apps', + } + ), + flyoutSubtitle: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.subtitle', { + defaultMessage: 'Select trusted apps to add to {policyName}', + values: { policyName }, + }), + flyoutSearchPlaceholder: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.search.label', + { + defaultMessage: 'Search trusted apps', + } + ), + flyoutErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastError.text', + { + defaultMessage: `An error occurred updating trusted apps`, + } + ), + flyoutSuccessMessageText: (updatedExceptions: ExceptionListItemSchema[]): string => + updatedExceptions.length > 1 + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textMultiples', + { + defaultMessage: '{count} trusted apps have been added to your list.', + values: { count: updatedExceptions.length }, + } + ) + : i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textSingle', + { + defaultMessage: '"{name}" has been added to your trusted app list.', + values: { name: updatedExceptions[0].name }, + } + ), + emptyUnassignedTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.title', + { defaultMessage: 'No assigned trusted apps' } + ), + emptyUnassignedMessage: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.content', { + defaultMessage: + 'There are currently no trusted apps assigned to {policyName}. Assign trusted apps now or add and manage them on the trusted apps page.', + values: { policyName }, + }), + emptyUnassignedPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.primaryAction', + { + defaultMessage: 'Assign trusted apps', + } + ), + emptyUnassignedSecondaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.secondaryAction', + { + defaultMessage: 'Manage trusted apps', + } + ), + emptyUnexistingTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.title', + { defaultMessage: 'No trusted apps exist' } + ), + emptyUnexistingMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.content', + { defaultMessage: 'There are currently no trusted apps applied to your endpoints.' } + ), + emptyUnexistingPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.action', + { defaultMessage: 'Add trusted apps' } + ), + listTotalItemCountMessage: (totalItemsCount: number): string => + i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.totalItemCount', { + defaultMessage: + 'Showing {totalItemsCount, plural, one {# trusted app} other {# trusted apps}}', + values: { totalItemsCount }, + }), + listRemoveActionNotAllowedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed', + { + defaultMessage: 'Globally applied trusted app cannot be removed from policy.', + } + ), + listSearchPlaceholderMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.search.placeholder', + { + defaultMessage: `Search on the fields below: name, description, value`, + } + ), + layoutTitle: i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.layout.title', { + defaultMessage: 'Assigned trusted apps', + }), + layoutAssignButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.assignToPolicy', + { + defaultMessage: 'Assign trusted apps to policy', + } + ), + layoutViewAllLinkMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.about.viewAllLinkLabel', + { + defaultMessage: 'view all trusted apps', + } + ), +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/index.ts deleted file mode 100644 index aa9048426c495..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { PolicyTrustedAppsEmptyUnassigned } from './policy_trusted_apps_empty_unassigned'; -export { PolicyTrustedAppsEmptyUnexisting } from './policy_trusted_apps_empty_unexisting'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx deleted file mode 100644 index 3a7308fef75f1..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback } from 'react'; -import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; -import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; - -interface CommonProps { - policyId: string; - policyName: string; -} - -export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, policyName }) => { - const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const navigateCallback = usePolicyDetailsNavigateCallback(); - const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); - const onClickPrimaryButtonHandler = useCallback( - () => - navigateCallback({ - show: 'list', - }), - [navigateCallback] - ); - return ( - - - - - } - body={ - - } - actions={[ - ...(canCreateArtifactsByPolicy - ? [ - - - , - ] - : []), - // eslint-disable-next-line @elastic/eui/href-or-on-click - - - , - ]} - /> - - ); -}); - -PolicyTrustedAppsEmptyUnassigned.displayName = 'PolicyTrustedAppsEmptyUnassigned'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx deleted file mode 100644 index 1fe834a9fce46..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiEmptyPrompt, EuiButton, EuiPageTemplate } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; - -interface CommonProps { - policyId: string; - policyName: string; -} - -export const PolicyTrustedAppsEmptyUnexisting = memo(({ policyId, policyName }) => { - const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName, { show: 'create' }); - return ( - - - - - } - body={ - - } - actions={ - // eslint-disable-next-line @elastic/eui/href-or-on-click - - - - } - /> - - ); -}); - -PolicyTrustedAppsEmptyUnexisting.displayName = 'PolicyTrustedAppsEmptyUnexisting'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/use_policy_trusted_apps_empty_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/use_policy_trusted_apps_empty_hooks.ts deleted file mode 100644 index f393f1f436d1c..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/use_policy_trusted_apps_empty_hooks.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { useAppUrl } from '../../../../../../common/lib/kibana/hooks'; -import { getPolicyTrustedAppsPath, getTrustedAppsListPath } from '../../../../../common/routing'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; - -export const useGetLinkTo = ( - policyId: string, - policyName: string, - location?: Partial -) => { - const { getAppUrl } = useAppUrl(); - const { toRoutePath, toRouteUrl } = useMemo(() => { - const path = getTrustedAppsListPath(location); - return { - toRoutePath: path, - toRouteUrl: getAppUrl({ path }), - }; - }, [getAppUrl, location]); - - const policyTrustedAppsPath = useMemo(() => getPolicyTrustedAppsPath(policyId), [policyId]); - const policyTrustedAppRouteState = useMemo(() => { - return { - backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.backButtonLabel', - { - defaultMessage: 'Back to {policyName} policy', - values: { - policyName, - }, - } - ), - onBackButtonNavigateTo: [ - APP_UI_ID, - { - path: policyTrustedAppsPath, - }, - ], - backButtonUrl: getAppUrl({ - appId: APP_UI_ID, - path: policyTrustedAppsPath, - }), - }; - }, [getAppUrl, policyName, policyTrustedAppsPath]); - - const onClickHandler = useNavigateToAppEventHandler(APP_UI_ID, { - state: policyTrustedAppRouteState, - path: toRoutePath, - }); - - return { - onClickHandler, - toRouteUrl, - state: policyTrustedAppRouteState, - }; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/index.ts deleted file mode 100644 index d3090a340fa2b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { PolicyTrustedAppsFlyout } from './policy_trusted_apps_flyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx deleted file mode 100644 index a9aabbdf34980..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { PolicyTrustedAppsFlyout } from './policy_trusted_apps_flyout'; -import * as reactTestingLibrary from '@testing-library/react'; -import { fireEvent } from '@testing-library/dom'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { MiddlewareActionSpyHelper } from '../../../../../../common/store/test_utils'; - -import { PolicyDetailsState } from '../../../types'; -import { createLoadedResourceState, isLoadedResourceState } from '../../../../../state'; -import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; -import { trustedAppsAllHttpMocks } from '../../../../mocks'; -import { HttpFetchOptionsWithPath } from 'kibana/public'; -import { isArtifactByPolicy } from '../../../../../../../common/endpoint/service/artifacts'; - -jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); - -let mockedContext: AppContextTestRender; -let waitForAction: MiddlewareActionSpyHelper['waitForAction']; -let render: () => ReturnType; -let mockedApis: ReturnType; -const act = reactTestingLibrary.act; -let getState: () => PolicyDetailsState; - -describe('Policy trusted apps flyout', () => { - beforeEach(() => { - mockedContext = createAppRootMockRenderer(); - waitForAction = mockedContext.middlewareSpy.waitForAction; - mockedApis = trustedAppsAllHttpMocks(mockedContext.coreStart.http); - getState = () => mockedContext.store.getState().management.policyDetails; - render = () => mockedContext.render(); - - const getTaListApiResponseMock = - mockedApis.responseProvider.trustedAppsList.getMockImplementation(); - mockedApis.responseProvider.trustedAppsList.mockImplementation((options) => { - const response = getTaListApiResponseMock!(options); - response.data = response.data.filter((ta) => isArtifactByPolicy(ta)); - return response; - }); - }); - - afterEach(() => reactTestingLibrary.cleanup()); - - it('should renders flyout open correctly without assignable data', async () => { - const waitAssignableListExist = waitForAction('policyArtifactsAssignableListExistDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - mockedApis.responseProvider.trustedAppsList.mockReturnValue({ - data: [], - total: 0, - per_page: 10, - page: 1, - }); - - const component = render(); - - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' })); - - await waitForAction('policyArtifactsAssignableListPageDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - await waitAssignableListExist; - - expect(component.getByTestId('confirmPolicyTrustedAppsFlyout')).not.toBeNull(); - expect(component.getByTestId('noAssignableItemsTrustedAppsFlyout')).not.toBeNull(); - }); - - it('should renders flyout open correctly without data', async () => { - mockedApis.responseProvider.trustedAppsList.mockReturnValue({ - data: [], - total: 0, - per_page: 10, - page: 1, - }); - const component = render(); - - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' })); - await waitForAction('policyArtifactsAssignableListPageDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - mockedContext.store.dispatch({ - type: 'policyArtifactsAssignableListExistDataChanged', - payload: createLoadedResourceState(true), - }); - - expect(component.getByTestId('confirmPolicyTrustedAppsFlyout')).not.toBeNull(); - expect(component.getByTestId('noItemsFoundTrustedAppsFlyout')).not.toBeNull(); - }); - - it('should renders flyout open correctly', async () => { - const component = render(); - - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' })); - await waitForAction('policyArtifactsAssignableListPageDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - expect(component.getByTestId('confirmPolicyTrustedAppsFlyout')).not.toBeNull(); - expect(component.getByTestId('Generated Exception (nng74)_checkbox')).not.toBeNull(); - }); - - it('should confirm flyout action', async () => { - const component = render(); - - mockedContext.history.push( - getPolicyDetailsArtifactsListPath('2d95bec3-b48f-4db7-9622-a2b061cc031d', { show: 'list' }) - ); - await waitForAction('policyArtifactsAssignableListPageDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - // TA name below in the selector matches the 3rd generated trusted app which is policy specific - const tACardCheckbox = component.getByTestId('Generated Exception (nng74)_checkbox'); - - act(() => { - fireEvent.click(tACardCheckbox); - }); - - const waitChangeUrl = waitForAction('userChangedUrl'); - const confirmButton = component.getByTestId('confirmPolicyTrustedAppsFlyout'); - - act(() => { - fireEvent.click(confirmButton); - }); - - await waitChangeUrl; - - const currentLocation = getState().artifacts.location; - - expect(currentLocation.show).toBeUndefined(); - }); - - it('should cancel flyout action', async () => { - const waitChangeUrl = waitForAction('userChangedUrl'); - const component = render(); - - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' })); - await waitForAction('policyArtifactsAssignableListPageDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - const cancelButton = component.getByTestId('cancelPolicyTrustedAppsFlyout'); - - await act(async () => { - fireEvent.click(cancelButton); - }); - - await waitChangeUrl; - const currentLocation = getState().artifacts.location; - expect(currentLocation.show).toBeUndefined(); - }); - - it('should display warning message when too much results', async () => { - const listResponse = { - ...mockedApis.responseProvider.trustedAppsList.getMockImplementation()!({ - query: {}, - } as HttpFetchOptionsWithPath), - total: 101, - }; - mockedApis.responseProvider.trustedAppsList.mockReturnValue(listResponse); - - const component = render(); - - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' })); - await waitForAction('policyArtifactsAssignableListPageDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - expect(component.getByTestId('tooMuchResultsWarningMessageTrustedAppsFlyout')).not.toBeNull(); - }); - - it('should not display warning message when few results', async () => { - const component = render(); - - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' })); - await waitForAction('policyArtifactsAssignableListPageDataChanged', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - expect(component.queryByTestId('tooMuchResultsWarningMessageTrustedAppsFlyout')).toBeNull(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx deleted file mode 100644 index a5bbff4a644b2..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.tsx +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useState, useCallback, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; - -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { isEmpty, without } from 'lodash/fp'; -import { - EuiButton, - EuiTitle, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiSpacer, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiCallOut, - EuiEmptyPrompt, -} from '@elastic/eui'; -import { Dispatch } from 'redux'; -import { - policyDetails, - getAssignableArtifactsList, - getAssignableArtifactsListIsLoading, - getUpdateArtifactsIsLoading, - getUpdateArtifactsLoaded, - getAssignableArtifactsListExist, - getAssignableArtifactsListExistIsLoading, -} from '../../../store/policy_details/selectors'; -import { - usePolicyDetailsNavigateCallback, - usePolicyDetailsSelector, - usePolicyTrustedAppsNotification, -} from '../../policy_hooks'; -import { PolicyArtifactsAssignableList } from '../../artifacts/assignable'; -import { SearchExceptions } from '../../../../../components/search_exceptions'; -import { AppAction } from '../../../../../../common/store/actions'; -import { MaybeImmutable, TrustedApp } from '../../../../../../../common/endpoint/types'; - -export const PolicyTrustedAppsFlyout = React.memo(() => { - usePolicyTrustedAppsNotification(); - const dispatch = useDispatch>(); - const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); - const policyItem = usePolicyDetailsSelector(policyDetails); - const assignableArtifactsList = usePolicyDetailsSelector(getAssignableArtifactsList); - const isAssignableArtifactsListLoading = usePolicyDetailsSelector( - getAssignableArtifactsListIsLoading - ); - const isUpdateArtifactsLoading = usePolicyDetailsSelector(getUpdateArtifactsIsLoading); - const isUpdateArtifactsLoaded = usePolicyDetailsSelector(getUpdateArtifactsLoaded); - const isAssignableArtifactsListExist = usePolicyDetailsSelector(getAssignableArtifactsListExist); - const isAssignableArtifactsListExistLoading = usePolicyDetailsSelector( - getAssignableArtifactsListExistIsLoading - ); - - const navigateCallback = usePolicyDetailsNavigateCallback(); - - const policyName = policyItem?.name ?? ''; - - const handleListFlyoutClose = useCallback( - () => - navigateCallback({ - show: undefined, - }), - [navigateCallback] - ); - - useEffect(() => { - if (isUpdateArtifactsLoaded) { - handleListFlyoutClose(); - dispatch({ - type: 'policyArtifactsUpdateTrustedAppsChanged', - payload: { type: 'UninitialisedResourceState' }, - }); - } - }, [dispatch, handleListFlyoutClose, isUpdateArtifactsLoaded]); - - const handleOnConfirmAction = useCallback(() => { - dispatch({ - type: 'policyArtifactsUpdateTrustedApps', - payload: { - action: 'assign', - artifacts: selectedArtifactIds.map>((selectedId) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return assignableArtifactsList?.data?.find((trustedApp) => trustedApp.id === selectedId)!; - }), - }, - }); - }, [assignableArtifactsList?.data, dispatch, selectedArtifactIds]); - - const handleOnSearch = useCallback( - (filter) => { - dispatch({ - type: 'policyArtifactsAssignableListPageDataFilter', - payload: { filter }, - }); - }, - [dispatch] - ); - - const searchWarningMessage = useMemo( - () => ( - <> - - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.searchWarning.text', - { - defaultMessage: - 'Only the first 100 trusted applications are displayed. Please use the search bar to refine the results.', - } - )} - - - - ), - [] - ); - - const canShowPolicyArtifactsAssignableList = useMemo( - () => - isAssignableArtifactsListExistLoading || - isAssignableArtifactsListLoading || - !isEmpty(assignableArtifactsList?.data), - [ - assignableArtifactsList?.data, - isAssignableArtifactsListExistLoading, - isAssignableArtifactsListLoading, - ] - ); - - const entriesExists = useMemo( - () => isEmpty(assignableArtifactsList?.data) && isAssignableArtifactsListExist, - [assignableArtifactsList?.data, isAssignableArtifactsListExist] - ); - - return ( - - - -

- -

-
- - -
- - {(assignableArtifactsList?.total || 0) > 100 ? searchWarningMessage : null} - - - - {canShowPolicyArtifactsAssignableList ? ( - { - setSelectedArtifactIds((currentSelectedArtifactIds) => - selected - ? [...currentSelectedArtifactIds, artifactId] - : without([artifactId], currentSelectedArtifactIds) - ); - }} - /> - ) : entriesExists ? ( - - } - /> - ) : ( - - } - /> - )} - - - - - - - - - - - - - - - -
- ); -}); - -PolicyTrustedAppsFlyout.displayName = 'PolicyTrustedAppsFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/index.ts deleted file mode 100644 index 1360b7ba60e37..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { PolicyTrustedAppsLayout } from './layout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts deleted file mode 100644 index 6819bc1695cfa..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { PolicyTrustedAppsLayout } from './policy_trusted_apps_layout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx deleted file mode 100644 index c0e0cbc71500e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { PolicyTrustedAppsLayout } from './policy_trusted_apps_layout'; -import * as reactTestingLibrary from '@testing-library/react'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { MiddlewareActionSpyHelper } from '../../../../../../common/store/test_utils'; - -import { createLoadedResourceState, isLoadedResourceState } from '../../../../../state'; -import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; -import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; -import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; -import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; -import { PACKAGE_POLICY_API_ROOT, AGENT_API_ROUTES } from '../../../../../../../../fleet/common'; -import { trustedAppsAllHttpMocks } from '../../../../mocks'; -import { HttpFetchOptionsWithPath } from 'kibana/public'; -import { ExceptionsListItemGenerator } from '../../../../../../../common/endpoint/data_generators/exceptions_list_item_generator'; - -jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); -const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; - -let mockedContext: AppContextTestRender; -let waitForAction: MiddlewareActionSpyHelper['waitForAction']; -let render: () => ReturnType; -let coreStart: AppContextTestRender['coreStart']; -let http: typeof coreStart.http; -let mockedApis: ReturnType; -const generator = new EndpointDocGenerator(); - -describe('Policy trusted apps layout', () => { - beforeEach(() => { - mockedContext = createAppRootMockRenderer(); - http = mockedContext.coreStart.http; - - const policyListApiHandlers = policyListApiPathHandlers(); - - http.get.mockImplementation((...args) => { - const [path] = args; - if (typeof path === 'string') { - // GET datasouce - if (path === `${PACKAGE_POLICY_API_ROOT}/1234`) { - return Promise.resolve({ - item: generator.generatePolicyPackagePolicy(), - success: true, - }); - } - - // GET Agent status for agent policy - if (path === `${AGENT_API_ROUTES.STATUS_PATTERN}`) { - return Promise.resolve({ - results: { events: 0, total: 5, online: 3, error: 1, offline: 1 }, - success: true, - }); - } - - // Get package data - // Used in tests that route back to the list - if (policyListApiHandlers[path]) { - return Promise.resolve(policyListApiHandlers[path]()); - } - } - - return Promise.reject(new Error(`unknown API call (not MOCKED): ${path}`)); - }); - - mockedApis = trustedAppsAllHttpMocks(http); - waitForAction = mockedContext.middlewareSpy.waitForAction; - render = () => mockedContext.render(); - }); - - afterAll(() => { - mockUseEndpointPrivileges.mockReset(); - }); - - afterEach(() => reactTestingLibrary.cleanup()); - - it('should renders layout with no existing TA data', async () => { - mockedApis.responseProvider.trustedAppsList.mockImplementation(() => ({ - data: [], - page: 1, - per_page: 10, - total: 0, - })); - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); - const component = render(); - - await waitForAction('policyArtifactsHasTrustedApps', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - expect(component.getByTestId('policy-trusted-apps-empty-unexisting')).not.toBeNull(); - }); - - it('should renders layout with no assigned TA data', async () => { - mockedApis.responseProvider.trustedAppsList.mockImplementation(() => ({ - data: [], - page: 1, - per_page: 10, - total: 0, - })); - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); - const component = render(); - - await waitForAction('policyArtifactsHasTrustedApps', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - mockedContext.store.dispatch({ - type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadedResourceState({ data: [], total: 1 }), - }); - - expect(component.getByTestId('policy-trusted-apps-empty-unassigned')).not.toBeNull(); - }); - - it('should renders layout with data', async () => { - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); - const component = render(); - - await waitForAction('policyArtifactsHasTrustedApps', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - expect(component.getAllByTestId('policyTrustedAppsGrid-card')).toHaveLength(10); - }); - - it('should renders layout with data but no results', async () => { - mockedApis.responseProvider.trustedAppsList.mockImplementation( - (options: HttpFetchOptionsWithPath) => { - const hasAnyQuery = - '(exception-list-agnostic.attributes.tags:"policy:1234" OR exception-list-agnostic.attributes.tags:"policy:all")'; - if (options.query?.filter === hasAnyQuery) { - const exceptionsGenerator = new ExceptionsListItemGenerator('seed'); - return { - data: Array.from({ length: 10 }, () => - exceptionsGenerator.generate({ os_types: ['windows'] }) - ), - total: 10, - page: 0, - per_page: 10, - }; - } else { - return { data: [], total: 0, page: 0, per_page: 10 }; - } - } - ); - - const component = render(); - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { filter: 'search' })); - - await waitForAction('policyArtifactsHasTrustedApps', { - validate: (action) => isLoadedResourceState(action.payload), - }); - - expect(component.queryAllByTestId('policyTrustedAppsGrid-card')).toHaveLength(0); - expect(component.queryByTestId('policy-trusted-apps-empty-unassigned')).toBeNull(); - expect(component.queryByTestId('policy-trusted-apps-empty-unexisting')).toBeNull(); - }); - - it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { - mockUseEndpointPrivileges.mockReturnValue( - getEndpointPrivilegesInitialStateMock({ - canCreateArtifactsByPolicy: false, - }) - ); - const component = render(); - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); - - await waitForAction('assignedTrustedAppsListStateChanged'); - - mockedContext.store.dispatch({ - type: 'policyArtifactsDeosAnyTrustedAppExists', - payload: createLoadedResourceState(true), - }); - expect(component.queryByTestId('assign-ta-button')).toBeNull(); - }); - - it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { - mockUseEndpointPrivileges.mockReturnValue( - getEndpointPrivilegesInitialStateMock({ - canCreateArtifactsByPolicy: false, - }) - ); - const component = render(); - mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); - - await waitForAction('assignedTrustedAppsListStateChanged'); - expect(component.queryByTestId('assignTrustedAppButton')).toBeNull(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx deleted file mode 100644 index dd89cca43c10d..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiButton, - EuiTitle, - EuiPageHeader, - EuiPageHeaderSection, - EuiPageContent, - EuiText, - EuiSpacer, - EuiLink, -} from '@elastic/eui'; -import { PolicyTrustedAppsEmptyUnassigned, PolicyTrustedAppsEmptyUnexisting } from '../empty'; -import { - getCurrentArtifactsLocation, - getDoesTrustedAppExists, - policyDetails, - doesTrustedAppExistsLoading, - getTotalPolicyTrustedAppsListPagination, - getHasTrustedApps, - getIsLoadedHasTrustedApps, -} from '../../../store/policy_details/selectors'; -import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; -import { PolicyTrustedAppsFlyout } from '../flyout'; -import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; -import { useAppUrl } from '../../../../../../common/lib/kibana'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { getTrustedAppsListPath } from '../../../../../common/routing'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { ManagementPageLoader } from '../../../../../components/management_page_loader'; - -export const PolicyTrustedAppsLayout = React.memo(() => { - const { getAppUrl } = useAppUrl(); - const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); - const doesTrustedAppExists = usePolicyDetailsSelector(getDoesTrustedAppExists); - const isDoesTrustedAppExistsLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); - const policyItem = usePolicyDetailsSelector(policyDetails); - const navigateCallback = usePolicyDetailsNavigateCallback(); - const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const totalAssignedCount = usePolicyDetailsSelector(getTotalPolicyTrustedAppsListPagination); - const hasTrustedApps = usePolicyDetailsSelector(getHasTrustedApps); - const isLoadedHasTrustedApps = usePolicyDetailsSelector(getIsLoadedHasTrustedApps); - - const showListFlyout = location.show === 'list'; - - const assignTrustedAppButton = useMemo( - () => ( - - navigateCallback({ - show: 'list', - }) - } - > - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.layout.assignToPolicy', - { - defaultMessage: 'Assign trusted applications to policy', - } - )} - - ), - [navigateCallback] - ); - - const isDisplaysEmptyStateLoading = useMemo( - () => !isLoadedHasTrustedApps || isDoesTrustedAppExistsLoading, - [isLoadedHasTrustedApps, isDoesTrustedAppExistsLoading] - ); - - const displaysEmptyState = useMemo( - () => !isDisplaysEmptyStateLoading && !hasTrustedApps, - [isDisplaysEmptyStateLoading, hasTrustedApps] - ); - - const displayHeaderAndContent = useMemo( - () => !isDisplaysEmptyStateLoading && !displaysEmptyState && isLoadedHasTrustedApps, - [displaysEmptyState, isDisplaysEmptyStateLoading, isLoadedHasTrustedApps] - ); - - const aboutInfo = useMemo(() => { - const link = ( - - - - ); - - return ( - - ); - }, [getAppUrl, totalAssignedCount]); - - return policyItem ? ( -
- {displayHeaderAndContent ? ( - <> - - - -

- {i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.layout.title', - { - defaultMessage: 'Assigned trusted applications', - } - )} -

-
- - - - -

{aboutInfo}

-
-
- - - {canCreateArtifactsByPolicy && assignTrustedAppButton} - -
- - - - ) : null} - - {displaysEmptyState && !isDoesTrustedAppExistsLoading ? ( - doesTrustedAppExists ? ( - - ) : ( - - ) - ) : displayHeaderAndContent ? ( - - ) : ( - - )} - - {canCreateArtifactsByPolicy && showListFlyout ? : null} -
- ) : null; -}); - -PolicyTrustedAppsLayout.displayName = 'PolicyTrustedAppsLayout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx deleted file mode 100644 index da304adc2db44..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; -import { PolicyTrustedAppsList, PolicyTrustedAppsListProps } from './policy_trusted_apps_list'; -import React from 'react'; -import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; -import { isFailedResourceState, isLoadedResourceState } from '../../../../../state'; -import { fireEvent, within, act, waitFor } from '@testing-library/react'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; -import { EndpointPrivileges } from '../../../../../../../common/endpoint/types'; - -jest.mock('../../../../../../common/components/user_privileges'); -const mockUseUserPrivileges = useUserPrivileges as jest.Mock; - -describe('when rendering the PolicyTrustedAppsList', () => { - // The index (zero based) of the card created by the generator that is policy specific - const POLICY_SPECIFIC_CARD_INDEX = 2; - - let appTestContext: AppContextTestRender; - let renderResult: ReturnType; - let render: (waitForLoadedState?: boolean) => Promise>; - let mockedApis: ReturnType; - let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; - let componentRenderProps: PolicyTrustedAppsListProps; - - const loadedUserEndpointPrivilegesState = ( - endpointOverrides: Partial = {} - ): EndpointPrivileges => ({ - ...getEndpointPrivilegesInitialStateMock(), - ...endpointOverrides, - }); - - const getCardByIndexPosition = (cardIndex: number = 0) => { - const card = renderResult.getAllByTestId('policyTrustedAppsGrid-card')[cardIndex]; - - if (!card) { - throw new Error(`Card at index [${cardIndex}] not found`); - } - - return card; - }; - - const toggleCardExpandCollapse = (cardIndex: number = 0) => { - act(() => { - fireEvent.click( - within(getCardByIndexPosition(cardIndex)).getByTestId( - 'policyTrustedAppsGrid-card-header-expandCollapse' - ) - ); - }); - }; - - const toggleCardActionMenu = async (cardIndex: number = 0) => { - act(() => { - fireEvent.click( - within(getCardByIndexPosition(cardIndex)).getByTestId( - 'policyTrustedAppsGrid-card-header-actions-button' - ) - ); - }); - - await waitFor(() => - expect(renderResult.getByTestId('policyTrustedAppsGrid-card-header-actions-contextMenuPanel')) - ); - }; - - afterAll(() => { - mockUseUserPrivileges.mockReset(); - }); - beforeEach(() => { - appTestContext = createAppRootMockRenderer(); - mockUseUserPrivileges.mockReturnValue({ - ...mockUseUserPrivileges(), - endpointPrivileges: loadedUserEndpointPrivilegesState(), - }); - - mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); - waitForAction = appTestContext.middlewareSpy.waitForAction; - componentRenderProps = { policyId: '9f08b220-342d-4c8d-8971-4cf96adcac29', policyName: 'test' }; - - render = async (waitForLoadedState: boolean = true) => { - appTestContext.history.push( - getPolicyDetailsArtifactsListPath('ddf6570b-9175-4a6d-b288-61a09771c647') - ); - const trustedAppDataReceived = waitForLoadedState - ? waitForAction('assignedTrustedAppsListStateChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }) - : Promise.resolve(); - - const checkTrustedAppDataAssignedReceived = waitForLoadedState - ? waitForAction('policyArtifactsHasTrustedApps', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }) - : Promise.resolve(); - - renderResult = appTestContext.render(); - await checkTrustedAppDataAssignedReceived; - await trustedAppDataReceived; - - return renderResult; - }; - }); - - it('should show total number of of items being displayed', async () => { - await render(); - - expect(renderResult.getByTestId('policyDetailsTrustedAppsCount').textContent).toBe( - 'Showing 20 trusted applications' - ); - }); - - it('should NOT show total number if `hideTotalShowingLabel` prop is true', async () => { - componentRenderProps.hideTotalShowingLabel = true; - await render(); - - expect(renderResult.queryByTestId('policyDetailsTrustedAppsCount')).toBeNull(); - }); - - it('should show card grid', async () => { - await render(); - - expect(renderResult.getByTestId('policyTrustedAppsGrid')).toBeTruthy(); - await expect(renderResult.findAllByTestId('policyTrustedAppsGrid-card')).resolves.toHaveLength( - 10 - ); - }); - - it('should expand cards', async () => { - await render(); - // expand - toggleCardExpandCollapse(); - toggleCardExpandCollapse(4); - - await waitFor(() => - expect( - renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions') - ).toHaveLength(2) - ); - }); - - it('should collapse cards', async () => { - await render(); - - // expand - toggleCardExpandCollapse(); - toggleCardExpandCollapse(4); - - await waitFor(() => - expect( - renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions') - ).toHaveLength(2) - ); - - // collapse - toggleCardExpandCollapse(); - toggleCardExpandCollapse(4); - - await waitFor(() => - expect( - renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions') - ).toHaveLength(0) - ); - }); - - it('should show action menu on card', async () => { - await render(); - expect( - renderResult.getAllByTestId('policyTrustedAppsGrid-card-header-actions-button') - ).toHaveLength(10); - }); - - it('should navigate to trusted apps page when view full details action is clicked', async () => { - await render(); - await toggleCardActionMenu(); - act(() => { - fireEvent.click(renderResult.getByTestId('policyTrustedAppsGrid-viewFullDetailsAction')); - }); - - expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith( - APP_UI_ID, - expect.objectContaining({ - path: '/administration/trusted_apps?filter=6f12b025-fcb0-4db4-99e5-4927e3502bb8', - }) - ); - }); - - it('should show dialog when remove action is clicked', async () => { - await render(); - await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX); - act(() => { - fireEvent.click(renderResult.getByTestId('policyTrustedAppsGrid-removeAction')); - }); - - await waitFor(() => expect(renderResult.getByTestId('confirmModalBodyText'))); - }); - - describe('and artifact is policy specific', () => { - const renderAndClickOnEffectScopePopupButton = async () => { - const retrieveAllPolicies = waitForAction('policyDetailsListOfAllPoliciesStateChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }); - await render(); - await retrieveAllPolicies; - act(() => { - fireEvent.click( - within(getCardByIndexPosition(POLICY_SPECIFIC_CARD_INDEX)).getByTestId( - 'policyTrustedAppsGrid-card-header-effectScope-popupMenu-button' - ) - ); - }); - await waitFor(() => - expect( - renderResult.getByTestId( - 'policyTrustedAppsGrid-card-header-effectScope-popupMenu-popoverPanel' - ) - ) - ); - }; - - it('should display policy names on assignment context menu', async () => { - await renderAndClickOnEffectScopePopupButton(); - - expect( - renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-0') - .textContent - ).toEqual('Endpoint Policy 0View details'); - expect( - renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-1') - .textContent - ).toEqual('Endpoint Policy 1View details'); - }); - - it('should navigate to policy details when clicking policy on assignment context menu', async () => { - await renderAndClickOnEffectScopePopupButton(); - - act(() => { - fireEvent.click( - renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-0') - ); - }); - - expect(appTestContext.history.location.pathname).toEqual( - '/administration/policy/ddf6570b-9175-4a6d-b288-61a09771c647/settings' - ); - }); - }); - - it('should handle pagination changes', async () => { - await render(); - - expect(appTestContext.history.location.search).not.toBeTruthy(); - - act(() => { - fireEvent.click(renderResult.getByTestId('pagination-button-next')); - }); - - expect(appTestContext.history.location.search).toMatch('?page_index=1'); - }); - - it('should reset `pageIndex` when a new pageSize is selected', async () => { - await render(); - // page ahead - act(() => { - fireEvent.click(renderResult.getByTestId('pagination-button-next')); - }); - await waitFor(() => { - expect(appTestContext.history.location.search).toBeTruthy(); - }); - - // now change the page size - await act(async () => { - fireEvent.click(renderResult.getByTestId('tablePaginationPopoverButton')); - await waitFor(() => expect(renderResult.getByTestId('tablePagination-50-rows'))); - }); - act(() => { - fireEvent.click(renderResult.getByTestId('tablePagination-50-rows')); - }); - - expect(appTestContext.history.location.search).toMatch('?page_size=50'); - }); - - it('should show toast message if trusted app list api call fails', async () => { - const error = new Error('oh no'); - // @ts-expect-error - mockedApis.responseProvider.trustedAppsList.mockRejectedValue(error); - await render(false); - await act(async () => { - await waitForAction('assignedTrustedAppsListStateChanged', { - validate: ({ payload }) => isFailedResourceState(payload), - }); - }); - - expect(appTestContext.startServices.notifications.toasts.addError).toHaveBeenCalledWith( - error, - expect.objectContaining({ - title: expect.any(String), - }) - ); - }); - - it('does not show remove option in actions menu if license is downgraded to gold or below', async () => { - mockUseUserPrivileges.mockReturnValue({ - ...mockUseUserPrivileges(), - endpointPrivileges: loadedUserEndpointPrivilegesState({ - canCreateArtifactsByPolicy: false, - }), - }); - await render(); - await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX); - - expect(renderResult.queryByTestId('policyTrustedAppsGrid-removeAction')).toBeNull(); - }); - - it('should handle search changes', async () => { - await render(); - - expect(appTestContext.history.location.search).not.toBeTruthy(); - - act(() => { - fireEvent.change(renderResult.getByTestId('searchField'), { - target: { value: 'search' }, - }); - fireEvent.submit(renderResult.getByTestId('searchField')); - }); - - expect(appTestContext.history.location.search).toMatch('?filter=search'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx deleted file mode 100644 index 54dabf87f474b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { EuiSpacer, EuiText, Pagination } from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { - ArtifactCardGrid, - ArtifactCardGridCardComponentProps, - ArtifactCardGridProps, -} from '../../../../../components/artifact_card_grid'; -import { usePolicyDetailsSelector, usePolicyDetailsNavigateCallback } from '../../policy_hooks'; -import { - getCurrentArtifactsLocation, - getPolicyTrustedAppList, - getPolicyTrustedAppListError, - getPolicyTrustedAppsListPagination, - getTrustedAppsAllPoliciesById, - isPolicyTrustedAppListLoading, - getCurrentPolicyArtifactsFilter, -} from '../../../store/policy_details/selectors'; -import { - getPolicyDetailPath, - getPolicyDetailsArtifactsListPath, - getTrustedAppsListPath, -} from '../../../../../common/routing'; -import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types'; -import { useAppUrl, useToasts } from '../../../../../../common/lib/kibana'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { SearchExceptions } from '../../../../../components/search_exceptions'; -import { ContextMenuItemNavByRouterProps } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; -import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; -import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; -import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { useGetLinkTo } from '../empty/use_policy_trusted_apps_empty_hooks'; - -const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; - -export interface PolicyTrustedAppsListProps { - hideTotalShowingLabel?: boolean; - policyId: string; - policyName: string; -} - -export const PolicyTrustedAppsList = memo( - ({ hideTotalShowingLabel = false, policyId, policyName }) => { - const getTestId = useTestIdGenerator(DATA_TEST_SUBJ); - const toasts = useToasts(); - const history = useHistory(); - const { getAppUrl } = useAppUrl(); - const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); - const defaultFilter = usePolicyDetailsSelector(getCurrentPolicyArtifactsFilter); - const trustedAppItems = usePolicyDetailsSelector(getPolicyTrustedAppList); - const pagination = usePolicyDetailsSelector(getPolicyTrustedAppsListPagination); - const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); - const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById); - const trustedAppsApiError = usePolicyDetailsSelector(getPolicyTrustedAppListError); - const navigateCallback = usePolicyDetailsNavigateCallback(); - const { state } = useGetLinkTo(policyId, policyName); - - const [isCardExpanded, setCardExpanded] = useState>({}); - const [trustedAppsForRemoval, setTrustedAppsForRemoval] = useState([]); - const [showRemovalModal, setShowRemovalModal] = useState(false); - - const handlePageChange = useCallback( - ({ pageIndex, pageSize }) => { - history.push( - getPolicyDetailsArtifactsListPath(policyId, { - ...urlParams, - // If user changed page size, then reset page index back to the first page - page_index: pageSize !== pagination.pageSize ? 0 : pageIndex, - page_size: pageSize, - }) - ); - }, - [history, pagination.pageSize, policyId, urlParams] - ); - - const handleExpandCollapse = useCallback( - ({ expanded, collapsed }) => { - const newCardExpandedSettings: Record = {}; - - for (const trustedApp of expanded) { - newCardExpandedSettings[trustedApp.id] = true; - } - - for (const trustedApp of collapsed) { - newCardExpandedSettings[trustedApp.id] = false; - } - - setCardExpanded(newCardExpandedSettings); - }, - [] - ); - - const totalItemsCountLabel = useMemo(() => { - return i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.totalCount', { - defaultMessage: - 'Showing {totalItemsCount, plural, one {# trusted application} other {# trusted applications}}', - values: { totalItemsCount: pagination.totalItemCount }, - }); - }, [pagination.totalItemCount]); - - const cardProps = useMemo< - Map, ArtifactCardGridCardComponentProps> - >(() => { - const newCardProps = new Map(); - - for (const trustedApp of trustedAppItems) { - const isGlobal = trustedApp.effectScope.type === 'global'; - const viewUrlPath = getTrustedAppsListPath({ filter: trustedApp.id }); - const assignedPoliciesMenuItems: ArtifactEntryCollapsibleCardProps['policies'] = - trustedApp.effectScope.type === 'global' - ? undefined - : trustedApp.effectScope.policies.reduce< - Required['policies'] - >((byIdPolicies, trustedAppAssignedPolicyId) => { - if (!allPoliciesById[trustedAppAssignedPolicyId]) { - byIdPolicies[trustedAppAssignedPolicyId] = { - children: trustedAppAssignedPolicyId, - }; - return byIdPolicies; - } - - const policyDetailsPath = getPolicyDetailPath(trustedAppAssignedPolicyId); - - const thisPolicyMenuProps: ContextMenuItemNavByRouterProps = { - navigateAppId: APP_UI_ID, - navigateOptions: { - path: policyDetailsPath, - }, - href: getAppUrl({ path: policyDetailsPath }), - children: allPoliciesById[trustedAppAssignedPolicyId].name, - }; - - byIdPolicies[trustedAppAssignedPolicyId] = thisPolicyMenuProps; - - return byIdPolicies; - }, {}); - - const fullDetailsAction: ArtifactCardGridCardComponentProps['actions'] = [ - { - icon: 'controlsHorizontal', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction', - { defaultMessage: 'View full details' } - ), - href: getAppUrl({ appId: APP_UI_ID, path: viewUrlPath }), - navigateAppId: APP_UI_ID, - navigateOptions: { path: viewUrlPath, state }, - 'data-test-subj': getTestId('viewFullDetailsAction'), - }, - ]; - const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = { - expanded: Boolean(isCardExpanded[trustedApp.id]), - actions: canCreateArtifactsByPolicy - ? [ - ...fullDetailsAction, - { - icon: 'trash', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction', - { defaultMessage: 'Remove from policy' } - ), - onClick: () => { - setTrustedAppsForRemoval([trustedApp]); - setShowRemovalModal(true); - }, - disabled: isGlobal, - toolTipContent: isGlobal - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed', - { - defaultMessage: - 'Globally applied trusted applications cannot be removed from policy.', - } - ) - : undefined, - toolTipPosition: 'top', - 'data-test-subj': getTestId('removeAction'), - }, - ] - : fullDetailsAction, - - policies: assignedPoliciesMenuItems, - }; - - newCardProps.set(trustedApp, thisTrustedAppCardProps); - } - - return newCardProps; - }, [ - allPoliciesById, - getAppUrl, - getTestId, - isCardExpanded, - trustedAppItems, - canCreateArtifactsByPolicy, - state, - ]); - - const provideCardProps = useCallback['cardComponentProps']>( - (item) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return cardProps.get(item as Immutable)!; - }, - [cardProps] - ); - - const handleRemoveModalClose = useCallback(() => { - setShowRemovalModal(false); - }, []); - - // Anytime a new set of data (trusted apps) is retrieved, reset the card expand state - useEffect(() => { - setCardExpanded({}); - }, [trustedAppItems]); - - // if an error occurred while loading the data, show toast - useEffect(() => { - if (trustedAppsApiError) { - toasts.addError(trustedAppsApiError as unknown as Error, { - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.apiError', - { - defaultMessage: 'Error while retrieving list of trusted applications', - } - ), - }); - } - }, [toasts, trustedAppsApiError]); - - return ( - <> - { - navigateCallback({ filter }); - }} - /> - - {!hideTotalShowingLabel && ( - - {totalItemsCountLabel} - - )} - - - - - - {showRemovalModal && ( - - )} - - ); - } -); -PolicyTrustedAppsList.displayName = 'PolicyTrustedAppsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx deleted file mode 100644 index 676080d180a6a..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.test.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; -import { isFailedResourceState, isLoadedResourceState } from '../../../../../state'; -import React from 'react'; -import { fireEvent, act } from '@testing-library/react'; -import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; -import { - RemoveTrustedAppFromPolicyModal, - RemoveTrustedAppFromPolicyModalProps, -} from './remove_trusted_app_from_policy_modal'; -import { - PolicyArtifactsUpdateTrustedApps, - PolicyDetailsTrustedAppsRemoveListStateChanged, -} from '../../../store/policy_details/action/policy_trusted_apps_action'; -import { Immutable } from '../../../../../../../common/endpoint/types'; -import { HttpFetchOptionsWithPath } from 'kibana/public'; -import { exceptionListItemToTrustedApp } from '../../../../trusted_apps/service/mappers'; - -describe('When using the RemoveTrustedAppFromPolicyModal component', () => { - let appTestContext: AppContextTestRender; - let renderResult: ReturnType; - let render: (waitForLoadedState?: boolean) => Promise>; - let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; - let mockedApis: ReturnType; - let onCloseHandler: jest.MockedFunction; - let trustedApps: RemoveTrustedAppFromPolicyModalProps['trustedApps']; - - beforeEach(() => { - appTestContext = createAppRootMockRenderer(); - waitForAction = appTestContext.middlewareSpy.waitForAction; - onCloseHandler = jest.fn(); - mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); - trustedApps = [ - // The 3rd trusted app generated by the HTTP mock is a Policy Specific one - exceptionListItemToTrustedApp( - mockedApis.responseProvider.trustedAppsList({ query: {} } as HttpFetchOptionsWithPath) - .data[2] - ), - ]; - - // Delay the Update Trusted App API response so that we can test UI states while the update is underway. - mockedApis.responseProvider.trustedAppUpdate.mockDelay.mockImplementation( - () => - new Promise((resolve) => { - setTimeout(resolve, 100); - }) - ); - - render = async (waitForLoadedState: boolean = true) => { - const pendingDataLoadState = waitForLoadedState - ? Promise.all([ - waitForAction('serverReturnedPolicyDetailsData'), - waitForAction('assignedTrustedAppsListStateChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }), - ]) - : Promise.resolve(); - - appTestContext.history.push( - getPolicyDetailsArtifactsListPath('ddf6570b-9175-4a6d-b288-61a09771c647') - ); - renderResult = appTestContext.render( - - ); - - await pendingDataLoadState; - - return renderResult; - }; - }); - - const getConfirmButton = (): HTMLButtonElement => - renderResult.getByTestId('confirmModalConfirmButton') as HTMLButtonElement; - - const clickConfirmButton = async ( - /* wait for the UI action to the store middleware to initialize the remove */ - waitForUpdateRequestActionDispatch: boolean = false, - /* wait for the removal to succeed */ - waitForRemoveSuccessActionDispatch: boolean = false - ): Promise< - Immutable< - Array - > - > => { - const pendingConfirmStoreAction = waitForAction('policyArtifactsUpdateTrustedApps'); - const pendingRemoveSuccessAction = waitForAction( - 'policyDetailsTrustedAppsRemoveListStateChanged', - { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - } - ); - - act(() => { - fireEvent.click(getConfirmButton()); - }); - - let response: Array< - PolicyArtifactsUpdateTrustedApps | PolicyDetailsTrustedAppsRemoveListStateChanged - > = []; - - if (waitForUpdateRequestActionDispatch || waitForRemoveSuccessActionDispatch) { - const pendingActions: Array< - Promise - > = []; - - if (waitForUpdateRequestActionDispatch) { - pendingActions.push(pendingConfirmStoreAction); - } - - if (waitForRemoveSuccessActionDispatch) { - pendingActions.push(pendingRemoveSuccessAction); - } - - await act(async () => { - response = await Promise.all(pendingActions); - }); - } - - return response; - }; - - const clickCancelButton = () => { - act(() => { - fireEvent.click(renderResult.getByTestId('confirmModalCancelButton')); - }); - }; - - const clickCloseButton = () => { - act(() => { - fireEvent.click(renderResult.baseElement.querySelector('button.euiModal__closeIcon')!); - }); - }; - - it.each([ - ['cancel', clickCancelButton], - ['close', clickCloseButton], - ])('should call `onClose` callback when %s button is clicked', async (__, clickButton) => { - await render(); - clickButton(); - - expect(onCloseHandler).toHaveBeenCalled(); - }); - - it('should dispatch action when confirmed', async () => { - await render(); - const confirmedAction = (await clickConfirmButton(true))[0]; - - expect(confirmedAction!.payload).toEqual({ - action: 'remove', - artifacts: trustedApps, - }); - }); - - it('should disable and show loading state on confirm button while update is underway', async () => { - await render(); - await clickConfirmButton(true); - const confirmButton = getConfirmButton(); - - expect(confirmButton.disabled).toBe(true); - expect(confirmButton.querySelector('.euiLoadingSpinner')).not.toBeNull(); - }); - - it.each([ - ['cancel', clickCancelButton], - ['close', clickCloseButton], - ])( - 'should prevent dialog dismissal if %s button is clicked while update is underway', - async (__, clickButton) => { - await render(); - await clickConfirmButton(true); - clickButton(); - - expect(onCloseHandler).not.toHaveBeenCalled(); - } - ); - - it('should show error toast if removal failed', async () => { - const error = new Error('oh oh'); - mockedApis.responseProvider.trustedAppUpdate.mockImplementation(() => { - throw error; - }); - await render(); - await clickConfirmButton(true); - await act(async () => { - await waitForAction('policyDetailsTrustedAppsRemoveListStateChanged', { - validate({ payload }) { - return isFailedResourceState(payload); - }, - }); - }); - - expect(appTestContext.coreStart.notifications.toasts.addError).toHaveBeenCalledWith( - expect.objectContaining({ message: expect.stringContaining('oh oh') }), - expect.objectContaining({ title: expect.any(String) }) - ); - }); - - it('should show success toast and close modal when removed is successful', async () => { - await render(); - await clickConfirmButton(true, true); - - expect(appTestContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: '"Generated Exception (nng74)" has been removed from Endpoint Policy policy', - title: 'Successfully removed', - }); - }); - - it('should show multiples removal success message', async () => { - trustedApps = [ - ...trustedApps, - { - ...trustedApps[0], - id: '123', - name: 'trusted app 2', - }, - ]; - - await render(); - await clickConfirmButton(true, true); - - expect(appTestContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: '2 trusted applications have been removed from Endpoint Policy policy', - title: 'Successfully removed', - }); - }); - - it('should trigger a refresh of trusted apps list data on successful removal', async () => { - await render(); - const pendingActions = Promise.all([ - // request list refresh - waitForAction('policyDetailsTrustedAppsForceListDataRefresh'), - - // list data refresh received - waitForAction('assignedTrustedAppsListStateChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }), - ]); - await clickConfirmButton(true, true); - - await expect(pendingActions).resolves.toHaveLength(2); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.tsx deleted file mode 100644 index 28ae58c7f11f6..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/remove_trusted_app_from_policy_modal.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback, useEffect, useMemo } from 'react'; -import { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; -import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types'; -import { AppAction } from '../../../../../../common/store/actions'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { - getTrustedAppsIsRemoving, - getTrustedAppsRemovalError, - getTrustedAppsWasRemoveSuccessful, - policyDetails, -} from '../../../store/policy_details/selectors'; -import { useToasts } from '../../../../../../common/lib/kibana'; - -export interface RemoveTrustedAppFromPolicyModalProps { - trustedApps: Immutable; - onClose: () => void; -} - -export const RemoveTrustedAppFromPolicyModal = memo( - ({ trustedApps, onClose }) => { - const toasts = useToasts(); - const dispatch = useDispatch>(); - - const policyName = usePolicyDetailsSelector(policyDetails)?.name; - const isRemoving = usePolicyDetailsSelector(getTrustedAppsIsRemoving); - const removeError = usePolicyDetailsSelector(getTrustedAppsRemovalError); - const wasSuccessful = usePolicyDetailsSelector(getTrustedAppsWasRemoveSuccessful); - - const removedToastMessage: string = useMemo(() => { - const count = trustedApps.length; - - if (count === 0) { - return ''; - } - - if (count > 1) { - return i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successMultiplesToastText', - { - defaultMessage: - '{count} trusted applications have been removed from {policyName} policy', - values: { count, policyName }, - } - ); - } - - return i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successToastText', - { - defaultMessage: '"{trustedAppName}" has been removed from {policyName} policy', - values: { trustedAppName: trustedApps[0].name, policyName }, - } - ); - }, [policyName, trustedApps]); - - const handleModalClose = useCallback(() => { - if (!isRemoving) { - onClose(); - } - }, [isRemoving, onClose]); - - const handleModalConfirm = useCallback(() => { - dispatch({ - type: 'policyArtifactsUpdateTrustedApps', - payload: { action: 'remove', artifacts: trustedApps }, - }); - }, [dispatch, trustedApps]); - - useEffect(() => { - // When component is un-mounted, reset the state for remove in the store - return () => { - dispatch({ type: 'policyDetailsArtifactsResetRemove' }); - }; - }, [dispatch]); - - useEffect(() => { - if (removeError) { - toasts.addError(removeError as unknown as Error, { - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.errorToastTitle', - { - defaultMessage: 'Error while attempt to remove trusted application', - } - ), - }); - } - }, [removeError, toasts]); - - useEffect(() => { - if (wasSuccessful) { - toasts.addSuccess({ - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successToastTitle', - { defaultMessage: 'Successfully removed' } - ), - text: removedToastMessage, - }); - handleModalClose(); - } - }, [handleModalClose, policyName, removedToastMessage, toasts, trustedApps, wasSuccessful]); - - return ( - - -

- -

-
- - - - -

- -

-
-
- ); - } -); -RemoveTrustedAppFromPolicyModal.displayName = 'RemoveTrustedAppFromPolicyModal'; diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts index e5c8d110b63f2..c27d983fec128 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -272,5 +272,24 @@ describe('Exceptions List Api Client', () => { await expect(exceptionsListApiClientInstance.hasData()).resolves.toBe(false); }); + + it('return new instance when HttpCore changes', async () => { + const initialInstance = ExceptionsListApiClient.getInstance( + fakeHttpServices, + getFakeListId(), + getFakeListDefinition() + ); + + fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); + fakeHttpServices = fakeCoreStart.http as jest.Mocked; + + const newInstance = ExceptionsListApiClient.getInstance( + fakeHttpServices, + getFakeListId(), + getFakeListDefinition() + ); + + expect(initialInstance).not.toStrictEqual(newInstance); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts index c995754dd1907..81f9f20182bec 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -83,6 +83,10 @@ export class ExceptionsListApiClient { } } + public isHttp(coreHttp: HttpStart): boolean { + return this.http === coreHttp; + } + /** * Static method to get a fresh or existing instance. * It will ensure we only check and create the list once. @@ -92,7 +96,10 @@ export class ExceptionsListApiClient { listId: string, listDefinition: CreateExceptionListSchema ): ExceptionsListApiClient { - if (!ExceptionsListApiClient.instance.has(listId)) { + if ( + !ExceptionsListApiClient.instance.has(listId) || + !ExceptionsListApiClient.instance.get(listId)?.isHttp(http) + ) { ExceptionsListApiClient.instance.set( listId, new ExceptionsListApiClient(http, listId, listDefinition) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 59dd27f94d1e5..ff06f07cc83cd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6266,6 +6266,19 @@ "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "1つのデータソースが返せるバケットの最大数です。値が大きいとブラウザのレンダリング速度が下がる可能性があります。", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "ヒートマップの最大バケット数", "visTypeVislib.aggResponse.allDocsTitle": "すべてのドキュメント", + "visTypeVislib.functions.pie.help": "パイビジュアライゼーション", + "visTypeVislib.functions.vislib.help": "Vislib ビジュアライゼーション", + "visTypeVislib.vislib.errors.noResultsFoundTitle": "結果が見つかりませんでした", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます({nr})。構成されている最大値は {max} です。", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", + "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", + "visTypeVislib.vislib.legend.loadingLabel": "読み込み中…", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", + "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", + "visTypeVislib.vislib.tooltip.valueLabel": "値", "visTypeGauge.controls.gaugeOptions.alignmentLabel": "アラインメント", "visTypeGauge.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張", "visTypeGauge.controls.gaugeOptions.extendRangeTooltip": "範囲をデータの最高値に広げます。", @@ -6278,8 +6291,6 @@ "visTypeGauge.controls.gaugeOptions.showScaleLabel": "縮尺を表示", "visTypeGauge.controls.gaugeOptions.styleTitle": "スタイル", "visTypeGauge.controls.gaugeOptions.subTextLabel": "サブラベル", - "visTypeVislib.functions.pie.help": "パイビジュアライゼーション", - "visTypeVislib.functions.vislib.help": "Vislib ビジュアライゼーション", "visTypeGauge.gauge.alignmentAutomaticTitle": "自動", "visTypeGauge.gauge.alignmentHorizontalTitle": "横", "visTypeGauge.gauge.alignmentVerticalTitle": "縦", @@ -6293,17 +6304,6 @@ "visTypeGauge.goal.goalTitle": "ゴール", "visTypeGauge.goal.groupTitle": "グループを分割", "visTypeGauge.goal.metricTitle": "メトリック", - "visTypeVislib.vislib.errors.noResultsFoundTitle": "結果が見つかりませんでした", - "visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます({nr})。構成されている最大値は {max} です。", - "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", - "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", - "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", - "visTypeVislib.vislib.legend.loadingLabel": "読み込み中…", - "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", - "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "凡例を切り替える", - "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", - "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", - "visTypeVislib.vislib.tooltip.valueLabel": "値", "visTypeXy.aggResponse.allDocsTitle": "すべてのドキュメント", "visTypeXy.area.areaDescription": "軸と線の間のデータを強調します。", "visTypeXy.area.areaTitle": "エリア", @@ -24200,7 +24200,6 @@ "xpack.securitySolution.endpoint.list.transformFailed.message": "現在、必須の変換{transformId}が失敗しています。通常、これは{transformsPage}で修正できます。ヘルプについては、{docsPage}をご覧ください", "xpack.securitySolution.endpoint.list.transformFailed.restartLink": "変換を再開中", "xpack.securitySolution.endpoint.list.transformFailed.title": "必須の変換が失敗しました", - "xpack.securitySolution.endpoint.olicy.trustedApps.layout.search.placeholder": "次のフィールドで検索:名前、説明、値", "xpack.securitySolution.endpoint.paginatedContent.noItemsFoundTitle": "項目が見つかりません", "xpack.securitySolution.endpoint.policy.advanced": "高度な設定", "xpack.securitySolution.endpoint.policy.advanced.calloutTitle": "十分ご注意ください!", @@ -24324,7 +24323,6 @@ "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", "xpack.securitySolution.endpoint.policy.details.upgradeToPlatinum": "Elastic Platinum へのアップグレード", - "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.backButtonLabel": "{policyName}ポリシー", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.content": "現在、{policyName}に割り当てられたイベントフィルターがありません。今するイベントフィルターを割り当てるか、イベントフィルターページで追加して管理してください。", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.primaryAction": "イベントフィルターの割り当て", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.secondaryAction": "イベントフィルターの管理", @@ -24332,77 +24330,26 @@ "xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.action": "イベントフィルターを追加", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.content": "現在、エンドポイントにはイベントフィルターが適用されていません。", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.title": "イベントフィルターが存在しません", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.about": "{count, plural, other {}} {count}個のイベント{count, plural, other {フィルター}}がこのポリシーに関連付けられています。ここをクリックすると、{link}", "xpack.securitySolution.endpoint.policy.eventFilters.layout.about.viewAllLinkLabel": "すべてのイベントフィルターを表示", "xpack.securitySolution.endpoint.policy.eventFilters.layout.assignToPolicy": "イベントフィルターをポリシーに割り当て", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.cancel": "キャンセル", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.confirm": "{policyName}に割り当てる", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.noAssignable": "このポリシーに割り当てられるイベントフィルターがありません。", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.noResults": "項目が見つかりません", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.searchWarning.text": "最初の100個のイベントフィルターのみが表示されます。検索バーを使用して、結果を絞り込んでください。", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.searchWarning.title": "絞り込まれた検索結果", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.subtitle": "{policyName}に追加するイベントフィルターを選択", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.title": "イベントフィルターの割り当て", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastError.text": "アーチファクトの更新中にエラーが発生しました", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textMultiples": "{count}個のイベントフィルターがリストに追加されました。", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textSingle": "\"{name}\"がイベントフィルターリストに追加されました。", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.title": "成功", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.searh.label": "イベントフィルターの検索", "xpack.securitySolution.endpoint.policy.eventFilters.layout.title": "割り当てられたイベントフィルター", - "xpack.securitySolution.endpoint.policy.eventFilters.list.fullDetailsAction": "詳細を表示", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeAction": "ポリシーから削除", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeActionNotAllowed": "グローバルで適用されたイベントフィルターはポリシーから削除できません。", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.cancelLabel": "キャンセル", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.confirmLabel": "ポリシーから削除", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.errorToastTitle": "イベントフィルターの削除エラー", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.message": "続行していいですか?", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.messageCallout": "このイベントフィルターはこのポリシーからのみ削除されます。イベントフィルターページには表示され、管理できます。", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.successToastTitle": "正常に削除されました", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.title": "ポリシーからイベントフィルターを削除", "xpack.securitySolution.endpoint.policy.eventFilters.list.search.placeholder": "次のフィールドで検索:名前、説明、コメント、値", "xpack.securitySolution.endpoint.policy.eventFilters.list.totalItemCount": "{totalItemsCount, plural, other {#個のイベントフィルター}}を表示中", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.backButtonLabel": "{policyName}ポリシー", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.content": "現在{policyName}に割り当てられたホスト分離例外はありません。今すぐ例外を割り当てるか、ホスト分離例外ページで追加して管理してください。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.primaryAction": "ホスト分離例外の割り当て", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.secondaryAction": "ホスト分離例外の管理", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.title": "割り当てられたホスト分離例外がありません", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unexisting.action": "ホスト分離例外の追加", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unexisting.content": "現在、エンドポイントにはホスト分離例外が適用されていません。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unexisting.title": "ホスト分離例外が存在しません", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.assignToPolicy": "ホスト分離例外をポリシーに割り当てる", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.cancel": "キャンセル", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.confirm": "{policyName}に割り当てる", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.noAssignable": "このポリシーに割り当てられるホスト分離例外はありません。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.noResults": "項目が見つかりません", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.searchWarning.text": "最初の100個のホスト分離例外のみが表示されます。検索バーを使用して、結果を絞り込んでください。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.searchWarning.title": "絞り込まれた検索結果", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.subtitle": "{policyName}に追加するホスト分離例外を選択", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.title": "ホスト分離例外の割り当て", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastError.text": "アーチファクトの更新中にエラーが発生しました", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.textMultiples": "{count}個のホスト分離例外がリストに追加されました。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.textSingle": "\"{name}\"はホスト分離例外リストに追加されました。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.title": "成功", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.searh.label": "ホスト分離例外の検索", "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.about": "{count, plural, other {}} {count}個の{count, plural, other {ホスト分離例外}}がこのポリシーに関連付けられています。ここをクリックすると、{link}", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.fullDetailsAction": "詳細を表示", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeAction": "ポリシーから削除", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeActionNotAllowed": "グローバルで適用されたホスト分離例外はポリシーから削除できません。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.cancelLabel": "キャンセル", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.confirmLabel": "ポリシーから削除", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.errorToastTitle": "ホスト分離例外の削除試行中のエラー", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.message": "続行していいですか?", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.messageCallout": "このホスト分離例外はこのポリシーからのみ削除されます。ホスト分離例外ページには表示され、管理できます。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.successToastTitle": "正常に削除されました", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.title": "ポリシーからホスト分離例外を削除", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.search.placeholder": "次のフィールドで検索:名前、説明、IP", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.title": "割り当てられたホスト分離例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.totalItemCount": "{totalItemsCount, plural, other {#個のホスト分離例外}}を表示中", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.viewAllLinkLabel": "ホスト分離例外を表示", "xpack.securitySolution.endpoint.policy.protections.behavior": "悪意ある動作に対する保護", "xpack.securitySolution.endpoint.policy.protections.malware": "マルウェア保護", "xpack.securitySolution.endpoint.policy.protections.memory": "メモリ脅威に対する保護", "xpack.securitySolution.endpoint.policy.protections.ransomware": "ランサムウェア保護", - "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.backButtonLabel": "{policyName}ポリシー", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.content": "現在、{policyName}に割り当てられたアプリケーションがありません。今すぐ信頼できるアプリケーションを割り当てるか、信頼できるアプリケーションページで管理します。", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.primaryAction": "信頼できるアプリケーションの割り当て", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.secondaryAction": "信頼できるアプリケーションを管理", @@ -24410,36 +24357,19 @@ "xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.action": "信頼できるアプリケーションを追加", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.content": "現在、エンドポイントには信頼できるアプリケーションが適用されていません。", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.title": "信頼できるアプリケーションが存在しません", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.about": "{count, plural, other {}} {count}個の信頼できる{count, plural, other {アプリケーション}}がこのポリシーに関連付けられています。\nここをクリックすると、{link}", "xpack.securitySolution.endpoint.policy.trustedApps.layout.about.viewAllLinkLabel": "信頼できるアプリケーションを表示", "xpack.securitySolution.endpoint.policy.trustedApps.layout.assignToPolicy": "信頼できるアプリケーションをポリシーに割り当てる", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.cancel": "キャンセル", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.confirm": "{policyName}に割り当てる", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.noAssignable": "このポリシーに割り当てられる信頼できるアプリケーションがありません。", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.noResults": "項目が見つかりません", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.searchWarning.text": "最初の100件の信頼できるアプリケーションのみが表示されます。検索バーを使用して、結果を絞り込んでください。", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.searchWarning.title": "絞り込まれた検索結果", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.subtitle": "{policyName}に追加する信頼できるアプリケーションを選択", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.title": "信頼できるアプリケーションの割り当て", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastError.text": "アーチファクトの更新中にエラーが発生しました", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textMultiples": "{count}個の信頼できるアプリケーションがリストに追加されました。", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textSingle": "\"{name}\"は信頼できるアプリケーションリストに追加されました。", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.title": "成功", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.searh.label": "信頼できるアプリケーションを検索", "xpack.securitySolution.endpoint.policy.trustedApps.layout.title": "割り当てられた信頼できるアプリケーション", - "xpack.securitySolution.endpoint.policy.trustedApps.list.apiError": "信頼できるアプリケーションのリストの取得エラー", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction": "ポリシーから削除", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed": "グローバルで適用された信頼できるアプリケーションはポリシーから削除できません。", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.cancelLabel": "キャンセル", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.confirmLabel": "ポリシーから削除", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.errorToastTitle": "信頼できるアプリケーションの削除エラー", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.message": "続行していいですか?", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.messageCallout": "この信頼できるアプリケーションはこのポリシーからのみ削除されます。信頼できるアプリケーションページには表示され、管理できます。", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successMultiplesToastText": "{count}個の信頼できるアプリケーションが{policyName}ポリシーから削除されました", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successToastText": "\"{trustedAppName}\"が{policyName}ポリシーから削除されました", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successToastTitle": "正常に削除されました", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.title": "信頼できるアプリケーションをポリシーから削除", - "xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction": "詳細を表示", "xpack.securitySolution.endpoint.policyDetail.behaviorProtectionTooltip": "悪意のある動作", "xpack.securitySolution.endpoint.policyDetail.filename": "ファイル名", "xpack.securitySolution.endpoint.policyDetail.memoryProtectionTooltip": "メモリ脅威", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5af6782572cda..27d3be935ddc6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6277,6 +6277,19 @@ "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "单个数据源可以返回的最大存储桶数目。较高的数目可能对浏览器呈现性能有负面影响", "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "热图最大存储桶数", "visTypeVislib.aggResponse.allDocsTitle": "所有文档", + "visTypeVislib.functions.pie.help": "饼图可视化", + "visTypeVislib.functions.vislib.help": "Vislib 可视化", + "visTypeVislib.vislib.errors.noResultsFoundTitle": "找不到结果", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", + "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", + "visTypeVislib.vislib.legend.loadingLabel": "正在加载……", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "切换图例", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "切换图例", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}, 切换选项", + "visTypeVislib.vislib.tooltip.fieldLabel": "字段", + "visTypeVislib.vislib.tooltip.valueLabel": "值", "visTypeGauge.controls.gaugeOptions.alignmentLabel": "对齐方式", "visTypeGauge.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围", "visTypeGauge.controls.gaugeOptions.extendRangeTooltip": "将数据范围扩展到数据中的最大值。", @@ -6289,8 +6302,6 @@ "visTypeGauge.controls.gaugeOptions.showScaleLabel": "显示比例", "visTypeGauge.controls.gaugeOptions.styleTitle": "样式", "visTypeGauge.controls.gaugeOptions.subTextLabel": "子标签", - "visTypeVislib.functions.pie.help": "饼图可视化", - "visTypeVislib.functions.vislib.help": "Vislib 可视化", "visTypeGauge.gauge.alignmentAutomaticTitle": "自动", "visTypeGauge.gauge.alignmentHorizontalTitle": "水平", "visTypeGauge.gauge.alignmentVerticalTitle": "垂直", @@ -6304,17 +6315,6 @@ "visTypeGauge.goal.goalTitle": "目标图", "visTypeGauge.goal.groupTitle": "拆分组", "visTypeGauge.goal.metricTitle": "指标", - "visTypeVislib.vislib.errors.noResultsFoundTitle": "找不到结果", - "visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", - "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", - "visTypeVislib.vislib.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", - "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", - "visTypeVislib.vislib.legend.loadingLabel": "正在加载……", - "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "切换图例", - "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "切换图例", - "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}, 切换选项", - "visTypeVislib.vislib.tooltip.fieldLabel": "字段", - "visTypeVislib.vislib.tooltip.valueLabel": "值", "visTypeXy.aggResponse.allDocsTitle": "所有文档", "visTypeXy.area.areaDescription": "突出轴与线之间的数据。", "visTypeXy.area.areaTitle": "面积图", @@ -24229,7 +24229,6 @@ "xpack.securitySolution.endpoint.list.transformFailed.message": "所需的转换 {transformId} 当前失败。多数时候,这可以通过 {transformsPage} 解决。要获取更多帮助,请访问{docsPage}", "xpack.securitySolution.endpoint.list.transformFailed.restartLink": "正在重新启动转换", "xpack.securitySolution.endpoint.list.transformFailed.title": "所需转换失败", - "xpack.securitySolution.endpoint.olicy.trustedApps.layout.search.placeholder": "搜索下面的字段:name、description、value", "xpack.securitySolution.endpoint.paginatedContent.noItemsFoundTitle": "找不到项目", "xpack.securitySolution.endpoint.policy.advanced": "高级设置", "xpack.securitySolution.endpoint.policy.advanced.calloutTitle": "谨慎操作!", @@ -24354,7 +24353,6 @@ "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", "xpack.securitySolution.endpoint.policy.details.upgradeToPlatinum": "升级到 Elastic 白金级", - "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.backButtonLabel": "返回到 {policyName} 策略", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.content": "当前没有事件筛选已分配给 {policyName}。立即分配事件筛选,或在事件筛选页面添加并管理事件筛选。", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.primaryAction": "分配事件筛选", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.secondaryAction": "管理事件筛选", @@ -24362,77 +24360,26 @@ "xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.action": "添加事件筛选", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.content": "当前没有任何事件筛选已应用于您的终端。", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.title": "不存在事件筛选", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.about": "有 {count, plural, other {}} {count} 个事件{count, plural, other {筛选}}与此策略关联。单击此处以 {link}", "xpack.securitySolution.endpoint.policy.eventFilters.layout.about.viewAllLinkLabel": "查看所有事件筛选", "xpack.securitySolution.endpoint.policy.eventFilters.layout.assignToPolicy": "分配事件筛选到策略", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.cancel": "取消", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.confirm": "分配给 {policyName}", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.noAssignable": "没有可分配给此策略的事件筛选。", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.noResults": "找不到项目", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.searchWarning.text": "仅显示前 100 个事件筛选。请使用搜索栏优化结果。", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.searchWarning.title": "有限的搜索结果", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.subtitle": "选择事件筛选以添加到 {policyName}", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.title": "分配事件筛选", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastError.text": "更新项目时出错", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textMultiples": "{count} 个事件筛选已添加到您的列表。", "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textSingle": "“{name}”已添加到您的事件筛选列表。", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.title": "成功", - "xpack.securitySolution.endpoint.policy.eventFilters.layout.searh.label": "搜索事件筛选", "xpack.securitySolution.endpoint.policy.eventFilters.layout.title": "已分配的事件筛选", - "xpack.securitySolution.endpoint.policy.eventFilters.list.fullDetailsAction": "查看完整详情", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeAction": "从策略中移除", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeActionNotAllowed": "无法从策略中移除全局应用的事件筛选。", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.cancelLabel": "取消", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.confirmLabel": "从策略中移除", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.errorToastTitle": "尝试移除事件筛选时出错", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.message": "是否确定要继续?", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.messageCallout": "此事件筛选将仅从该策略中移除,但仍可从事件筛选页面找到并进行管理。", - "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.successToastTitle": "已成功移除", "xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.title": "从策略中移除事件筛选", "xpack.securitySolution.endpoint.policy.eventFilters.list.search.placeholder": "搜索下面的字段:name、description、comments、value", "xpack.securitySolution.endpoint.policy.eventFilters.list.totalItemCount": "正在显示 {totalItemsCount, plural, other {# 个事件筛选}}", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.backButtonLabel": "返回到 {policyName} 策略", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.content": "当前没有主机隔离例外分配给 {policyName}。立即分配例外,或在主机隔离例外页面添加并管理例外。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.primaryAction": "分配主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.secondaryAction": "管理主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unassigned.title": "无已分配的主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unexisting.action": "添加主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unexisting.content": "当前没有主机隔离例外已应用于您的终端。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.empty.unexisting.title": "不存在主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.assignToPolicy": "将主机隔离例外分配给策略", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.cancel": "取消", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.confirm": "分配给 {policyName}", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.noAssignable": "没有可分配给此策略的主机隔离例外。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.noResults": "找不到项目", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.searchWarning.text": "仅显示前 100 个主机隔离例外。请使用搜索栏优化结果。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.searchWarning.title": "有限的搜索结果", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.subtitle": "选择主机隔离例外以添加到 {policyName}", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.title": "分配主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastError.text": "更新项目时出错", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.textMultiples": "已将 {count} 个主机隔离例外添加到您的列表。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.textSingle": "已将“{name}”添加到您的主机隔离例外列表。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.flyout.toastSuccess.title": "成功", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.layout.searh.label": "搜索主机隔离例外", "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.about": "有 {count, plural, other {}} {count} 个{count, plural, other {主机隔离例外}}与此策略关联。单击此处以 {link}", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.fullDetailsAction": "查看完整详情", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeAction": "从策略中移除", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeActionNotAllowed": "无法从策略中移除全局应用的主机隔离例外。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.cancelLabel": "取消", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.confirmLabel": "从策略中移除", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.errorToastTitle": "尝试移除主机隔离例外时出错", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.message": "是否确定要继续?", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.messageCallout": "此主机隔离例外将仅从该策略中移除,但仍可从主机隔离例外页面找到并进行管理。", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.successToastTitle": "已成功移除", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.title": "从策略中移除主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.search.placeholder": "搜索下面的字段:name、description、IP", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.title": "已分配的主机隔离例外", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.totalItemCount": "正显示 {totalItemsCount, plural, other {# 个主机隔离例外}}", - "xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.viewAllLinkLabel": "查看所有主机隔离例外", "xpack.securitySolution.endpoint.policy.protections.behavior": "恶意行为防护", "xpack.securitySolution.endpoint.policy.protections.malware": "恶意软件防护", "xpack.securitySolution.endpoint.policy.protections.memory": "内存威胁防护", "xpack.securitySolution.endpoint.policy.protections.ransomware": "勒索软件防护", - "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.backButtonLabel": "返回到 {policyName} 策略", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.content": "当前没有受信任应用程序已分配给 {policyName}。立即分配受信任的应用程序,或在受信任的应用程序页面添加并管理这些应用程序。", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.primaryAction": "分配受信任的应用程序", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.secondaryAction": "管理受信任的应用程序", @@ -24440,37 +24387,19 @@ "xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.action": "添加受信任的应用程序", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.content": "当前没有受信任的应用程序已应用于您的终端。", "xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.title": "不存在受信任的应用程序", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.about": "有 {count, plural, other {}} {count} 个受信任的{count, plural, other {应用程序}}与此策略关联。单击此处以 {link}", "xpack.securitySolution.endpoint.policy.trustedApps.layout.about.viewAllLinkLabel": "查看所有受信任的应用程序", "xpack.securitySolution.endpoint.policy.trustedApps.layout.assignToPolicy": "将受信任的应用程序分配给策略", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.cancel": "取消", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.confirm": "分配给 {policyName}", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.noAssignable": "没有可分配给此策略的受信任应用程序。", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.noResults": "找不到项目", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.searchWarning.text": "仅显示前 100 个受信任的应用程序。请使用搜索栏优化结果。", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.searchWarning.title": "有限的搜索结果", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.subtitle": "选择受信任的应用程序以添加到 {policyName}", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.title": "分配受信任的应用程序", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastError.text": "更新项目时出错", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textMultiples": "{count} 个受信任的应用程序已添加到列表中。", "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.textSingle": "“{name}”已添加到受信任的应用程序列表。", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastSuccess.title": "成功", - "xpack.securitySolution.endpoint.policy.trustedApps.layout.searh.label": "搜索受信任的应用程序", "xpack.securitySolution.endpoint.policy.trustedApps.layout.title": "已分配的受信任应用程序", - "xpack.securitySolution.endpoint.policy.trustedApps.list.apiError": "检索受信任的应用程序列表时出错", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction": "从策略中移除", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed": "无法从策略中移除全局应用的受信任应用程序。", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.cancelLabel": "取消", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.confirmLabel": "从策略中移除", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.errorToastTitle": "尝试移除受信任的应用程序时出错", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.message": "是否确定要继续?", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.messageCallout": "此受信任的应用程序将从该策略中移除,但仍可从受信任的应用程序页面找到并进行管理。", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successMultiplesToastText": "已从 {policyName} 策略中移除 {count} 个受信任的应用程序", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successToastText": "已从 {policyName} 策略中移除“{trustedAppName}”", - "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.successToastTitle": "已成功移除", "xpack.securitySolution.endpoint.policy.trustedApps.list.removeDialog.title": "从策略中移除受信任的应用程序", - "xpack.securitySolution.endpoint.policy.trustedApps.list.totalCount": "正在显示 {totalItemsCount, plural, other {# 个受信任的应用程序}}", - "xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction": "查看完整详情", "xpack.securitySolution.endpoint.policyDetail.behaviorProtectionTooltip": "恶意行为", "xpack.securitySolution.endpoint.policyDetail.filename": "文件名", "xpack.securitySolution.endpoint.policyDetail.memoryProtectionTooltip": "内存威胁",