diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap index 4b02d23568d2..ce0c797c2b2b 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap @@ -10,6 +10,7 @@ exports[`Markdown markdown links it renders the expected content containing a li rawSourcePos={false} renderers={ Object { + "blockquote": [Function], "link": [Function], "root": [Function], "table": [Function], @@ -35,6 +36,7 @@ exports[`Markdown markdown tables it renders the expected table content 1`] = ` rawSourcePos={false} renderers={ Object { + "blockquote": [Function], "link": [Function], "root": [Function], "table": [Function], diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx index 1368c13619d6..8e051685af56 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx @@ -9,12 +9,20 @@ import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import ReactMarkdown from 'react-markdown'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; const TableHeader = styled.thead` font-weight: bold; `; +const MyBlockquote = styled.div` + ${({ theme }) => css` + padding: 0 ${theme.eui.euiSize}; + color: ${theme.eui.euiColorMediumShade}; + border-left: ${theme.eui.euiSizeXS} solid ${theme.eui.euiColorLightShade}; + `} +`; + TableHeader.displayName = 'TableHeader'; /** prevents links to the new pages from accessing `window.opener` */ @@ -63,6 +71,9 @@ export const Markdown = React.memo<{ ), + blockquote: ({ children }: { children: React.ReactNode[] }) => ( + {children} + ), }; return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx index 836595c7c45d..21e4724797c5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -30,13 +30,14 @@ const initialCommentValue: CommentRequest = { interface AddCommentProps { caseId: string; + insertQuote: string | null; onCommentSaving?: () => void; onCommentPosted: (commentResponse: Comment) => void; showLoading?: boolean; } export const AddComment = React.memo( - ({ caseId, showLoading = true, onCommentPosted, onCommentSaving }) => { + ({ caseId, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { const { commentData, isLoading, postComment, resetCommentData } = usePostComment(caseId); const { form } = useForm({ defaultValue: initialCommentValue, @@ -48,6 +49,16 @@ export const AddComment = React.memo( 'comment' ); + useEffect(() => { + if (insertQuote !== null) { + const { comment } = form.getFormData(); + form.setFieldValue( + 'comment', + `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` + ); + } + }, [insertQuote]); + useEffect(() => { if (commentData !== null) { onCommentPosted(commentData); @@ -67,7 +78,7 @@ export const AddComment = React.memo( }, [form]); return ( - <> + {isLoading && showLoading && }
( }} /> - +
); } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 32a29483e9c7..5ca54c7f429d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -3,13 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType, EuiTableActionsColumnType, EuiAvatar, + EuiLink, + EuiLoadingSpinner, } from '@elastic/eui'; import styled from 'styled-components'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; @@ -19,6 +21,7 @@ import { FormattedRelativePreferenceDate } from '../../../../components/formatte import { CaseDetailsLink } from '../../../../components/links'; import { TruncatableText } from '../../../../components/truncatable_text'; import * as i18n from './translations'; +import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; export type CasesColumns = | EuiTableFieldDataColumnType @@ -60,7 +63,6 @@ export const getCasesColumns = ( } return getEmptyTagValue(); }, - width: '25%', }, { field: 'createdBy', @@ -105,7 +107,6 @@ export const getCasesColumns = ( return getEmptyTagValue(); }, truncateText: true, - width: '20%', }, { align: 'right', @@ -148,8 +149,47 @@ export const getCasesColumns = ( return getEmptyTagValue(); }, }, + { + name: 'ServiceNow Incident', + render: (theCase: Case) => { + if (theCase.id != null) { + return ; + } + return getEmptyTagValue(); + }, + }, { name: 'Actions', actions, }, ]; + +interface Props { + theCase: Case; +} + +const ServiceNowColumn: React.FC = ({ theCase }) => { + const { hasDataToPush, isLoading } = useGetCaseUserActions(theCase.id); + const handleRenderDataToPush = useCallback( + () => + isLoading ? ( + + ) : ( +

+ + {theCase.externalService?.externalTitle} + + {hasDataToPush ? i18n.REQUIRES_UPDATE : i18n.UP_TO_DATE} +

+ ), + [hasDataToPush, isLoading, theCase.externalService] + ); + if (theCase.externalService !== null) { + return handleRenderDataToPush(); + } + return renderStringField(i18n.NOT_PUSHED, `case-table-column-external-notPushed`); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index cbb9ddae22d0..27316ab8427c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -109,19 +109,21 @@ export const AllCases = React.memo(() => { const { dispatchResetIsUpdated, isUpdated, updateBulkStatus } = useUpdateCases(); + const refreshCases = useCallback(() => { + refetchCases(filterOptions, queryParams); + fetchCasesStatus(); + }, [filterOptions, queryParams]); + useEffect(() => { if (isDeleted) { - refetchCases(filterOptions, queryParams); - fetchCasesStatus(); + refreshCases(); dispatchResetIsDeleted(); } if (isUpdated) { - refetchCases(filterOptions, queryParams); - fetchCasesStatus(); + refreshCases(); dispatchResetIsUpdated(); } - }, [isDeleted, isUpdated, filterOptions, queryParams]); - + }, [isDeleted, isUpdated]); const [deleteThisCase, setDeleteThisCase] = useState({ title: '', id: '', @@ -327,6 +329,10 @@ export const AllCases = React.memo(() => { > {i18n.BULK_ACTIONS} + + + {i18n.REFRESH} + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index b18134f6d093..e8459454576e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -62,3 +62,17 @@ export const CLOSED = i18n.translate('xpack.siem.case.caseTable.closed', { export const DELETE = i18n.translate('xpack.siem.case.caseTable.delete', { defaultMessage: 'Delete', }); +export const REQUIRES_UPDATE = i18n.translate('xpack.siem.case.caseTable.requiresUpdate', { + defaultMessage: ' requires update', +}); + +export const UP_TO_DATE = i18n.translate('xpack.siem.case.caseTable.upToDate', { + defaultMessage: ' is up to date', +}); +export const NOT_PUSHED = i18n.translate('xpack.siem.case.caseTable.notPushed', { + defaultMessage: 'Not pushed', +}); + +export const REFRESH = i18n.translate('xpack.siem.case.caseTable.refreshTitle', { + defaultMessage: 'Refresh', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index beba80ccd934..c081567e3be7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -59,6 +59,10 @@ export const EDIT_DESCRIPTION = i18n.translate('xpack.siem.case.caseView.edit.de defaultMessage: 'Edit description', }); +export const QUOTE = i18n.translate('xpack.siem.case.caseView.edit.quote', { + defaultMessage: 'Quote', +}); + export const EDIT_COMMENT = i18n.translate('xpack.siem.case.caseView.edit.comment', { defaultMessage: 'Edit comment', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx index 01ccf3c510b6..25332982dca1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx @@ -45,7 +45,7 @@ export const PropertyActions = React.memo(({ propertyActio const onClosePopover = useCallback((cb?: () => void) => { setShowActions(false); - if (cb) { + if (cb != null) { cb(); } }, []); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index 8b77186f76f7..d8b9ac115426 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -57,6 +57,7 @@ export const UserActionTree = React.memo( ); const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); + const [insertQuote, setInsertQuote] = useState(null); const handleManageMarkdownEditId = useCallback( (id: string) => { @@ -92,6 +93,9 @@ export const UserActionTree = React.memo( top: y, behavior: 'smooth', }); + if (id === 'add-comment') { + moveToTarget.getElementsByTagName('textarea')[0].focus(); + } } window.clearTimeout(handlerTimeoutId.current); setSelectedOutlineCommentId(id); @@ -103,6 +107,15 @@ export const UserActionTree = React.memo( [handlerTimeoutId.current] ); + const handleManageQuote = useCallback( + (quote: string) => { + const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); + setInsertQuote(`> ${addCarrots} \n`); + handleOutlineComment('add-comment'); + }, + [handleOutlineComment] + ); + const handleUpdate = useCallback( (comment: Comment) => { addPostedComment(comment); @@ -131,12 +144,13 @@ export const UserActionTree = React.memo( () => ( ), - [caseData.id, handleUpdate] + [caseData.id, handleUpdate, insertQuote] ); useEffect(() => { @@ -156,10 +170,12 @@ export const UserActionTree = React.memo( isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} isLoading={isLoadingDescription} labelEditAction={i18n.EDIT_DESCRIPTION} + labelQuoteAction={i18n.QUOTE} labelTitle={<>{i18n.ADDED_DESCRIPTION}} fullName={caseData.createdBy.fullName ?? caseData.createdBy.username} markdown={MarkdownDescription} onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)} + onQuote={handleManageQuote.bind(null, caseData.description)} userName={caseData.createdBy.username} /> @@ -176,6 +192,7 @@ export const UserActionTree = React.memo( isEditable={manageMarkdownEditIds.includes(comment.id)} isLoading={isLoadingIds.includes(comment.id)} labelEditAction={i18n.EDIT_COMMENT} + labelQuoteAction={i18n.QUOTE} labelTitle={<>{i18n.ADDED_COMMENT}} fullName={comment.createdBy.fullName ?? comment.createdBy.username} markdown={ @@ -188,6 +205,7 @@ export const UserActionTree = React.memo( /> } onEdit={handleManageMarkdownEditId.bind(null, comment.id)} + onQuote={handleManageQuote.bind(null, comment.comment)} outlineComment={handleOutlineComment} userName={comment.createdBy.username} updatedAt={comment.updatedAt} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx index 10a7c56e2eb2..c1dbe3b5fdbf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -25,11 +25,13 @@ interface UserActionItemProps { isEditable: boolean; isLoading: boolean; labelEditAction?: string; + labelQuoteAction?: string; labelTitle?: JSX.Element; linkId?: string | null; fullName: string; markdown?: React.ReactNode; onEdit?: (id: string) => void; + onQuote?: (id: string) => void; userName: string; updatedAt?: string | null; outlineComment?: (id: string) => void; @@ -113,11 +115,13 @@ export const UserActionItem = ({ isEditable, isLoading, labelEditAction, + labelQuoteAction, labelTitle, linkId, fullName, markdown, onEdit, + onQuote, outlineComment, showBottomFooter, showTopFooter, @@ -147,11 +151,13 @@ export const UserActionItem = ({ id={id} isLoading={isLoading} labelEditAction={labelEditAction} + labelQuoteAction={labelQuoteAction} labelTitle={labelTitle ?? <>} linkId={linkId} userName={userName} updatedAt={updatedAt} onEdit={onEdit} + onQuote={onQuote} outlineComment={outlineComment} /> {markdown} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx index 6ca81667d971..391f54da7e97 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -30,11 +30,13 @@ interface UserActionTitleProps { id: string; isLoading: boolean; labelEditAction?: string; + labelQuoteAction?: string; labelTitle: JSX.Element; linkId?: string | null; updatedAt?: string | null; userName: string; onEdit?: (id: string) => void; + onQuote?: (id: string) => void; outlineComment?: (id: string) => void; } @@ -43,27 +45,39 @@ export const UserActionTitle = ({ id, isLoading, labelEditAction, + labelQuoteAction, labelTitle, linkId, userName, updatedAt, onEdit, + onQuote, outlineComment, }: UserActionTitleProps) => { const { detailName: caseId } = useParams(); const urlSearch = useGetUrlSearch(navTabs.case); const propertyActions = useMemo(() => { - if (labelEditAction != null && onEdit != null) { - return [ - { - iconType: 'pencil', - label: labelEditAction, - onClick: () => onEdit(id), - }, - ]; - } - return []; - }, [id, labelEditAction, onEdit]); + return [ + ...(labelEditAction != null && onEdit != null + ? [ + { + iconType: 'pencil', + label: labelEditAction, + onClick: () => onEdit(id), + }, + ] + : []), + ...(labelQuoteAction != null && onQuote != null + ? [ + { + iconType: 'quote', + label: labelQuoteAction, + onClick: () => onQuote(id), + }, + ] + : []), + ]; + }, [id, labelEditAction, onEdit, labelQuoteAction, onQuote]); const handleAnchorLink = useCallback(() => { copy(