diff --git a/packages/deeplinks/security/deep_links.ts b/packages/deeplinks/security/deep_links.ts index c7d5b54fb202a..54b18dcaf9206 100644 --- a/packages/deeplinks/security/deep_links.ts +++ b/packages/deeplinks/security/deep_links.ts @@ -87,5 +87,5 @@ export enum SecurityPageName { entityAnalyticsManagement = 'entity_analytics-management', entityAnalyticsAssetClassification = 'entity_analytics-asset-classification', coverageOverview = 'coverage-overview', - notesManagement = 'notes-management', + notes = 'notes', } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5da9b87a4e267..e2b85fd123f91 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -133,7 +133,7 @@ export const APP_HOST_ISOLATION_EXCEPTIONS_PATH = export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const; export const APP_RESPONSE_ACTIONS_HISTORY_PATH = `${APP_PATH}${RESPONSE_ACTIONS_HISTORY_PATH}` as const; -export const NOTES_MANAGEMENT_PATH = `/notes_management` as const; +export const NOTES_PATH = `${MANAGEMENT_PATH}/notes` as const; // cloud logs to exclude from default index pattern export const EXCLUDE_ELASTIC_CLOUD_INDICES = ['-*elastic-cloud-logs-*']; diff --git a/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts b/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts index 3107bb93de269..f6a51f1d25f4f 100644 --- a/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/app/solution_navigation/links/app_links.ts @@ -6,9 +6,13 @@ */ import { SecurityPageName } from '@kbn/security-solution-navigation'; -import { cloneDeep, remove } from 'lodash'; +import { cloneDeep, remove, find } from 'lodash'; import type { AppLinkItems, LinkItem } from '../../../common/links/types'; -import { createInvestigationsLinkFromTimeline } from './sections/investigations_links'; +import { + createInvestigationsLinkFromNotes, + createInvestigationsLinkFromTimeline, + updateInvestigationsLinkFromNotes, +} from './sections/investigations_links'; import { mlAppLink } from './sections/ml_links'; import { createAssetsLinkFromManage } from './sections/assets_links'; import { createSettingsLinksFromManage } from './sections/settings_links'; @@ -26,6 +30,19 @@ export const solutionAppLinksSwitcher = (appLinks: AppLinkItems): AppLinkItems = solutionAppLinks.push(createInvestigationsLinkFromTimeline(timelineLinkItem)); } + // Remove note link + const investigationsLinkItem = find(solutionAppLinks, { id: SecurityPageName.investigations }); + const [noteLinkItem] = remove(solutionAppLinks, { id: SecurityPageName.notes }); + if (noteLinkItem) { + if (!investigationsLinkItem) { + solutionAppLinks.push(createInvestigationsLinkFromNotes(noteLinkItem)); + } else { + solutionAppLinks.push( + updateInvestigationsLinkFromNotes(investigationsLinkItem, noteLinkItem) + ); + } + } + // Remove manage link const [manageLinkItem] = remove(solutionAppLinks, { id: SecurityPageName.administration }); diff --git a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts index 33bc73ee3ef77..3c4d059474993 100644 --- a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts +++ b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts @@ -6,6 +6,7 @@ */ import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation'; +import IconFilebeatChart from './icons/filebeat_chart'; import { INVESTIGATIONS_PATH } from '../../../../../common/constants'; import { SERVER_APP_ID } from '../../../../../common'; import type { LinkItem } from '../../../../common/links/types'; @@ -21,7 +22,7 @@ const investigationsAppLink: LinkItem = { capabilities: [`${SERVER_APP_ID}.show`], hideTimeline: true, skipUrlState: true, - links: [], // timeline link are added in createInvestigationsLinkFromTimeline + links: [], // timeline and note links are added via the methods below }; export const createInvestigationsLinkFromTimeline = (timelineLink: LinkItem): LinkItem => { @@ -33,6 +34,29 @@ export const createInvestigationsLinkFromTimeline = (timelineLink: LinkItem): Li }; }; +export const createInvestigationsLinkFromNotes = (noteLink: LinkItem): LinkItem => { + return { + ...investigationsAppLink, + links: [{ ...noteLink, description: i18n.NOTE_DESCRIPTION, landingIcon: IconTimelineLazy }], + }; +}; + +export const updateInvestigationsLinkFromNotes = ( + investigationsLink: LinkItem, + noteLink: LinkItem +): LinkItem => { + const currentLinks = investigationsLink.links ?? []; + currentLinks.push({ + ...noteLink, + description: i18n.NOTE_DESCRIPTION, + landingIcon: IconFilebeatChart, + }); + return { + ...investigationsLink, + links: currentLinks, + }; +}; + // navLinks define the navigation links for the Security Solution pages and External pages as well export const investigationsNavLinks: SolutionNavLink[] = [ { diff --git a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts index 55c6fe74f846d..d70717783870a 100644 --- a/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts +++ b/x-pack/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_translations.ts @@ -21,6 +21,13 @@ export const TIMELINE_DESCRIPTION = i18n.translate( } ); +export const NOTE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.navLinks.investigations.note.title', + { + defaultMessage: 'Oversee, revise and revisit the annotations within each document and timeline', + } +); + export const OSQUERY_TITLE = i18n.translate( 'xpack.securitySolution.navLinks.investigations.osquery.title', { diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index dcadd74245f24..97f07ee6706b9 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -25,7 +25,7 @@ export const ENTITY_ANALYTICS_RISK_SCORE = i18n.translate( } ); -export const NOTES = i18n.translate('xpack.securitySolution.navigation.notesManagement', { +export const NOTES = i18n.translate('xpack.securitySolution.navigation.notes', { defaultMessage: 'Notes', }); diff --git a/x-pack/plugins/security_solution/public/app_links.ts b/x-pack/plugins/security_solution/public/app_links.ts index 4140f6bfcd322..334209b744580 100644 --- a/x-pack/plugins/security_solution/public/app_links.ts +++ b/x-pack/plugins/security_solution/public/app_links.ts @@ -6,6 +6,7 @@ */ import type { CoreStart } from '@kbn/core/public'; +import { links as notesLink } from './notes/links'; import { links as attackDiscoveryLinks } from './attack_discovery/links'; import type { AppLinkItems } from './common/links/types'; import { indicatorsLinks } from './threat_intelligence/links'; @@ -35,6 +36,7 @@ export const appLinks: AppLinkItems = Object.freeze([ rulesLinks, gettingStartedLinks, managementLinks, + notesLink, ]); export const getFilteredLinks = async ( @@ -55,5 +57,6 @@ export const getFilteredLinks = async ( rulesLinks, gettingStartedLinks, managementFilteredLinks, + notesLink, ]); }; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx index 3fa8cd2be11bc..07ff05f507029 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx @@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp'; import type { TimelineType } from '../../../../common/api/timeline'; import { appendSearch } from './helpers'; -export const getTimelineTabsUrl = (tabName: TimelineType | 'notes', search?: string) => +export const getTimelineTabsUrl = (tabName: TimelineType, search?: string) => `/${tabName}${appendSearch(search)}`; export const getTimelineUrl = (id: string, graphEventId?: string) => diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts index aca8f865fce8a..82b321abdcd6e 100644 --- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -14,6 +14,7 @@ import { BLOCKLIST, RESPONSE_ACTIONS_HISTORY, PROTECTION_UPDATES, + NOTES, } from '../../app/translations'; const TabNameMappedToI18nKey: Record = { @@ -25,6 +26,7 @@ const TabNameMappedToI18nKey: Record = { [AdministrationSubTab.blocklist]: BLOCKLIST, [AdministrationSubTab.responseActionsHistory]: RESPONSE_ACTIONS_HISTORY, [AdministrationSubTab.protectionUpdates]: PROTECTION_UPDATES, + [AdministrationSubTab.notes]: NOTES, }; export function getTrailingBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] { diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 4b050266f3ac3..b319adbd0faeb 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -18,6 +18,7 @@ export const MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH = `${MANAGEMEN export const MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/hostIsolationExceptions`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/blocklists`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_PROTECTION_UPDATES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/protectionUpdates`; +export const MANAGEMENT_ROUTING_NOTES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.notes})`; /** @deprecated use the paths defined above instead */ export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`; export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`; diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 91bf4e958f6fb..7247c45c366f4 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -8,6 +8,7 @@ import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import IconFilebeatChart from '../app/solution_navigation/links/sections/icons/filebeat_chart'; import { checkArtifactHasData } from './services/exceptions_list/check_artifact_has_data'; import { calculateEndpointAuthz, @@ -22,6 +23,7 @@ import { EVENT_FILTERS_PATH, HOST_ISOLATION_EXCEPTIONS_PATH, MANAGE_PATH, + NOTES_PATH, POLICIES_PATH, RESPONSE_ACTIONS_HISTORY_PATH, SecurityPageName, @@ -39,6 +41,7 @@ import { TRUSTED_APPLICATIONS, ENTITY_ANALYTICS_RISK_SCORE, ASSET_CRITICALITY, + NOTES, } from '../app/translations'; import { licenseService } from '../common/hooks/use_license'; import type { LinkItem } from '../common/links/types'; @@ -85,6 +88,12 @@ const categories = [ }), linkIds: [SecurityPageName.cloudDefendPolicies], }, + { + label: i18n.translate('xpack.securitySolution.appLinks.category.investigations', { + defaultMessage: 'Investigations', + }), + linkIds: [SecurityPageName.notes], + }, ]; export const links: LinkItem = { @@ -215,6 +224,19 @@ export const links: LinkItem = { hideTimeline: true, }, cloudDefendLink, + { + id: SecurityPageName.notes, + title: NOTES, + description: i18n.translate('xpack.securitySolution.appLinks.notesDescription', { + defaultMessage: + 'Oversee, revise and revisit the annotations within each document and timeline.', + }), + landingIcon: IconFilebeatChart, + path: NOTES_PATH, + skipUrlState: true, + hideTimeline: true, + experimentalKey: 'securitySolutionNotesEnabled', + }, ], }; diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 144ae26815f18..dc8314acc276c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -11,6 +11,8 @@ import { Routes, Route } from '@kbn/shared-ux-router'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; +import { NotesContainer } from './notes'; import { ManagementEmptyStateWrapper } from '../components/management_empty_state_wrapper'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, @@ -20,6 +22,7 @@ import { MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_BLOCKLIST_PATH, MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH, + MANAGEMENT_ROUTING_NOTES_PATH, } from '../common/constants'; import { NotFoundPage } from '../../app/404'; import { EndpointsContainer } from './endpoint_hosts'; @@ -77,7 +80,18 @@ const ResponseActionsTelemetry = () => ( ); +const NotesTelemetry = () => ( + + + + +); + export const ManagementContainer = memo(() => { + const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( + 'securitySolutionNotesEnabled' + ); + const { loading, canReadPolicyManagement, @@ -148,6 +162,10 @@ export const ManagementContainer = memo(() => { hasPrivilege={canReadActionsLogManagement} /> + {securitySolutionNotesEnabled && ( + + )} + {canReadEndpointList && ( diff --git a/x-pack/plugins/security_solution/public/management/pages/notes/index.tsx b/x-pack/plugins/security_solution/public/management/pages/notes/index.tsx new file mode 100644 index 0000000000000..5c57509432a93 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/notes/index.tsx @@ -0,0 +1,21 @@ +/* + * 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 { Routes, Route } from '@kbn/shared-ux-router'; +import React from 'react'; +import { NoteManagementPage } from '../../../notes'; +import { NotFoundPage } from '../../../app/404'; +import { MANAGEMENT_ROUTING_NOTES_PATH } from '../../common/constants'; + +export const NotesContainer = () => { + return ( + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 94f671ac09e2f..ef0e3e56c7285 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -33,6 +33,7 @@ export enum AdministrationSubTab { blocklist = 'blocklist', responseActionsHistory = 'response_actions_history', protectionUpdates = 'protection_updates', + notes = 'notes', } /** diff --git a/x-pack/plugins/security_solution/public/notes/components/translations.ts b/x-pack/plugins/security_solution/public/notes/components/translations.ts index f2846d6daab62..8d7a5b4262815 100644 --- a/x-pack/plugins/security_solution/public/notes/components/translations.ts +++ b/x-pack/plugins/security_solution/public/notes/components/translations.ts @@ -14,56 +14,10 @@ export const BATCH_ACTIONS = i18n.translate( } ); -export const CREATED_COLUMN = i18n.translate( - 'xpack.securitySolution.notes.management.createdColumnTitle', - { - defaultMessage: 'Created', - } -); - -export const CREATED_BY_COLUMN = i18n.translate( - 'xpack.securitySolution.notes.management.createdByColumnTitle', - { - defaultMessage: 'Created by', - } -); - -export const EVENT_ID_COLUMN = i18n.translate( - 'xpack.securitySolution.notes.management.eventIdColumnTitle', - { - defaultMessage: 'View Document', - } -); - -export const TIMELINE_ID_COLUMN = i18n.translate( - 'xpack.securitySolution.notes.management.timelineColumnTitle', - { - defaultMessage: 'Timeline', - } -); - -export const NOTE_CONTENT_COLUMN = i18n.translate( - 'xpack.securitySolution.notes.management.noteContentColumnTitle', - { - defaultMessage: 'Note content', - } -); - export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', { defaultMessage: 'Delete', }); -export const DELETE_SINGLE_NOTE_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.notes.management.deleteDescription', - { - defaultMessage: 'Delete this note', - } -); - -export const TABLE_ERROR = i18n.translate('xpack.securitySolution.notes.management.tableError', { - defaultMessage: 'Unable to load notes', -}); - export const DELETE_NOTES_MODAL_TITLE = i18n.translate( 'xpack.securitySolution.notes.management.deleteNotesModalTitle', { @@ -96,13 +50,6 @@ export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.r defaultMessage: 'Refresh', }); -export const OPEN_TIMELINE = i18n.translate( - 'xpack.securitySolution.notes.management.openTimeline', - { - defaultMessage: 'Open timeline', - } -); - export const VIEW_EVENT_IN_TIMELINE = i18n.translate( 'xpack.securitySolution.notes.management.viewEventInTimeline', { diff --git a/x-pack/plugins/security_solution/public/notes/links.ts b/x-pack/plugins/security_solution/public/notes/links.ts new file mode 100644 index 0000000000000..ef6c691b6246a --- /dev/null +++ b/x-pack/plugins/security_solution/public/notes/links.ts @@ -0,0 +1,25 @@ +/* + * 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 { NOTES_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import { NOTES } from '../app/translations'; +import type { LinkItem } from '../common/links/types'; + +export const links: LinkItem = { + id: SecurityPageName.notes, + title: NOTES, + path: NOTES_PATH, + capabilities: [`${SERVER_APP_ID}.show`], + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.notes', { + defaultMessage: 'Notes', + }), + ], + links: [], + experimentalKey: 'securitySolutionNotesEnabled', +}; diff --git a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx index 574501b7d03c0..ddfed3fbb6287 100644 --- a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx +++ b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx @@ -7,8 +7,11 @@ import React, { useCallback, useMemo, useEffect } from 'react'; import type { DefaultItemAction, EuiBasicTableColumn } from '@elastic/eui'; -import { EuiBasicTable, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { EuiBasicTable, EuiEmptyPrompt, EuiLink, EuiSpacer } from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; +import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers'; +import { Title } from '../../common/components/header_page/title'; // TODO unify this type from the api with the one in public/common/lib/note import type { Note } from '../../../common/api/timeline'; import { FormattedRelativePreferenceDate } from '../../common/components/formatted_date'; @@ -32,12 +35,11 @@ import type { NotesState } from '..'; import { SearchRow } from '../components/search_row'; import { NotesUtilityBar } from '../components/utility_bar'; import { DeleteConfirmModal } from '../components/delete_confirm_modal'; -import * as i18n from '../components/translations'; -import type { OpenTimelineProps } from '../../timelines/components/open_timeline/types'; +import * as i18n from './translations'; import { OpenEventInTimeline } from '../components/open_event_in_timeline'; const columns: ( - onOpenTimeline: OpenTimelineProps['onOpenTimeline'] + onOpenTimeline: (timelineId: string) => void ) => Array> = (onOpenTimeline) => { return [ { @@ -61,9 +63,7 @@ const columns: ( name: i18n.TIMELINE_ID_COLUMN, render: (timelineId: Note['timelineId']) => timelineId ? ( - onOpenTimeline({ timelineId, duplicate: false })}> - {i18n.OPEN_TIMELINE} - + onOpenTimeline(timelineId)}>{i18n.OPEN_TIMELINE} ) : null, }, { @@ -80,11 +80,7 @@ const pageSizeOptions = [10, 25, 50, 100]; * This component uses the same slices of state as the notes functionality of the rest of the Security Solution applicaiton. * Therefore, changes made in this page (like fetching or deleting notes) will have an impact everywhere. */ -export const NoteManagementPage = ({ - onOpenTimeline, -}: { - onOpenTimeline: OpenTimelineProps['onOpenTimeline']; -}) => { +export const NoteManagementPage = () => { const dispatch = useDispatch(); const notes = useSelector(selectAllNotes); const pagination = useSelector(selectNotesPagination); @@ -152,6 +148,19 @@ export const NoteManagementPage = ({ return item.noteId; }, []); + const unifiedComponentsInTimelineDisabled = useIsExperimentalFeatureEnabled( + 'unifiedComponentsInTimelineDisabled' + ); + const queryTimelineById = useQueryTimelineById(); + const openTimeline = useCallback( + (timelineId: string) => + queryTimelineById({ + timelineId, + unifiedComponentsInTimelineDisabled, + }), + [queryTimelineById, unifiedComponentsInTimelineDisabled] + ); + const columnWithActions = useMemo(() => { const actions: Array> = [ { @@ -164,13 +173,13 @@ export const NoteManagementPage = ({ }, ]; return [ - ...columns(onOpenTimeline), + ...columns(openTimeline), { name: 'actions', actions, }, ]; - }, [selectRowForDeletion, onOpenTimeline]); + }, [selectRowForDeletion, openTimeline]); const currentPagination = useMemo(() => { return { @@ -207,6 +216,8 @@ export const NoteManagementPage = ({ return ( <> + + <EuiSpacer size="m" /> <SearchRow /> <NotesUtilityBar /> <EuiBasicTable diff --git a/x-pack/plugins/security_solution/public/notes/pages/translations.ts b/x-pack/plugins/security_solution/public/notes/pages/translations.ts new file mode 100644 index 0000000000000..5f5d9b19477e6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/notes/pages/translations.ts @@ -0,0 +1,73 @@ +/* + * 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 NOTES = i18n.translate('xpack.securitySolution.notes.management.title', { + defaultMessage: 'Notes', +}); + +export const CREATED_COLUMN = i18n.translate( + 'xpack.securitySolution.notes.management.createdColumnTitle', + { + defaultMessage: 'Created', + } +); + +export const CREATED_BY_COLUMN = i18n.translate( + 'xpack.securitySolution.notes.management.createdByColumnTitle', + { + defaultMessage: 'Created by', + } +); + +export const EVENT_ID_COLUMN = i18n.translate( + 'xpack.securitySolution.notes.management.eventIdColumnTitle', + { + defaultMessage: 'View Document', + } +); + +export const TIMELINE_ID_COLUMN = i18n.translate( + 'xpack.securitySolution.notes.management.timelineColumnTitle', + { + defaultMessage: 'Timeline', + } +); + +export const NOTE_CONTENT_COLUMN = i18n.translate( + 'xpack.securitySolution.notes.management.noteContentColumnTitle', + { + defaultMessage: 'Note content', + } +); + +export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', { + defaultMessage: 'Delete', +}); + +export const DELETE_SINGLE_NOTE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.notes.management.deleteDescription', + { + defaultMessage: 'Delete this note', + } +); + +export const TABLE_ERROR = i18n.translate('xpack.securitySolution.notes.management.tableError', { + defaultMessage: 'Unable to load notes', +}); + +export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', { + defaultMessage: 'Refresh', +}); + +export const OPEN_TIMELINE = i18n.translate( + 'xpack.securitySolution.notes.management.openTimeline', + { + defaultMessage: 'Open timeline', + } +); diff --git a/x-pack/plugins/security_solution/public/notes/routes.tsx b/x-pack/plugins/security_solution/public/notes/routes.tsx new file mode 100644 index 0000000000000..7bd17c2b012ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/notes/routes.tsx @@ -0,0 +1,42 @@ +/* + * 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 { Switch } from 'react-router-dom'; +import { Route } from '@kbn/shared-ux-router'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; +import { NoteManagementPage } from './pages/note_management_page'; +import { SpyRoute } from '../common/utils/route/spy_routes'; +import { NotFoundPage } from '../app/404'; +import { NOTES_PATH, SecurityPageName } from '../../common/constants'; +import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; + +const NotesManagementTelemetry = () => ( + <PluginTemplateWrapper> + <TrackApplicationView viewId={SecurityPageName.notes}> + <NoteManagementPage /> + <SpyRoute pageName={SecurityPageName.notes} /> + </TrackApplicationView> + </PluginTemplateWrapper> +); + +const NotesManagementContainer: React.FC = React.memo(() => { + return ( + <Switch> + <Route path={NOTES_PATH} exact component={NotesManagementTelemetry} /> + <Route component={NotFoundPage} /> + </Switch> + ); +}); +NotesManagementContainer.displayName = 'NotesManagementContainer'; + +export const routes = [ + { + path: NOTES_PATH, + component: NotesManagementContainer, + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index ea86d5eaa54fb..cdb61ecf61f6e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -70,7 +70,7 @@ interface OwnProps<TCache = object> { export type OpenTimelineOwnProps = OwnProps & Pick< OpenTimelineProps, - 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' | 'tabName' + 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' >; /** Returns a collection of selected timeline ids */ @@ -131,7 +131,6 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( importDataModalToggle, onOpenTimeline, setImportDataModalToggle, - tabName, title, }) => { const dispatch = useDispatch(); @@ -423,7 +422,6 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( selectedItems={selectedItems} sortDirection={sortDirection} sortField={sortField} - tabName={tabName} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} timelineStatus={timelineStatus} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 524d3bee9640a..5a1a9155bb5c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -30,7 +30,6 @@ import { TimelinesTable } from './timelines_table'; import * as i18n from './translations'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; import type { OpenTimelineProps, ActionTimelineToShow, OpenTimelineResult } from './types'; -import { NoteManagementPage } from '../../../notes'; const QueryText = styled.span` white-space: normal; @@ -64,7 +63,6 @@ export const OpenTimeline = React.memo<OpenTimelineProps>( sortDirection, setImportDataModalToggle, sortField, - tabName, timelineType = TimelineTypeEnum.default, timelineStatus, timelineFilter, @@ -228,92 +226,86 @@ export const OpenTimeline = React.memo<OpenTimelineProps>( <div data-test-subj="timelines-page-container" className={OPEN_TIMELINE_CLASS_NAME}> {!!timelineFilter && timelineFilter} - {tabName !== 'notes' ? ( - <> - <SearchRow - data-test-subj="search-row" - favoriteCount={favoriteCount} - onlyFavorites={onlyFavorites} - onQueryChange={onQueryChange} - onToggleOnlyFavorites={onToggleOnlyFavorites} - query={query} - timelineType={timelineType} - > - {SearchRowContent} - </SearchRow> + <> + <SearchRow + data-test-subj="search-row" + favoriteCount={favoriteCount} + onlyFavorites={onlyFavorites} + onQueryChange={onQueryChange} + onToggleOnlyFavorites={onToggleOnlyFavorites} + query={query} + timelineType={timelineType} + > + {SearchRowContent} + </SearchRow> - <UtilityBar border> - <UtilityBarSection> - <UtilityBarGroup> - <UtilityBarText data-test-subj="query-message"> - <> - {i18n.SHOWING}{' '} - {timelineType === TimelineTypeEnum.template ? nTemplates : nTimelines} - </> - </UtilityBarText> - </UtilityBarGroup> - <UtilityBarGroup> - {timelineStatus !== TimelineStatusEnum.immutable && ( - <> - <UtilityBarText data-test-subj="selected-count"> - {timelineType === TimelineTypeEnum.template - ? i18n.SELECTED_TEMPLATES((selectedItems || []).length) - : i18n.SELECTED_TIMELINES((selectedItems || []).length)} - </UtilityBarText> - <UtilityBarAction - dataTestSubj="batchActions" - iconSide="right" - iconType="arrowDown" - popoverContent={getBatchItemsPopoverContent} - data-test-subj="utility-bar-action" - > - <span data-test-subj="utility-bar-action-button"> - {i18n.BATCH_ACTIONS} - </span> - </UtilityBarAction> - </> - )} - <UtilityBarAction - dataTestSubj="refreshButton" - iconSide="right" - iconType="refresh" - onClick={onRefreshBtnClick} - > - {i18n.REFRESH} - </UtilityBarAction> - </UtilityBarGroup> - </UtilityBarSection> - </UtilityBar> + <UtilityBar border> + <UtilityBarSection> + <UtilityBarGroup> + <UtilityBarText data-test-subj="query-message"> + <> + {i18n.SHOWING}{' '} + {timelineType === TimelineTypeEnum.template ? nTemplates : nTimelines} + </> + </UtilityBarText> + </UtilityBarGroup> + <UtilityBarGroup> + {timelineStatus !== TimelineStatusEnum.immutable && ( + <> + <UtilityBarText data-test-subj="selected-count"> + {timelineType === TimelineTypeEnum.template + ? i18n.SELECTED_TEMPLATES((selectedItems || []).length) + : i18n.SELECTED_TIMELINES((selectedItems || []).length)} + </UtilityBarText> + <UtilityBarAction + dataTestSubj="batchActions" + iconSide="right" + iconType="arrowDown" + popoverContent={getBatchItemsPopoverContent} + data-test-subj="utility-bar-action" + > + <span data-test-subj="utility-bar-action-button">{i18n.BATCH_ACTIONS}</span> + </UtilityBarAction> + </> + )} + <UtilityBarAction + dataTestSubj="refreshButton" + iconSide="right" + iconType="refresh" + onClick={onRefreshBtnClick} + > + {i18n.REFRESH} + </UtilityBarAction> + </UtilityBarGroup> + </UtilityBarSection> + </UtilityBar> - <TimelinesTable - actionTimelineToShow={actionTimelineToShow} - data-test-subj="timelines-table" - deleteTimelines={deleteTimelines} - defaultPageSize={defaultPageSize} - loading={isLoading} - itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} - enableExportTimelineDownloader={enableExportTimelineDownloader} - onCreateRule={onCreateRule} - onCreateRuleFromEql={onCreateRuleFromEql} - onOpenDeleteTimelineModal={onOpenDeleteTimelineModal} - onOpenTimeline={onOpenTimeline} - onSelectionChange={onSelectionChange} - onTableChange={onTableChange} - onToggleShowNotes={onToggleShowNotes} - pageIndex={pageIndex} - pageSize={pageSize} - searchResults={searchResults} - showExtendedColumns={true} - sortDirection={sortDirection} - sortField={sortField} - timelineType={timelineType} - totalSearchResultsCount={totalSearchResultsCount} - tableRef={tableRef} - /> - </> - ) : ( - <NoteManagementPage onOpenTimeline={onOpenTimeline} /> - )} + <TimelinesTable + actionTimelineToShow={actionTimelineToShow} + data-test-subj="timelines-table" + deleteTimelines={deleteTimelines} + defaultPageSize={defaultPageSize} + loading={isLoading} + itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} + enableExportTimelineDownloader={enableExportTimelineDownloader} + onCreateRule={onCreateRule} + onCreateRuleFromEql={onCreateRuleFromEql} + onOpenDeleteTimelineModal={onOpenDeleteTimelineModal} + onOpenTimeline={onOpenTimeline} + onSelectionChange={onSelectionChange} + onTableChange={onTableChange} + onToggleShowNotes={onToggleShowNotes} + pageIndex={pageIndex} + pageSize={pageSize} + searchResults={searchResults} + showExtendedColumns={true} + sortDirection={sortDirection} + sortField={sortField} + timelineType={timelineType} + totalSearchResultsCount={totalSearchResultsCount} + tableRef={tableRef} + /> + </> </div> </> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index 14ddedf5b9688..d750ec08c24b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -206,7 +206,6 @@ export interface OpenTimelineProps { totalSearchResultsCount: number; /** Hide action on timeline if needed it */ hideActions?: ActionTimelineToShow[]; - tabName?: string; } export interface ResolveTimelineConfig { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 45dd7d9e95bbf..e6ad5ad0e1f11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -8,7 +8,6 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; - import { noop } from 'lodash/fp'; import { type TimelineType, TimelineTypeEnum } from '../../../../common/api/timeline'; import { SecurityPageName } from '../../../app/types'; @@ -17,7 +16,7 @@ import * as i18n from './translations'; import type { TimelineTab } from './types'; import { TimelineTabsStyle } from './types'; import { useKibana } from '../../../common/lib/kibana'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; + export interface UseTimelineTypesArgs { defaultTimelineCount?: number | null; templateTimelineCount?: number | null; @@ -42,8 +41,6 @@ export const useTimelineTypes = ({ : TimelineTypeEnum.default ); - const notesEnabled = useIsExperimentalFeatureEnabled('securitySolutionNotesEnabled'); - const timelineUrl = useMemo(() => { return formatUrl(getTimelineTabsUrl(TimelineTypeEnum.default, urlSearch)); }, [formatUrl, urlSearch]); @@ -51,10 +48,6 @@ export const useTimelineTypes = ({ return formatUrl(getTimelineTabsUrl(TimelineTypeEnum.template, urlSearch)); }, [formatUrl, urlSearch]); - const notesUrl = useMemo(() => { - return formatUrl(getTimelineTabsUrl('notes', urlSearch)); - }, [formatUrl, urlSearch]); - const goToTimeline = useCallback( (ev: React.SyntheticEvent) => { ev.preventDefault(); @@ -71,14 +64,6 @@ export const useTimelineTypes = ({ [navigateToUrl, templateUrl] ); - const goToNotes = useCallback( - (ev: React.SyntheticEvent) => { - ev.preventDefault(); - navigateToUrl(notesUrl); - }, - [navigateToUrl, notesUrl] - ); - const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = useCallback( (timelineTabsStyle: TimelineTabsStyle) => [ { @@ -132,17 +117,6 @@ export const useTimelineTypes = ({ {tab.name} </EuiTab> ))} - {notesEnabled && ( - <EuiTab - data-test-subj="timeline-notes" - isSelected={tabName === 'notes'} - key="timeline-notes" - href={notesUrl} - onClick={goToNotes} - > - {'Notes'} - </EuiTab> - )} </EuiTabs> <EuiSpacer size="m" /> </> diff --git a/x-pack/plugins/security_solution/public/timelines/links.ts b/x-pack/plugins/security_solution/public/timelines/links.ts index 169ef6da01910..9315417d97646 100644 --- a/x-pack/plugins/security_solution/public/timelines/links.ts +++ b/x-pack/plugins/security_solution/public/timelines/links.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { SecurityPageName, SERVER_APP_ID, TIMELINES_PATH } from '../../common/constants'; -import { TIMELINES, NOTES } from '../app/translations'; +import { TIMELINES } from '../app/translations'; import type { LinkItem } from '../common/links/types'; export const links: LinkItem = { @@ -30,14 +30,5 @@ export const links: LinkItem = { path: `${TIMELINES_PATH}/template`, sideNavDisabled: true, }, - { - id: SecurityPageName.notesManagement, - title: NOTES, - description: i18n.translate('xpack.securitySolution.appLinks.notesManagementDescription', { - defaultMessage: 'Visualize and delete notes.', - }), - path: `${TIMELINES_PATH}/notes`, - experimentalKey: 'securitySolutionNotesEnabled', - }, ], }; diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index c7e8cb9887efe..2151a2624aeb4 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -17,7 +17,7 @@ import { appendSearch } from '../../common/components/link_to/helpers'; import { TIMELINES_PATH } from '../../../common/constants'; -const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineTypeEnum.default}|${TimelineTypeEnum.template}|notes)`; +const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineTypeEnum.default}|${TimelineTypeEnum.template})`; const timelinesDefaultPath = `${TIMELINES_PATH}/${TimelineTypeEnum.default}`; export const Timelines = React.memo(() => ( diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 9d3c05c97b685..08cf46b41ad35 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -41,7 +41,7 @@ export const TimelinesPage = React.memo(() => { {indicesExist ? ( <SecuritySolutionPageWrapper> <HeaderPage title={i18n.PAGE_TITLE}> - {capabilitiesCanUserCRUD && tabName !== 'notes' ? ( + {capabilitiesCanUserCRUD && ( <EuiFlexGroup gutterSize="s" alignItems="center"> <EuiFlexItem> <EuiButton @@ -56,7 +56,7 @@ export const TimelinesPage = React.memo(() => { <NewTimelineButton type={timelineType} /> </EuiFlexItem> </EuiFlexGroup> - ) : null} + )} </HeaderPage> <StatefulOpenTimeline @@ -66,7 +66,6 @@ export const TimelinesPage = React.memo(() => { setImportDataModalToggle={setImportDataModal} title={i18n.ALL_TIMELINES_PANEL_TITLE} data-test-subj="stateful-open-timeline" - tabName={tabName} /> </SecuritySolutionPageWrapper> ) : ( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a0a863eec7828..b6beb169a4627 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35255,7 +35255,6 @@ "xpack.securitySolution.appLinks.network.flows": "Flux", "xpack.securitySolution.appLinks.network.http": "HTTP", "xpack.securitySolution.appLinks.network.tls": "TLS", - "xpack.securitySolution.appLinks.notesManagementDescription": "Visualisez et supprimez des notes.", "xpack.securitySolution.appLinks.overview": "Aperçu", "xpack.securitySolution.appLinks.overviewDescription": "Résumé de votre activité d'environnement de sécurité, y compris les alertes, les événements, les éléments récents et un fil d'actualités !", "xpack.securitySolution.appLinks.policiesDescription": "Utilisez les politiques pour personnaliser les protections des points de terminaison et de charge de travail cloud, et d'autres configurations.", @@ -39510,7 +39509,6 @@ "xpack.securitySolution.navigation.manage": "Gérer", "xpack.securitySolution.navigation.network": "Réseau", "xpack.securitySolution.navigation.newRuleTitle": "Créer une nouvelle règle", - "xpack.securitySolution.navigation.notesManagement": "Notes", "xpack.securitySolution.navigation.overview": "Aperçu", "xpack.securitySolution.navigation.protectionUpdates": "Mises à jour de la protection", "xpack.securitySolution.navigation.responseActionsHistory": "Historique des actions de réponse", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ba7cc23d4aeae..0522491b9fa38 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34999,7 +34999,6 @@ "xpack.securitySolution.appLinks.network.flows": "Flow", "xpack.securitySolution.appLinks.network.http": "HTTP", "xpack.securitySolution.appLinks.network.tls": "TLS", - "xpack.securitySolution.appLinks.notesManagementDescription": "メモを可視化して、削除します。", "xpack.securitySolution.appLinks.overview": "概要", "xpack.securitySolution.appLinks.overviewDescription": "アラート、イベント、最近のアイテム、ニュースフィードを含む、セキュリティ環境アクティビティの概要。", "xpack.securitySolution.appLinks.policiesDescription": "ポリシーを使用して、エンドポイントおよびクラウドワークロード保護、ならびに他の構成をカスタマイズします。", @@ -39253,7 +39252,6 @@ "xpack.securitySolution.navigation.manage": "管理", "xpack.securitySolution.navigation.network": "ネットワーク", "xpack.securitySolution.navigation.newRuleTitle": "新規ルールを作成", - "xpack.securitySolution.navigation.notesManagement": "メモ", "xpack.securitySolution.navigation.overview": "概要", "xpack.securitySolution.navigation.protectionUpdates": "保護更新", "xpack.securitySolution.navigation.responseActionsHistory": "対応アクション履歴", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 68d15c252f33d..57a1158f4513e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35043,7 +35043,6 @@ "xpack.securitySolution.appLinks.network.flows": "流", "xpack.securitySolution.appLinks.network.http": "HTTP", "xpack.securitySolution.appLinks.network.tls": "TLS", - "xpack.securitySolution.appLinks.notesManagementDescription": "可视化并删除备注。", "xpack.securitySolution.appLinks.overview": "概览", "xpack.securitySolution.appLinks.overviewDescription": "您的安全环境活动摘要,包括告警、事件、最近项和新闻源!", "xpack.securitySolution.appLinks.policiesDescription": "使用策略定制终端和云工作负载防护及其他配置。", @@ -39299,7 +39298,6 @@ "xpack.securitySolution.navigation.manage": "管理", "xpack.securitySolution.navigation.network": "网络", "xpack.securitySolution.navigation.newRuleTitle": "创建新规则", - "xpack.securitySolution.navigation.notesManagement": "备注", "xpack.securitySolution.navigation.overview": "概览", "xpack.securitySolution.navigation.protectionUpdates": "防护更新", "xpack.securitySolution.navigation.responseActionsHistory": "响应操作历史记录",