From 1af26aa4b53d5bc8fbb176e3272a211827c116ff Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:00:11 -0700 Subject: [PATCH] Alerting comments (#971) (#974) * added support for comments * deduped code * updated feedback link --------- (cherry picked from commit 722047ddefe593b96109157e8da8d903968eefdb) Signed-off-by: Amardeepsingh Siglani Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- package.json | 3 +- .../Comments/AlertCommentsFlyout.tsx | 239 ++++++++++++++++++ public/components/Comments/CommentEditor.tsx | 51 ++++ .../components/Comments/ShowAlertComments.tsx | 40 +++ .../AlertsDashboardFlyoutComponent.js | 25 +- public/models/Comments.ts | 13 + .../containers/DefineMonitor/DefineMonitor.js | 41 +-- .../AcknowledgeAlertsModal.js | 21 +- .../pages/Dashboard/containers/Dashboard.js | 43 ++-- public/pages/utils/constants.ts | 6 + public/pages/utils/helpers.js | 65 +++++ server/clusters/alerting/alertingPlugin.js | 52 ++++ server/plugin.js | 5 + server/routes/comments.js | 63 +++++ server/routes/index.js | 3 +- server/services/CommentsService.ts | 106 ++++++++ server/services/index.js | 2 + server/services/utils/constants.js | 1 + yarn.lock | 11 +- 19 files changed, 734 insertions(+), 56 deletions(-) create mode 100644 public/components/Comments/AlertCommentsFlyout.tsx create mode 100644 public/components/Comments/CommentEditor.tsx create mode 100644 public/components/Comments/ShowAlertComments.tsx create mode 100644 public/models/Comments.ts create mode 100644 public/pages/utils/constants.ts create mode 100644 server/routes/comments.js create mode 100644 server/services/CommentsService.ts diff --git a/package.json b/package.json index afdc6b6c1..77104b0f4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards", "cypress": "^6.0.0", "husky": "^3.0.0", - "lint-staged": "^10.2.0" + "lint-staged": "^10.2.0", + "@types/react": "^16.14.23" }, "dependencies": { "brace": "0.11.1", diff --git a/public/components/Comments/AlertCommentsFlyout.tsx b/public/components/Comments/AlertCommentsFlyout.tsx new file mode 100644 index 000000000..f112c4c49 --- /dev/null +++ b/public/components/Comments/AlertCommentsFlyout.tsx @@ -0,0 +1,239 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +*/ + +import React, { useCallback, useEffect, useState } from "react"; +import { Comment } from "../../models/Comments"; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiCommentList, + EuiText, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiTitle, + EuiSpacer, + EuiCallOut, + EuiLink +} from "@elastic/eui"; +import { CommentEditor } from "./CommentEditor"; +import moment from "moment"; +import { getTimeZone } from "../../pages/CreateTrigger/utils/helper"; +import { title } from "vega-lite/src/channeldef"; + +export interface AlertCommentsFlyoutProps { + alertId: string; + httpClient: any; + closeFlyout: () => void; +} + +type CommentItem = Comment & { state: 'edit' | 'readonly', draft: string; }; + +export const AlertCommentsFlyout: React.FC = ({ alertId, httpClient, closeFlyout }) => { + const [comments, setComments] = useState([]); + const [commentIdWithOpenActionMenu, setCommentIdWithOpenActionMenu] = useState(undefined); + const [draftCommentContent, setDraftCommentContent] = useState(''); + const [createPending, setCreatePending] = useState(false); + const [updatePending, setUpdatePending] = useState(false); + const toggleCommentActionMenu = (commentId: string) => { + setCommentIdWithOpenActionMenu(commentIdWithOpenActionMenu ? undefined : commentId); + }; + const isACommentBeingEdited = comments.some(comment => comment.state === 'edit'); + + const loadComments = useCallback(async () => { + const getComments = async () => { + const res = await httpClient.post('../api/alerting/comments/_search', { body: JSON.stringify({ + query: { + match: { + entity_id: alertId + } + } + }) + }); + + if (res.ok) { + setComments(res.resp.comments.map((comment: Comment) => ({ + ...comment, + state: 'readonly', + draft: comment.content + })).sort((a: Comment, b: Comment) => b.created_time - a.created_time)); + } + } + + getComments(); + }, [httpClient, alertId]); + + useEffect(() => { + loadComments(); + }, [alertId]); + + const closeCommentActionMenu = () => { + setCommentIdWithOpenActionMenu(undefined); + }; + + const createComment = async () => { + setCreatePending(true); + await httpClient.post(`../api/alerting/comments/${alertId}`, { body: JSON.stringify({ + content: draftCommentContent + })}); + + setDraftCommentContent(''); + loadComments(); + setCreatePending(false); + } + + const updateComment = async (commentId: string, content: string) => { + setUpdatePending(true); + await httpClient.put(`../api/alerting/comments/${commentId}`, { body: JSON.stringify({ content })}); + loadComments(); + setUpdatePending(false); + } + + const deleteComment = async (commentId: string) => { + await httpClient.delete(`../api/alerting/comments/${commentId}`); + loadComments(); + } + + const onCommentContentChange = (comment: CommentItem, commentIdx: number, newContent: string) => { + setComments([ + ...comments.slice(0, commentIdx), + { + ...comment, + draft: newContent + }, + ...comments.slice(commentIdx + 1) + ]); + } + + const onEditClick = (comment: CommentItem, idx: number) => { + setComments([ + ...comments.slice(0, idx), + { + ...comment, + state: 'edit' + }, + ...comments.slice(idx + 1) + ]); + setCommentIdWithOpenActionMenu(undefined); + } + + const onEditCancel = (comment: CommentItem, idx: number) => { + setComments([ + ...comments.slice(0, idx), + { + ...comment, + state: 'readonly' + }, + ...comments.slice(idx + 1) + ]); + } + + const commentListItems = comments.map((comment, idx) => { + const content = comment.state === 'readonly' ? ( + +

+ {comment.content} +

+
+ ) : ( + { + onCommentContentChange(comment, idx, event.target.value); + }} + onSave={() => updateComment(comment.id, comment.draft)} + onCancel={() => onEditCancel(comment, idx)} + /> + ); + + const customActions = comment.state === 'readonly' && ( + toggleCommentActionMenu(comment.id)} + /> + } + isOpen={commentIdWithOpenActionMenu === comment.id} + closePopover={closeCommentActionMenu} + panelPaddingSize="none" + anchorPosition="leftCenter"> + onEditClick(comment, idx)}> + Edit + , + { + deleteComment(comment.id); + }}> + Delete + + ]} + /> + + ); + + return { + username: comment.user || 'Unknown', + event: `${comment.last_updated_time ? 'edited' : 'added'} comment on`, + timestamp: moment.utc(comment.last_updated_time ?? comment.created_time).tz(getTimeZone()).format(), + children: content, + actions: customActions, + } + }); + + return ( + + + +

Comments

+
+
+ + + The feature is experimental and should not be used in a production environment. + The posted comments will be impacted if the feature is deactivated. + For more information see Documentation. + To leave feedback, visit github.com. + + + + +

Add comment

+
+ + { + setDraftCommentContent(event.target.value); + }} + onSave={createComment} + saveDisabled={isACommentBeingEdited} + /> + + +

Comments ({comments.length})

+
+ + +
+
+ ) +} \ No newline at end of file diff --git a/public/components/Comments/CommentEditor.tsx b/public/components/Comments/CommentEditor.tsx new file mode 100644 index 000000000..1b6a00e05 --- /dev/null +++ b/public/components/Comments/CommentEditor.tsx @@ -0,0 +1,51 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +*/ + +import React from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton +} from "@elastic/eui"; + +export interface CommentEditorProps { + isLoading: boolean; + saveDisabled?: boolean; + draftCommentContent: string; + onSave: React.MouseEventHandler; + onCancel?: React.MouseEventHandler; + onContentChange: React.ChangeEventHandler; +} + +export const CommentEditor: React.FC = ({ + isLoading, + draftCommentContent, + saveDisabled, + onSave, + onCancel, + onContentChange, +}) => ( + + +