Skip to content

Commit

Permalink
feat(rca): edit notes (#191546)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdelemme authored Aug 29, 2024
1 parent 4c77c7a commit d06b063
Show file tree
Hide file tree
Showing 15 changed files with 380 additions and 102 deletions.
2 changes: 2 additions & 0 deletions packages/kbn-investigation-shared/src/rest_specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type * from './create_item';
export type * from './delete_item';
export type * from './get_items';
export type * from './investigation_item';
export type * from './update_note';

export * from './create';
export * from './create_note';
Expand All @@ -31,3 +32,4 @@ export * from './create_item';
export * from './delete_item';
export * from './get_items';
export * from './investigation_item';
export * from './update_note';
30 changes: 30 additions & 0 deletions packages/kbn-investigation-shared/src/rest_specs/update_note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import * as t from 'io-ts';
import { investigationNoteResponseSchema } from './investigation_note';

const updateInvestigationNoteParamsSchema = t.type({
path: t.type({
investigationId: t.string,
noteId: t.string,
}),
body: t.type({
content: t.string,
}),
});

const updateInvestigationNoteResponseSchema = investigationNoteResponseSchema;

type UpdateInvestigationNoteParams = t.TypeOf<
typeof updateInvestigationNoteParamsSchema.props.body
>;
type UpdateInvestigationNoteResponse = t.OutputOf<typeof updateInvestigationNoteResponseSchema>;

export { updateInvestigationNoteParamsSchema, updateInvestigationNoteResponseSchema };
export type { UpdateInvestigationNoteParams, UpdateInvestigationNoteResponse };
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { UpdateInvestigationNoteParams } from '@kbn/investigation-shared';
import { useMutation } from '@tanstack/react-query';
import { useKibana } from './use_kibana';

type ServerError = IHttpFetchError<ResponseErrorBody>;

export function useUpdateInvestigationNote() {
const {
core: {
http,
notifications: { toasts },
},
} = useKibana();

return useMutation<
void,
ServerError,
{ investigationId: string; noteId: string; note: UpdateInvestigationNoteParams },
{ investigationId: string }
>(
['deleteInvestigationNote'],
({ investigationId, noteId, note }) => {
const body = JSON.stringify(note);
return http.put<void>(
`/api/observability/investigations/${investigationId}/notes/${noteId}`,
{ body, version: '2023-10-31' }
);
},
{
onSuccess: (response, {}) => {
toasts.addSuccess('Note updated');
},
onError: (error, {}, context) => {
toasts.addError(new Error(error.body?.message ?? 'An error occurred'), { title: 'Error' });
},
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export function registerEmbeddableItem({
services,
}: Options) {
investigate.registerItemDefinition<EmbeddableItemParams, {}>({
type: 'esql',
type: 'embeddable',
generate: async (option: {
itemParams: EmbeddableItemParams;
globalParams: GlobalWidgetParameters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ interface EsqlItemData {
};
}

export const ESQL_ITEM_TYPE = 'esql';

export function EsqlWidget({
suggestion,
dataView,
Expand Down Expand Up @@ -228,7 +230,7 @@ export function registerEsqlItem({
services,
}: Options) {
investigate.registerItemDefinition<EsqlItemParams, EsqlItemData>({
type: 'esql',
type: ESQL_ITEM_TYPE,
generate: async (option: {
itemParams: EsqlItemParams;
globalParams: GlobalWidgetParameters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ErrorMessage } from '../../../../components/error_message';
import { SuggestVisualizationList } from '../../../../components/suggest_visualization_list';
import { useKibana } from '../../../../hooks/use_kibana';
import { getDateHistogramResults } from '../../../../items/esql_item/get_date_histogram_results';
import { EsqlWidget } from '../../../../items/esql_item/register_esql_item';
import { ESQL_ITEM_TYPE, EsqlWidget } from '../../../../items/esql_item/register_esql_item';
import { getEsFilterFromOverrides } from '../../../../utils/get_es_filter_from_overrides';

function getItemFromSuggestion({
Expand All @@ -29,7 +29,7 @@ function getItemFromSuggestion({
}): Item {
return {
title: suggestion.title,
type: 'esql',
type: ESQL_ITEM_TYPE,
params: {
esql: query,
suggestion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ export function InvestigationDetails({ user, investigationId }: Props) {
return (
<EuiFlexGroup direction="row">
<EuiFlexItem grow={8}>
<InvestigationItems investigationId={investigationId} investigation={investigation} />
<InvestigationItems investigation={investigation} />
</EuiFlexItem>

<EuiFlexItem grow={2}>
<InvestigationNotes investigationId={investigationId} investigation={investigation} />
<InvestigationNotes investigation={investigation} user={user} />
</EuiFlexItem>
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ import { InvestigationItemsList } from '../investigation_items_list/investigatio
import { InvestigationSearchBar } from '../investigation_search_bar/investigation_search_bar';

export interface Props {
investigationId: string;
investigation: GetInvestigationResponse;
}

export function InvestigationItems({ investigationId, investigation }: Props) {
export function InvestigationItems({ investigation }: Props) {
const { data: items, refetch } = useFetchInvestigationItems({
investigationId,
investigationId: investigation.id,
initialItems: investigation.items,
});
const renderableItems = useRenderItems({ items, params: investigation.params });
Expand All @@ -34,12 +33,12 @@ export function InvestigationItems({ investigationId, investigation }: Props) {
useDeleteInvestigationItem();

const onAddItem = async (item: Item) => {
await addInvestigationItem({ investigationId, item });
await addInvestigationItem({ investigationId: investigation.id, item });
refetch();
};

const onDeleteItem = async (itemId: string) => {
await deleteInvestigationItem({ investigationId, itemId });
await deleteInvestigationItem({ investigationId: investigation.id, itemId });
refetch();
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { InvestigationNoteResponse } from '@kbn/investigation-shared';
import React, { useState } from 'react';
import { ResizableTextInput } from './resizable_text_input';
import { useUpdateInvestigationNote } from '../../../../hooks/use_update_investigation_note';

interface Props {
investigationId: string;
note: InvestigationNoteResponse;
onCancel: () => void;
onUpdate: () => void;
}

export function EditNoteForm({ investigationId, note, onCancel, onUpdate }: Props) {
const [noteInput, setNoteInput] = useState(note.content);
const { mutateAsync: updateNote, isLoading: isUpdating } = useUpdateInvestigationNote();

const handleUpdateNote = async () => {
await updateNote({ investigationId, noteId: note.id, note: { content: noteInput.trim() } });
onUpdate();
};

return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<ResizableTextInput
disabled={isUpdating}
value={noteInput}
onChange={(value) => {
setNoteInput(value);
}}
onSubmit={() => {
handleUpdateNote();
}}
placeholder={note.content}
/>
</EuiFlexItem>

<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
disabled={isUpdating}
data-test-subj="cancelEditNoteButton"
color="text"
aria-label={i18n.translate(
'xpack.investigateApp.investigationNotes.cancelEditButtonLabel',
{ defaultMessage: 'Cancel' }
)}
size="m"
onClick={() => onCancel()}
>
{i18n.translate('xpack.investigateApp.investigationNotes.cancelEditButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="updateNoteButton"
color="primary"
aria-label={i18n.translate(
'xpack.investigateApp.investigationNotes.updateNoteButtonLabel',
{ defaultMessage: 'Update note' }
)}
disabled={isUpdating || noteInput.trim() === ''}
isLoading={isUpdating}
size="m"
onClick={() => {
handleUpdateNote();
}}
>
{i18n.translate('xpack.investigateApp.investigationNotes.updateNoteButtonLabel', {
defaultMessage: 'Update note',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import {
EuiAvatar,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
Expand All @@ -16,43 +15,36 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import { InvestigationNoteResponse, GetInvestigationResponse } from '@kbn/investigation-shared';
import { GetInvestigationResponse, InvestigationNoteResponse } from '@kbn/investigation-shared';
import { AuthenticatedUser } from '@kbn/security-plugin/common';
import React, { useState } from 'react';
import { useAddInvestigationNote } from '../../../../hooks/use_add_investigation_note';
import { useDeleteInvestigationNote } from '../../../../hooks/use_delete_investigation_note';
import { useFetchInvestigationNotes } from '../../../../hooks/use_fetch_investigation_notes';
import { useTheme } from '../../../../hooks/use_theme';
import { Note } from './note';
import { ResizableTextInput } from './resizable_text_input';
import { TimelineMessage } from './timeline_message';

export interface Props {
investigationId: string;
investigation: GetInvestigationResponse;
user: AuthenticatedUser;
}

export function InvestigationNotes({ investigationId, investigation }: Props) {
export function InvestigationNotes({ investigation, user }: Props) {
const theme = useTheme();
const [noteInput, setNoteInput] = useState('');

const { data: notes, refetch } = useFetchInvestigationNotes({
investigationId,
investigationId: investigation.id,
initialNotes: investigation.notes,
});
const { mutateAsync: addInvestigationNote, isLoading: isAdding } = useAddInvestigationNote();
const { mutateAsync: deleteInvestigationNote, isLoading: isDeleting } =
useDeleteInvestigationNote();

const onAddNote = async (content: string) => {
await addInvestigationNote({ investigationId, note: { content } });
await addInvestigationNote({ investigationId: investigation.id, note: { content } });
refetch();
setNoteInput('');
};

const onDeleteNote = async (noteId: string) => {
await deleteInvestigationNote({ investigationId, noteId });
refetch();
};

const panelClassName = css`
background-color: ${theme.colors.lightShade};
`;
Expand All @@ -72,12 +64,12 @@ export function InvestigationNotes({ investigationId, investigation }: Props) {
<EuiFlexGroup direction="column" gutterSize="m">
{notes?.map((currNote: InvestigationNoteResponse) => {
return (
<TimelineMessage
<Note
key={currNote.id}
icon={<EuiAvatar name={currNote.createdBy} size="s" />}
investigationId={investigation.id}
note={currNote}
onDelete={() => onDeleteNote(currNote.id)}
isDeleting={isDeleting}
disabled={currNote.createdBy !== user.username}
onUpdateOrDeleteCompleted={() => refetch()}
/>
);
})}
Expand Down Expand Up @@ -110,7 +102,7 @@ export function InvestigationNotes({ investigationId, investigation }: Props) {
<EuiButton
data-test-subj="investigateAppInvestigationNotesAddButton"
fullWidth
color="text"
color="primary"
aria-label={i18n.translate('xpack.investigateApp.investigationNotes.addButtonLabel', {
defaultMessage: 'Add',
})}
Expand Down
Loading

0 comments on commit d06b063

Please sign in to comment.