mirror of
https://github.com/grafana/grafana.git
synced 2025-01-27 16:57:14 -06:00
c412a3b052
* logs: track the usage of certain features * Add report interaction for logs interactions * mock reportInteraction in test * mock reportInteraction Co-authored-by: Ivana Huckova <ivana.huckova@gmail.com>
230 lines
7.4 KiB
TypeScript
230 lines
7.4 KiB
TypeScript
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<LogsPage[]>([]);
|
|
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<DataQuery[]>();
|
|
const expectedRangeRef = useRef<AbsoluteTimeRange>();
|
|
// 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 = (
|
|
<Button
|
|
data-testid="olderLogsButton"
|
|
className={styles.navButton}
|
|
variant="secondary"
|
|
onClick={() => {
|
|
//If we are not on the last page, use next page's range
|
|
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
|
pageType: 'olderLogsButton',
|
|
});
|
|
if (!onLastPage) {
|
|
const indexChange = oldestLogsFirst ? -1 : 1;
|
|
changeTime({
|
|
from: pages[currentPageIndex + indexChange].queryRange.from,
|
|
to: pages[currentPageIndex + indexChange].queryRange.to,
|
|
});
|
|
} else {
|
|
//If we are on the last page, create new range
|
|
changeTime({ from: visibleRange.from - rangeSpanRef.current, to: visibleRange.from });
|
|
}
|
|
}}
|
|
disabled={loading}
|
|
>
|
|
<div className={styles.navButtonContent}>
|
|
{loading ? <Spinner /> : <Icon name={oldestLogsFirst ? 'angle-up' : 'angle-down'} size="lg" />}
|
|
Older logs
|
|
</div>
|
|
</Button>
|
|
);
|
|
|
|
const newerLogsButton = (
|
|
<Button
|
|
data-testid="newerLogsButton"
|
|
className={styles.navButton}
|
|
variant="secondary"
|
|
onClick={() => {
|
|
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
|
pageType: 'newerLogsButton',
|
|
});
|
|
//If we are not on the first page, use previous page's range
|
|
if (!onFirstPage) {
|
|
const indexChange = oldestLogsFirst ? 1 : -1;
|
|
changeTime({
|
|
from: pages[currentPageIndex + indexChange].queryRange.from,
|
|
to: pages[currentPageIndex + indexChange].queryRange.to,
|
|
});
|
|
}
|
|
//If we are on the first page, button is disabled and we do nothing
|
|
}}
|
|
disabled={loading || onFirstPage}
|
|
>
|
|
<div className={styles.navButtonContent}>
|
|
{loading && <Spinner />}
|
|
{onFirstPage || loading ? null : <Icon name={oldestLogsFirst ? 'angle-down' : 'angle-up'} size="lg" />}
|
|
{onFirstPage ? 'Start of range' : 'Newer logs'}
|
|
</div>
|
|
</Button>
|
|
);
|
|
|
|
return (
|
|
<div className={styles.navContainer}>
|
|
{oldestLogsFirst ? olderLogsButton : newerLogsButton}
|
|
<LogsNavigationPages
|
|
pages={pages}
|
|
currentPageIndex={currentPageIndex}
|
|
oldestLogsFirst={oldestLogsFirst}
|
|
timeZone={timeZone}
|
|
loading={loading}
|
|
changeTime={changeTime}
|
|
/>
|
|
{oldestLogsFirst ? newerLogsButton : olderLogsButton}
|
|
<Button
|
|
data-testid="scrollToTop"
|
|
className={styles.scrollToTopButton}
|
|
variant="secondary"
|
|
onClick={scrollToTopLogs}
|
|
title="Scroll to top"
|
|
>
|
|
<Icon name="arrow-up" size="lg" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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)};
|
|
`,
|
|
};
|
|
};
|