feat: Implement Incident Management and Tab Dashboard
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { format } from 'date-fns';
|
||||
import { IncidentItem } from '@/services/incident/types';
|
||||
import { IncidentTableRow } from './IncidentTableRow';
|
||||
import { IncidentDetailDialog } from '../detail-dialog/IncidentDetailDialog';
|
||||
import { EditIncidentDialog } from '../EditIncidentDialog';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
import { IncidentTableSkeleton } from './IncidentTableSkeleton';
|
||||
|
||||
interface IncidentTableProps {
|
||||
data: IncidentItem[];
|
||||
isLoading: boolean;
|
||||
onIncidentUpdated: () => void;
|
||||
onViewDetails?: (incident: IncidentItem) => void;
|
||||
onEditIncident?: (incident: IncidentItem) => void;
|
||||
}
|
||||
|
||||
export const IncidentTable = ({
|
||||
data,
|
||||
isLoading,
|
||||
onIncidentUpdated,
|
||||
onViewDetails,
|
||||
onEditIncident
|
||||
}: IncidentTableProps) => {
|
||||
const { t } = useLanguage();
|
||||
const [selectedIncident, setSelectedIncident] = useState<IncidentItem | null>(null);
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
|
||||
const formatDate = useCallback((dateString: string | undefined) => {
|
||||
if (!dateString) return '-';
|
||||
try {
|
||||
return format(new Date(dateString), 'PPp');
|
||||
} catch (error) {
|
||||
console.error('Error formatting date:', dateString, error);
|
||||
return dateString;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getAffectedSystemsArray = useCallback((affectedSystems: string | undefined): string[] => {
|
||||
if (!affectedSystems) return [];
|
||||
return affectedSystems.split(',').map(system => system.trim()).filter(Boolean);
|
||||
}, []);
|
||||
|
||||
const handleViewDetails = useCallback((incident: IncidentItem) => {
|
||||
setSelectedIncident(incident);
|
||||
setIsDetailOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditIncident = useCallback((incident: IncidentItem) => {
|
||||
setSelectedIncident(incident);
|
||||
setIsEditOpen(true);
|
||||
}, []);
|
||||
|
||||
// Handle status updates efficiently
|
||||
const handleIncidentUpdated = useCallback(() => {
|
||||
console.log("Incident updated in IncidentTable, propagating event");
|
||||
onIncidentUpdated();
|
||||
}, [onIncidentUpdated]);
|
||||
|
||||
// Handle dialog closing
|
||||
const handleDetailDialogClose = useCallback((open: boolean) => {
|
||||
setIsDetailOpen(open);
|
||||
if (!open) {
|
||||
onIncidentUpdated();
|
||||
}
|
||||
}, [onIncidentUpdated]);
|
||||
|
||||
// Handle edit dialog closing
|
||||
const handleEditDialogClose = useCallback((open: boolean) => {
|
||||
setIsEditOpen(open);
|
||||
if (!open) {
|
||||
onIncidentUpdated();
|
||||
}
|
||||
}, [onIncidentUpdated]);
|
||||
|
||||
if (isLoading) {
|
||||
return <IncidentTableSkeleton />;
|
||||
}
|
||||
|
||||
// Add a safety check to prevent map of undefined error
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.error('Data is not an array:', data);
|
||||
return (
|
||||
<div className="p-4 text-center">
|
||||
<p>No incident data available</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('title')}</TableHead>
|
||||
<TableHead>{t('status')}</TableHead>
|
||||
<TableHead>{t('priority')}</TableHead>
|
||||
<TableHead>{t('time')}</TableHead>
|
||||
<TableHead>{t('affected')}</TableHead>
|
||||
<TableHead>{t('impact')}</TableHead>
|
||||
<TableHead>{t('assignedTo')}</TableHead>
|
||||
<TableHead className="text-right">{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item) => (
|
||||
<IncidentTableRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
formatDate={formatDate}
|
||||
getAffectedSystemsArray={getAffectedSystemsArray}
|
||||
onViewDetails={onViewDetails || handleViewDetails}
|
||||
onEditIncident={onEditIncident || handleEditIncident}
|
||||
onIncidentUpdated={handleIncidentUpdated}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Incident detail dialog */}
|
||||
<IncidentDetailDialog
|
||||
open={isDetailOpen}
|
||||
onOpenChange={handleDetailDialogClose}
|
||||
incident={selectedIncident}
|
||||
/>
|
||||
|
||||
{/* Edit incident dialog */}
|
||||
{selectedIncident && (
|
||||
<EditIncidentDialog
|
||||
open={isEditOpen}
|
||||
onOpenChange={handleEditDialogClose}
|
||||
incident={selectedIncident}
|
||||
onIncidentUpdated={onIncidentUpdated}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
|
||||
import React, { memo, useState } from 'react';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Eye } from 'lucide-react';
|
||||
import { IncidentStatusDropdown } from '../IncidentStatusDropdown';
|
||||
import { IncidentActionsMenu } from '../IncidentActionsMenu';
|
||||
import { IncidentItem } from '@/services/incident';
|
||||
import { AssignedUserCell } from './IncidentTableUtils';
|
||||
|
||||
interface IncidentTableRowProps {
|
||||
item: IncidentItem;
|
||||
formatDate: (date: string | undefined) => string;
|
||||
getAffectedSystemsArray: (systems: string | undefined) => string[];
|
||||
onViewDetails?: (incident: IncidentItem) => void;
|
||||
onEditIncident?: (incident: IncidentItem) => void;
|
||||
onIncidentUpdated: () => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export const IncidentTableRow = memo(({
|
||||
item,
|
||||
formatDate,
|
||||
getAffectedSystemsArray,
|
||||
onViewDetails,
|
||||
onEditIncident,
|
||||
onIncidentUpdated,
|
||||
t
|
||||
}: IncidentTableRowProps) => {
|
||||
// Use local state for optimistic UI updates
|
||||
const [localItem, setLocalItem] = useState(item);
|
||||
|
||||
// Update local state when props change
|
||||
React.useEffect(() => {
|
||||
setLocalItem(item);
|
||||
}, [item]);
|
||||
|
||||
// Handle status updates locally
|
||||
const handleStatusUpdated = () => {
|
||||
console.log("Status updated in TableRow, calling onIncidentUpdated");
|
||||
onIncidentUpdated();
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={localItem.id}
|
||||
className="hover:bg-muted/40 cursor-pointer"
|
||||
onClick={() => onViewDetails && onViewDetails(localItem)}
|
||||
>
|
||||
<TableCell className="font-medium max-w-[200px] truncate">
|
||||
{localItem.title || localItem.description || '-'}
|
||||
</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<IncidentStatusDropdown
|
||||
status={localItem.impact_status || localItem.status || 'investigating'}
|
||||
id={localItem.id}
|
||||
onStatusUpdated={handleStatusUpdated}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={
|
||||
localItem.priority?.toLowerCase() === 'critical' ? 'destructive' :
|
||||
localItem.priority?.toLowerCase() === 'high' ? 'default' :
|
||||
localItem.priority?.toLowerCase() === 'medium' ? 'secondary' : 'outline'
|
||||
}>
|
||||
{t(localItem.priority?.toLowerCase() || 'low')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(localItem.created)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{getAffectedSystemsArray(localItem.affected_systems).map((system, idx) => (
|
||||
<Badge key={`${localItem.id}-system-${idx}`} variant="outline">{system}</Badge>
|
||||
))}
|
||||
{getAffectedSystemsArray(localItem.affected_systems).length === 0 && '-'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={
|
||||
localItem.impact?.toLowerCase() === 'critical' ? 'destructive' :
|
||||
localItem.impact?.toLowerCase() === 'high' ? 'default' :
|
||||
localItem.impact?.toLowerCase() === 'medium' ? 'secondary' : 'outline'
|
||||
}>
|
||||
{t(localItem.impact?.toLowerCase() || 'low')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<AssignedUserCell userId={localItem.assigned_to} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex justify-end items-center space-x-2">
|
||||
{onViewDetails && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewDetails(localItem);
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
<span className="sr-only">{t('view')}</span>
|
||||
</Button>
|
||||
)}
|
||||
<IncidentActionsMenu
|
||||
item={localItem}
|
||||
onIncidentUpdated={onIncidentUpdated}
|
||||
onViewDetails={onViewDetails}
|
||||
onEditIncident={onEditIncident}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
IncidentTableRow.displayName = 'IncidentTableRow';
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
import React from 'react';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export const IncidentTableRowSkeleton = () => (
|
||||
<TableRow>
|
||||
<TableCell><Skeleton className="h-5 w-[180px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-5 w-[100px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-5 w-[80px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-5 w-[120px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-5 w-[150px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-5 w-[80px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-5 w-[100px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-5 w-[60px]" /></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
export const IncidentTableSkeleton = () => (
|
||||
<div className="overflow-x-auto">
|
||||
<TableRow>
|
||||
{Array(3).fill(0).map((_, index) => (
|
||||
<IncidentTableRowSkeleton key={`skeleton-${index}`} />
|
||||
))}
|
||||
</TableRow>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { userService, User } from '@/services/userService';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
// Helper function to get user initials
|
||||
export const getUserInitials = (user: User): string => {
|
||||
if (user.full_name) {
|
||||
const nameParts = user.full_name.split(' ');
|
||||
if (nameParts.length > 1) {
|
||||
return `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase();
|
||||
}
|
||||
return user.full_name.substring(0, 2).toUpperCase();
|
||||
}
|
||||
return user.username.substring(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
interface AssignedUserCellProps {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export const AssignedUserCell: React.FC<AssignedUserCellProps> = ({ userId }) => {
|
||||
const { data: user, isLoading } = useQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: async () => {
|
||||
if (!userId) return null;
|
||||
try {
|
||||
return await userService.getUser(userId);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch assigned user:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
enabled: !!userId,
|
||||
staleTime: 300000 // Cache for 5 minutes
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-6 w-20" />;
|
||||
}
|
||||
|
||||
if (!user || !userId) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={user.avatar} alt={user.full_name || user.username} />
|
||||
<AvatarFallback>{getUserInitials(user)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="truncate max-w-[100px]">{user.full_name || user.username}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
export * from './IncidentTable';
|
||||
export * from './IncidentTableRow';
|
||||
export * from './IncidentTableSkeleton';
|
||||
export * from './IncidentTableUtils';
|
||||
Reference in New Issue
Block a user