Skip to content

Commit

Permalink
feat: add status for incidents (keephq#1905)
Browse files Browse the repository at this point in the history
Signed-off-by: Vladimir Filonov <[email protected]>
Co-authored-by: Tal <[email protected]>
  • Loading branch information
VladimirFilonov and talboren authored Sep 18, 2024
1 parent 026d12b commit f97c63b
Show file tree
Hide file tree
Showing 33 changed files with 484 additions and 168 deletions.
2 changes: 1 addition & 1 deletion keep-ui/app/incidents/[id]/incident-alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import IncidentAlertMenu from "./incident-alert-menu";
import IncidentPagination from "../incident-pagination";
import React, {Dispatch, SetStateAction, useEffect, useState} from "react";
import {IncidentDto} from "../model";
import {IncidentDto} from "../models";

interface Props {
incident: IncidentDto;
Expand Down
2 changes: 1 addition & 1 deletion keep-ui/app/incidents/[id]/incident-info.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import {Button, Title} from "@tremor/react";
import { IncidentDto } from "../model";
import { IncidentDto } from "../models";
import CreateOrUpdateIncident from "../create-or-update-incident";
import Modal from "@/components/ui/Modal";
import React, {useState} from "react";
Expand Down
2 changes: 1 addition & 1 deletion keep-ui/app/incidents/create-or-update-incident.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useSession } from "next-auth/react";
import { FormEvent, useEffect, useState } from "react";
import { toast } from "react-toastify";
import { getApiURL } from "utils/apiUrl";
import { IncidentDto } from "./model";
import { IncidentDto } from "./models";
import { useIncidents } from "utils/hooks/useIncidents";

interface Props {
Expand Down
2 changes: 1 addition & 1 deletion keep-ui/app/incidents/incident-candidate-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {getApiURL} from "../../utils/apiUrl";
import {toast} from "react-toastify";
import {IncidentDto, PaginatedIncidentsDto} from "./model";
import {IncidentDto, PaginatedIncidentsDto} from "./models";
import {Session} from "next-auth";

interface Props {
Expand Down
153 changes: 153 additions & 0 deletions keep-ui/app/incidents/incident-change-status-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
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 { getApiURL } from "utils/apiUrl";
import { useSession } from "next-auth/react";
import { toast } from "react-toastify";
import {
CheckCircleIcon,
ExclamationCircleIcon,
PauseIcon,
} from "@heroicons/react/24/outline";

const statusIcons = {
[Status.Firing]: <ExclamationCircleIcon className="w-4 h-4 mr-2" />,
[Status.Resolved]: <CheckCircleIcon className="w-4 h-4 mr-2" />,
[Status.Acknowledged]: <PauseIcon className="w-4 h-4 mr-2" />,
};

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<Status | null>(null);
const [comment, setComment] = useState<string>("");

if (!incident) return null;

const statusOptions = Object.values(Status)
.filter((status) => status !== incident.status) // Exclude current status
.map((status) => ({
value: status,
label: (
<div className="flex items-center">
{statusIcons[status]}
<span>{status.charAt(0).toUpperCase() + status.slice(1)}</span>
</div>
),
}));

const clearAndClose = () => {
setSelectedStatus(null);
handleClose();
};

const handleChangeStatus = async () => {
if (!selectedStatus) {
toast.error("Please select a new status.");
return;
}

try {
const response = await fetch(`${getApiURL()}/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 (
<Modal onClose={handleClose} isOpen={!!incident}>
<Title>Change Incident Status</Title>
<Subtitle className="flex items-center">
Change status from <strong className="mx-2">{incident.status}</strong> to:
<div className="flex-1">
<Select
options={statusOptions}
value={statusOptions.find(
(option) => option.value === selectedStatus
)}
onChange={(option) => setSelectedStatus(option?.value || null)}
placeholder="Select new status"
className="ml-2"
styles={customSelectStyles}
/>
</div>
</Subtitle>
<div className="flex justify-end mt-4 space-x-2">
<Button onClick={handleChangeStatus} color="orange">
Change Status
</Button>
<Button onClick={handleClose} color="orange" variant="secondary">
Cancel
</Button>
</div>
</Modal>
);
}
2 changes: 1 addition & 1 deletion keep-ui/app/incidents/incident-pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Button, Text } from "@tremor/react";
import { StylesConfig, SingleValueProps, components, GroupBase } from 'react-select';
import Select from 'react-select';
import { Table } from "@tanstack/react-table";
import {IncidentDto} from "./model";
import {IncidentDto} from "./models";
import {AlertDto} from "../alerts/models";

interface Props {
Expand Down
2 changes: 1 addition & 1 deletion keep-ui/app/incidents/incident-table-component.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow} from "@tremor/react";
import {flexRender, Header, Table as ReactTable} from "@tanstack/react-table";
import React, {ReactNode} from "react";
import { IncidentDto } from "./model";
import { IncidentDto } from "./models";
import { useRouter } from "next/navigation";
import {FaArrowDown, FaArrowRight, FaArrowUp} from "react-icons/fa";
interface Props {
Expand Down
2 changes: 1 addition & 1 deletion keep-ui/app/incidents/incident.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Card, Title, Subtitle, Button, Badge } from "@tremor/react";
import Loading from "app/loading";
import { useState } from "react";
import { IncidentDto } from "./model";
import { IncidentDto } from "./models";
import CreateOrUpdateIncident from "./create-or-update-incident";
import IncidentsTable from "./incidents-table";
import { useIncidents, usePollIncidents } from "utils/hooks/useIncidents";
Expand Down
71 changes: 63 additions & 8 deletions keep-ui/app/incidents/incidents-table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Button,
Badge,
Icon,
} from "@tremor/react";
import {
ExpandedState,
Expand All @@ -11,14 +12,20 @@ import {
getSortedRowModel,
ColumnDef,
} from "@tanstack/react-table";
import { MdRemoveCircle, MdModeEdit } from "react-icons/md";
import {MdRemoveCircle, MdModeEdit, MdKeyboardDoubleArrowRight} from "react-icons/md";
import { useSession } from "next-auth/react";
import {IncidentDto, PaginatedIncidentsDto} from "./model";
import {IncidentDto, PaginatedIncidentsDto, Status} from "./models";
import React, {Dispatch, SetStateAction, useEffect, useState} from "react";
import Image from "next/image";
import IncidentPagination from "./incident-pagination";
import IncidentTableComponent from "./incident-table-component";
import {deleteIncident} from "./incident-candidate-actions";
import {
CheckCircleIcon,
ExclamationCircleIcon,
PauseIcon,
} from "@heroicons/react/24/outline";
import IncidentChangeStatusModal from "./incident-change-status-modal";

const columnHelper = createColumnHelper<IncidentDto>();

Expand All @@ -31,6 +38,27 @@ interface Props {
editCallback: (rule: IncidentDto) => void;
}

const STATUS_ICONS = {
[Status.Firing]: <Icon
icon={ExclamationCircleIcon}
tooltip={Status.Firing}
color="red"
className="w-4 h-4 mr-2"
/>,
[Status.Resolved]: <Icon
icon={CheckCircleIcon}
tooltip={Status.Resolved}
color="green"
className="w-4 h-4 mr-2"
/>,
[Status.Acknowledged]: <Icon
icon={PauseIcon}
tooltip={Status.Acknowledged}
color="gray"
className="w-4 h-4 mr-2"
/>,
};

export default function IncidentsTable({
incidents: incidents,
mutate,
Expand All @@ -45,6 +73,13 @@ export default function IncidentsTable({
pageIndex: Math.ceil(incidents.offset / incidents.limit),
pageSize: incidents.limit,
});
const [changeStatusIncident, setChangeStatusIncident] = useState<IncidentDto | null>();

const handleChangeStatus = (e: React.MouseEvent, incident: IncidentDto) => {
e.preventDefault();
e.stopPropagation();
setChangeStatusIncident(incident);
}

useEffect(() => {
if (incidents.limit != pagination.pageSize) {
Expand All @@ -63,6 +98,11 @@ export default function IncidentsTable({
}, [pagination])

const columns = [
columnHelper.display({
id: "status",
header: "Status",
cell: ({ row }) => <span onClick={(e) => handleChangeStatus(e, row.original!)}>{STATUS_ICONS[row.original.status]}</span>,
}),
columnHelper.display({
id: "name",
header: "Name",
Expand Down Expand Up @@ -101,8 +141,8 @@ export default function IncidentsTable({
columnHelper.display({
id: "alert_sources",
header: "Alert Sources",
cell: (context) =>
context.row.original.alert_sources.map((alert_sources, index) => (
cell: ({ row }) =>
row.original.alert_sources.map((alert_sources, index) => (
<Image
className={`inline-block ${index == 0 ? "" : "-ml-2"}`}
key={alert_sources}
Expand All @@ -126,7 +166,7 @@ export default function IncidentsTable({
columnHelper.display({
id: "assignee",
header: "Assignee",
cell: ({row}) => row.original.assignee
cell: ({ row }) => row.original.assignee
}),
columnHelper.accessor("creation_time", {
id: "creation_time",
Expand All @@ -137,29 +177,39 @@ export default function IncidentsTable({
columnHelper.display({
id: "delete",
header: "",
cell: (context) => (
cell: ({ row }) => (
<div className={"space-x-1 flex flex-row items-center justify-center"}>
{/*If user wants to edit the mapping. We use the callback to set the data in mapping.tsx which is then passed to the create-new-mapping.tsx form*/}
<Button
color="orange"
size="xs"
variant="secondary"
tooltip="Edit"
icon={MdModeEdit}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
editCallback(context.row.original!);
editCallback(row.original!);
}}
/>
<Button
color="orange"
size="xs"
variant="secondary"
icon={MdKeyboardDoubleArrowRight}
tooltip="Change status"
onClick={(e) => handleChangeStatus(e, row.original!)}
/>
<Button
color="red"
size="xs"
variant="secondary"
tooltip="Remove"
icon={MdRemoveCircle}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
await deleteIncident({incidentId: context.row.original.id!, mutate, session});
await deleteIncident({incidentId: row.original.id!, mutate, session});
}}
/>
</div>
Expand Down Expand Up @@ -191,6 +241,11 @@ export default function IncidentsTable({
return (
<div>
<IncidentTableComponent table={table} />
<IncidentChangeStatusModal
incident={changeStatusIncident}
mutate={mutate}
handleClose={() => setChangeStatusIncident(null)}
/>
<div className="mt-4 mb-8">
<IncidentPagination table={table} isRefreshAllowed={true}/>
</div>
Expand Down
Loading

0 comments on commit f97c63b

Please sign in to comment.