import { css, cx } from '@emotion/css'; import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; import { DataQueryResponse, DataSourceWithLogsContextSupport, GrafanaTheme2, LogRowContextOptions, LogRowContextQueryDirection, LogRowModel, LogsDedupStrategy, LogsSortOrder, SelectableValue, dateTime, TimeRange, } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { Icon, Button, LoadingBar, Modal, useTheme2 } from '@grafana/ui'; import { dataFrameToLogsModel } from 'app/core/logsModel'; import store from 'app/core/store'; import { SETTINGS_KEYS } from 'app/features/explore/Logs/utils/logs'; import { splitOpen } from 'app/features/explore/state/main'; import { useDispatch } from 'app/types'; import { sortLogRows } from '../../utils'; import { LogRows } from '../LogRows'; import { LoadMoreOptions, LogContextButtons } from './LogContextButtons'; const getStyles = (theme: GrafanaTheme2) => { return { modal: css` width: 85vw; ${theme.breakpoints.down('md')} { width: 100%; } top: 50%; left: 50%; transform: translate(-50%, -50%); `, entry: css` position: sticky; z-index: 1; top: -1px; bottom: -1px; & > td { padding: ${theme.spacing(1)} 0 ${theme.spacing(1)} 0; } background: ${theme.colors.emphasize(theme.colors.background.secondary)}; & > table { margin-bottom: 0; } `, datasourceUi: css` padding-bottom: ${theme.spacing(1.25)}; display: flex; align-items: center; `, logRowGroups: css` overflow: auto; max-height: 75%; align-self: stretch; display: inline-block; & > table { min-width: 100%; } `, flexColumn: css` display: flex; flex-direction: column; padding: 0 ${theme.spacing(3)} ${theme.spacing(3)} ${theme.spacing(3)}; `, flexRow: css` display: flex; flex-direction: row; align-items: center; & > div:last-child { margin-left: auto; } `, noMarginBottom: css` & > table { margin-bottom: 0; } `, hidden: css` display: none; `, paddingTop: css` padding-top: ${theme.spacing(1)}; `, paddingBottom: css` padding-bottom: ${theme.spacing(1)}; `, link: css` color: ${theme.colors.text.secondary}; font-size: ${theme.typography.bodySmall.fontSize}; :hover { color: ${theme.colors.text.link}; } `, }; }; export enum LogGroupPosition { Bottom = 'bottom', Top = 'top', } interface LogRowContextModalProps { row: LogRowModel; open: boolean; timeZone: TimeZone; onClose: () => void; getRowContext: (row: LogRowModel, options?: LogRowContextOptions) => Promise; getRowContextQuery?: (row: LogRowModel, options?: LogRowContextOptions) => Promise; logsSortOrder?: LogsSortOrder | null; runContextQuery?: () => void; getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; } export const LogRowContextModal: React.FunctionComponent = ({ row, open, logsSortOrder, timeZone, getLogRowContextUi, getRowContextQuery, onClose, getRowContext, }) => { const scrollElement = React.createRef(); const entryElement = React.createRef(); // We can not use `entryElement` to scroll to the right element because it's // sticky. That's why we add another row and use this ref to scroll to that // first. const preEntryElement = React.createRef(); const dispatch = useDispatch(); const theme = useTheme2(); const styles = getStyles(theme); const [context, setContext] = useState<{ after: LogRowModel[]; before: LogRowModel[] }>({ after: [], before: [] }); // LoadMoreOptions[2] refers to 50 lines const defaultLimit = LoadMoreOptions[2]; const [limit, setLimit] = useState(defaultLimit.value!); const [loadingWidth, setLoadingWidth] = useState(0); const [loadMoreOption, setLoadMoreOption] = useState>(defaultLimit); const [contextQuery, setContextQuery] = useState(null); const [wrapLines, setWrapLines] = useState( store.getBool(SETTINGS_KEYS.logContextWrapLogMessage, store.getBool(SETTINGS_KEYS.wrapLogMessage, true)) ); const getFullTimeRange = useCallback(() => { const { before, after } = context; const allRows = sortLogRows([...before, row, ...after], LogsSortOrder.Ascending); const fromMs = allRows[0].timeEpochMs; let toMs = allRows[allRows.length - 1].timeEpochMs; // In case we have a lot of logs and from and to have same millisecond // we add 1 millisecond to toMs to make sure we have a range if (fromMs === toMs) { toMs += 1; } const from = dateTime(fromMs); const to = dateTime(toMs); const range: TimeRange = { from, to, raw: { from, to, }, }; return range; }, [context, row]); const onChangeLimitOption = (option: SelectableValue) => { setLoadMoreOption(option); if (option.value) { setLimit(option.value); reportInteraction('grafana_explore_logs_log_context_load_more_clicked', { datasourceType: row.datasourceType, logRowUid: row.uid, new_limit: option.value, }); } }; const updateContextQuery = async () => { const contextQuery = getRowContextQuery ? await getRowContextQuery(row) : null; setContextQuery(contextQuery); }; const [{ loading }, fetchResults] = useAsyncFn(async () => { if (open && row && limit) { await updateContextQuery(); const rawResults = await Promise.all([ getRowContext(row, { limit: logsSortOrder === LogsSortOrder.Descending ? limit + 1 : limit, direction: logsSortOrder === LogsSortOrder.Descending ? LogRowContextQueryDirection.Forward : LogRowContextQueryDirection.Backward, }), getRowContext(row, { limit: logsSortOrder === LogsSortOrder.Ascending ? limit + 1 : limit, direction: logsSortOrder === LogsSortOrder.Ascending ? LogRowContextQueryDirection.Forward : LogRowContextQueryDirection.Backward, }), ]); const logsModels = rawResults.map((result) => { return dataFrameToLogsModel(result.data); }); const afterRows = logsSortOrder === LogsSortOrder.Ascending ? logsModels[0].rows.reverse() : logsModels[0].rows; const beforeRows = logsSortOrder === LogsSortOrder.Ascending ? logsModels[1].rows.reverse() : logsModels[1].rows; setContext({ after: afterRows.filter((r) => { return r.timeEpochNs !== row.timeEpochNs && r.entry !== row.entry; }), before: beforeRows.filter((r) => { return r.timeEpochNs !== row.timeEpochNs && r.entry !== row.entry; }), }); } else { setContext({ after: [], before: [] }); } }, [row, open, limit]); useEffect(() => { if (open) { fetchResults(); } }, [fetchResults, open]); const [displayedFields, setDisplayedFields] = useState([]); const showField = (key: string) => { const index = displayedFields.indexOf(key); if (index === -1) { setDisplayedFields([...displayedFields, key]); } }; const hideField = (key: string) => { const index = displayedFields.indexOf(key); if (index > -1) { displayedFields.splice(index, 1); setDisplayedFields([...displayedFields]); } }; useLayoutEffect(() => { if (!loading && entryElement.current && preEntryElement.current) { preEntryElement.current.scrollIntoView({ block: 'center' }); entryElement.current.scrollIntoView({ block: 'center' }); } }, [entryElement, preEntryElement, context, loading]); useLayoutEffect(() => { const width = scrollElement?.current?.parentElement?.clientWidth; if (width && width > 0) { setLoadingWidth(width); } }, [scrollElement]); useAsync(updateContextQuery, [getRowContextQuery, row]); return ( {config.featureToggles.logsContextDatasourceUi && getLogRowContextUi && (
{getLogRowContextUi(row, fetchResults)}
)}
Showing {context.after.length} lines {logsSortOrder === LogsSortOrder.Ascending ? 'after' : 'before'} match.
Showing {context.before.length} lines {logsSortOrder === LogsSortOrder.Descending ? 'after' : 'before'} match.
{ reportInteraction('grafana_explore_logs_log_context_give_feedback_clicked', { datasourceType: row.datasourceType, logRowUid: row.uid, }); }} > Give feedback {contextQuery?.datasource?.uid && ( )}
); };