Improve Activity event handling in the UI (#254)
Improve Activity event handling in the UI - fixes #252 found that the Activity page showed activity inconsistent with /api/metrics - Change data structure for event metrics to array. - Add Event stream connections status indicator
This commit is contained in:
@@ -4,6 +4,7 @@ import { APIProvider } from "./contexts/APIProvider";
|
||||
import LogViewerPage from "./pages/LogViewer";
|
||||
import ModelPage from "./pages/Models";
|
||||
import ActivityPage from "./pages/Activity";
|
||||
import ConnectionStatus from "./components/ConnectionStatus";
|
||||
import { RiSunFill, RiMoonFill } from "react-icons/ri";
|
||||
|
||||
function App() {
|
||||
@@ -31,6 +32,7 @@ function App() {
|
||||
<button className="" onClick={toggleTheme}>
|
||||
{isDarkMode ? <RiMoonFill /> : <RiSunFill />}
|
||||
</button>
|
||||
<ConnectionStatus />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
36
ui/src/components/ConnectionStatus.tsx
Normal file
36
ui/src/components/ConnectionStatus.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useAPI } from "../contexts/APIProvider";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
|
||||
type ConnectionStatus = "disconnected" | "connecting" | "connected";
|
||||
|
||||
const ConnectionStatus = () => {
|
||||
const { getConnectionStatus } = useAPI();
|
||||
const [eventStreamStatus, setEventStreamStatus] = useState<ConnectionStatus>("disconnected");
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setEventStreamStatus(getConnectionStatus());
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
const eventStatusColor = useMemo(() => {
|
||||
switch (eventStreamStatus) {
|
||||
case "connected":
|
||||
return "bg-green-500";
|
||||
case "connecting":
|
||||
return "bg-yellow-500";
|
||||
case "disconnected":
|
||||
default:
|
||||
return "bg-red-500";
|
||||
}
|
||||
}, [eventStreamStatus]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center" title={`event stream: ${eventStreamStatus}`}>
|
||||
<span className={`inline-block w-3 h-3 rounded-full ${eventStatusColor} mr-2`}></span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionStatus;
|
||||
@@ -20,6 +20,7 @@ interface APIProviderType {
|
||||
proxyLogs: string;
|
||||
upstreamLogs: string;
|
||||
metrics: Metrics[];
|
||||
getConnectionStatus: () => "connected" | "connecting" | "disconnected";
|
||||
}
|
||||
|
||||
interface Metrics {
|
||||
@@ -63,6 +64,16 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getConnectionStatus = useCallback(() => {
|
||||
if (apiEventSource.current?.readyState === EventSource.OPEN) {
|
||||
return "connected";
|
||||
} else if (apiEventSource.current?.readyState === EventSource.CONNECTING) {
|
||||
return "connecting";
|
||||
} else {
|
||||
return "disconnected";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const enableAPIEvents = useCallback((enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
apiEventSource.current?.close();
|
||||
@@ -77,6 +88,14 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider
|
||||
const connect = () => {
|
||||
const eventSource = new EventSource("/api/events");
|
||||
|
||||
eventSource.onopen = () => {
|
||||
// clear everything out on connect to keep things in sync
|
||||
setProxyLogs("");
|
||||
setUpstreamLogs("");
|
||||
setMetrics([]); // clear metrics on reconnect
|
||||
setModels([]); // clear models on reconnect
|
||||
};
|
||||
|
||||
eventSource.onmessage = (e: MessageEvent) => {
|
||||
try {
|
||||
const message = JSON.parse(e.data) as APIEventEnvelope;
|
||||
@@ -108,9 +127,9 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider
|
||||
|
||||
case "metrics":
|
||||
{
|
||||
const newMetric = JSON.parse(message.data) as Metrics;
|
||||
const newMetrics = JSON.parse(message.data) as Metrics[];
|
||||
setMetrics((prevMetrics) => {
|
||||
return [newMetric, ...prevMetrics];
|
||||
return [...newMetrics, ...prevMetrics];
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -194,6 +213,7 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider
|
||||
proxyLogs,
|
||||
upstreamLogs,
|
||||
metrics,
|
||||
getConnectionStatus,
|
||||
}),
|
||||
[models, listModels, unloadAllModels, loadModel, enableAPIEvents, proxyLogs, upstreamLogs, metrics]
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useAPI } from "../contexts/APIProvider";
|
||||
|
||||
const formatTimestamp = (timestamp: string): string => {
|
||||
@@ -15,25 +15,10 @@ const formatDuration = (ms: number): string => {
|
||||
|
||||
const ActivityPage = () => {
|
||||
const { metrics } = useAPI();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (metrics.length > 0) {
|
||||
setError(null);
|
||||
}
|
||||
const sortedMetrics = useMemo(() => {
|
||||
return [...metrics].sort((a, b) => b.id - a.id);
|
||||
}, [metrics]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Activity</h1>
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<p className="text-red-800">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Activity</h1>
|
||||
@@ -47,6 +32,7 @@ const ActivityPage = () => {
|
||||
<table className="min-w-full divide-y">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider">Id</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Timestamp</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Model</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Input Tokens</th>
|
||||
@@ -57,8 +43,9 @@ const ActivityPage = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{metrics.map((metric, index) => (
|
||||
<tr key={`${metric.id}-${index}`}>
|
||||
{sortedMetrics.map((metric) => (
|
||||
<tr key={`metric_${metric.id}`}>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm">{metric.id + 1 /* un-zero index */}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatTimestamp(metric.timestamp)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.model}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.input_tokens.toLocaleString()}</td>
|
||||
|
||||
Reference in New Issue
Block a user