Files
khata-ui/react-openapi/src/components/SseStreamView.tsx

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&hellip;
</Typography>
)}
<Snackbar
open={snackbarOpen}
autoHideDuration={2000}
onClose={() => setSnackbarOpen(false)}
message={snackbarMsg}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
/>
</Paper>
);
}