| | |
| | | const MIN_HEIGHT = 240; |
| | | const EDGE_MARGIN = 16; |
| | | |
| | | const MAX_VISIBLE_LOGS = 200; |
| | | const MAX_BUFFER_LOGS = 600; |
| | | |
| | | const clamp = (value, min, max) => Math.min(Math.max(value, min), max); |
| | | |
| | | const NewsLogDialog = ({ open, onClose }) => { |
| | |
| | | const programmaticScrollRef = useRef(false); |
| | | const initialScrollDoneRef = useRef(false); |
| | | const userScrollRef = useRef(false); |
| | | const autoScrollRef = useRef(true); |
| | | const nearBottomRef = useRef(true); |
| | | const pendingLogsRef = useRef(null); |
| | | const resizeRef = useRef({ |
| | | active: false, |
| | | startX: 0, |
| | |
| | | }; |
| | | }, [getSizeLimits]); |
| | | |
| | | const applyLogs = useCallback((payload, { programmatic = false } = {}) => { |
| | | if (!payload) { |
| | | return; |
| | | } |
| | | programmaticScrollRef.current = programmatic; |
| | | pendingLogsRef.current = null; |
| | | setLogs(payload.logs); |
| | | setLastUpdated(payload.timestamp); |
| | | }, []); |
| | | |
| | | const flushPendingLogs = useCallback((options = {}) => { |
| | | if (!pendingLogsRef.current) { |
| | | return false; |
| | | } |
| | | applyLogs(pendingLogsRef.current, options); |
| | | return true; |
| | | }, [applyLogs]); |
| | | |
| | | const computeNearBottom = useCallback(() => { |
| | | const container = scrollRef.current; |
| | | if (!container) { |
| | | return true; |
| | | } |
| | | const { scrollTop, scrollHeight, clientHeight } = container; |
| | | return scrollHeight - (scrollTop + clientHeight) < 32; |
| | | }, []); |
| | | |
| | | const fetchLogs = useCallback(async () => { |
| | | const data = await Http.fetchNewsLogs(); |
| | | if (Array.isArray(data)) { |
| | | programmaticScrollRef.current = true; |
| | | setLogs(data); |
| | | setLastUpdated(new Date()); |
| | | let nextLogs = data; |
| | | if (data.length > MAX_VISIBLE_LOGS) { |
| | | const shouldStickToBottom = autoScrollRef.current && nearBottomRef.current; |
| | | const limit = shouldStickToBottom |
| | | ? MAX_VISIBLE_LOGS |
| | | : Math.min(MAX_BUFFER_LOGS, data.length); |
| | | nextLogs = data.slice(data.length - limit); |
| | | } |
| | | const payload = { logs: nextLogs, timestamp: new Date() }; |
| | | if (autoScrollRef.current && nearBottomRef.current) { |
| | | applyLogs(payload, { programmatic: true }); |
| | | } else { |
| | | pendingLogsRef.current = payload; |
| | | } |
| | | } |
| | | }, []); |
| | | }, [applyLogs]); |
| | | |
| | | useEffect(() => { |
| | | if (!open) { |
| | |
| | | setPosition(null); |
| | | setSize(null); |
| | | initialScrollDoneRef.current = false; |
| | | nearBottomRef.current = true; |
| | | pendingLogsRef.current = null; |
| | | return; |
| | | } |
| | | setSize(buildInitialSize()); |
| | |
| | | userScrollRef.current = false; |
| | | programmaticScrollRef.current = true; |
| | | initialScrollDoneRef.current = false; |
| | | nearBottomRef.current = true; |
| | | pendingLogsRef.current = null; |
| | | fetchLogs(); |
| | | pollingRef.current = setInterval(fetchLogs, POLLING_INTERVAL); |
| | | return () => { |
| | |
| | | return; |
| | | } |
| | | programmaticScrollRef.current = true; |
| | | nearBottomRef.current = true; |
| | | const step = (remaining) => { |
| | | if (!scrollRef.current) { |
| | | return; |
| | |
| | | initialScrollDoneRef.current = true; |
| | | }, [logs, autoScroll, scrollToBottom]); |
| | | |
| | | useEffect(() => { |
| | | autoScrollRef.current = autoScroll; |
| | | }, [autoScroll]); |
| | | |
| | | const handleScroll = useCallback(() => { |
| | | if (!scrollRef.current) { |
| | | return; |
| | | } |
| | | if (programmaticScrollRef.current) { |
| | | programmaticScrollRef.current = false; |
| | | nearBottomRef.current = computeNearBottom(); |
| | | return; |
| | | } |
| | | const isNearBottom = computeNearBottom(); |
| | | nearBottomRef.current = isNearBottom; |
| | | if (!autoScroll) { |
| | | userScrollRef.current = !isNearBottom; |
| | | return; |
| | | } |
| | | const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; |
| | | const isNearBottom = scrollHeight - (scrollTop + clientHeight) < 32; |
| | | if (isNearBottom) { |
| | | if (userScrollRef.current) { |
| | | userScrollRef.current = false; |
| | |
| | | } else { |
| | | userScrollRef.current = true; |
| | | } |
| | | }, [autoScroll]); |
| | | }, [autoScroll, computeNearBottom]); |
| | | |
| | | const handleJumpLatest = () => { |
| | | flushPendingLogs({ programmatic: true }); |
| | | userScrollRef.current = false; |
| | | setAutoScroll(true); |
| | | scrollToBottom('smooth', 2); |
| | |
| | | const { checked } = event.target; |
| | | setAutoScroll(checked); |
| | | if (checked) { |
| | | flushPendingLogs({ programmatic: true }); |
| | | userScrollRef.current = false; |
| | | initialScrollDoneRef.current = false; |
| | | nearBottomRef.current = true; |
| | | scrollToBottom('auto', 5); |
| | | } else { |
| | | userScrollRef.current = true; |
| | |
| | | flex: 1, |
| | | minHeight: 0, |
| | | overflowY: 'auto', |
| | | overflowX: 'auto', |
| | | px: 0, |
| | | py: 0, |
| | | backgroundColor: theme.palette.background.default, |
| | |
| | | {translate('page.map.monitor.log.empty', { _: 'No Logs' })} |
| | | </Typography> |
| | | ) : ( |
| | | <Stack spacing={0} divider={<Divider sx={{ borderColor: theme.palette.divider, opacity: 0.8 }} />}> |
| | | <Stack |
| | | spacing={0} |
| | | divider={<Divider sx={{ borderColor: theme.palette.divider, opacity: 0.8 }} />} |
| | | sx={{ minWidth: '100%', width: 'fit-content' }} |
| | | > |
| | | {logs.map((item, index) => { |
| | | const level = item?.l; |
| | | const levelMeta = LOG_LEVEL_META[level] || LOG_LEVEL_META[1]; |
| | |
| | | sx={{ |
| | | px: 2, |
| | | py: 1.25, |
| | | width: '100%', |
| | | backgroundColor: index % 2 === 0 |
| | | ? theme.palette.background.paper |
| | | : theme.palette.background.default, |