feat: Implement Incident Management and Tab Dashboard
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { incidentService, IncidentItem } from '@/services/incident';
|
||||
|
||||
interface UseIncidentDataProps {
|
||||
refreshTrigger?: number;
|
||||
}
|
||||
|
||||
export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) => {
|
||||
const [incidents, setIncidents] = useState<IncidentItem[]>([]);
|
||||
const [filter, setFilter] = useState("unresolved");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
// Use a ref to prevent multiple simultaneous fetch requests
|
||||
const isFetchingRef = useRef(false);
|
||||
// Use a ref to track the refresh trigger
|
||||
const lastRefreshTriggerRef = useRef(refreshTrigger);
|
||||
|
||||
// Simplified fetch function with improved controls to prevent duplicate calls
|
||||
const fetchIncidentData = useCallback(async (force = false) => {
|
||||
// Skip if already fetching
|
||||
if (isFetchingRef.current) {
|
||||
console.log('Already fetching data, skipping additional request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if not forced and already initialized
|
||||
if (initialized && !force) {
|
||||
console.log('Data already initialized and no force refresh, skipping fetch');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set fetching flags
|
||||
isFetchingRef.current = true;
|
||||
|
||||
if (!initialized || force) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
if (force) {
|
||||
setIsRefreshing(true);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log(`Fetching incident data (force=${force})`);
|
||||
const allIncidents = await incidentService.getAllIncidents(force);
|
||||
|
||||
if (Array.isArray(allIncidents)) {
|
||||
setIncidents(allIncidents);
|
||||
console.log(`Successfully set ${allIncidents.length} incidents to state`);
|
||||
} else {
|
||||
setIncidents([]);
|
||||
console.warn('No incidents returned from service');
|
||||
}
|
||||
|
||||
setInitialized(true);
|
||||
setLoading(false);
|
||||
setIsRefreshing(false);
|
||||
} catch (error) {
|
||||
console.error('Error fetching incident data:', error);
|
||||
setError('Failed to load incident data. Please try again later.');
|
||||
setIncidents([]);
|
||||
setInitialized(true);
|
||||
setLoading(false);
|
||||
setIsRefreshing(false);
|
||||
} finally {
|
||||
// Reset fetching flag after a slight delay to prevent rapid consecutive calls
|
||||
setTimeout(() => {
|
||||
isFetchingRef.current = false;
|
||||
}, 500);
|
||||
}
|
||||
}, [initialized]);
|
||||
|
||||
// Only fetch when component mounts or refreshTrigger changes
|
||||
useEffect(() => {
|
||||
// Skip if the refresh trigger hasn't changed (prevents duplicate effect calls)
|
||||
if (refreshTrigger === lastRefreshTriggerRef.current && initialized) {
|
||||
console.log('Refresh trigger unchanged, skipping fetch');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update last refresh trigger ref
|
||||
lastRefreshTriggerRef.current = refreshTrigger;
|
||||
|
||||
// Create an abort controller for cleanup
|
||||
const abortController = new AbortController();
|
||||
let isMounted = true;
|
||||
|
||||
console.log(`useIncidentData effect running, refreshTrigger: ${refreshTrigger}`);
|
||||
|
||||
// Use a longer delay to ensure we don't trigger too many API calls
|
||||
const fetchTimer = setTimeout(() => {
|
||||
if (isMounted) {
|
||||
fetchIncidentData(refreshTrigger > 0);
|
||||
}
|
||||
}, 500); // Use a longer delay
|
||||
|
||||
// Cleanup function to abort any in-flight requests and clear timers
|
||||
return () => {
|
||||
console.log('Cleaning up incident data fetch effect');
|
||||
isMounted = false;
|
||||
clearTimeout(fetchTimer);
|
||||
abortController.abort();
|
||||
};
|
||||
}, [fetchIncidentData, refreshTrigger, initialized]);
|
||||
|
||||
// Filter the data based on the current filter
|
||||
const incidentData = useMemo(() => {
|
||||
if (!initialized || incidents.length === 0) return [];
|
||||
|
||||
console.log(`Filtering incidents by: ${filter}`);
|
||||
|
||||
if (filter === "unresolved") {
|
||||
return incidents.filter(item => {
|
||||
const status = (item.status || item.impact_status || '').toLowerCase();
|
||||
return status !== 'resolved';
|
||||
});
|
||||
} else if (filter === "resolved") {
|
||||
return incidents.filter(item => {
|
||||
const status = (item.status || item.impact_status || '').toLowerCase();
|
||||
return status === 'resolved';
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [filter, incidents, initialized]);
|
||||
|
||||
// Calculate stats for overview cards
|
||||
const overviewStats = useMemo(() => {
|
||||
if (!initialized || incidents.length === 0) {
|
||||
return {
|
||||
unresolved: 0,
|
||||
resolved: 0,
|
||||
critical: 0,
|
||||
highPriority: 0,
|
||||
avgResolutionTime: "0h"
|
||||
};
|
||||
}
|
||||
|
||||
const unresolvedIncidents = incidents.filter(item => {
|
||||
const status = (item.status || item.impact_status || '').toLowerCase();
|
||||
return status !== 'resolved';
|
||||
});
|
||||
|
||||
const resolvedIncidents = incidents.filter(item => {
|
||||
const status = (item.status || item.impact_status || '').toLowerCase();
|
||||
return status === 'resolved';
|
||||
});
|
||||
|
||||
const criticalCount = unresolvedIncidents.filter(item =>
|
||||
(item.priority?.toLowerCase() === 'critical') ||
|
||||
(item.impact?.toLowerCase() === 'critical')
|
||||
).length;
|
||||
|
||||
const highPriorityCount = unresolvedIncidents.filter(item =>
|
||||
(item.priority?.toLowerCase() === 'high')
|
||||
).length;
|
||||
|
||||
let avgResolutionTime = "0h";
|
||||
if (resolvedIncidents.length > 0) {
|
||||
const totalHours = resolvedIncidents.reduce((total, item) => {
|
||||
if (!item.created || !item.resolution_time) return total;
|
||||
|
||||
const createdAt = new Date(item.created).getTime();
|
||||
const resolvedAt = new Date(item.resolution_time).getTime();
|
||||
const durationHours = (resolvedAt - createdAt) / (1000 * 60 * 60);
|
||||
return isNaN(durationHours) ? total : total + durationHours;
|
||||
}, 0);
|
||||
|
||||
avgResolutionTime = `${(totalHours / resolvedIncidents.length).toFixed(1)}h`;
|
||||
}
|
||||
|
||||
return {
|
||||
unresolved: unresolvedIncidents.length,
|
||||
resolved: resolvedIncidents.length,
|
||||
critical: criticalCount,
|
||||
highPriority: highPriorityCount,
|
||||
avgResolutionTime
|
||||
};
|
||||
}, [incidents, initialized]);
|
||||
|
||||
const isEmpty = !incidentData.length && initialized && !loading;
|
||||
|
||||
return {
|
||||
filter,
|
||||
setFilter,
|
||||
incidentData,
|
||||
overviewStats,
|
||||
fetchIncidentData,
|
||||
isEmpty,
|
||||
error,
|
||||
loading,
|
||||
initialized,
|
||||
isRefreshing
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user