diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 24bc46790a5..f5c6912b76f 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1065,17 +1065,25 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { )} {visualisationType === 'logs' && hasData && config.featureToggles.newLogsPanel && ( <> -
+
{logsContainerRef.current && ( void; - onPinToContentOutlineClick?: (row: LogRowModel, onOpenContext: (row: LogRowModel) => void) => void; addonBefore?: ReactNode[]; addonAfter?: ReactNode[]; } @@ -66,31 +66,9 @@ export const LogRowMenuCell = memo( e.stopPropagation(); }, []); const onShowContextClick = useCallback( - async (event: MouseEvent) => { + async (event: MouseEvent) => { event.stopPropagation(); - // if ctrl or meta key is pressed, open query in new Explore tab - if ( - getRowContextQuery && - (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey || event.nativeEvent.shiftKey) - ) { - const win = window.open('about:blank'); - // for this request we don't want to use the cached filters from a context provider, but always want to refetch and clear - const query = await getRowContextQuery(row, undefined, false); - if (query && win) { - const url = urlUtil.renderUrl(locationUtil.assureBaseUrl(`${getConfig().appSubUrl}explore`), { - left: JSON.stringify({ - datasource: query.datasource, - queries: [query], - range: getDefaultTimeRange(), - }), - }); - win.location = url; - - return; - } - win?.close(); - } - onOpenContext(row); + handleOpenLogsContextClick(event, row, getRowContextQuery, onOpenContext); }, [onOpenContext, getRowContextQuery, row] ); diff --git a/public/app/features/logs/components/__mocks__/logRow.ts b/public/app/features/logs/components/__mocks__/logRow.ts index 0968f242a84..d7777eed731 100644 --- a/public/app/features/logs/components/__mocks__/logRow.ts +++ b/public/app/features/logs/components/__mocks__/logRow.ts @@ -1,4 +1,6 @@ -import { FieldType, LogLevel, LogRowModel, toDataFrame } from '@grafana/data'; +import { FieldType, LogLevel, LogRowModel, LogsSortOrder, toDataFrame } from '@grafana/data'; + +import { LogListModel, preProcessLogs, PreProcessOptions } from '../panel/processing'; export const createLogRow = (overrides?: Partial): LogRowModel => { const uid = overrides?.uid || '1'; @@ -36,3 +38,16 @@ export const createLogRow = (overrides?: Partial): LogRowModel => { ...overrides, }; }; + +export const createLogLine = ( + overrides?: Partial, + processOptions: PreProcessOptions = { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrap: false, + } +): LogListModel => { + const logs = preProcessLogs([createLogRow(overrides)], processOptions); + return logs[0]; +}; diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 3790f5f0b0f..e09d67577e4 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -4,7 +4,7 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window' import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { Spinner, useTheme2 } from '@grafana/ui'; +import { Spinner, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll'; @@ -59,8 +59,7 @@ export const InfiniteScroll = ({ const lastEvent = useRef(null); const countRef = useRef(0); const lastLogOfPage = useRef([]); - const theme = useTheme2(); - const styles = getStyles(theme); + const styles = useStyles2(getStyles); useEffect(() => { // Logs have not changed, ignore effect diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx new file mode 100644 index 00000000000..15640f13ea8 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLine.test.tsx @@ -0,0 +1,105 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { createTheme } from '@grafana/data'; + +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { createLogLine } from '../__mocks__/logRow'; + +import { getStyles, LogLine } from './LogLine'; +import { LogListModel } from './processing'; + +const theme = createTheme(); +const styles = getStyles(theme); + +describe('LogLine', () => { + let log: LogListModel; + beforeEach(() => { + log = createLogLine({ labels: { place: 'luna' } }); + }); + + test('Renders a log line', () => { + render( + + ); + expect(screen.getByText(log.timestamp)).toBeInTheDocument(); + expect(screen.getByText(log.body)).toBeInTheDocument(); + }); + + test('Renders a log line with no timestamp', () => { + render( + + ); + expect(screen.queryByText(log.timestamp)).not.toBeInTheDocument(); + expect(screen.getByText(log.body)).toBeInTheDocument(); + }); + + test('Renders a log line with displayed fields', () => { + render( + + ); + expect(screen.getByText(log.timestamp)).toBeInTheDocument(); + expect(screen.queryByText(log.body)).not.toBeInTheDocument(); + expect(screen.getByText('luna')).toBeInTheDocument(); + }); + + test('Renders a log line with body displayed fields', () => { + render( + + ); + expect(screen.getByText(log.timestamp)).toBeInTheDocument(); + expect(screen.getByText(log.body)).toBeInTheDocument(); + expect(screen.getByText('luna')).toBeInTheDocument(); + }); + + describe('Log line menu', () => { + test('Renders a log line menu', async () => { + render( + + ); + expect(screen.queryByText('Copy log line')).not.toBeInTheDocument(); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.getByText('Copy log line')).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index d484675d424..58004ccc456 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -1,12 +1,15 @@ import { css } from '@emotion/css'; import { CSSProperties, useEffect, useRef } from 'react'; +import tinycolor from 'tinycolor2'; import { GrafanaTheme2 } from '@grafana/data'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { LogLineMenu } from './LogLineMenu'; +import { useLogIsPinned } from './LogListContext'; import { LogFieldDimension, LogListModel } from './processing'; -import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow } from './virtualization'; +import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow, getLineHeight } from './virtualization'; interface Props { displayedFields: string[]; @@ -32,6 +35,7 @@ export const LogLine = ({ wrapLogMessage, }: Props) => { const logLineRef = useRef(null); + const pinned = useLogIsPinned(log); useEffect(() => { if (!onOverflow || !logLineRef.current) { @@ -45,7 +49,12 @@ export const LogLine = ({ }, [index, log.uid, onOverflow, style.height]); return ( -
+
+
@@ -57,7 +66,7 @@ interface LogProps { displayedFields: string[]; log: LogListModel; showTime: boolean; - styles: ReturnType; + styles: LogLineStyles; } const Log = ({ displayedFields, log, showTime, styles }: LogProps) => { @@ -67,7 +76,7 @@ const Log = ({ displayedFields, log, showTime, styles }: LogProps) => { {log.displayLevel} {displayedFields.length > 0 ? ( displayedFields.map((field) => ( - + {getDisplayedFieldValue(field, log)} )) @@ -111,6 +120,9 @@ export const getStyles = (theme: GrafanaTheme2) => { return { logLine: css({ color: theme.colors.text.primary, + display: 'flex', + gap: theme.spacing(0.5), + flexDirection: 'row', fontFamily: theme.typography.fontFamilyMonospace, fontSize: theme.typography.fontSize, wordBreak: 'break-all', @@ -129,6 +141,14 @@ export const getStyles = (theme: GrafanaTheme2) => { }, }, }), + pinnedLogLine: css({ + backgroundColor: tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString(), + }), + menuIcon: css({ + height: getLineHeight(), + margin: 0, + padding: theme.spacing(0, 0, 0, 0.5), + }), logLineMessage: css({ fontFamily: theme.typography.fontFamily, textAlign: 'center', diff --git a/public/app/features/logs/components/panel/LogLineMenu.test.tsx b/public/app/features/logs/components/panel/LogLineMenu.test.tsx new file mode 100644 index 00000000000..27c8b78f9b9 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineMenu.test.tsx @@ -0,0 +1,94 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { createTheme } from '@grafana/data'; + +import { createLogLine } from '../__mocks__/logRow'; + +import { getStyles } from './LogLine'; +import { LogLineMenu } from './LogLineMenu'; +import { LogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +const theme = createTheme(); +const styles = getStyles(theme); + +describe('LogLineMenu', () => { + let log: LogListModel; + beforeEach(() => { + log = createLogLine({ labels: { place: 'luna' }, rowId: '1' }); + }); + + test('Renders the component', async () => { + render(); + expect(screen.queryByText('Copy log line')).not.toBeInTheDocument(); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.getByText('Copy log line')).toBeInTheDocument(); + }); + + describe('Options', () => { + test('Allows to copy a permalink', async () => { + const onPermalinkClick = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + await userEvent.click(screen.getByText('Copy link to log line')); + expect(onPermalinkClick).toHaveBeenCalledTimes(1); + }); + + test('Allows to open show context', async () => { + const onOpenContext = jest.fn(); + const logSupportsContext = jest.fn().mockReturnValue(true); + const getRowContextQuery = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + await userEvent.click(screen.getByText('Show context')); + expect(onOpenContext).toHaveBeenCalledTimes(1); + }); + + test('Uses logSupportsContext to control the display of show context', async () => { + const onOpenContext = jest.fn(); + const logSupportsContext = jest.fn().mockReturnValue(false); + const getRowContextQuery = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.queryByText('Show context')).not.toBeInTheDocument(); + }); + + test('Allows to pin log line', async () => { + const onPinLine = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + await userEvent.click(screen.getByText('Pin log')); + expect(onPinLine).toHaveBeenCalledTimes(1); + }); + + test('Allows to unpin log line', async () => { + const onUnpinLine = jest.fn(); + render( + + + + ); + await userEvent.click(screen.getByLabelText('Log menu')); + expect(screen.queryByText('Pin log')).not.toBeInTheDocument(); + await userEvent.click(screen.getByText('Unpin log')); + expect(onUnpinLine).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/public/app/features/logs/components/panel/LogLineMenu.tsx b/public/app/features/logs/components/panel/LogLineMenu.tsx new file mode 100644 index 00000000000..b10c2f17ec6 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineMenu.tsx @@ -0,0 +1,102 @@ +import { useCallback, useMemo, useRef, MouseEvent } from 'react'; + +import { LogRowContextOptions, LogRowModel } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; +import { Dropdown, IconButton, Menu } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; + +import { copyText, handleOpenLogsContextClick } from '../../utils'; + +import { LogLineStyles } from './LogLine'; +import { useLogIsPinned, useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +export type GetRowContextQueryFn = ( + row: LogRowModel, + options?: LogRowContextOptions, + cacheFilters?: boolean +) => Promise; + +interface Props { + log: LogListModel; + styles: LogLineStyles; +} + +export const LogLineMenu = ({ log, styles }: Props) => { + const { getRowContextQuery, onOpenContext, onPermalinkClick, onPinLine, onUnpinLine, logSupportsContext } = + useLogListContext(); + const pinned = useLogIsPinned(log); + const menuRef = useRef(null); + + const copyLogLine = useCallback(() => { + copyText(log.entry, menuRef); + }, [log.entry]); + + const copyLinkToLogLine = useCallback(() => { + onPermalinkClick?.(log); + }, [log, onPermalinkClick]); + + const shouldlogSupportsContext = useMemo( + () => (logSupportsContext ? logSupportsContext(log) : false), + [log, logSupportsContext] + ); + + const showContext = useCallback( + async (event: MouseEvent) => { + handleOpenLogsContextClick(event, log, getRowContextQuery, (log: LogRowModel) => onOpenContext?.(log, () => {})); + }, + [onOpenContext, getRowContextQuery, log] + ); + + const togglePinning = useCallback(() => { + if (pinned) { + onUnpinLine?.(log); + } else { + onPinLine?.(log); + } + }, [log, onPinLine, onUnpinLine, pinned]); + + const menu = useCallback( + () => ( + + + {onPermalinkClick && log.rowId !== undefined && log.uid && ( + + )} + {(shouldlogSupportsContext || onPinLine || onUnpinLine) && } + {shouldlogSupportsContext && ( + + )} + {!pinned && onPinLine && ( + + )} + {pinned && onUnpinLine && ( + + )} + + ), + [ + copyLinkToLogLine, + copyLogLine, + log.rowId, + log.uid, + onPermalinkClick, + onPinLine, + onUnpinLine, + pinned, + shouldlogSupportsContext, + showContext, + togglePinning, + ] + ); + + return ( + + + + ); +}; diff --git a/public/app/features/logs/components/panel/LogLineMessage.test.tsx b/public/app/features/logs/components/panel/LogLineMessage.test.tsx new file mode 100644 index 00000000000..0034dac14aa --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineMessage.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { createTheme } from '@grafana/data'; + +import { getStyles } from './LogLine'; +import { LogLineMessage } from './LogLineMessage'; + +const theme = createTheme(); +const styles = getStyles(theme); + +describe('LogLineMessage', () => { + test('Renders a log line message', () => { + render( + + Message + + ); + expect(screen.getByText('Message')).toBeInTheDocument(); + }); + + test('Renders a button with the message', async () => { + const handleClick = jest.fn(); + render( + + Message + + ); + await userEvent.click(screen.getByText('Message')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index abc48cbe9ce..ae8234bd060 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -14,10 +14,12 @@ import { LogsSortOrder, TimeRange, } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; +import { PopoverContent, useTheme2 } from '@grafana/ui'; import { InfiniteScroll } from './InfiniteScroll'; import { getGridTemplateColumns } from './LogLine'; +import { GetRowContextQueryFn } from './LogLineMenu'; +import { LogListContext } from './LogListContext'; import { preProcessLogs, LogListModel, calculateFieldDimensions, LogFieldDimension } from './processing'; import { getLogLineSize, @@ -36,9 +38,17 @@ interface Props { eventBus: EventBus; forceEscape?: boolean; getFieldLinks?: GetFieldLinksFn; + getRowContextQuery?: GetRowContextQueryFn; initialScrollPosition?: 'top' | 'bottom'; loadMore?: (range: AbsoluteTimeRange) => void; logs: LogRowModel[]; + logSupportsContext?: (row: LogRowModel) => boolean; + onPermalinkClick?: (row: LogRowModel) => Promise; + onPinLine?: (row: LogRowModel) => void; + onOpenContext?: (row: LogRowModel, onClose: () => void) => void; + onUnpinLine?: (row: LogRowModel) => void; + pinLineButtonTooltipTitle?: PopoverContent; + pinnedLogs?: string[]; showTime: boolean; sortOrder: LogsSortOrder; timeRange: TimeRange; @@ -61,6 +71,7 @@ export const LogList = ({ timeRange, timeZone, wrapLogMessage, + ...logListContext }: Props) => { const [processedLogs, setProcessedLogs] = useState([]); const [listHeight, setListHeight] = useState( @@ -134,40 +145,42 @@ export const LogList = ({ } return ( - - {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( - - {Renderer} - - )} - + + + {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( + + {Renderer} + + )} + + ); }; diff --git a/public/app/features/logs/components/panel/LogListContext.test.tsx b/public/app/features/logs/components/panel/LogListContext.test.tsx new file mode 100644 index 00000000000..3505c554797 --- /dev/null +++ b/public/app/features/logs/components/panel/LogListContext.test.tsx @@ -0,0 +1,46 @@ +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; + +import { createLogLine } from '../__mocks__/logRow'; + +import { useLogListContextData, useLogListContext, useLogIsPinned, LogListContext } from './LogListContext'; + +const log = createLogLine({ rowId: 'yep' }); +const value = { + getRowContextQuery: jest.fn(), + logSupportsContext: jest.fn(), + onPermalinkClick: jest.fn(), + onPinLine: jest.fn(), + onOpenContext: jest.fn(), + onUnpinLine: jest.fn(), + pinLineButtonTooltipTitle: 'test', + pinnedLogs: ['yep'], +}; +const wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +test('Provides the Log List Context data', () => { + const { result } = renderHook(() => useLogListContext(), { wrapper }); + + expect(result.current).toEqual(value); +}); + +test('Allows to access context attributes', () => { + const { result } = renderHook(() => useLogListContextData('pinnedLogs'), { wrapper }); + + expect(result.current).toEqual(value.pinnedLogs); +}); + +test('Allows to tell if a log is pinned', () => { + const { result } = renderHook(() => useLogIsPinned(log), { wrapper }); + + expect(result.current).toBe(true); +}); + +test('Allows to tell if a log is pinned', () => { + const otherLog = createLogLine({ rowId: 'nope' }); + const { result } = renderHook(() => useLogIsPinned(otherLog), { wrapper }); + + expect(result.current).toBe(false); +}); diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx new file mode 100644 index 00000000000..9f654cf4dce --- /dev/null +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -0,0 +1,33 @@ +import { createContext, useContext } from 'react'; + +import { LogRowModel } from '@grafana/data'; +import { PopoverContent } from '@grafana/ui'; + +import { GetRowContextQueryFn } from './LogLineMenu'; + +export interface LogListContextData { + getRowContextQuery?: GetRowContextQueryFn; + logSupportsContext?: (row: LogRowModel) => boolean; + onPermalinkClick?: (row: LogRowModel) => Promise; + onPinLine?: (row: LogRowModel) => void; + onOpenContext?: (row: LogRowModel, onClose: () => void) => void; + onUnpinLine?: (row: LogRowModel) => void; + pinLineButtonTooltipTitle?: PopoverContent; + pinnedLogs?: string[]; +} + +export const LogListContext = createContext({}); + +export const useLogListContextData = (key: keyof LogListContextData) => { + const data: LogListContextData = useContext(LogListContext); + return data[key]; +}; + +export const useLogListContext = (): LogListContextData => { + return useContext(LogListContext); +}; + +export const useLogIsPinned = (log: LogRowModel) => { + const { pinnedLogs } = useContext(LogListContext); + return pinnedLogs?.some((logId) => logId === log.rowId); +}; diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 2d62db31370..ef2c2c81b12 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -20,7 +20,7 @@ export interface LogFieldDimension { width: number; } -interface PreProcessOptions { +export interface PreProcessOptions { escape: boolean; getFieldLinks?: GetFieldLinksFn; order: LogsSortOrder; diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index a367b41c050..873e7555c58 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -8,10 +8,13 @@ let gridSize = 8; let paddingBottom = gridSize * 0.75; let lineHeight = 22; let measurementMode: 'canvas' | 'dom' = 'canvas'; +const iconWidth = 24; // Controls the space between fields in the log line, timestamp, level, displayed fields, and log line body export const FIELD_GAP_MULTIPLIER = 1.5; +export const getLineHeight = () => lineHeight; + export function init(theme: GrafanaTheme2) { const font = `${theme.typography.fontSize}px ${theme.typography.fontFamilyMonospace}`; const letterSpacing = theme.typography.body.letterSpacing; @@ -193,7 +196,7 @@ export function hasUnderOrOverflow(element: HTMLDivElement, calculatedHeight?: n if (element.scrollHeight > height) { return element.scrollHeight; } - const child = element.firstChild; + const child = element.children[1]; if (child instanceof HTMLDivElement && child.clientHeight < height) { return child.clientHeight; } @@ -203,7 +206,7 @@ export function hasUnderOrOverflow(element: HTMLDivElement, calculatedHeight?: n const scrollBarWidth = getScrollbarWidth(); export function getLogContainerWidth(container: HTMLDivElement) { - return container.clientWidth - scrollBarWidth; + return container.clientWidth - scrollBarWidth - iconWidth; } export function getScrollbarWidth() { diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index c4a19c0aa6c..b55e2f7a3a2 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -1,4 +1,5 @@ import { countBy, chain } from 'lodash'; +import { MouseEvent } from 'react'; import { LogLevel, @@ -15,9 +16,14 @@ import { LogsVolumeType, NumericLogLevel, getFieldDisplayName, + getDefaultTimeRange, + locationUtil, + urlUtil, } from '@grafana/data'; +import { getConfig } from 'app/core/config'; import { getDataframeFields } from './components/logParser'; +import { GetRowContextQueryFn } from './components/panel/LogLineMenu'; /** * Returns the log level of a log line. @@ -303,6 +309,34 @@ export const copyText = async (text: string, buttonRef: React.MutableRefObject, + row: LogRowModel, + getRowContextQuery: GetRowContextQueryFn | undefined, + onOpenContext: (row: LogRowModel) => void +) { + // if ctrl or meta key is pressed, open query in new Explore tab + if (getRowContextQuery && (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey || event.nativeEvent.shiftKey)) { + const win = window.open('about:blank'); + // for this request we don't want to use the cached filters from a context provider, but always want to refetch and clear + const query = await getRowContextQuery(row, undefined, false); + if (query && win) { + const url = urlUtil.renderUrl(locationUtil.assureBaseUrl(`${getConfig().appSubUrl}explore`), { + left: JSON.stringify({ + datasource: query.datasource, + queries: [query], + range: getDefaultTimeRange(), + }), + }); + win.location = url; + + return; + } + win?.close(); + } + onOpenContext(row); +} + export function getLogLevelInfo(dataFrame: DataFrame, allDataFrames: DataFrame[]) { const fieldCache = new FieldCache(dataFrame); const timeField = fieldCache.getFirstFieldOfType(FieldType.time); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a064ce993a6..fe2fea8fc02 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2257,6 +2257,14 @@ "log-line": "Log line", "no-details": "No details available" }, + "log-line-menu": { + "copy-link": "Copy link to log line", + "copy-log": "Copy log line", + "icon-label": "Log menu", + "pin-to-outline": "Pin log", + "show-context": "Show context", + "unpin-from-outline": "Unpin log" + }, "log-row-message": { "ellipsis": "… ", "more": "more", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 317a107f796..d6af162cbf2 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2257,6 +2257,14 @@ "log-line": "Ŀőģ ľįʼnę", "no-details": "Ńő đęŧäįľş äväįľäþľę" }, + "log-line-menu": { + "copy-link": "Cőpy ľįʼnĸ ŧő ľőģ ľįʼnę", + "copy-log": "Cőpy ľőģ ľįʼnę", + "icon-label": "Ŀőģ męʼnū", + "pin-to-outline": "Pįʼn ľőģ", + "show-context": "Ŝĥőŵ čőʼnŧęχŧ", + "unpin-from-outline": "Ůʼnpįʼn ľőģ" + }, "log-row-message": { "ellipsis": "… ", "more": "mőřę",