feat: Enhance Uptime heartbeat bar

- Frontend: Increase heartbeat history to 60 items (1 hour)
- Frontend: Add empty bars for alignment when history is sparse
- Frontend: Improve tooltips with detailed status info
- Frontend: Update API client to support limit parameter
This commit is contained in:
Wikid82
2025-11-23 23:50:04 +00:00
parent 527c54582f
commit 642d4d2437
3 changed files with 51 additions and 12 deletions

View File

@@ -26,7 +26,7 @@ export const getMonitors = async () => {
return response.data;
};
export const getMonitorHistory = async (id: string) => {
const response = await client.get<UptimeHeartbeat[]>(`/uptime/monitors/${id}/history`);
export const getMonitorHistory = async (id: string, limit: number = 50) => {
const response = await client.get<UptimeHeartbeat[]>(`/uptime/monitors/${id}/history?limit=${limit}`);
return response.data;
};

View File

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getProviders, createProvider, updateProvider, deleteProvider, testProvider, NotificationProvider } from '../api/notifications';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Bell, Plus, Trash2, Edit2, Send } from 'lucide-react';
import { Bell, Plus, Trash2, Edit2, Send, Check, X, Loader2 } from 'lucide-react';
import { useForm } from 'react-hook-form';
const ProviderForm: React.FC<{
@@ -24,6 +24,25 @@ const ProviderForm: React.FC<{
}
});
const [testStatus, setTestStatus] = useState<'idle' | 'success' | 'error'>('idle');
const testMutation = useMutation({
mutationFn: testProvider,
onSuccess: () => {
setTestStatus('success');
setTimeout(() => setTestStatus('idle'), 3000);
},
onError: () => {
setTestStatus('error');
setTimeout(() => setTestStatus('idle'), 3000);
}
});
const handleTest = () => {
const formData = watch();
testMutation.mutate(formData as any);
};
const type = watch('type');
const setTemplate = (template: string) => {
@@ -141,6 +160,18 @@ const ProviderForm: React.FC<{
<div className="flex justify-end gap-2 pt-4">
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button
type="button"
variant="secondary"
onClick={handleTest}
disabled={testMutation.isPending}
className="min-w-[80px]"
>
{testMutation.isPending ? <Loader2 className="w-4 h-4 animate-spin mx-auto" /> :
testStatus === 'success' ? <Check className="w-4 h-4 text-green-500 mx-auto" /> :
testStatus === 'error' ? <X className="w-4 h-4 text-red-500 mx-auto" /> :
"Test"}
</Button>
<Button type="submit">Save</Button>
</div>
</form>

View File

@@ -7,7 +7,7 @@ import { formatDistanceToNow } from 'date-fns';
const MonitorCard: React.FC<{ monitor: any }> = ({ monitor }) => {
const { data: history } = useQuery({
queryKey: ['uptimeHistory', monitor.id],
queryFn: () => getMonitorHistory(monitor.id),
queryFn: () => getMonitorHistory(monitor.id, 60),
refetchInterval: 60000,
});
@@ -50,22 +50,30 @@ const MonitorCard: React.FC<{ monitor: any }> = ({ monitor }) => {
</div>
</div>
{/* Simple History Bar */}
<div className="flex gap-0.5 h-8 items-end">
{history?.slice(0, 20).reverse().map((beat: any, i: number) => (
{/* Heartbeat Bar (Last 60 checks / 1 Hour) */}
<div className="flex gap-[2px] h-8 items-end" title="Last 60 checks (1 Hour)">
{/* Fill with empty bars if not enough history to keep alignment right-aligned */}
{Array.from({ length: Math.max(0, 60 - (history?.length || 0)) }).map((_, i) => (
<div key={`empty-${i}`} className="flex-1 bg-gray-100 dark:bg-gray-700 rounded-sm h-full opacity-50" />
))}
{history?.slice().reverse().map((beat: any, i: number) => (
<div
key={i}
className={`flex-1 rounded-sm ${
className={`flex-1 rounded-sm transition-all duration-200 hover:scale-110 ${
beat.status === 'up'
? 'bg-green-400 dark:bg-green-600 hover:bg-green-500'
: 'bg-red-400 dark:bg-red-600 hover:bg-red-500'
? 'bg-green-400 dark:bg-green-500 hover:bg-green-300'
: 'bg-red-400 dark:bg-red-500 hover:bg-red-300'
}`}
style={{ height: '100%' }}
title={`${new Date(beat.created_at).toLocaleString()} - ${beat.latency}ms - ${beat.message}`}
title={`${new Date(beat.created_at).toLocaleString()}
Status: ${beat.status.toUpperCase()}
Latency: ${beat.latency}ms
Message: ${beat.message}`}
/>
))}
{(!history || history.length === 0) && (
<div className="w-full text-center text-xs text-gray-400">No history available</div>
<div className="absolute w-full text-center text-xs text-gray-400">No history available</div>
)}
</div>
</div>