diff --git a/client/src/Components/v1/GenericFallback/index.jsx b/client/src/Components/v1/GenericFallback/index.jsx index 137cdd1da..188c09f37 100644 --- a/client/src/Components/v1/GenericFallback/index.jsx +++ b/client/src/Components/v1/GenericFallback/index.jsx @@ -40,13 +40,13 @@ const GenericFallback = ({ children }) => { { - await resolveIncident(incidentId); - handleUpdateTrigger(); + try { + await resolveIncident(incidentId); + handleUpdateTrigger(); + } catch (error) { + console.error(t("incidentsPage.errorResolvingIncident"), error); + } }; const headers = [ @@ -156,7 +160,7 @@ const IncidentTable = ({ ); } - if (!isLoading && incidents == undefined) { + if (!isLoading && !networkError && incidents?.length === 0) { return ( {t("incidentsTableNoIncidents", "No incidents found")} diff --git a/client/src/Pages/v1/Incidents2/Components/StatisticsPanel/index.jsx b/client/src/Pages/v1/Incidents2/Components/StatisticsPanel/index.jsx index 5db635e56..ede9c619a 100644 --- a/client/src/Pages/v1/Incidents2/Components/StatisticsPanel/index.jsx +++ b/client/src/Pages/v1/Incidents2/Components/StatisticsPanel/index.jsx @@ -34,7 +34,7 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { const theme = useTheme(); const { t } = useTranslation(); const totalResolutions = - summary?.totalManualResolutions + summary?.totalAutomaticResolutions; + (summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0); const automaticPercentage = totalResolutions > 0 ? (summary?.totalAutomaticResolutions / totalResolutions) * 100 diff --git a/client/src/Pages/v1/Incidents2/hooks/useFetchIncidents.js b/client/src/Pages/v1/Incidents2/hooks/useFetchIncidents.js index d65bb48db..a67f6b7ee 100644 --- a/client/src/Pages/v1/Incidents2/hooks/useFetchIncidents.js +++ b/client/src/Pages/v1/Incidents2/hooks/useFetchIncidents.js @@ -39,7 +39,7 @@ const useFetchIncidents = () => { * @param {number} [config.page] - Page number * @param {number} [config.rowsPerPage] - Rows per page */ - const fetchIncidentsByTeam = async (config = {}) => { + const fetchIncidentsByTeam = useCallback(async (config = {}) => { try { setIsLoading(true); setNetworkError(false); @@ -55,42 +55,51 @@ const useFetchIncidents = () => { } finally { setIsLoading(false); } - }; + }, []); /** * Fetch active (open) incidents - Default function * * @param {Object} config - Additional configuration */ - const fetchActiveIncidents = async (config = {}) => { - await fetchIncidentsByTeam({ - status: true, - sortOrder: "desc", - ...config, - }); - }; + const fetchActiveIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam({ + status: true, + sortOrder: "desc", + ...config, + }); + }, + [fetchIncidentsByTeam] + ); /** * Fetch resolved incidents * * @param {Object} config - Additional configuration */ - const fetchResolvedIncidents = async (config = {}) => { - await fetchIncidentsByTeam({ - status: false, - sortOrder: "desc", - ...config, - }); - }; + const fetchResolvedIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam({ + status: false, + sortOrder: "desc", + ...config, + }); + }, + [fetchIncidentsByTeam] + ); /** * Fetch incidents with custom filters (alias for fetchIncidentsByTeam) * * @param {Object} config - Configuration object */ - const fetchIncidents = async (config = {}) => { - await fetchIncidentsByTeam(config); - }; + const fetchIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam(config); + }, + [fetchIncidentsByTeam] + ); /** * Fetch a single incident by ID @@ -98,9 +107,9 @@ const useFetchIncidents = () => { * @param {string} incidentId - The ID of the incident to fetch * @returns {Promise} The incident object or null if not found */ - const fetchIncidentById = async (incidentId) => { + const fetchIncidentById = useCallback(async (incidentId) => { if (!incidentId) { - console.error(t("incidentsPage.noIcidentIdProvided")); + console.error(t("incidentsPage.noIncidentIdProvided")); return null; } @@ -118,7 +127,7 @@ const useFetchIncidents = () => { } finally { setIsLoading(false); } - }; + }, []); /** * Resolve an incident manually @@ -129,9 +138,9 @@ const useFetchIncidents = () => { * @param {Function} [onSuccess] - Callback function to call on success * @param {Function} [onError] - Callback function to call on error */ - const resolveIncident = async (incidentId, options = {}) => { + const resolveIncident = useCallback(async (incidentId, options = {}) => { if (!incidentId) { - console.error(t("incidentsPage.noIcidentIdProvided")); + console.error(t("incidentsPage.noIncidentIdProvided")); return; } @@ -156,7 +165,7 @@ const useFetchIncidents = () => { } finally { setIsLoading(false); } - }; + }, []); /** * Fetch incident summary diff --git a/client/src/Pages/v1/Incidents2/index.jsx b/client/src/Pages/v1/Incidents2/index.jsx index 19425fa37..2983a38e8 100644 --- a/client/src/Pages/v1/Incidents2/index.jsx +++ b/client/src/Pages/v1/Incidents2/index.jsx @@ -23,8 +23,8 @@ const Incidents2 = () => { { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, ]; - const [selectedMonitor, setSelectedMonitor] = useState(undefined); - const [filter, setFilter] = useState(undefined); + const [selectedMonitor, setSelectedMonitor] = useState("0"); + const [filter, setFilter] = useState("all"); const [dateRange, setDateRange] = useState("all"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); @@ -73,7 +73,17 @@ const Incidents2 = () => { } else { fetchIncidents(config); } - }, [selectedMonitor, filter, dateRange, page, rowsPerPage, updateTrigger]); // Refetch when filters or pagination change + }, [ + selectedMonitor, + filter, + dateRange, + page, + rowsPerPage, + updateTrigger, + fetchActiveIncidents, + fetchResolvedIncidents, + fetchIncidents, + ]); useEffect(() => { const lookup = monitors?.reduce((acc, monitor) => { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 259b64641..79834584d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1178,7 +1178,7 @@ "incidentResolvedFailed": "Error resolving incident", "errorFetchingIncidents": "Error fetching incidents", "errorFetchingIncident": "Error fetching incident", - "noIcidentIdProvided": "No incident ID provided", + "noIncidentIdProvided": "No incident ID provided", "errorFetchingIncidentSummary": "Error fetching incident summary", "incidentsOptionsHeaderFilterManual": "Manual", "incidentsOptionsHeaderFilterAll": "All", diff --git a/server/src/db/v1/modules/incidentModule.js b/server/src/db/v1/modules/incidentModule.js index 7e9048eaf..366e309f6 100644 --- a/server/src/db/v1/modules/incidentModule.js +++ b/server/src/db/v1/modules/incidentModule.js @@ -166,6 +166,38 @@ class IncidentModule { } }; + /** + * Get incidents by team with filtering + * + * STATUS PARAMETER CONTRACT (IMPORTANT - Frontend dependency): + * This method has an implicit contract with frontend's useFetchIncidents hook: + * + * - status === undefined: Frontend uses this to represent "all incidents" (filter === "all") + * Backend applies $or logic: { status: true } OR { status: false, endTime: >= dateRange } + * This shows all active incidents + resolved incidents within the date range + * + * - status === true: Frontend uses for "active incidents" (filter === "active") + * Backend shows all active incidents (no date filter applied) + * Active incidents are currently happening, so date range doesn't apply + * + * - status === false: Frontend uses for "resolved incidents" (filter === "resolved") + * Backend filters by endTime >= dateRange + * Only shows incidents that were resolved within the specified date range + * + * WARNING: Changing this logic will break frontend filtering behavior. + * The frontend depends on undefined status triggering the $or logic for "all incidents". + * This contract must be maintained when modifying either frontend or backend code. + * + * @param {Object} params - Query parameters + * @param {string} params.teamId - Team ID + * @param {string} [params.sortOrder] - Sort order (asc/desc) + * @param {string} [params.dateRange] - Date range filter + * @param {number} [params.page] - Page number + * @param {number} [params.rowsPerPage] - Rows per page + * @param {boolean|undefined} [params.status] - Status filter. undefined = all, true = active, false = resolved + * @param {string} [params.monitorId] - Monitor ID filter + * @param {string} [params.resolutionType] - Resolution type filter + */ getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType }) => { try { page = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; @@ -187,10 +219,10 @@ class IncidentModule { ...(resolutionType && { resolutionType }), }; - // Date range filter: - // - Active incidents: always show (no date filter) - they're currently happening - // - Resolved incidents: filter by endTime (when they were resolved) - // - All incidents: show all active + resolved in the range + // Date range filter logic (see contract documentation above): + // - Active incidents (statusBoolean === true): always show (no date filter) - they're currently happening + // - Resolved incidents (statusBoolean === false): filter by endTime (when they were resolved) + // - All incidents (statusBoolean === undefined): show all active + resolved in the range using $or if (dateRangeLookup[dateRange]) { const dateThreshold = dateRangeLookup[dateRange]; if (statusBoolean === true) { @@ -201,6 +233,7 @@ class IncidentModule { matchStage.endTime = { $gte: dateThreshold }; } else { // All incidents: show all active + resolved incidents in the range + // This is the critical contract: undefined status triggers $or logic matchStage.$or = [ { status: true }, // All active incidents { status: false, endTime: { $gte: dateThreshold } }, // Resolved in range @@ -374,7 +407,7 @@ class IncidentModule { const latestIncidentsPipeline = [ { $match: matchStage }, { $sort: { createdAt: -1 } }, - { $limit: parseInt(limit) }, + { $limit: Math.max(1, parseInt(limit) || 10) }, { $lookup: { from: "monitors",