Skip to content

Commit

Permalink
Alerting comments (#971) (#974)
Browse files Browse the repository at this point in the history
* added support for comments



* deduped code



* updated feedback link



---------


(cherry picked from commit 722047d)

Signed-off-by: Amardeepsingh Siglani <[email protected]>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 4cef900 commit 1af26aa
Show file tree
Hide file tree
Showing 19 changed files with 734 additions and 56 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
239 changes: 239 additions & 0 deletions public/components/Comments/AlertCommentsFlyout.tsx
Original file line number Diff line number Diff line change
@@ -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<AlertCommentsFlyoutProps> = ({ alertId, httpClient, closeFlyout }) => {
const [comments, setComments] = useState<CommentItem[]>([]);
const [commentIdWithOpenActionMenu, setCommentIdWithOpenActionMenu] = useState<string | undefined>(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' ? (
<EuiText size="s">
<p>
{comment.content}
</p>
</EuiText>
) : (
<CommentEditor
isLoading={updatePending}
draftCommentContent={comment.draft}
onContentChange={(event) => {
onCommentContentChange(comment, idx, event.target.value);
}}
onSave={() => updateComment(comment.id, comment.draft)}
onCancel={() => onEditCancel(comment, idx)}
/>
);

const customActions = comment.state === 'readonly' && (
<EuiPopover
button={
<EuiButtonIcon
aria-label="Actions"
iconType="boxesHorizontal"
size="s"
color="text"
onClick={() => toggleCommentActionMenu(comment.id)}
/>
}
isOpen={commentIdWithOpenActionMenu === comment.id}
closePopover={closeCommentActionMenu}
panelPaddingSize="none"
anchorPosition="leftCenter">
<EuiContextMenuPanel
items={[
<EuiContextMenuItem
key="A"
icon="pencil"
onClick={() => onEditClick(comment, idx)}>
Edit
</EuiContextMenuItem>,
<EuiContextMenuItem
key="B"
icon="trash"
onClick={() => {
deleteComment(comment.id);
}}>
Delete
</EuiContextMenuItem>
]}
/>
</EuiPopover>
);

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 (
<EuiFlyout onClose={closeFlyout}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>Comments</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiCallOut
iconType='iInCircle'
title='Experimental'>
<span>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 <EuiLink href="https://opensearch.org/docs/latest/observing-your-data/alerting/index/" target="_blank">Documentation.</EuiLink>
To leave feedback, visit <EuiLink href="https://github.com/opensearch-project/OpenSearch-Dashboards/issues/6999" target="_blank">github.com</EuiLink>.
</span>
</EuiCallOut>
<EuiSpacer />
<EuiTitle size="xs">
<h4>Add comment</h4>
</EuiTitle>
<EuiSpacer size="m" />
<CommentEditor
isLoading={createPending}
draftCommentContent={draftCommentContent}
onContentChange={(event) => {
setDraftCommentContent(event.target.value);
}}
onSave={createComment}
saveDisabled={isACommentBeingEdited}
/>
<EuiSpacer />
<EuiTitle size="xs">
<h4>Comments ({comments.length})</h4>
</EuiTitle>
<EuiSpacer />
<EuiCommentList comments={commentListItems}/>
</EuiFlyoutBody>
</EuiFlyout>
)
}
51 changes: 51 additions & 0 deletions public/components/Comments/CommentEditor.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLTextAreaElement>;
}

export const CommentEditor: React.FC<CommentEditorProps> = ({
isLoading,
draftCommentContent,
saveDisabled,
onSave,
onCancel,
onContentChange,
}) => (
<EuiFlexGroup gutterSize="s" direction="column" >
<EuiFlexItem>
<textarea style={{ resize: 'vertical', fontSize: 14, minHeight: 45 }} value={draftCommentContent} onChange={onContentChange}/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ alignSelf: 'flex-end' }}>
<EuiFlexGroup gutterSize="s">
{onCancel && (
<EuiFlexItem grow={false}>
<EuiButton onClick={onCancel}>
Cancel
</EuiButton>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton onClick={onSave} color="primary" isLoading={isLoading} disabled={saveDisabled} fill>
Save
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)
40 changes: 40 additions & 0 deletions public/components/Comments/ShowAlertComments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useCallback, useEffect, useState } from "react";
import { Comment } from "../../models/Comments";
import { EuiButtonIcon, EuiToolTip } from "@elastic/eui";
import { AlertCommentsFlyout } from "./AlertCommentsFlyout";

export interface ShowAlertCommentsProps {
alert: any;
httpClient: any;
}

export const ShowAlertComments: React.FC<ShowAlertCommentsProps> = ({ alert, httpClient }) => {
const [commentsFlyout, setCommentsFlyout] = useState<React.ReactNode | null>(null);

const showCommentsFlyout = useCallback(() => {
setCommentsFlyout(<AlertCommentsFlyout
alertId={alert.id}
httpClient={httpClient}
closeFlyout={() => setCommentsFlyout(null)}
/>);
}, [setCommentsFlyout]);

return (
<>
<EuiToolTip content={'Show comments'}>
<EuiButtonIcon
aria-label={'Show comments'}
data-test-subj={`show-comments-icon`}
iconType={'editorComment'}
onClick={showCommentsFlyout}
/>
</EuiToolTip>
{commentsFlyout}
</>
)
}
Loading

0 comments on commit 1af26aa

Please sign in to comment.