96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import {
|
|
Box, Typography, Paper, Chip, Snackbar,
|
|
} from "@mui/material";
|
|
import type { ResourceConfig } from "../types";
|
|
import { useResource, readSseCache, appendSseCache, clearSseCache, nextSseSeq, setSseConnected } from "../context/useResource";
|
|
import { applyDisplayFormat } from "./fields";
|
|
import { SseConnectionStatus } from "./SseConnectionStatus";
|
|
|
|
interface SseStreamViewProps {
|
|
resource: ResourceConfig;
|
|
}
|
|
|
|
export function SseStreamView({ resource }: SseStreamViewProps) {
|
|
const { stream } = useResource(resource.name);
|
|
const [events, setEvents] = useState<any[]>(() => readSseCache(resource.name));
|
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
const [snackbarMsg, setSnackbarMsg] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (!stream) return;
|
|
setSseConnected(resource.name, false);
|
|
|
|
const sub = stream({
|
|
onEvent: (evt) => {
|
|
const enriched = { ...evt, _received_at: new Date().toISOString(), _seq: nextSseSeq() };
|
|
const updated = appendSseCache(resource.name, enriched);
|
|
setEvents([...updated]);
|
|
setSnackbarMsg(applyDisplayFormat(evt, resource.displayFormat));
|
|
setSnackbarOpen(true);
|
|
},
|
|
onOpen: () => setSseConnected(resource.name, true),
|
|
onError: () => setSseConnected(resource.name, false),
|
|
});
|
|
|
|
return () => {
|
|
setSseConnected(resource.name, false);
|
|
sub.close();
|
|
};
|
|
}, [resource.name]);
|
|
|
|
const eventCount = events.length;
|
|
const latestEvent = events[events.length - 1] ?? null;
|
|
|
|
return (
|
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2.5 }}>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
|
<Typography variant="subtitle1" fontWeight={700}>
|
|
{resource.displayName}
|
|
</Typography>
|
|
<SseConnectionStatus resourceName={resource.name} />
|
|
</Box>
|
|
<Chip
|
|
label={eventCount > 0 ? `${eventCount} event${eventCount !== 1 ? "s" : ""}` : "No events"}
|
|
size="small"
|
|
variant="outlined"
|
|
color={eventCount > 0 ? "primary" : "default"}
|
|
/>
|
|
</Box>
|
|
|
|
{latestEvent ? (
|
|
<Box
|
|
sx={{
|
|
bgcolor: "grey.50",
|
|
borderRadius: 1,
|
|
p: 2,
|
|
border: "1px solid",
|
|
borderColor: "divider",
|
|
fontFamily: "monospace",
|
|
fontSize: "0.875rem",
|
|
}}
|
|
>
|
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: "block" }}>
|
|
Latest event (#{latestEvent._seq})
|
|
</Typography>
|
|
<Typography>
|
|
{applyDisplayFormat(latestEvent, resource.displayFormat)}
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Typography variant="body2" color="text.secondary" sx={{ py: 2, textAlign: "center" }}>
|
|
Waiting for events…
|
|
</Typography>
|
|
)}
|
|
|
|
<Snackbar
|
|
open={snackbarOpen}
|
|
autoHideDuration={2000}
|
|
onClose={() => setSnackbarOpen(false)}
|
|
message={snackbarMsg}
|
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
|
/>
|
|
</Paper>
|
|
);
|
|
} |