| | |
| | | 3: { label: 'ERROR', color: 'error' }, |
| | | }; |
| | | |
| | | const POLLING_INTERVAL = 2000; |
| | | const PANEL_WIDTH = 480; |
| | | const PANEL_HEIGHT = 360; |
| | | const POLLING_INTERVAL = 1000; |
| | | const PANEL_WIDTH = 600; |
| | | const PANEL_HEIGHT = 420; |
| | | const MIN_WIDTH = 320; |
| | | const MIN_HEIGHT = 240; |
| | | const EDGE_MARGIN = 16; |
| | |
| | | const panelRef = useRef(null); |
| | | const pollingRef = useRef(null); |
| | | const dragRef = useRef({ active: false, offsetX: 0, offsetY: 0 }); |
| | | const programmaticScrollRef = useRef(false); |
| | | const initialScrollDoneRef = useRef(false); |
| | | const userScrollRef = useRef(false); |
| | | const resizeRef = useRef({ |
| | | active: false, |
| | | startX: 0, |
| | |
| | | const fetchLogs = useCallback(async () => { |
| | | const data = await Http.fetchNewsLogs(); |
| | | if (Array.isArray(data)) { |
| | | programmaticScrollRef.current = true; |
| | | setLogs(data); |
| | | setLastUpdated(new Date()); |
| | | } |
| | |
| | | setLastUpdated(null); |
| | | setPosition(null); |
| | | setSize(null); |
| | | initialScrollDoneRef.current = false; |
| | | return; |
| | | } |
| | | setSize(buildInitialSize()); |
| | | setAutoScroll(true); |
| | | userScrollRef.current = false; |
| | | programmaticScrollRef.current = true; |
| | | initialScrollDoneRef.current = false; |
| | | fetchLogs(); |
| | | pollingRef.current = setInterval(fetchLogs, POLLING_INTERVAL); |
| | | return () => { |
| | |
| | | }; |
| | | }, [open, fetchLogs, buildInitialSize]); |
| | | |
| | | useEffect(() => { |
| | | if (!autoScroll || !scrollRef.current) { |
| | | const scrollToBottom = useCallback((behavior = 'smooth', attempts = 3) => { |
| | | if (!scrollRef.current) { |
| | | return; |
| | | } |
| | | scrollRef.current.scrollTo({ |
| | | top: scrollRef.current.scrollHeight, |
| | | behavior: 'smooth', |
| | | }); |
| | | }, [logs, autoScroll]); |
| | | programmaticScrollRef.current = true; |
| | | const step = (remaining) => { |
| | | if (!scrollRef.current) { |
| | | return; |
| | | } |
| | | scrollRef.current.scrollTo({ |
| | | top: scrollRef.current.scrollHeight, |
| | | behavior, |
| | | }); |
| | | if (remaining > 0) { |
| | | requestAnimationFrame(() => step(remaining - 1)); |
| | | } |
| | | }; |
| | | requestAnimationFrame(() => step(attempts)); |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | if (!autoScroll || userScrollRef.current) { |
| | | return; |
| | | } |
| | | if (!logs.length) { |
| | | return; |
| | | } |
| | | const behavior = initialScrollDoneRef.current ? 'smooth' : 'auto'; |
| | | const attempts = initialScrollDoneRef.current ? 1 : 5; |
| | | scrollToBottom(behavior, attempts); |
| | | initialScrollDoneRef.current = true; |
| | | }, [logs, autoScroll, scrollToBottom]); |
| | | |
| | | const handleScroll = useCallback(() => { |
| | | if (!scrollRef.current || !autoScroll) { |
| | | if (!scrollRef.current) { |
| | | return; |
| | | } |
| | | if (programmaticScrollRef.current) { |
| | | programmaticScrollRef.current = false; |
| | | return; |
| | | } |
| | | if (!autoScroll) { |
| | | return; |
| | | } |
| | | const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; |
| | | if (scrollHeight - (scrollTop + clientHeight) > 32) { |
| | | setAutoScroll(false); |
| | | const isNearBottom = scrollHeight - (scrollTop + clientHeight) < 32; |
| | | if (isNearBottom) { |
| | | if (userScrollRef.current) { |
| | | userScrollRef.current = false; |
| | | } |
| | | } else { |
| | | userScrollRef.current = true; |
| | | } |
| | | }, [autoScroll]); |
| | | |
| | | const handleJumpLatest = () => { |
| | | userScrollRef.current = false; |
| | | setAutoScroll(true); |
| | | if (scrollRef.current) { |
| | | scrollRef.current.scrollTo({ |
| | | top: scrollRef.current.scrollHeight, |
| | | behavior: 'smooth', |
| | | }); |
| | | } |
| | | scrollToBottom('smooth', 2); |
| | | initialScrollDoneRef.current = true; |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | | onClose?.(); |
| | | }; |
| | | |
| | | const handleAutoScrollChange = (event) => { |
| | | const { checked } = event.target; |
| | | setAutoScroll(checked); |
| | | if (checked) { |
| | | userScrollRef.current = false; |
| | | initialScrollDoneRef.current = false; |
| | | scrollToBottom('auto', 5); |
| | | } else { |
| | | userScrollRef.current = true; |
| | | } |
| | | }; |
| | | |
| | | const handleDragging = useCallback((event) => { |
| | |
| | | <Switch |
| | | size="small" |
| | | checked={autoScroll} |
| | | onChange={(event) => setAutoScroll(event.target.checked)} |
| | | onChange={handleAutoScrollChange} |
| | | /> |
| | | )} |
| | | label={translate('page.map.monitor.log.autoScroll', { _: 'Auto Scroll' })} |
| | |
| | | flex: 1, |
| | | minHeight: 0, |
| | | overflowY: 'auto', |
| | | px: 3, |
| | | py: 2, |
| | | px: 0, |
| | | py: 0, |
| | | backgroundColor: theme.palette.background.default, |
| | | }} |
| | | > |
| | |
| | | {translate('page.map.monitor.log.empty', { _: 'No Logs' })} |
| | | </Typography> |
| | | ) : ( |
| | | <Stack spacing={1.25}> |
| | | <Stack spacing={0} divider={<Divider sx={{ borderColor: theme.palette.divider, opacity: 0.8 }} />}> |
| | | {logs.map((item, index) => { |
| | | const level = item?.l; |
| | | const levelMeta = LOG_LEVEL_META[level] || LOG_LEVEL_META[1]; |
| | |
| | | <Box |
| | | key={`${item?.t}-${index}`} |
| | | sx={{ |
| | | borderRadius: 1.5, |
| | | border: `1px solid ${theme.palette.divider}`, |
| | | backgroundColor: theme.palette.background.paper, |
| | | px: 2, |
| | | py: 1.25, |
| | | backgroundColor: index % 2 === 0 |
| | | ? theme.palette.background.paper |
| | | : theme.palette.background.default, |
| | | }} |
| | | > |
| | | <Stack direction="row" spacing={1} alignItems="center"> |
| | | <Chip |
| | | label={levelMeta.label} |
| | | size="small" |
| | | color={levelMeta.color} |
| | | variant="outlined" |
| | | /> |
| | | <Divider orientation="vertical" flexItem /> |
| | | <Typography variant="caption" color="text.secondary"> |
| | | <Stack direction="row" spacing={1.5} alignItems="center"> |
| | | <Typography |
| | | variant="caption" |
| | | sx={{ |
| | | fontWeight: 700, |
| | | letterSpacing: 0.6, |
| | | textTransform: 'uppercase', |
| | | color: theme.palette[levelMeta.color]?.main || theme.palette.text.secondary, |
| | | }} |
| | | > |
| | | {levelMeta.label} |
| | | </Typography> |
| | | <Typography |
| | | variant="caption" |
| | | color="text.secondary" |
| | | sx={{ fontFamily: 'SFMono-Regular, Menlo, monospace', letterSpacing: 0.3 }} |
| | | > |
| | | {item?.t || '--'} |
| | | </Typography> |
| | | </Stack> |
| | | <Typography variant="body2" sx={{ mt: 1, whiteSpace: 'pre-wrap' }}> |
| | | <Typography |
| | | variant="body2" |
| | | sx={{ |
| | | mt: 0.5, |
| | | whiteSpace: 'pre-wrap', |
| | | fontSize: 13, |
| | | lineHeight: 1.5, |
| | | color: theme.palette.text.primary, |
| | | }} |
| | | > |
| | | {item?.v || ''} |
| | | </Typography> |
| | | </Box> |