import { Box, Icon, Scrollable } from '@rocket.chat/fuselage'; import React, { useEffect, useRef, useState, useCallback, ReactElement } from 'react'; import { Serialized } from '../../../../definition/Serialized'; import { useEndpoint, useStream } from '../../../contexts/ServerContext'; import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../contexts/TranslationContext'; import { ansispan } from './ansispan'; type StdOutLogEntry = { id: string; string: string; ts: Date; }; const compareEntries = (a: StdOutLogEntry, b: StdOutLogEntry): number => a.ts.getTime() - b.ts.getTime(); const unserializeEntry = ({ ts, ...entry }: Serialized): StdOutLogEntry => ({ ts: new Date(ts), ...entry, }); const ServerLogs = (): ReactElement => { const [entries, setEntries] = useState([]); const dispatchToastMessage = useToastMessageDispatch(); const getStdoutQueue = useEndpoint('GET', 'stdout.queue'); const subscribeToStdout = useStream('stdout'); useEffect(() => { const fetchLines = async (): Promise => { try { const { queue } = await getStdoutQueue(undefined); setEntries(queue.map(unserializeEntry).sort(compareEntries)); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }; fetchLines(); }, [dispatchToastMessage, getStdoutQueue]); useEffect( () => subscribeToStdout('stdout', (entry: StdOutLogEntry) => { setEntries((entries) => [...entries, entry]); }), [subscribeToStdout], ); const t = useTranslation(); const wrapperRef = useRef(); const atBottomRef = useRef(false); const [newLogsVisible, setNewLogsVisible] = useState(false); const isAtBottom = useCallback((scrollThreshold = 0) => { const wrapper = wrapperRef.current; if (!wrapper) { return false; } if (wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight) { setNewLogsVisible(false); return true; } return false; }, []); const sendToBottom = useCallback(() => { const wrapper = wrapperRef.current; if (!wrapper) { return; } wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight; setNewLogsVisible(false); }, []); const checkIfScrollIsAtBottom = useCallback(() => { atBottomRef.current = isAtBottom(100); }, [isAtBottom]); const sendToBottomIfNecessary = useCallback(() => { if (atBottomRef.current === true && isAtBottom() !== true) { sendToBottom(); } else if (atBottomRef.current === false) { setNewLogsVisible(true); } }, [isAtBottom, sendToBottom]); useEffect(() => { const wrapper = wrapperRef.current; if (!wrapper) { return; } if (window.MutationObserver) { const observer = new MutationObserver((mutations) => { mutations.forEach(() => { sendToBottomIfNecessary(); }); }); observer.observe(wrapper, { childList: true }); return (): void => { observer.disconnect(); }; } const handleSubtreeModified = (): void => { sendToBottomIfNecessary(); }; wrapper.addEventListener('DOMSubtreeModified', handleSubtreeModified); return (): void => { wrapper.removeEventListener('DOMSubtreeModified', handleSubtreeModified); }; }, [sendToBottomIfNecessary]); useEffect(() => { const handleWindowResize = (): void => { setTimeout(() => { sendToBottomIfNecessary(); }, 100); }; window.addEventListener('resize', handleWindowResize); return (): void => { window.removeEventListener('resize', handleWindowResize); }; }, [sendToBottomIfNecessary]); const handleWheel = useCallback(() => { atBottomRef.current = false; setTimeout(() => { checkIfScrollIsAtBottom(); }, 100); }, [checkIfScrollIsAtBottom]); const handleTouchStart = (): void => { atBottomRef.current = false; }; const handleTouchEnd = useCallback(() => { setTimeout(() => { checkIfScrollIsAtBottom(); }, 100); }, [checkIfScrollIsAtBottom]); const handleScroll = useCallback(() => { atBottomRef.current = false; setTimeout(() => { checkIfScrollIsAtBottom(); }, 100); }, [checkIfScrollIsAtBottom]); const handleClick = useCallback(() => { atBottomRef.current = true; sendToBottomIfNecessary(); }, [sendToBottomIfNecessary]); return ( {entries.sort(compareEntries).map(({ string }, i) => ( ))} {t('New_logs')} ); }; export default ServerLogs;