Merge pull request #3167 from bluewave-labs/incidents-feat-modal

Incidents action button and detail modal
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-22 09:06:53 -08:00
commit 5a0078cf92
11 files changed
+769 -57

No files matched your search

@@ -0,0 +1,404 @@
// Components
import { Stack, Typography, Grid, Box, Button, Divider } from "@mui/material";
import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx";
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import SummaryCard from "../SummaryCard/index.jsx";
import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx";
// Utils
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { formatDateWithTz } from "@/Utils/timeUtils";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
// Hooks
import useFetchIncidents from "../../hooks/useFetchIncidents.js";
import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration.js";
const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
const theme = useTheme();
const { t } = useTranslation();
const { fetchIncidentById, resolveIncident } = useFetchIncidents();
const [incident, setIncident] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false);
const uiTimezone = useSelector((state) => state.ui.timezone);
const isActive = incident?.status === true;
const duration = useGetIncidentsDuration(incident || null, isActive);
const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main;
const toCapitalLetter = (text) =>
typeof text === "string" && text.length > 0
? text.charAt(0).toUpperCase() + text.slice(1)
: text;
const KeyValueRow = ({ label, value, sx = {} }) => (
<Grid
container
spacing={2}
alignItems="flex-start"
sx={{ ...sx }}
>
<Grid
item
xs={4}
>
<Typography variant="body1">{label}</Typography>
</Grid>
<Grid
item
xs
>
<Typography
variant="body1"
textAlign="right"
>
{value ?? "-"}
</Typography>
</Grid>
</Grid>
);
useEffect(() => {
const loadIncident = async () => {
if (!incidentId || !open) {
setIncident(null);
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const incidentData = await fetchIncidentById(incidentId);
setIncident(incidentData);
} catch (error) {
console.error("Error fetching incident:", error);
setIncident(null);
} finally {
setIsLoading(false);
}
};
loadIncident();
}, [incidentId, open, fetchIncidentById]);
const openResolveDialog = () => {
setIsResolveDialogOpen(true);
};
const closeResolveDialog = () => {
setIsResolveDialogOpen(false);
};
const handleAfterResolve = async () => {
if (!incidentId) return;
const updatedIncident = await fetchIncidentById(incidentId);
if (updatedIncident) {
setIncident(updatedIncident);
}
};
const renderContent = () => {
if (isLoading) {
return (
<GenericFallback
isLoading={true}
message={t("incidentsPage.loadingIncidentDetails")}
/>
);
}
if (!incident) {
return (
<GenericFallback
isLoading={false}
message={t("incidentsPage.incidentNotFound")}
/>
);
}
return (
<Stack gap={theme.spacing(8)}>
<Grid
container
spacing={3}
>
<Grid
item
xs={12}
md={12}
>
<SummaryCard title={t("incidentsPage.overview")}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={{ mb: theme.spacing(3) }}
>
<Box
sx={{
width: 8,
height: 8,
borderRadius: "50%",
bgcolor: statusColor,
}}
/>
<Typography
variant="body2"
sx={{
color: statusColor,
fontWeight: 700,
letterSpacing: 0.4,
}}
>
{isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
</Typography>
</Stack>
<Stack
direction="column"
alignItems="start"
gap={theme.spacing(3)}
>
<Stack
direction="row"
gap={2}
sx={{ alignItems: "baseline" }}
>
<Typography
variant="body1"
fontWeight={500}
sx={{ minWidth: 70 }}
>
{t("incidentsPage.incidentItemMonitor")}:
</Typography>
<Typography
variant="body1"
sx={{ flex: 1 }}
>
{incident?.monitorId?.name || t("incidentsPage.unknownMonitor")}
</Typography>
</Stack>
<Stack
direction="row"
gap={2}
sx={{ alignItems: "baseline" }}
>
<Typography
variant="body1"
fontWeight={500}
sx={{ minWidth: 70 }}
>
{t("incidentsPage.URL")}:
</Typography>
<Typography
variant="body1"
sx={{ flex: 1, wordBreak: "break-word", fontFamily: "monospace" }}
>
{incident?.monitorId?.url || "-"}
</Typography>
</Stack>
</Stack>
</SummaryCard>
</Grid>
</Grid>
<Grid
container
spacing={3}
>
<Grid
item
xs={12}
sm={12}
>
<SummaryCard title={t("incidentsPage.incidentAnalysis")}>
<Grid
container
spacing={3}
alignItems="stretch"
>
<Grid
item
xs={12}
sm={6}
>
<Stack gap={theme.spacing(2)}>
<Typography
variant="subtitle1"
fontWeight={700}
>
{t("incidentsPage.timeline")}
</Typography>
<Divider sx={{ mt: 2 }} />
<KeyValueRow
label={t("incidentsPage.startedAt")}
value={
formatDateWithTz(
incident?.startTime,
"D MMM YYYY, h:mm A",
uiTimezone
) || "-"
}
sx={{ paddingBottom: theme.spacing(3) }}
/>
{!isActive && (
<KeyValueRow
label={t("incidentsPage.endedAt")}
value={
formatDateWithTz(
incident?.endTime,
"D MMM YYYY, h:mm A",
uiTimezone
) || "-"
}
sx={{ paddingBottom: theme.spacing(3) }}
/>
)}
<KeyValueRow
label={t("incidentsPage.downtime")}
value={duration || "-"}
/>
</Stack>
</Grid>
<Grid
item
sm="auto"
sx={{ display: { xs: "none", sm: "flex" } }}
>
<Divider
orientation="vertical"
flexItem
/>
</Grid>
<Grid
item
xs={12}
sm
>
<Stack gap={theme.spacing(2)}>
<Typography
variant="subtitle1"
sx={{ letterSpacing: 0.6, fontWeight: 700 }}
>
{isActive
? t("incidentsPage.errorDetails")
: t("incidentsPage.resolutionDetails")}
</Typography>
<Divider sx={{ mt: 2 }} />
<KeyValueRow
label={t("incidentsPage.statusCode")}
value={incident?.statusCode ?? "-"}
/>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
wordBreak: "break-word",
lineHeight: 1.6,
}}
>
{incident?.message || "-"}
</Typography>
{!isActive && (
<>
<Divider sx={{ mb: theme.spacing(2) }} />
<KeyValueRow
label={t("incidentsPage.resolutionMethod")}
value={toCapitalLetter(incident?.resolutionType) || "-"}
/>
{incident?.resolutionType === "manual" && (
<>
<KeyValueRow
label={t("incidentsPage.resolvedBy")}
value={incident?.resolvedBy?.email || "Unknown"}
/>
<KeyValueRow
label={t("incidentsPage.comment")}
value={
incident?.comment?.trim()
? incident.comment
: t("incidentsPage.noCommentProvided")
}
/>
</>
)}
</>
)}
</Stack>
</Grid>
</Grid>
</SummaryCard>
</Grid>
</Grid>
</Stack>
);
};
return (
<GenericDialog
open={open}
onClose={onClose}
theme={theme}
title={t("incidentsPage.incidentDetails")}
width={{ xs: "95%", md: "45%", lg: "40%", xl: "35%" }}
>
<Stack
gap={theme.spacing(6)}
sx={{ maxHeight: "70vh", overflowY: "auto", pr: 1 }}
>
{renderContent()}
<Stack
direction="row"
justifyContent="flex-end"
gap={2}
sx={{ pt: theme.spacing(3) }}
>
{isActive && (
<Button
variant="outlined"
color="error"
onClick={openResolveDialog}
>
{t("incidentsPage.incidentsTableActionResolveManually")}
</Button>
)}
<Button
variant="contained"
color="secondary"
onClick={onClose}
>
{t("close")}
</Button>
</Stack>
<ResolveIncidentDialog
open={isResolveDialogOpen}
incidentId={incidentId}
onClose={closeResolveDialog}
onResolve={resolveIncident}
onAfterResolve={handleAfterResolve}
onResolved={onResolved}
/>
</Stack>
</GenericDialog>
);
};
IncidentDetailsModal.propTypes = {
open: PropTypes.bool.isRequired,
incidentId: PropTypes.string,
onClose: PropTypes.func.isRequired,
onResolved: PropTypes.func,
};
export default IncidentDetailsModal;
@@ -0,0 +1,156 @@
// Components
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Icon from "@/Components/v1/Icon/index.jsx";
import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx";
import useFetchIncidents from "../../hooks/useFetchIncidents";
// Utils
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { TypeToPathMap } from "@/Utils/monitorUtils.js";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => {
const [anchorEl, setAnchorEl] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const theme = useTheme();
const { t } = useTranslation();
const { fetchIncidentById } = useFetchIncidents();
const navigate = useNavigate();
const openMenu = (event) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const closeMenu = (e) => {
if (e) {
e.stopPropagation();
}
setAnchorEl(null);
};
const openResolveDialog = (e) => {
closeMenu(e);
setIsDialogOpen(true);
};
const closeDialog = (e) => {
if (e) {
e.stopPropagation();
}
setIsDialogOpen(false);
};
const isActive = incident.status === true;
const handleGoToDetails = (e) => {
e.stopPropagation();
closeMenu(e);
if (onOpenDetails) {
onOpenDetails(incident.id);
}
};
const handleGoToMonitor = async (e) => {
e.stopPropagation();
closeMenu(e);
try {
const incidentData = await fetchIncidentById(incident.id);
if (incidentData) {
const { type, _id: monitorId } = incidentData.monitorId;
const path = TypeToPathMap[type];
if (path && monitorId) {
navigate(`/${path}/${monitorId}`);
} else {
console.error(`Monitor type not found`);
}
} else {
console.error("Monitor information not found in incident data");
}
} catch (error) {
console.error("Error fetching incident details:", error);
}
};
return (
<>
<IconButton
aria-label="incident actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Icon
name="Settings"
size={20}
/>
</IconButton>
<Menu
className="incident-actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeMenu}
disableScrollLock
slotProps={{
paper: {
sx: {
"& ul": {
p: theme.spacing(2.5),
backgroundColor: theme.palette.primary.main,
},
"& li": { m: 0, color: theme.palette.primary.contrastTextSecondary },
},
},
}}
>
<MenuItem onClick={handleGoToDetails}>
{t("incidentsPage.incidentsTableActionDetails", "Details")}
</MenuItem>
<MenuItem onClick={handleGoToMonitor}>
{t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")}
</MenuItem>
{isActive && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openResolveDialog(e);
}}
>
{t("incidentsPage.incidentsTableActionResolveManually")}
</MenuItem>
)}
</Menu>
<ResolveIncidentDialog
open={isDialogOpen}
incidentId={incident.id}
onClose={closeDialog}
onResolve={onResolve}
/>
</>
);
};
IncidentActionsMenu.propTypes = {
incident: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.bool.isRequired,
}).isRequired,
onResolve: PropTypes.func.isRequired,
onOpenDetails: PropTypes.func,
};
export default IncidentActionsMenu;
@@ -6,13 +6,14 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
import IncidentActionsMenu from "./IncidentActionsMenu.jsx";
//Utils
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { Button, Typography, useTheme } from "@mui/material";
import { Typography, useTheme } from "@mui/material";
const IncidentTable = ({
monitors = [],
@@ -26,15 +27,16 @@ const IncidentTable = ({
handleChangeRowsPerPage,
resolveIncident,
handleUpdateTrigger,
onOpenDetails,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const { t } = useTranslation();
const theme = useTheme();
const handleResolveIncident = async (incidentId) => {
const handleResolveIncident = async (incidentId, options = {}) => {
try {
await resolveIncident(incidentId);
await resolveIncident(incidentId, options);
handleUpdateTrigger();
} catch (error) {
console.error(t("incidentsPage.errorResolvingIncident"), error);
@@ -46,7 +48,6 @@ const IncidentTable = ({
id: "monitorName",
content: t("incidentsTableMonitorName"),
render: (row) => {
console.log(monitors, row);
const monitor = monitors.find((monitor) => monitor.id === row.monitorId);
return monitor ? monitor.name : "N/A";
},
@@ -125,32 +126,13 @@ const IncidentTable = ({
id: "action",
content: t("actions"),
render: (row) => {
if (row.status === true) {
return (
<Button
variant="contained"
color="accent"
sx={{
minHeight: "max-content",
lineHeight: 1.2,
}}
onClick={() => {
handleResolveIncident(row.id);
}}
>
{t("incidentsPage.incidentsTableActionResolveManually")}
</Button>
);
} else {
return (
<Typography
variant="body2"
color={theme.palette.primary.contrastTextSecondary}
>
{t("incidentsPage.incidentsTableResolved")}
</Typography>
);
}
return (
<IncidentActionsMenu
incident={row}
onResolve={handleResolveIncident}
onOpenDetails={onOpenDetails}
/>
);
},
},
];
@@ -204,6 +186,7 @@ IncidentTable.propTypes = {
handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change
resolveIncident: PropTypes.func.isRequired,
handleUpdateTrigger: PropTypes.func.isRequired,
onOpenDetails: PropTypes.func,
};
export default IncidentTable;
@@ -5,36 +5,13 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
import Icon from "@/Components/v1/Icon";
import { useTranslation } from "react-i18next";
import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration";
const IncidentItem = ({ incident }) => {
const { t } = useTranslation();
const theme = useTheme();
const isActive = incident.status === true;
const calculateDuration = () => {
if (!incident.startTime) {
return "-";
}
const startTime = new Date(incident.startTime);
const endTime = isActive
? new Date()
: incident.endTime
? new Date(incident.endTime)
: null;
if (!endTime) {
return "-";
}
const durationMs = endTime.getTime() - startTime.getTime();
if (durationMs < 0) {
return "-";
}
return getHumanReadableDuration(durationMs);
};
const duration = calculateDuration();
const duration = useGetIncidentsDuration(incident, isActive);
const iconWrapperStyle = {
display: "flex",
justifyContent: "center",
@@ -84,7 +84,7 @@ const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null
) : (
<Stack gap={theme.spacing(4)}>
{incidents.map((incident, index) => (
<Box key={incident._id}>
<Box key={incident.id}>
<IncidentItem incident={incident} />
{index < incidents.length - 1 && <Divider sx={{ mt: theme.spacing(2) }} />}
</Box>
@@ -26,6 +26,7 @@ const OptionsHeader = ({
{ _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") },
{ _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") },
{ _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") },
{ _id: "automatic", name: t("incidentsPage.incidentsOptionsHeaderFilterAutomatic") },
];
const stackStyles = {
@@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Button, Stack } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
const ResolveIncidentDialog = ({
open,
incidentId,
onClose,
onResolve,
onAfterResolve,
onResolved,
isLoading = false,
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (open) {
setComment("");
}
}, [open]);
const handleConfirm = async () => {
if (!incidentId) return;
const trimmed = comment.trim();
try {
setIsSubmitting(true);
await onResolve(incidentId, trimmed ? { comment: trimmed } : {});
if (onAfterResolve) {
await onAfterResolve();
}
onResolved?.();
onClose();
} catch (error) {
console.error("Error resolving incident:", error);
} finally {
setIsSubmitting(false);
}
};
return (
<GenericDialog
open={open}
theme={theme}
title={t("incidentsPage.resolveIncidentDialogTitle")}
description={t("incidentsPage.resolveIncidentDialogDescription")}
onClose={onClose}
>
<Stack gap={theme.spacing(4)}>
<TextInput
label={t("incidentsPage.resolveIncidentDialogCommentLabel")}
placeholder={t("incidentsPage.resolveIncidentDialogCommentPlaceholder")}
value={comment}
onChange={(e) => setComment(e.target.value)}
maxWidth="100%"
/>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
justifyContent="flex-end"
>
<Button
variant="contained"
color="secondary"
onClick={onClose}
disabled={isLoading || isSubmitting}
>
{t("cancel", "Cancel")}
</Button>
<Button
variant="contained"
color="error"
onClick={handleConfirm}
disabled={isLoading || isSubmitting}
>
{t("incidentsPage.resolveIncidentDialogConfirm")}
</Button>
</Stack>
</Stack>
</GenericDialog>
);
};
ResolveIncidentDialog.propTypes = {
open: PropTypes.bool.isRequired,
incidentId: PropTypes.string,
onClose: PropTypes.func.isRequired,
onResolve: PropTypes.func.isRequired,
onAfterResolve: PropTypes.func,
isLoading: PropTypes.bool,
onResolved: PropTypes.func,
};
export default ResolveIncidentDialog;
@@ -72,6 +72,16 @@ const useFetchIncidents = () => {
[fetchIncidentsByTeam]
);
const fetchIncidentsByResolutionType = useCallback(
async (resolutionType, config = {}) => {
await fetchIncidentsByTeam({
resolutionType,
sortOrder: "desc",
...config,
});
},
[fetchIncidentsByTeam]
);
/**
* Fetch resolved incidents
*
@@ -198,6 +208,7 @@ const useFetchIncidents = () => {
fetchIncidentById,
resolveIncident,
fetchIncidentSummary,
fetchIncidentsByResolutionType,
};
};
@@ -0,0 +1,25 @@
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
const useGetIncidentsDuration = (incident, isActive) => {
if (!incident?.startTime) {
return "-";
}
const startTime = new Date(incident?.startTime);
const endTime = isActive
? new Date()
: incident?.endTime
? new Date(incident.endTime)
: null;
if (!endTime) {
return "-";
}
const durationMs = endTime.getTime() - startTime.getTime();
if (durationMs < 0) {
return "-";
}
return getHumanReadableDuration(durationMs);
};
export default useGetIncidentsDuration;
+26 -1
View File
@@ -5,7 +5,7 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import IncidentTable from "./Components/IncidentTable/index.jsx";
import OptionsHeader from "./Components/OptionsHeader/index.jsx";
import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx";
import IncidentDetailsModal from "./Components/IncidentDetailsModal/index.jsx";
//Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
@@ -30,6 +30,8 @@ const Incidents2 = () => {
const [rowsPerPage, setRowsPerPage] = useState(10);
const [monitorLookup, setMonitorLookup] = useState(undefined);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
const [selectedIncidentId, setSelectedIncidentId] = useState(null);
const handleUpdateTrigger = () => {
setUpdateTrigger((prev) => !prev);
};
@@ -47,6 +49,7 @@ const Incidents2 = () => {
fetchActiveIncidents,
fetchResolvedIncidents,
resolveIncident,
fetchIncidentsByResolutionType,
} = useFetchIncidents();
const networkError = monitorsNetworkError || incidentsNetworkError;
@@ -70,6 +73,10 @@ const Incidents2 = () => {
fetchActiveIncidents(config);
} else if (filter === "resolved") {
fetchResolvedIncidents(config);
} else if (filter === "manual") {
fetchIncidentsByResolutionType("manual", config);
} else if (filter === "automatic") {
fetchIncidentsByResolutionType("automatic", config);
} else {
fetchIncidents(config);
}
@@ -83,6 +90,7 @@ const Incidents2 = () => {
fetchActiveIncidents,
fetchResolvedIncidents,
fetchIncidents,
fetchIncidentsByResolutionType,
]);
useEffect(() => {
@@ -113,6 +121,16 @@ const Incidents2 = () => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleOpenDetails = (id) => {
if (!id) return;
setSelectedIncidentId(id);
setIsDetailsOpen(true);
};
const handleCloseDetails = () => {
setIsDetailsOpen(false);
setSelectedIncidentId(null);
};
return (
<Stack gap={theme.spacing(10)}>
@@ -143,6 +161,13 @@ const Incidents2 = () => {
handleChangeRowsPerPage={handleChangeRowsPerPage}
resolveIncident={resolveIncident}
handleUpdateTrigger={handleUpdateTrigger}
onOpenDetails={handleOpenDetails}
/>
<IncidentDetailsModal
open={isDetailsOpen}
incidentId={selectedIncidentId}
onClose={handleCloseDetails}
onResolved={handleUpdateTrigger}
/>
</Stack>
);
+29
View File
@@ -249,6 +249,7 @@
"addMonitors": "Add monitors",
"window": "window",
"cancel": "Cancel",
"close": "Close",
"message": "Message",
"low": "low",
"high": "high",
@@ -1201,12 +1202,40 @@
"noIncidentIdProvided": "No incident ID provided",
"errorFetchingIncidentSummary": "Error fetching incident summary",
"incidentsOptionsHeaderFilterManual": "Manual",
"incidentsOptionsHeaderFilterAutomatic": "Automatic",
"incidentsOptionsHeaderFilterAll": "All",
"incidentsOptionsHeaderFilterActive": "Active",
"incidentsOptionsHeaderFilterResolved": "Resolved",
"incidentsTableResolved": "Closed",
"incidentsTableActionResolveManually": "Resolve Manually",
"incidentsTableActionDetails": "Details",
"incidentsTableActionGoToMonitor": "Go to monitor",
"resolveIncidentDialogTitle": "Resolve Incident",
"resolveIncidentDialogDescription": "Are you sure you want to resolve this incident manually?",
"resolveIncidentDialogCommentLabel": "Comment (optional)",
"resolveIncidentDialogCommentPlaceholder": "Add a comment about the resolution...",
"resolveIncidentDialogConfirm": "Resolve",
"hours": "hours",
"loading": "Loading incident details...",
"incidentNotFound": "Incident not found",
"incidentDetails": "INCIDENT DETAILS",
"activeIncident": "ACTIVE INCIDENT",
"resolvedIncident": "RESOLVED INCIDENT",
"overview": "OVERVIEW",
"URL": "URL",
"incidentAnalysis": "INCIDENT ANALYSIS",
"timeline": "Timeline",
"startedAt": "Started at",
"endedAt": "Ended at",
"downtime": "Downtime",
"errorDetails": "Error details",
"resolutionDetails": "Resolution details",
"statusCode": "Status code",
"resolutionMethod": "Resolution method",
"resolvedBy": "Resolved by",
"comment": "Comment",
"noCommentProvided": "No comment provided",
"loadingIncidentDetails": "Loading incident details...",
"none": "None"
}
}