diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx
index 1a099f950e9..3ab836aaa18 100644
--- a/public/app/features/logs/components/panel/LogLine.test.tsx
+++ b/public/app/features/logs/components/panel/LogLine.test.tsx
@@ -8,13 +8,14 @@ import { createLogLine } from '../mocks/logRow';
import { getGridTemplateColumns, getStyles, LogLine, Props } from './LogLine';
import { LogListFontSize } from './LogList';
-import { LogListContextProvider } from './LogListContext';
+import { LogListContextProvider, LogListContext } from './LogListContext';
import { LogListSearchContext } from './LogListSearchContext';
-import { defaultProps } from './__mocks__/LogListContext';
+import { defaultProps, defaultValue } from './__mocks__/LogListContext';
import { LogListModel } from './processing';
import { LogLineVirtualization } from './virtualization';
jest.mock('./LogListContext');
+jest.mock('../LogDetails');
const theme = createTheme();
const virtualization = new LogLineVirtualization(theme, 'default');
@@ -424,6 +425,40 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => {
expect(screen.getByText('un')).toBeInTheDocument();
});
});
+
+ describe('Inline details', () => {
+ test('Details are not rendered if details mode is not inline', () => {
+ render(
+
+
+
+ );
+ expect(screen.queryByPlaceholderText('Search field names and values')).not.toBeInTheDocument();
+ });
+
+ test('Details are rendered if details mode is inline', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByPlaceholderText('Search field names and values')).toBeInTheDocument();
+ });
+ });
});
describe('getGridTemplateColumns', () => {
diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx
index ec3062ee3b5..9f96fb3a3b7 100644
--- a/public/app/features/logs/components/panel/LogLine.tsx
+++ b/public/app/features/logs/components/panel/LogLine.tsx
@@ -10,6 +10,7 @@ import { Button, Icon, Tooltip } from '@grafana/ui';
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
import { LogMessageAnsi } from '../LogMessageAnsi';
+import { InlineLogLineDetails } from './LogLineDetails';
import { LogLineMenu } from './LogLineMenu';
import { useLogIsPermalinked, useLogIsPinned, useLogListContext } from './LogListContext';
import { useLogListSearchContext } from './LogListSearchContext';
@@ -50,7 +51,7 @@ export const LogLine = ({
wrapLogMessage,
}: Props) => {
return (
-
+
{
const {
detailsDisplayed,
+ detailsMode,
dedupStrategy,
enableLogDetails,
fontSize,
@@ -235,6 +237,7 @@ const LogLineComponent = memo(
)}
+ {detailsMode === 'inline' && detailsShown &&
}
>
);
}
diff --git a/public/app/features/logs/components/panel/LogLineDetails.tsx b/public/app/features/logs/components/panel/LogLineDetails.tsx
index 212da50add7..dbd2990ea6b 100644
--- a/public/app/features/logs/components/panel/LogLineDetails.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetails.tsx
@@ -17,9 +17,11 @@ export interface Props {
onResize(): void;
}
+export type LogLineDetailsMode = 'inline' | 'sidebar';
+
export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize }: Props) => {
- const { detailsWidth, logOptionsStorageKey, setDetailsWidth, showDetails } = useLogListContext();
- const styles = useStyles2(getStyles);
+ const { detailsWidth, setDetailsWidth, showDetails } = useLogListContext();
+ const styles = useStyles2(getStyles, 'sidebar');
const dragStyles = useStyles2(getDragStyles);
const containerRef = useRef
(null);
@@ -54,20 +56,51 @@ export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize
>
);
};
-const getStyles = (theme: GrafanaTheme2) => ({
+export interface InlineLogLineDetailsProps {
+ logs: LogListModel[];
+}
+
+export const InlineLogLineDetails = ({ logs }: InlineLogLineDetailsProps) => {
+ const { showDetails } = useLogListContext();
+ const styles = useStyles2(getStyles, 'inline');
+
+ if (!showDetails.length) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+export const LOG_LINE_DETAILS_HEIGHT = 35;
+
+const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode) => ({
+ inlineWrapper: css({
+ gridColumn: '1 / -1',
+ height: `${LOG_LINE_DETAILS_HEIGHT}vh`,
+ paddingBottom: theme.spacing(0.5),
+ marginRight: 1,
+ }),
container: css({
overflow: 'auto',
height: '100%',
boxShadow: theme.shadows.z1,
border: `1px solid ${theme.colors.border.medium}`,
- borderRight: 'none',
+ borderRight: mode === 'sidebar' ? 'none' : undefined,
}),
scrollContainer: css({
overflow: 'auto',
diff --git a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx
index fefd7004879..0024860a93a 100644
--- a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx
@@ -18,12 +18,11 @@ import { LogListModel } from './processing';
interface LogLineDetailsComponentProps {
log: LogListModel;
- logOptionsStorageKey?: string;
logs: LogListModel[];
}
-export const LogLineDetailsComponent = ({ log, logOptionsStorageKey, logs }: LogLineDetailsComponentProps) => {
- const { displayedFields, setDisplayedFields } = useLogListContext();
+export const LogLineDetailsComponent = ({ log, logs }: LogLineDetailsComponentProps) => {
+ const { displayedFields, logOptionsStorageKey, setDisplayedFields } = useLogListContext();
const [search, setSearch] = useState('');
const inputRef = useRef('');
const styles = useStyles2(getStyles);
diff --git a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx
index 2c76d814cb6..140a7f86fca 100644
--- a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx
@@ -1,13 +1,14 @@
import { css } from '@emotion/css';
import { useCallback, useMemo, MouseEvent, useRef, ChangeEvent } from 'react';
-import { colorManipulator, GrafanaTheme2, LogRowModel } from '@grafana/data';
+import { colorManipulator, GrafanaTheme2, LogRowModel, store } from '@grafana/data';
import { t } from '@grafana/i18n';
import { IconButton, Input, useStyles2 } from '@grafana/ui';
import { copyText, handleOpenLogsContextClick } from '../../utils';
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
+import { LogLineDetailsMode } from './LogLineDetails';
import { useLogIsPinned, useLogListContext } from './LogListContext';
import { LogListModel } from './processing';
@@ -20,18 +21,22 @@ interface Props {
export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
const {
closeDetails,
+ detailsMode,
displayedFields,
getRowContextQuery,
+ logOptionsStorageKey,
logSupportsContext,
+ setDetailsMode,
onClickHideField,
onClickShowField,
onOpenContext,
onPermalinkClick,
onPinLine,
onUnpinLine,
+ wrapLogMessage,
} = useLogListContext();
const pinned = useLogIsPinned(log);
- const styles = useStyles2(getStyles);
+ const styles = useStyles2(getStyles, detailsMode, wrapLogMessage);
const containerRef = useRef(null);
const inputRef = useRef(null);
@@ -66,6 +71,15 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
const showLogLineToggle = onClickHideField && onClickShowField && displayedFields.length > 0;
const logLineDisplayed = displayedFields.includes(LOG_LINE_BODY_FIELD_NAME);
+ const toggleDetailsMode = useCallback(() => {
+ const newMode = detailsMode === 'inline' ? 'sidebar' : 'inline';
+ if (logOptionsStorageKey) {
+ store.set(`${logOptionsStorageKey}.detailsMode`, newMode);
+ }
+
+ setDetailsMode(newMode);
+ }, [detailsMode, logOptionsStorageKey, setDetailsMode]);
+
const toggleLogLine = useCallback(() => {
if (logLineDisplayed) {
onClickHideField?.(LOG_LINE_BODY_FIELD_NAME);
@@ -121,65 +135,76 @@ export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => {
variant={logLineDisplayed ? 'primary' : undefined}
/>
)}
-
- {onPermalinkClick && log.rowId !== undefined && log.uid && (
+
- )}
- {pinned && onUnpinLine && (
-
- )}
- {!pinned && onPinLine && (
+ {onPermalinkClick && log.rowId !== undefined && log.uid && (
+
+ )}
+ {pinned && onUnpinLine && (
+
+ )}
+ {!pinned && onPinLine && (
+
+ )}
+ {shouldlogSupportsContext && (
+
+ )}
- )}
- {shouldlogSupportsContext && (
- )}
-
+
);
};
-const getStyles = (theme: GrafanaTheme2) => ({
+const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode, wrapLogMessage: boolean) => ({
container: css({
overflow: 'auto',
height: '100%',
@@ -192,7 +217,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
alignItems: 'center',
background: theme.colors.background.canvas,
display: 'flex',
- flexDirection: 'row',
+ flexDirection: !wrapLogMessage && mode === 'inline' ? 'row-reverse' : 'row',
gap: theme.spacing(0.75),
zIndex: theme.zIndex.navbarFixed,
height: theme.spacing(5.5),
@@ -201,6 +226,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
position: 'sticky',
top: 0,
}),
+ icons: css({
+ display: 'flex',
+ gap: theme.spacing(0.75),
+ }),
copyLogButton: css({
padding: 0,
height: theme.spacing(4),
diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx
index 17eda97f4f6..fde0ea89dec 100644
--- a/public/app/features/logs/components/panel/LogList.test.tsx
+++ b/public/app/features/logs/components/panel/LogList.test.tsx
@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
+import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder, store } from '@grafana/data';
import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils';
import { createLogRow } from '../mocks/logRow';
@@ -75,6 +75,55 @@ describe('LogList', () => {
});
test('Supports showing log details', async () => {
+ jest.spyOn(store, 'get').mockImplementation((option: string) => {
+ if (option === 'storage-key.detailsMode') {
+ return 'sidebar';
+ }
+ return undefined;
+ });
+ const onClickFilterLabel = jest.fn();
+ const onClickFilterOutLabel = jest.fn();
+ const onClickShowField = jest.fn();
+
+ render(
+
+ );
+
+ await userEvent.click(screen.getByText('log message 1'));
+ await screen.findByText('Fields');
+
+ expect(screen.getByText('name_of_the_label')).toBeInTheDocument();
+ expect(screen.getByText('value of the label')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByLabelText('Filter for value in query A'));
+ expect(onClickFilterLabel).toHaveBeenCalledTimes(1);
+
+ await userEvent.click(screen.getByLabelText('Filter out value in query A'));
+ expect(onClickFilterOutLabel).toHaveBeenCalledTimes(1);
+
+ await userEvent.click(screen.getByLabelText('Show this field instead of the message'));
+ expect(onClickShowField).toHaveBeenCalledTimes(1);
+
+ await userEvent.click(screen.getByLabelText('Close log details'));
+
+ expect(screen.queryByText('Fields')).not.toBeInTheDocument();
+ expect(screen.queryByText('Close log details')).not.toBeInTheDocument();
+ });
+
+ test('Supports showing inline log details', async () => {
+ jest.spyOn(store, 'get').mockImplementation((option: string) => {
+ if (option === 'storage-key.detailsMode') {
+ return 'inline';
+ }
+ return undefined;
+ });
const onClickFilterLabel = jest.fn();
const onClickFilterOutLabel = jest.fn();
const onClickShowField = jest.fn();
@@ -86,6 +135,7 @@ describe('LogList', () => {
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickShowField={onClickShowField}
+ logOptionsStorageKey="storage-key"
/>
);
diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx
index 489741fd59b..a5115f5e447 100644
--- a/public/app/features/logs/components/panel/LogList.tsx
+++ b/public/app/features/logs/components/panel/LogList.tsx
@@ -26,7 +26,7 @@ import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
import { InfiniteScroll } from './InfiniteScroll';
import { getGridTemplateColumns } from './LogLine';
-import { LogLineDetails } from './LogLineDetails';
+import { LogLineDetails, LogLineDetailsMode } from './LogLineDetails';
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
import { LogListContextProvider, LogListState, useLogListContext } from './LogListContext';
import { LogListControls } from './LogListControls';
@@ -41,6 +41,7 @@ export interface Props {
app: CoreApp;
containerElement: HTMLDivElement;
dedupStrategy: LogsDedupStrategy;
+ detailsMode?: LogLineDetailsMode;
displayedFields: string[];
enableLogDetails: boolean;
eventBus?: EventBus;
@@ -105,11 +106,12 @@ export const LogList = ({
app,
displayedFields,
containerElement,
+ logOptionsStorageKey,
+ detailsMode = logOptionsStorageKey ? (store.get(`${logOptionsStorageKey}.detailsMode`) ?? 'sidebar') : 'sidebar',
dedupStrategy,
enableLogDetails,
eventBus,
filterLevels,
- logOptionsStorageKey,
fontSize = logOptionsStorageKey ? (store.get(`${logOptionsStorageKey}.fontSize`) ?? 'default') : 'default',
getFieldLinks,
getRowContextQuery,
@@ -152,6 +154,7 @@ export const LogList = ({
app={app}
containerElement={containerElement}
dedupStrategy={dedupStrategy}
+ detailsMode={detailsMode}
displayedFields={displayedFields}
enableLogDetails={enableLogDetails}
filterLevels={filterLevels}
@@ -222,6 +225,7 @@ const LogListComponent = ({
app,
displayedFields,
dedupStrategy,
+ detailsMode,
filterLevels,
fontSize,
forceEscape,
@@ -456,9 +460,11 @@ const LogListComponent = ({
height={listHeight}
itemCount={itemCount}
itemSize={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, {
+ detailsMode,
hasLogsWithErrors,
hasSampledLogs,
showDuplicates: dedupStrategy !== LogsDedupStrategy.none,
+ showDetails,
showTime,
wrap: wrapLogMessage,
})}
@@ -476,7 +482,7 @@ const LogListComponent = ({
)}
- {showDetails.length > 0 && (
+ {detailsMode === 'sidebar' && showDetails.length > 0 && (
{
closeDetails: () => void;
detailsDisplayed: (log: LogListModel) => boolean;
+ detailsMode: LogLineDetailsMode;
detailsWidth: number;
downloadLogs: (format: DownloadFormat) => void;
enableLogDetails: boolean;
@@ -43,6 +45,7 @@ export interface LogListContextData extends Omit void;
+ setDetailsMode: (mode: LogLineDetailsMode) => void;
setDetailsWidth: (width: number) => void;
setFilterLevels: (filterLevels: LogLevel[]) => void;
setFontSize: (size: LogListFontSize) => void;
@@ -64,6 +67,7 @@ export const LogListContext = createContext({
closeDetails: () => {},
dedupStrategy: LogsDedupStrategy.none,
detailsDisplayed: () => false,
+ detailsMode: 'sidebar',
detailsWidth: 0,
displayedFields: [],
downloadLogs: () => {},
@@ -72,6 +76,7 @@ export const LogListContext = createContext({
fontSize: 'default',
hasUnescapedContent: false,
setDedupStrategy: () => {},
+ setDetailsMode: () => {},
setDetailsWidth: () => {},
setFilterLevels: () => {},
setFontSize: () => {},
@@ -132,6 +137,7 @@ export interface Props {
children?: ReactNode;
// Only ControlledLogRows can send an undefined containerElement. See LogList.tsx
containerElement?: HTMLDivElement;
+ detailsMode?: LogLineDetailsMode;
dedupStrategy: LogsDedupStrategy;
displayedFields: string[];
enableLogDetails: boolean;
@@ -176,6 +182,7 @@ export const LogListContextProvider = ({
children,
containerElement,
enableLogDetails,
+ detailsMode: detailsModeProp,
dedupStrategy,
displayedFields,
filterLevels,
@@ -230,6 +237,7 @@ export const LogListContextProvider = ({
});
const [showDetails, setShowDetails] = useState([]);
const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey));
+ const [detailsMode, setDetailsMode] = useState(detailsModeProp ?? 'sidebar');
useEffect(() => {
// Props are updated in the context only of the panel is being externally controlled.
@@ -480,6 +488,7 @@ export const LogListContextProvider = ({
closeDetails,
detailsDisplayed,
dedupStrategy: logListState.dedupStrategy,
+ detailsMode,
detailsWidth,
displayedFields,
downloadLogs,
@@ -511,6 +520,7 @@ export const LogListContextProvider = ({
pinnedLogs: logListState.pinnedLogs,
prettifyJSON: logListState.prettifyJSON,
setDedupStrategy,
+ setDetailsMode,
setDetailsWidth,
setDisplayedFields,
setFilterLevels,
@@ -563,7 +573,9 @@ function getDetailsWidth(
const defaultWidth = containerElement.clientWidth * 0.4;
const detailsWidth =
currentWidth ||
- (logOptionsStorageKey ? parseInt(store.get(`${logOptionsStorageKey}.detailsWidth`), 10) : defaultWidth);
+ (logOptionsStorageKey
+ ? parseInt(store.get(`${logOptionsStorageKey}.detailsWidth`) ?? defaultWidth, 10)
+ : defaultWidth);
const maxWidth = containerElement.clientWidth - LOG_LIST_MIN_WIDTH;
diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
index f0cb9be3ce6..1a991f79ecf 100644
--- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
+++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
@@ -3,6 +3,7 @@ import { createContext, useContext } from 'react';
import { CoreApp, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
import { checkLogsError, checkLogsSampled } from 'app/features/logs/utils';
+import { LogLineDetailsMode } from '../LogLineDetails';
import { LogListContextData, Props } from '../LogListContext';
import { LogListModel } from '../processing';
@@ -37,6 +38,10 @@ export const LogListContext = createContext({
syntaxHighlighting: true,
toggleDetails: () => {},
wrapLogMessage: false,
+ detailsMode: 'sidebar',
+ setDetailsMode: function (mode: LogLineDetailsMode): void {
+ throw new Error('Function not implemented.');
+ },
});
export const useLogListContextData = (key: keyof LogListContextData) => {
@@ -59,6 +64,8 @@ export const useLogIsPermalinked = (log: LogListModel) => {
};
export const defaultValue: LogListContextData = {
+ detailsMode: 'sidebar',
+ setDetailsMode: jest.fn(),
setDedupStrategy: jest.fn(),
setFilterLevels: jest.fn(),
setFontSize: jest.fn(),
diff --git a/public/app/features/logs/components/panel/virtualization.test.ts b/public/app/features/logs/components/panel/virtualization.test.ts
index 75534d4e170..479b7b4a746 100644
--- a/public/app/features/logs/components/panel/virtualization.test.ts
+++ b/public/app/features/logs/components/panel/virtualization.test.ts
@@ -3,14 +3,17 @@ import { createTheme, LogsSortOrder } from '@grafana/data';
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
import { createLogLine } from '../mocks/logRow';
+import { LOG_LINE_DETAILS_HEIGHT } from './LogLineDetails';
import { LogListModel, PreProcessOptions } from './processing';
-import { LogLineVirtualization, getLogLineSize, DisplayOptions } from './virtualization';
+import { LogLineVirtualization, getLogLineSize, DisplayOptions, FIELD_GAP_MULTIPLIER } from './virtualization';
describe('Virtualization', () => {
let log: LogListModel, container: HTMLDivElement;
let virtualization = new LogLineVirtualization(createTheme(), 'default');
+ const GAP = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER;
+ const DETAILS_HEIGHT = window.innerHeight * (LOG_LINE_DETAILS_HEIGHT / 100) + GAP / 2;
const PADDING_BOTTOM = 6;
const LINE_HEIGHT = virtualization.getLineHeight();
const SINGLE_LINE_HEIGHT = LINE_HEIGHT + PADDING_BOTTOM;
@@ -21,8 +24,10 @@ describe('Virtualization', () => {
let TWO_LINES_OF_CHARACTERS: number;
const defaultOptions: DisplayOptions = {
+ detailsMode: 'sidebar',
wrap: false,
showTime: false,
+ showDetails: [],
showDuplicates: false,
hasLogsWithErrors: false,
hasSampledLogs: false,
@@ -50,6 +55,18 @@ describe('Virtualization', () => {
expect(size).toBe(SINGLE_LINE_HEIGHT);
});
+ test('Returns the a single line plus inline details if the display mode is unwrapped', () => {
+ const size = getLogLineSize(
+ virtualization,
+ [log],
+ container,
+ [],
+ { ...defaultOptions, showTime: true, showDetails: [log], detailsMode: 'inline' },
+ 0
+ );
+ expect(size).toBe(SINGLE_LINE_HEIGHT + DETAILS_HEIGHT);
+ });
+
test('Returns the a single line if the line is not loaded yet', () => {
const logs = [log];
const size = getLogLineSize(
@@ -78,6 +95,21 @@ describe('Virtualization', () => {
expect(size).toBe((virtualization.getTruncationLineCount() + 1) * LINE_HEIGHT);
});
+ test('Returns the size of a truncated long line with inline details', () => {
+ // Very small container
+ log.collapsed = true;
+ jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(10);
+ const size = getLogLineSize(
+ virtualization,
+ [log],
+ container,
+ [],
+ { ...defaultOptions, wrap: true, showTime: true, showDetails: [log], detailsMode: 'inline' },
+ 0
+ );
+ expect(size).toBe((virtualization.getTruncationLineCount() + 1) * LINE_HEIGHT + DETAILS_HEIGHT);
+ });
+
test.each([true, false])('Measures a log line with controls %s and displayed time %s', (showTime: boolean) => {
const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, wrap: true, showTime }, 0);
expect(size).toBe(SINGLE_LINE_HEIGHT);
@@ -115,6 +147,24 @@ describe('Virtualization', () => {
expect(size).toBe(THREE_LINES_HEIGHT);
});
+ test('Measures a multi-line log line with level, controls, displayed time, and inline details', () => {
+ log = createLogLine(
+ { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') },
+ preProcessOptions
+ );
+
+ const size = getLogLineSize(
+ virtualization,
+ [log],
+ container,
+ [],
+ { ...defaultOptions, wrap: true, showTime: true, showDetails: [log], detailsMode: 'inline' },
+ 0
+ );
+ // Two lines for the log and one extra for level and time
+ expect(size).toBe(THREE_LINES_HEIGHT + DETAILS_HEIGHT);
+ });
+
test('Measures a multi-line log line with displayed fields', () => {
log = createLogLine(
{
diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts
index 2f9b6b6773a..f9e7676a5f5 100644
--- a/public/app/features/logs/components/panel/virtualization.ts
+++ b/public/app/features/logs/components/panel/virtualization.ts
@@ -2,6 +2,7 @@ import ansicolor from 'ansicolor';
import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data';
+import { LOG_LINE_DETAILS_HEIGHT, LogLineDetailsMode } from './LogLineDetails';
import { LogListFontSize } from './LogList';
import { LogListModel } from './processing';
@@ -232,8 +233,10 @@ export class LogLineVirtualization {
}
export interface DisplayOptions {
+ detailsMode: LogLineDetailsMode;
hasLogsWithErrors?: boolean;
hasSampledLogs?: boolean;
+ showDetails: LogListModel[];
showDuplicates: boolean;
showTime: boolean;
wrap: boolean;
@@ -244,20 +247,25 @@ export function getLogLineSize(
logs: LogListModel[],
container: HTMLDivElement | null,
displayedFields: string[],
- { hasLogsWithErrors, hasSampledLogs, showDuplicates, showTime, wrap }: DisplayOptions,
+ { detailsMode, hasLogsWithErrors, hasSampledLogs, showDuplicates, showDetails, showTime, wrap }: DisplayOptions,
index: number
) {
if (!container) {
return 0;
}
+ const gap = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER;
+ const detailsHeight =
+ detailsMode === 'inline' && showDetails.findIndex((log) => log.uid === logs[index].uid) >= 0
+ ? window.innerHeight * (LOG_LINE_DETAILS_HEIGHT / 100) + gap / 2
+ : 0;
// !logs[index] means the line is not yet loaded by infinite scrolling
if (!wrap || !logs[index]) {
- return virtualization.getLineHeight() + virtualization.getPaddingBottom();
+ return virtualization.getLineHeight() + virtualization.getPaddingBottom() + detailsHeight;
}
// If a long line is collapsed, we show the line count + an extra line for the expand/collapse control
logs[index].updateCollapsedState(displayedFields, container);
if (logs[index].collapsed) {
- return (virtualization.getTruncationLineCount() + 1) * virtualization.getLineHeight();
+ return (virtualization.getTruncationLineCount() + 1) * virtualization.getLineHeight() + detailsHeight;
}
const storedSize = virtualization.retrieveLogLineSize(logs[index].uid, container);
@@ -266,7 +274,6 @@ export function getLogLineSize(
}
let textToMeasure = '';
- const gap = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER;
const iconsGap = virtualization.getGridSize() * 0.5;
let optionsWidth = 0;
if (showDuplicates) {
@@ -296,7 +303,9 @@ export function getLogLineSize(
const { height } = virtualization.measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth);
// When the log is collapsed, add an extra line for the expand/collapse control
- return logs[index].collapsed === false ? height + virtualization.getLineHeight() : height;
+ return logs[index].collapsed === false
+ ? height + virtualization.getLineHeight() + detailsHeight
+ : height + detailsHeight;
}
export interface LogFieldDimension {
@@ -313,14 +322,28 @@ export function hasUnderOrOverflow(
if (collapsed !== undefined && calculatedHeight) {
calculatedHeight -= virtualization.getLineHeight();
}
+ const inlineDetails = element.parentElement
+ ? Array.from(element.parentElement.children).filter((element) =>
+ element.classList.contains('log-line-inline-details')
+ )
+ : undefined;
+ const detailsHeight = inlineDetails?.length ? inlineDetails[0].clientHeight : 0;
+
+ // Line overflows container
+ let measuredHeight = element.scrollHeight + detailsHeight;
const height = calculatedHeight ?? element.clientHeight;
- if (element.scrollHeight > height) {
- return collapsed !== undefined ? element.scrollHeight + virtualization.getLineHeight() : element.scrollHeight;
+ if (measuredHeight > height) {
+ return collapsed !== undefined ? measuredHeight + virtualization.getLineHeight() : measuredHeight;
}
+
+ // Line is smaller than container
const child = element.children[1];
- if (child instanceof HTMLDivElement && child.clientHeight < height) {
- return collapsed !== undefined ? child.clientHeight + virtualization.getLineHeight() : child.clientHeight;
+ measuredHeight = child.clientHeight + detailsHeight;
+ if (child instanceof HTMLDivElement && measuredHeight < height) {
+ return collapsed !== undefined ? measuredHeight + virtualization.getLineHeight() : measuredHeight;
}
+
+ // No overflow or undermeasurement
return null;
}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 8cfdb522034..d80bd556f31 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -8739,6 +8739,7 @@
},
"fields-section": "Fields",
"hide-log-line": "Hide log line",
+ "inline-mode": "Display inline",
"links-section": "Links",
"log-line-field": "Log line",
"log-line-section": "Log line",
@@ -8751,6 +8752,7 @@
"search-placeholder": "Search field names and values",
"show-context": "Show context",
"show-log-line": "Show log line",
+ "sidebar-mode": "Anchor to the right",
"unpin-line": "Unpin log"
},
"log-line-menu": {