diff --git a/public/app/features/logs/components/LogLabels.tsx b/public/app/features/logs/components/LogLabels.tsx
index 8b86a900408..da8107663fb 100644
--- a/public/app/features/logs/components/LogLabels.tsx
+++ b/public/app/features/logs/components/LogLabels.tsx
@@ -68,6 +68,7 @@ export const LogLabels = memo(
size="sm"
fill="outline"
variant="secondary"
+ className={styles.button}
aria-label={t('logs.log-labels.expand', 'Expand labels')}
onClick={() => {
setDisplayAll(true);
@@ -83,6 +84,7 @@ export const LogLabels = memo(
size="sm"
fill="outline"
variant="secondary"
+ className={styles.button}
aria-label={t('logs.log-labels.collapse', 'Collapse labels')}
onClick={() => {
setDisplayAll(false);
@@ -137,7 +139,7 @@ LogLabel.displayName = 'LogLabel';
const getStyles = (theme: GrafanaTheme2) => {
return {
logsLabels: css({
- display: 'flex',
+ display: 'inline-flex',
flexWrap: 'wrap',
fontSize: theme.typography.size.xs,
alignItems: 'center',
@@ -161,5 +163,8 @@ const getStyles = (theme: GrafanaTheme2) => {
textOverflow: 'ellipsis',
overflow: 'hidden',
}),
+ button: css({
+ height: theme.spacing(2.75),
+ }),
};
};
diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx
index 5b765c49343..b79bf06e2e0 100644
--- a/public/app/features/logs/components/panel/LogLine.test.tsx
+++ b/public/app/features/logs/components/panel/LogLine.test.tsx
@@ -613,4 +613,34 @@ describe('getGridTemplateColumns', () => {
)
).toBe('23px 4px 4px 20px');
});
+
+ test('Gets the template columns with unique labels', () => {
+ expect(
+ getGridTemplateColumns(
+ [
+ {
+ field: 'timestamp',
+ width: 23,
+ },
+ {
+ field: 'level',
+ width: 4,
+ },
+ {
+ field: 'unique-labels',
+ width: 0,
+ },
+ {
+ field: 'field',
+ width: 4,
+ },
+ {
+ field: LOG_LINE_BODY_FIELD_NAME,
+ width: 20,
+ },
+ ],
+ ['field']
+ )
+ ).toBe('23px 4px max-content 4px 20px');
+ });
});
diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx
index 0c95184fca0..62a19356407 100644
--- a/public/app/features/logs/components/panel/LogLine.tsx
+++ b/public/app/features/logs/components/panel/LogLine.tsx
@@ -1,5 +1,15 @@
import { css } from '@emotion/css';
-import { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from 'react';
+import {
+ CSSProperties,
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ MouseEvent,
+ useLayoutEffect,
+} from 'react';
import Highlighter from 'react-highlight-words';
import tinycolor from 'tinycolor2';
@@ -8,6 +18,7 @@ import { t } from '@grafana/i18n';
import { Button, Icon, Tooltip } from '@grafana/ui';
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
+import { LogLabels } from '../LogLabels';
import { LogMessageAnsi } from '../LogMessageAnsi';
import { HighlightedLogRenderer } from './HighlightedLogRenderer';
@@ -18,7 +29,7 @@ import { useLogListSearchContext } from './LogListSearchContext';
import { LogListModel } from './processing';
import {
FIELD_GAP_MULTIPLIER,
- hasUnderOrOverflow,
+ getLogLineDOMHeight,
LogFieldDimension,
LogLineVirtualization,
DEFAULT_LINE_HEIGHT,
@@ -108,6 +119,7 @@ const LogLineComponent = memo(
fontSize,
hasLogsWithErrors,
hasSampledLogs,
+ showUniqueLabels,
timestampResolution,
onLogLineHover,
} = useLogListContext();
@@ -118,16 +130,41 @@ const LogLineComponent = memo(
const pinned = useLogIsPinned(log);
const permalinked = useLogIsPermalinked(log);
- useEffect(() => {
- if (!onOverflow || !logLineRef.current || !virtualization || !height || !wrapLogMessage) {
+ const handleLogLineResize = useCallback(() => {
+ if (!onOverflow || !logLineRef.current || !virtualization || !height) {
return;
}
const calculatedHeight = typeof height === 'number' ? height : undefined;
- const actualHeight = hasUnderOrOverflow(virtualization, logLineRef.current, calculatedHeight, log.collapsed);
+ const actualHeight = getLogLineDOMHeight(virtualization, logLineRef.current, calculatedHeight, log.collapsed);
if (actualHeight) {
onOverflow(index, log.uid, actualHeight);
}
- });
+ }, [height, index, log.collapsed, log.uid, onOverflow, virtualization]);
+
+ useLayoutEffect(() => {
+ handleLogLineResize();
+ }, [handleLogLineResize, detailsMode]);
+
+ useLayoutEffect(() => {
+ if (!logLineRef.current) {
+ return;
+ }
+ let frameId: number;
+ const handleResize = () => {
+ if (frameId) {
+ cancelAnimationFrame(frameId);
+ }
+ frameId = requestAnimationFrame(() => handleLogLineResize());
+ };
+ const observer = new ResizeObserver(handleResize);
+ observer.observe(logLineRef.current);
+ return () => {
+ observer.disconnect();
+ if (frameId) {
+ cancelAnimationFrame(frameId);
+ }
+ };
+ }, [handleLogLineResize]);
useEffect(() => {
if (!wrapLogMessage) {
@@ -157,16 +194,6 @@ const LogLineComponent = memo(
[log, onClick]
);
- const handleLogDetailsResize = useCallback(() => {
- if (!onOverflow || !logLineRef.current || !virtualization) {
- return;
- }
- const actualHeight = hasUnderOrOverflow(virtualization, logLineRef.current, undefined, log.collapsed);
- if (actualHeight) {
- onOverflow(index, log.uid, actualHeight);
- }
- }, [index, log.collapsed, log.uid, onOverflow, virtualization]);
-
const detailsShown = detailsDisplayed(log);
return (
@@ -233,6 +260,7 @@ const LogLineComponent = memo(
displayedFields={displayedFields}
log={log}
showTime={showTime}
+ showUniqueLabels={showUniqueLabels}
styles={styles}
timestampResolution={timestampResolution}
wrapLogMessage={wrapLogMessage}
@@ -269,7 +297,7 @@ const LogLineComponent = memo(
@@ -287,33 +315,53 @@ interface LogProps {
displayedFields: string[];
log: LogListModel;
showTime: boolean;
+ showUniqueLabels?: boolean;
styles: LogLineStyles;
timestampResolution: LogLineTimestampResolution;
wrapLogMessage: boolean;
}
-const Log = memo(({ displayedFields, log, showTime, styles, timestampResolution, wrapLogMessage }: LogProps) => {
- return (
- <>
- {showTime && (
-
- {timestampResolution === 'ms' ? log.timestamp : log.timestampNs}
-
- )}
- {
- // When logs are unwrapped, we want an empty column space to align with other log lines.
- }
- {(log.displayLevel || !wrapLogMessage) && (
- {log.displayLevel}
- )}
- {displayedFields.length > 0 ? (
-
- ) : (
-
- )}
- >
- );
-});
+const Log = memo(
+ ({ displayedFields, log, showTime, showUniqueLabels, styles, timestampResolution, wrapLogMessage }: LogProps) => {
+ const handleLabelsToggle = useCallback(
+ (expanded: boolean) => {
+ log.uniqueLabelsExpanded = expanded;
+ },
+ [log]
+ );
+ return (
+ <>
+ {showTime && (
+
+ {timestampResolution === 'ms' ? log.timestamp : log.timestampNs}
+
+ )}
+ {
+ // When logs are unwrapped, we want an empty column space to align with other log lines.
+ }
+ {(log.displayLevel || !wrapLogMessage) && (
+ {log.displayLevel}
+ )}
+ {showUniqueLabels && log.uniqueLabels && (
+
+
+
+ )}
+ {displayedFields.length > 0 ? (
+
+ ) : (
+
+ )}
+ >
+ );
+ }
+);
Log.displayName = 'Log';
const DisplayedFields = ({
@@ -402,9 +450,11 @@ const LogLineBody = ({ log, styles }: { log: LogListModel; styles: LogLineStyles
};
export function getGridTemplateColumns(dimensions: LogFieldDimension[], displayedFields: string[]) {
- const columns = dimensions.map((dimension) => dimension.width).join('px ');
+ const columns = dimensions
+ .map((dimension) => (dimension.width > 0 ? `${dimension.width}px` : 'max-content'))
+ .join(' ');
const logLineWidth = displayedFields.length > 0 ? '' : ' 1fr';
- return `${columns}px${logLineWidth}`;
+ return `${columns}${logLineWidth}`;
}
export type LogLineStyles = ReturnType;
diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx
index c3156bbf80c..d6fff7bcfc1 100644
--- a/public/app/features/logs/components/panel/LogLineDetails.test.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx
@@ -64,7 +64,6 @@ const setup = (
containerElement: document.createElement('div'),
focusLogLine: jest.fn(),
logs,
- onResize: jest.fn(),
timeRange: getDefaultTimeRange(),
timeZone: 'browser',
showControls: true,
@@ -613,7 +612,6 @@ describe('LogLineDetails', () => {
timeRange: getDefaultTimeRange(),
timeZone: 'browser',
showControls: true,
- onResize: jest.fn(),
};
const contextData: LogListContextData = {
diff --git a/public/app/features/logs/components/panel/LogLineDetails.tsx b/public/app/features/logs/components/panel/LogLineDetails.tsx
index 31251be7b46..2a7b63005f9 100644
--- a/public/app/features/logs/components/panel/LogLineDetails.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetails.tsx
@@ -19,14 +19,13 @@ export interface Props {
logs: LogListModel[];
timeRange: TimeRange;
timeZone: string;
- onResize(): void;
showControls: boolean;
}
export type LogLineDetailsMode = 'inline' | 'sidebar';
export const LogLineDetails = memo(
- ({ containerElement, focusLogLine, logs, timeRange, timeZone, onResize, showControls }: Props) => {
+ ({ containerElement, focusLogLine, logs, timeRange, timeZone, showControls }: Props) => {
const { detailsWidth, noInteractions, setDetailsWidth } = useLogListContext();
const styles = useStyles2(getStyles, 'sidebar', showControls);
const dragStyles = useStyles2(getDragStyles);
@@ -36,8 +35,7 @@ export const LogLineDetails = memo(
if (containerRef.current) {
setDetailsWidth(containerRef.current.clientWidth);
}
- onResize();
- }, [onResize, setDetailsWidth]);
+ }, [setDetailsWidth]);
const reportResize = useCallback(() => {
if (containerRef.current && !noInteractions) {
@@ -71,13 +69,17 @@ LogLineDetails.displayName = 'LogLineDetails';
const LogLineDetailsTabs = memo(
({ focusLogLine, logs, timeRange, timeZone }: Pick) => {
- const { app, closeDetails, noInteractions, showDetails, toggleDetails } = useLogListContext();
+ const { app, closeDetails, noInteractions, showDetails, toggleDetails, wrapLogMessage } = useLogListContext();
const [currentLog, setCurrentLog] = useState(showDetails[0]);
const previousShowDetails = usePrevious(showDetails);
const styles = useStyles2(getStyles, 'sidebar');
useEffect(() => {
- focusLogLine(currentLog);
+ // When wrapping is enabled and details is in sidebar mode, the logs panel width changes and the
+ // user may lose focus of the log line, so we scroll to it.
+ if (wrapLogMessage) {
+ focusLogLine(currentLog);
+ }
if (!noInteractions) {
reportInteraction('logs_log_line_details_displayed', {
mode: 'sidebar',
@@ -164,11 +166,8 @@ export const InlineLogLineDetails = memo(({ logs, log, onResize, timeRange, time
}, [app, noInteractions]);
useEffect(() => {
- function handleResize() {
- onResize();
- }
- window.addEventListener('resize', handleResize);
- return () => window.removeEventListener('resize', handleResize);
+ window.addEventListener('resize', onResize);
+ return () => window.removeEventListener('resize', onResize);
}, [onResize]);
const saveScroll = useCallback(() => {
diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx
index 0916541c4fc..d2e43f0512a 100644
--- a/public/app/features/logs/components/panel/LogList.tsx
+++ b/public/app/features/logs/components/panel/LogList.tsx
@@ -79,6 +79,7 @@ export interface Props {
setDisplayedFields?: (displayedFields: string[]) => void;
showControls: boolean;
showTime: boolean;
+ showUniqueLabels?: boolean;
sortOrder: LogsSortOrder;
timeRange: TimeRange;
timestampResolution?: LogLineTimestampResolution;
@@ -148,6 +149,7 @@ export const LogList = ({
setDisplayedFields,
showControls,
showTime,
+ showUniqueLabels,
sortOrder,
syntaxHighlighting = logOptionsStorageKey ? store.getBool(`${logOptionsStorageKey}.syntaxHighlighting`, true) : true,
timeRange,
@@ -192,6 +194,7 @@ export const LogList = ({
setDisplayedFields={setDisplayedFields}
showControls={showControls}
showTime={showTime}
+ showUniqueLabels={showUniqueLabels}
sortOrder={sortOrder}
syntaxHighlighting={syntaxHighlighting}
timestampResolution={timestampResolution}
@@ -247,6 +250,7 @@ const LogListComponent = ({
prettifyJSON,
showDetails,
showTime,
+ showUniqueLabels,
sortOrder,
timestampResolution,
toggleDetails,
@@ -264,8 +268,13 @@ const LogListComponent = ({
() =>
wrapLogMessage
? []
- : virtualization.calculateFieldDimensions(processedLogs, displayedFields, timestampResolution),
- [displayedFields, processedLogs, timestampResolution, virtualization, wrapLogMessage]
+ : virtualization.calculateFieldDimensions(
+ processedLogs,
+ displayedFields,
+ timestampResolution,
+ showUniqueLabels
+ ),
+ [displayedFields, processedLogs, showUniqueLabels, timestampResolution, virtualization, wrapLogMessage]
);
const styles = useStyles2(getStyles, dimensions, displayedFields, { showTime });
const widthContainer = wrapperRef.current ?? containerElement;
@@ -295,11 +304,13 @@ const LogListComponent = ({
[filterLogs, levelFilteredLogs, matchingUids]
);
+ // When log lines report size discrepancies, we debounce the calculation reset to give time to
+ // use the smallest log index to reset the heights.
const debouncedResetAfterIndex = useMemo(() => {
return debounce((index: number) => {
listRef.current?.resetAfterIndex(index);
overflowIndexRef.current = Infinity;
- }, 25);
+ }, 0);
}, []);
const debouncedScrollToItem = useMemo(() => {
@@ -340,17 +351,17 @@ const LogListComponent = ({
}, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]);
useLayoutEffect(() => {
- if (widthRef.current !== widthContainer.clientWidth) {
- widthRef.current = widthContainer.clientWidth;
- debouncedResetAfterIndex(0);
- }
- });
-
- useLayoutEffect(() => {
- const handleResize = debounce(() => {
+ const handleResize = (entry: ResizeObserverEntry) => {
setListHeight(getListHeight(containerElement, app, searchVisible));
- }, 50);
- const observer = new ResizeObserver(() => handleResize());
+ if (widthRef.current !== entry.contentRect.width) {
+ widthRef.current = entry.contentRect.width;
+ }
+ };
+ const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
+ if (entries.length) {
+ handleResize(entries[0]);
+ }
+ });
observer.observe(containerElement);
return () => observer.disconnect();
}, [app, containerElement, searchVisible]);
@@ -396,10 +407,6 @@ const LogListComponent = ({
[handleTextSelection, toggleDetails]
);
- const handleLogDetailsResize = useCallback(() => {
- debouncedResetAfterIndex(0);
- }, [debouncedResetAfterIndex]);
-
const focusLogLine = useCallback(
(log: LogListModel) => {
const index = filteredLogs.findIndex((filteredLog) => filteredLog.uid === log.uid);
@@ -425,7 +432,6 @@ const LogListComponent = ({
logs={filteredLogs}
timeRange={timeRange}
timeZone={timeZone}
- onResize={handleLogDetailsResize}
showControls={showControls}
/>
)}
diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts
index dbb060a744c..0c8559d5f47 100644
--- a/public/app/features/logs/components/panel/processing.ts
+++ b/public/app/features/logs/components/panel/processing.ts
@@ -53,6 +53,7 @@ export class LogListModel implements LogRowModel {
timeUtc: string;
uid: string;
uniqueLabels: Labels | undefined;
+ uniqueLabelsExpanded = false;
private _body: string | undefined = undefined;
private _currentSearch: string | undefined = undefined;
diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts
index 52835fbb8c4..fde63fc3422 100644
--- a/public/app/features/logs/components/panel/virtualization.ts
+++ b/public/app/features/logs/components/panel/virtualization.ts
@@ -182,7 +182,8 @@ export class LogLineVirtualization {
calculateFieldDimensions = (
logs: LogListModel[],
displayedFields: string[] = [],
- timestampResolution: LogLineTimestampResolution
+ timestampResolution: LogLineTimestampResolution,
+ showUniqueLabels?: boolean
) => {
if (!logs.length) {
return [];
@@ -214,6 +215,12 @@ export class LogLineVirtualization {
width: levelWidth,
},
];
+ if (showUniqueLabels) {
+ dimensions.push({
+ field: 'unique-labels',
+ width: 0,
+ });
+ }
for (const field in fieldWidths) {
dimensions.push({
field,
@@ -319,7 +326,7 @@ export interface LogFieldDimension {
width: number;
}
-export function hasUnderOrOverflow(
+export function getLogLineDOMHeight(
virtualization: LogLineVirtualization,
element: HTMLDivElement,
calculatedHeight?: number,
diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx
index 4fb86cbf427..80bedeef841 100644
--- a/public/app/plugins/panel/logs/LogsPanel.tsx
+++ b/public/app/plugins/panel/logs/LogsPanel.tsx
@@ -610,6 +610,7 @@ export const LogsPanel = ({
setDisplayedFields={setDisplayedFieldsFn}
showControls={Boolean(showControls)}
showTime={showTime}
+ showUniqueLabels={showLabels}
sortOrder={sortOrder}
logOptionsStorageKey={storageKey}
syntaxHighlighting={syntaxHighlighting}
diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx
index 40ab939b347..e73604b13ea 100644
--- a/public/app/plugins/panel/logs/module.tsx
+++ b/public/app/plugins/panel/logs/module.tsx
@@ -9,30 +9,29 @@ import { LogsPanelSuggestionsSupplier } from './suggestions';
export const plugin = new PanelPlugin(LogsPanel)
.setPanelOptions((builder, context) => {
const category = [t('logs.category-logs', 'Logs')];
- builder.addBooleanSwitch({
- path: 'showTime',
- name: t('logs.name-time', 'Show timestamps'),
- category,
- description: '',
- defaultValue: false,
- });
+ builder
+ .addBooleanSwitch({
+ path: 'showTime',
+ name: t('logs.name-time', 'Show timestamps'),
+ category,
+ description: '',
+ defaultValue: false,
+ })
+ .addBooleanSwitch({
+ path: 'showLabels',
+ name: t('logs.name-unique-labels', 'Unique labels'),
+ category,
+ description: '',
+ });
if (!config.featureToggles.newLogsPanel) {
- builder
- .addBooleanSwitch({
- path: 'showLabels',
- name: t('logs.name-unique-labels', 'Unique labels'),
- category,
- description: '',
- defaultValue: false,
- })
- .addBooleanSwitch({
- path: 'showCommonLabels',
- name: t('logs.name-common-labels', 'Common labels'),
- category,
- description: '',
- defaultValue: false,
- });
+ builder.addBooleanSwitch({
+ path: 'showCommonLabels',
+ name: t('logs.name-common-labels', 'Common labels'),
+ category,
+ description: '',
+ defaultValue: false,
+ });
} else if (context.options?.showTime) {
builder.addRadio({
path: 'timestampResolution',