import { css } from '@emotion/css'; import { isEqual } from 'lodash'; import React, { memo, useState, useEffect, useRef } from 'react'; import { LogsSortOrder, AbsoluteTimeRange, TimeZone, DataQuery, GrafanaTheme2 } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui'; import { LogsNavigationPages } from './LogsNavigationPages'; type Props = { absoluteRange: AbsoluteTimeRange; timeZone: TimeZone; queries: DataQuery[]; loading: boolean; visibleRange: AbsoluteTimeRange; logsSortOrder?: LogsSortOrder | null; onChangeTime: (range: AbsoluteTimeRange) => void; scrollToTopLogs: () => void; addResultsToCache: () => void; clearCache: () => void; }; export type LogsPage = { logsRange: AbsoluteTimeRange; queryRange: AbsoluteTimeRange; }; function LogsNavigation({ absoluteRange, logsSortOrder, timeZone, loading, onChangeTime, scrollToTopLogs, visibleRange, queries, clearCache, addResultsToCache, }: Props) { const [pages, setPages] = useState([]); const [currentPageIndex, setCurrentPageIndex] = useState(0); // These refs are to determine, if we want to clear up logs navigation when totally new query is run const expectedQueriesRef = useRef(); const expectedRangeRef = useRef(); // This ref is to store range span for future queres based on firstly selected time range // e.g. if last 5 min selected, always run 5 min range const rangeSpanRef = useRef(0); const oldestLogsFirst = logsSortOrder === LogsSortOrder.Ascending; const onFirstPage = oldestLogsFirst ? currentPageIndex === pages.length - 1 : currentPageIndex === 0; const onLastPage = oldestLogsFirst ? currentPageIndex === 0 : currentPageIndex === pages.length - 1; const theme = useTheme2(); const styles = getStyles(theme, oldestLogsFirst, loading); // Main effect to set pages and index useEffect(() => { const newPage = { logsRange: visibleRange, queryRange: absoluteRange }; let newPages: LogsPage[] = []; // We want to start new pagination if queries change or if absolute range is different than expected if (!isEqual(expectedRangeRef.current, absoluteRange) || !isEqual(expectedQueriesRef.current, queries)) { clearCache(); setPages([newPage]); setCurrentPageIndex(0); expectedQueriesRef.current = queries; rangeSpanRef.current = absoluteRange.to - absoluteRange.from; } else { setPages((pages) => { // Remove duplicates with new query newPages = pages.filter((page) => !isEqual(newPage.queryRange, page.queryRange)); // Sort pages based on logsOrder so they visually align with displayed logs newPages = [...newPages, newPage].sort((a, b) => sortPages(a, b, logsSortOrder)); // Set new pages return newPages; }); // Set current page index const index = newPages.findIndex((page) => page.queryRange.to === absoluteRange.to); setCurrentPageIndex(index); } addResultsToCache(); }, [visibleRange, absoluteRange, logsSortOrder, queries, clearCache, addResultsToCache]); useEffect(() => { clearCache(); // We can't enforce the eslint rule here because we only want to run when component is mounted. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const changeTime = ({ from, to }: AbsoluteTimeRange) => { expectedRangeRef.current = { from, to }; onChangeTime({ from, to }); }; const sortPages = (a: LogsPage, b: LogsPage, logsSortOrder?: LogsSortOrder | null) => { if (logsSortOrder === LogsSortOrder.Ascending) { return a.queryRange.to > b.queryRange.to ? 1 : -1; } return a.queryRange.to > b.queryRange.to ? -1 : 1; }; const olderLogsButton = ( ); const newerLogsButton = ( ); return (
{oldestLogsFirst ? olderLogsButton : newerLogsButton} {oldestLogsFirst ? newerLogsButton : olderLogsButton}
); } export default memo(LogsNavigation); const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean, loading: boolean) => { return { navContainer: css` max-height: 95vh; display: flex; flex-direction: column; justify-content: ${oldestLogsFirst ? 'flex-start' : 'space-between'}; position: sticky; top: ${theme.spacing(2)}; right: 0; `, navButton: css` width: 58px; height: 68px; display: flex; flex-direction: column; justify-content: center; align-items: center; line-height: 1; `, navButtonContent: css` display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; white-space: normal; `, scrollToTopButton: css` width: 40px; height: 40px; display: flex; flex-direction: column; justify-content: center; align-items: center; margin-top: ${theme.spacing(1)}; `, }; };