From 65d08a7710ddc2bfd5da90bb65e8db38c2902c49 Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Tue, 29 Oct 2024 16:16:24 +0400 Subject: [PATCH] refactor: move incident CRUD to `useIncidentActions()` hook, unify revalidation logic --- keep-ui/app/incidents/[id]/incident-chat.tsx | 43 ++--- .../incidents/create-or-update-incident.tsx | 127 ++++----------- .../incident-change-same-in-the-past.tsx | 36 ++--- .../incident-change-status-modal.tsx | 148 ----------------- .../app/incidents/incident-dropdown-menu.tsx | 8 +- keep-ui/app/incidents/incident-list.tsx | 6 - .../app/incidents/incident-merge-modal.tsx | 44 +---- keep-ui/app/incidents/incidents-table.tsx | 31 +--- .../incidents/model/useIncidentActions.tsx | 152 +++++++++++++++++- keep-ui/utils/hooks/useIncidents.ts | 8 +- 10 files changed, 226 insertions(+), 377 deletions(-) delete mode 100644 keep-ui/app/incidents/incident-change-status-modal.tsx diff --git a/keep-ui/app/incidents/[id]/incident-chat.tsx b/keep-ui/app/incidents/[id]/incident-chat.tsx index 942ba125d..084dd7075 100644 --- a/keep-ui/app/incidents/[id]/incident-chat.tsx +++ b/keep-ui/app/incidents/[id]/incident-chat.tsx @@ -4,32 +4,23 @@ import { useCopilotChatSuggestions, } from "@copilotkit/react-ui"; import { IncidentDto } from "../models"; -import { - useIncident, - useIncidentAlerts, - useIncidents, -} from "utils/hooks/useIncidents"; +import { useIncidentAlerts } from "utils/hooks/useIncidents"; import { EmptyStateCard } from "@/components/ui/EmptyStateCard"; import { useRouter } from "next/navigation"; import Loading from "app/loading"; import { useCopilotAction, useCopilotReadable } from "@copilotkit/react-core"; -import { updateIncidentRequest } from "../create-or-update-incident"; -import { useApiUrl } from "utils/hooks/useConfig"; -import { useSession } from "next-auth/react"; -import { toast } from "react-toastify"; import "@copilotkit/react-ui/styles.css"; import "./incident-chat.css"; import { Card } from "@tremor/react"; +import { useIncidentActions } from "@/entities/incidents/model/useIncidentActions"; export default function IncidentChat({ incident }: { incident: IncidentDto }) { const router = useRouter(); - const apiUrl = useApiUrl(); - const { mutate } = useIncidents(true, 20); - const { mutate: mutateIncident } = useIncident(incident.id); const { data: alerts, isLoading: alertsLoading } = useIncidentAlerts( incident.id ); - const { data: session } = useSession(); + + const { updateIncident } = useIncidentActions(); useCopilotReadable({ description: "incidentDetails", @@ -79,22 +70,16 @@ export default function IncidentChat({ incident }: { incident: IncidentDto }) { }, ], handler: async ({ name, summary }) => { - const response = await updateIncidentRequest({ - session: session, - incidentId: incident.id, - incidentName: name, - incidentUserSummary: summary, - incidentAssignee: incident.assignee, - incidentSameIncidentInThePastId: incident.same_incident_in_the_past_id, - generatedByAi: true, - apiUrl: apiUrl!, - }); - - if (response.ok) { - mutate(); - mutateIncident(); - toast.success("Incident updated successfully"); - } + await updateIncident( + incident.id, + { + user_generated_name: name, + user_summary: summary, + assignee: incident.assignee, + same_incident_in_the_past_id: incident.same_incident_in_the_past_id, + }, + true + ); }, }); diff --git a/keep-ui/app/incidents/create-or-update-incident.tsx b/keep-ui/app/incidents/create-or-update-incident.tsx index 2755b6746..3421c362d 100644 --- a/keep-ui/app/incidents/create-or-update-incident.tsx +++ b/keep-ui/app/incidents/create-or-update-incident.tsx @@ -2,7 +2,6 @@ import { TextInput, - Textarea, Divider, Subtitle, Text, @@ -10,18 +9,14 @@ import { Select, SelectItem, } from "@tremor/react"; -import { useSession } from "next-auth/react"; import { FormEvent, useEffect, useState } from "react"; -import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; import { IncidentDto } from "./models"; -import { useIncidents } from "utils/hooks/useIncidents"; -import { Session } from "next-auth"; import { useUsers } from "utils/hooks/useUsers"; const ReactQuill = typeof window === "object" ? require("react-quill") : () => false; import "react-quill/dist/quill.snow.css"; import "./react-quill-override.css"; +import { useIncidentActions } from "@/entities/incidents/model/useIncidentActions"; interface Props { incidentToEdit: IncidentDto | null; @@ -29,56 +24,17 @@ interface Props { exitCallback?: () => void; } -export const updateIncidentRequest = async ({ - session, - incidentId, - incidentName, - incidentUserSummary, - incidentAssignee, - incidentSameIncidentInThePastId, - generatedByAi, - apiUrl, -}: { - session: Session | null; - incidentId: string; - incidentName: string; - incidentUserSummary: string; - incidentAssignee: string; - incidentSameIncidentInThePastId: string | null; - generatedByAi: boolean; - apiUrl: string; -}) => { - const response = await fetch( - `${apiUrl}/incidents/${incidentId}?generatedByAi=${generatedByAi}`, - { - method: "PUT", - headers: { - Authorization: `Bearer ${session?.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user_generated_name: incidentName, - user_summary: incidentUserSummary, - assignee: incidentAssignee, - same_incident_in_the_past_id: incidentSameIncidentInThePastId, - }), - } - ); - return response; -}; - export default function CreateOrUpdateIncident({ incidentToEdit, createCallback, exitCallback, }: Props) { - const { data: session } = useSession(); - const { mutate } = useIncidents(true, 20); const [incidentName, setIncidentName] = useState(""); const [incidentUserSummary, setIncidentUserSummary] = useState(""); const [incidentAssignee, setIncidentAssignee] = useState(""); const { data: users = [] } = useUsers(); - const apiUrl = useApiUrl(); + const { addIncident, updateIncident } = useIncidentActions(); + const editMode = incidentToEdit !== null; // Display cancel btn if editing or we need to cancel for another reason (eg. going one step back in the modal etc.) @@ -104,65 +60,38 @@ export default function CreateOrUpdateIncident({ setIncidentAssignee(""); }; - const addIncident = async (e: FormEvent) => { - e.preventDefault(); - const response = await fetch(`${apiUrl}/incidents`, { - method: "POST", - headers: { - Authorization: `Bearer ${session?.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user_generated_name: incidentName, - user_summary: incidentUserSummary, - assignee: incidentAssignee, - }), - }); - if (response.ok) { - exitEditMode(); - await mutate(); - toast.success("Incident created successfully"); - - const created = await response.json(); - createCallback?.(created.id); // close the modal and associate the alert incident - } else { - toast.error( - "Failed to create incident, please contact us if this issue persists." - ); - } + // If the Incident is successfully updated or the user cancels the update we exit the editMode and set the editRule in the incident.tsx to null. + const exitEditMode = () => { + exitCallback?.(); + clearForm(); }; - // This is the function that will be called on submitting the form in the editMode, it sends a PUT request to the backend. - const updateIncident = async (e: FormEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - const response = await updateIncidentRequest({ - session: session, - incidentId: incidentToEdit?.id!, - incidentName: incidentName, - incidentUserSummary: incidentUserSummary, - incidentAssignee: incidentAssignee, - incidentSameIncidentInThePastId: - incidentToEdit?.same_incident_in_the_past_id!, - generatedByAi: false, - apiUrl: apiUrl!, - }); - if (response.ok) { + if (editMode) { + await updateIncident( + incidentToEdit!.id, + { + user_generated_name: incidentName, + user_summary: incidentUserSummary, + assignee: incidentAssignee, + same_incident_in_the_past_id: + incidentToEdit!.same_incident_in_the_past_id, + }, + false + ); exitEditMode(); - await mutate(); - toast.success("Incident updated successfully"); } else { - toast.error( - "Failed to update incident, please contact us if this issue persists." - ); + const newIncident = await addIncident({ + user_generated_name: incidentName, + user_summary: incidentUserSummary, + assignee: incidentAssignee, + }); + createCallback?.(newIncident.id); + exitEditMode(); } }; - // If the Incident is successfully updated or the user cancels the update we exit the editMode and set the editRule in the incident.tsx to null. - const exitEditMode = () => { - exitCallback?.(); - clearForm(); - }; - const submitEnabled = (): boolean => { return !!incidentName; }; @@ -194,7 +123,7 @@ export default function CreateOrUpdateIncident({ }; return ( -
+ Incident Metadata
diff --git a/keep-ui/app/incidents/incident-change-same-in-the-past.tsx b/keep-ui/app/incidents/incident-change-same-in-the-past.tsx index eb44026bf..c59ed9a38 100644 --- a/keep-ui/app/incidents/incident-change-same-in-the-past.tsx +++ b/keep-ui/app/incidents/incident-change-same-in-the-past.tsx @@ -1,14 +1,11 @@ import { Button, Divider, Title } from "@tremor/react"; import Select from "@/components/ui/Select"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { FormEvent, useState } from "react"; -import { toast } from "react-toastify"; import { useIncidents, usePollIncidents } from "../../utils/hooks/useIncidents"; import Loading from "../loading"; -import { updateIncidentRequest } from "./create-or-update-incident"; import { IncidentDto } from "./models"; -import { useApiUrl } from "../../utils/hooks/useConfig"; +import { useIncidentActions } from "@/entities/incidents/model/useIncidentActions"; interface ChangeSameIncidentInThePast { incident: IncidentDto; @@ -27,33 +24,32 @@ const ChangeSameIncidentInThePast = ({ const [selectedIncident, setSelectedIncident] = useState< string | undefined >(); - const { data: session } = useSession(); + const { updateIncident } = useIncidentActions(); const router = useRouter(); - const apiUrl = useApiUrl(); const associateIncidentHandler = async ( selectedIncidentId: string | null ) => { - const response = await updateIncidentRequest({ - session: session, - incidentId: incident.id, - incidentSameIncidentInThePastId: selectedIncidentId, - incidentName: incident.user_generated_name, - incidentUserSummary: incident.user_summary, - incidentAssignee: incident.assignee, - generatedByAi: false, - apiUrl: apiUrl!, - }); - if (response.ok) { - mutate(); - toast.success("Incident updated successfully!"); + try { + await updateIncident( + incident.id, + { + same_incident_in_the_past_id: selectedIncidentId, + }, + false + ); handleClose(); + } catch (error) { + console.error(error); } }; const handleLinkIncident = (e: FormEvent) => { e.preventDefault(); - if (selectedIncident) associateIncidentHandler(selectedIncident); + if (!selectedIncident) { + return; + } + associateIncidentHandler(selectedIncident); }; const handleUnlinkIncident = (e: FormEvent) => { diff --git a/keep-ui/app/incidents/incident-change-status-modal.tsx b/keep-ui/app/incidents/incident-change-status-modal.tsx deleted file mode 100644 index 7c3d701f5..000000000 --- a/keep-ui/app/incidents/incident-change-status-modal.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { Button, Title, Subtitle } from "@tremor/react"; -import Modal from "@/components/ui/Modal"; -import Select, { - CSSObjectWithLabel, - ControlProps, - OptionProps, - GroupBase, -} from "react-select"; -import { useState } from "react"; -import { IncidentDto, Status } from "./models"; -import { useApiUrl } from "utils/hooks/useConfig"; -import { useSession } from "next-auth/react"; -import { toast } from "react-toastify"; -import { STATUS_ICONS } from "@/app/incidents/statuses"; - -const customSelectStyles = { - control: ( - base: CSSObjectWithLabel, - state: ControlProps< - { value: Status; label: JSX.Element }, - false, - GroupBase<{ value: Status; label: JSX.Element }> - > - ) => ({ - ...base, - borderColor: state.isFocused ? "orange" : base.borderColor, - boxShadow: state.isFocused ? "0 0 0 1px orange" : base.boxShadow, - "&:hover": { - borderColor: "orange", - }, - }), - option: ( - base: CSSObjectWithLabel, - { - isFocused, - }: OptionProps< - { value: Status; label: JSX.Element }, - false, - GroupBase<{ value: Status; label: JSX.Element }> - > - ) => ({ - ...base, - backgroundColor: isFocused ? "rgba(255,165,0,0.1)" : base.backgroundColor, - "&:hover": { - backgroundColor: "rgba(255,165,0,0.2)", - }, - }), -}; - -interface Props { - incident: IncidentDto | null | undefined; - mutate: () => void; - handleClose: () => void; -} - -export default function IncidentChangeStatusModal({ - incident, - mutate, - handleClose, -}: Props) { - const { data: session } = useSession(); - const [selectedStatus, setSelectedStatus] = useState(null); - const [comment, setComment] = useState(""); - const apiUrl = useApiUrl(); - - if (!incident) return null; - - const statusOptions = Object.values(Status) - .filter((status) => status !== incident.status) // Exclude current status - .map((status) => ({ - value: status, - label: ( -
- {STATUS_ICONS[status]} - {status.charAt(0).toUpperCase() + status.slice(1)} -
- ), - })); - - const clearAndClose = () => { - setSelectedStatus(null); - handleClose(); - }; - - const handleChangeStatus = async () => { - if (!selectedStatus) { - toast.error("Please select a new status."); - return; - } - - try { - const response = await fetch( - `${apiUrl}/incidents/${incident.id}/status`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${session?.accessToken}`, - }, - body: JSON.stringify({ - status: selectedStatus, - comment: comment, - }), - } - ); - - if (response.ok) { - toast.success("Incident status changed successfully!"); - clearAndClose(); - await mutate(); - } else { - toast.error("Failed to change incident status."); - } - } catch (error) { - toast.error("An error occurred while changing incident status."); - } - }; - - return ( - - Change Incident Status - - Change status from {incident.status}{" "} - to: -
-