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:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user