import {VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; import {Registry} from "tc-shared/events"; import * as React from "react"; import {useContext, useEffect, useRef, useState} from "react"; import {findLogEventRenderer} from "./RendererEvent"; import {LogMessage} from "tc-shared/connectionlog/Definitions"; import {ServerEventLogUiEvents} from "tc-shared/ui/frames/log/Definitions"; import {useDependentState} from "tc-shared/ui/react-elements/Helper"; import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary"; const cssStyle = require("./Renderer.scss"); const HandlerIdContext = React.createContext<string>(undefined); const EventsContext = React.createContext<Registry<ServerEventLogUiEvents>>(undefined); const LogFallbackDispatcher = (_unused, __unused, eventType) => ( <div className={cssStyle.errorMessage}> <VariadicTranslatable text={"Missing log entry builder for {0}"}> <>{eventType}</> </VariadicTranslatable> </div> ); const LogEntryRenderer = React.memo((props: { entry: LogMessage }) => { const handlerId = useContext(HandlerIdContext); const dispatcher = findLogEventRenderer(props.entry.type as any) || LogFallbackDispatcher; const rendered = dispatcher(props.entry.data, handlerId, props.entry.type); if(!rendered) { /* hide message */ return null; } const date = new Date(props.entry.timestamp); return ( <div className={cssStyle.logEntry}> <div className={cssStyle.timestamp}><{ ("0" + date.getHours()).substr(-2) + ":" + ("0" + date.getMinutes()).substr(-2) + ":" + ("0" + date.getSeconds()).substr(-2) }></div> {rendered} </div> ); }); const ServerLogRenderer = () => { const handlerId = useContext(HandlerIdContext); const events = useContext(EventsContext); const [ logs, setLogs ] = useDependentState<LogMessage[] | "loading">(() => { events.fire_react("query_log"); return "loading"; }, [ handlerId ]); const [ revision, setRevision ] = useState(0); const refContainer = useRef<HTMLDivElement>(); const scrollOffset = useRef<number | "bottom">("bottom"); events.reactUse("notify_log", event => { const logs = event.events.slice(0); logs.splice(0, Math.max(0, logs.length - 100)); logs.sort((a, b) => a.timestamp - b.timestamp); setLogs(logs); }); events.reactUse("notify_log_add", event => { if(logs === "loading") { return; } logs.push(event.event); logs.splice(0, Math.max(0, logs.length - 100)); logs.sort((a, b) => a.timestamp - b.timestamp); setRevision(revision + 1); }); const fixScroll = () => { if(!refContainer.current) { return; } refContainer.current.scrollTop = scrollOffset.current === "bottom" ? refContainer.current.scrollHeight : scrollOffset.current; }; useEffect(() => { const id = requestAnimationFrame(fixScroll); return () => cancelAnimationFrame(id); }); return ( <div className={cssStyle.logContainer} ref={refContainer} onScroll={event => { const target = event.target as HTMLDivElement; const top = target.scrollTop; const total = target.scrollHeight - target.clientHeight; const shouldFollow = top + 100 > total; scrollOffset.current = shouldFollow ? "bottom" : top; }}> {logs === "loading" ? null : logs.map(e => <LogEntryRenderer key={e.uniqueId} entry={e} />)} </div> ); }; export const ServerLogFrame = (props: { events: Registry<ServerEventLogUiEvents> }) => { const [ handlerId, setHandlerId ] = useState<string>(() => { props.events.fire("query_handler_id"); return undefined; }); props.events.reactUse("notify_handler_id", event => setHandlerId(event.handlerId)); return ( <EventsContext.Provider value={props.events}> <HandlerIdContext.Provider value={handlerId}> <ErrorBoundary> <ServerLogRenderer /> </ErrorBoundary> </HandlerIdContext.Provider> </EventsContext.Provider> ); }