Add UI Enhancements

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 20:36:50 +00:00
parent db84e99746
commit d620787f42
2 changed files with 693 additions and 28 deletions

View File

@@ -5,6 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import {
FileEdit,
Plus,
@@ -27,7 +28,11 @@ import {
Ban,
UserCheck,
MessageSquare,
MessageSquareX
MessageSquareX,
Search,
RefreshCw,
Filter,
X
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import {
@@ -169,11 +174,18 @@ export const SystemActivityLog = forwardRef<SystemActivityLogRef, SystemActivity
({ limit = 50, showFilters = true }, ref) => {
const [activities, setActivities] = useState<SystemActivity[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [filterType, setFilterType] = useState<ActivityType | 'all'>('all');
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState('');
const [showFiltersPanel, setShowFiltersPanel] = useState(false);
const loadActivities = async () => {
setIsLoading(true);
const loadActivities = async (showLoader = true) => {
if (showLoader) {
setIsLoading(true);
} else {
setIsRefreshing(true);
}
try {
const data = await fetchSystemActivities(limit, {
type: filterType === 'all' ? undefined : filterType,
@@ -183,9 +195,14 @@ export const SystemActivityLog = forwardRef<SystemActivityLogRef, SystemActivity
console.error('Error loading system activities:', error);
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};
const handleRefresh = () => {
loadActivities(false);
};
useEffect(() => {
loadActivities();
}, [limit, filterType]);
@@ -206,6 +223,39 @@ export const SystemActivityLog = forwardRef<SystemActivityLogRef, SystemActivity
});
};
const clearFilters = () => {
setFilterType('all');
setSearchQuery('');
};
const hasActiveFilters = filterType !== 'all' || searchQuery.length > 0;
// Filter activities based on search query
const filteredActivities = activities.filter(activity => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
// Search in actor username/display name
if (activity.actor?.username?.toLowerCase().includes(query)) return true;
if (activity.actor?.display_name?.toLowerCase().includes(query)) return true;
// Search in action
if (activity.action.toLowerCase().includes(query)) return true;
// Search in type
if (activity.type.toLowerCase().includes(query)) return true;
// Search in details based on activity type
const details = activity.details;
if ('entity_name' in details && details.entity_name?.toLowerCase().includes(query)) return true;
if ('target_username' in details && details.target_username?.toLowerCase().includes(query)) return true;
if ('username' in details && details.username?.toLowerCase().includes(query)) return true;
if ('submission_type' in details && details.submission_type?.toLowerCase().includes(query)) return true;
return false;
});
const renderActivityDetails = (activity: SystemActivity) => {
const isExpanded = expandedIds.has(activity.id);
@@ -706,38 +756,184 @@ export const SystemActivityLog = forwardRef<SystemActivityLogRef, SystemActivity
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>System Activity Log</CardTitle>
<CardDescription>
Complete audit trail of all system changes and actions
</CardDescription>
<div className="flex flex-col gap-4">
<div className="flex items-start justify-between">
<div>
<CardTitle>System Activity Log</CardTitle>
<CardDescription>
Complete audit trail of all system changes and actions
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isRefreshing}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
Refresh
</Button>
{showFilters && (
<Button
variant={showFiltersPanel ? 'default' : 'outline'}
size="sm"
onClick={() => setShowFiltersPanel(!showFiltersPanel)}
>
<Filter className="h-4 w-4 mr-2" />
Filters
{hasActiveFilters && (
<Badge variant="secondary" className="ml-2 px-1.5 py-0.5 text-xs">
{(filterType !== 'all' ? 1 : 0) + (searchQuery ? 1 : 0)}
</Badge>
)}
</Button>
)}
</div>
</div>
{showFilters && (
<Select value={filterType} onValueChange={(value) => setFilterType(value as ActivityType | 'all')}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Activities</SelectItem>
{Object.entries(activityTypeConfig).map(([key, config]) => (
<SelectItem key={key} value={key}>
{config.label}
</SelectItem>
))}
</SelectContent>
</Select>
{showFilters && showFiltersPanel && (
<div className="flex flex-col gap-3 p-4 bg-muted/50 rounded-lg border">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium">Filter Activities</h4>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="h-7 text-xs"
>
<X className="h-3 w-3 mr-1" />
Clear
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
Activity Type
</label>
<Select value={filterType} onValueChange={(value) => setFilterType(value as ActivityType | 'all')}>
<SelectTrigger>
<SelectValue placeholder="All types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
<div className="flex items-center gap-2">
All Activities
</div>
</SelectItem>
{Object.entries(activityTypeConfig).map(([key, config]) => {
const Icon = config.icon;
return (
<SelectItem key={key} value={key}>
<div className="flex items-center gap-2">
<Icon className={`h-4 w-4 ${config.color}`} />
{config.label}
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
Search
</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by user, entity, or action..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
</div>
</div>
{hasActiveFilters && (
<div className="flex items-center gap-2 pt-2 border-t">
<span className="text-xs text-muted-foreground">Active filters:</span>
{filterType !== 'all' && (
<Badge variant="secondary" className="text-xs">
{activityTypeConfig[filterType as keyof typeof activityTypeConfig]?.label || filterType}
</Badge>
)}
{searchQuery && (
<Badge variant="secondary" className="text-xs">
Search: "{searchQuery}"
</Badge>
)}
</div>
)}
</div>
)}
</div>
</CardHeader>
<CardContent>
{activities.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No activities found
{filteredActivities.length === 0 ? (
<div className="text-center py-12">
{hasActiveFilters ? (
<div className="space-y-3">
<div className="flex justify-center">
<div className="p-4 bg-muted rounded-full">
<Search className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="font-medium text-lg mb-1">No activities found</h3>
<p className="text-muted-foreground text-sm">
Try adjusting your filters or search query
</p>
</div>
<Button variant="outline" size="sm" onClick={clearFilters}>
Clear Filters
</Button>
</div>
) : (
<div className="space-y-3">
<div className="flex justify-center">
<div className="p-4 bg-muted rounded-full">
<History className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="font-medium text-lg mb-1">No activities yet</h3>
<p className="text-muted-foreground text-sm">
System activities will appear here as they occur
</p>
</div>
</div>
)}
</div>
) : (
<div className="space-y-4">
{activities.map((activity) => {
<div className="space-y-1">
<div className="flex items-center justify-between mb-3 pb-3 border-b">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">
Showing {filteredActivities.length} {filteredActivities.length === 1 ? 'activity' : 'activities'}
</span>
{hasActiveFilters && (
<span className="text-xs text-muted-foreground">
(filtered from {activities.length})
</span>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setExpandedIds(new Set())}
className="h-7 text-xs"
disabled={expandedIds.size === 0}
>
Collapse All
</Button>
</div>
{filteredActivities.map((activity) => {
const config = activityTypeConfig[activity.type];
const Icon = config.icon;
const isExpanded = expandedIds.has(activity.id);