New Logs Panel: Displayed fields support (#100643)
* LogList: add displayedFields and getFieldLinks props * Render displayed fields * LogLine: rename function * Refactor log dimensions * Generate styles in parent component * Log List: implement tabular unwrapped logs * Rename class * Log line: center fields * Parametrize field gap * Virtualization: update measurement to support displayed fields * Shorten visible level * Do not calculate dimensions when logs are wrapped * Logs Navigation: fix width when flag is enabled * Pass styles to LogLineMessage * Formatting * Fix unwrapped logs when showTime is off * LogLine: update css selectors for fields
This commit is contained in:
@@ -1070,8 +1070,10 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
<LogList
|
||||
app={CoreApp.Explore}
|
||||
containerElement={logsContainerRef.current}
|
||||
displayedFields={displayedFields}
|
||||
eventBus={eventBus}
|
||||
forceEscape={forceEscape}
|
||||
getFieldLinks={getFieldLinks}
|
||||
loadMore={loadMoreLogs}
|
||||
logs={dedupedRows}
|
||||
showTime={showTime}
|
||||
|
||||
@@ -235,7 +235,7 @@ const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean) => {
|
||||
return {
|
||||
navContainer: css({
|
||||
maxHeight: navContainerHeight,
|
||||
width: oldestLogsFirst ? '58px' : 'auto',
|
||||
width: oldestLogsFirst && !config.featureToggles.newLogsPanel ? '58px' : 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: config.featureToggles.logsInfiniteScrolling
|
||||
|
||||
@@ -4,12 +4,12 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window'
|
||||
|
||||
import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Spinner } from '@grafana/ui';
|
||||
import { Spinner, useTheme2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll';
|
||||
|
||||
import { LogLine } from './LogLine';
|
||||
import { getStyles, LogLine } from './LogLine';
|
||||
import { LogLineMessage } from './LogLineMessage';
|
||||
import { LogListModel } from './processing';
|
||||
|
||||
@@ -22,6 +22,7 @@ interface ChildrenProps {
|
||||
|
||||
interface Props {
|
||||
children: (props: ChildrenProps) => ReactNode;
|
||||
displayedFields: string[];
|
||||
handleOverflow: (index: number, id: string, height: number) => void;
|
||||
loadMore?: (range: AbsoluteTimeRange) => void;
|
||||
logs: LogListModel[];
|
||||
@@ -38,6 +39,7 @@ type InfiniteLoaderState = 'idle' | 'out-of-bounds' | 'pre-scroll' | 'loading';
|
||||
|
||||
export const InfiniteScroll = ({
|
||||
children,
|
||||
displayedFields,
|
||||
handleOverflow,
|
||||
loadMore,
|
||||
logs,
|
||||
@@ -57,6 +59,8 @@ export const InfiniteScroll = ({
|
||||
const lastEvent = useRef<Event | WheelEvent | null>(null);
|
||||
const countRef = useRef(0);
|
||||
const lastLogOfPage = useRef<string[]>([]);
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
useEffect(() => {
|
||||
// Logs have not changed, ignore effect
|
||||
@@ -132,24 +136,40 @@ export const InfiniteScroll = ({
|
||||
({ index, style }: ListChildComponentProps) => {
|
||||
if (!logs[index] && infiniteLoaderState !== 'idle') {
|
||||
return (
|
||||
<LogLineMessage style={style} onClick={infiniteLoaderState === 'pre-scroll' ? onLoadMore : undefined}>
|
||||
<LogLineMessage
|
||||
style={style}
|
||||
styles={styles}
|
||||
onClick={infiniteLoaderState === 'pre-scroll' ? onLoadMore : undefined}
|
||||
>
|
||||
{getMessageFromInfiniteLoaderState(infiniteLoaderState, sortOrder)}
|
||||
</LogLineMessage>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LogLine
|
||||
displayedFields={displayedFields}
|
||||
index={index}
|
||||
log={logs[index]}
|
||||
showTime={showTime}
|
||||
style={style}
|
||||
styles={styles}
|
||||
variant={getLogLineVariant(logs, index, lastLogOfPage.current)}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
onOverflow={handleOverflow}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[handleOverflow, infiniteLoaderState, logs, onLoadMore, showTime, sortOrder, wrapLogMessage]
|
||||
[
|
||||
displayedFields,
|
||||
handleOverflow,
|
||||
infiniteLoaderState,
|
||||
logs,
|
||||
onLoadMore,
|
||||
showTime,
|
||||
sortOrder,
|
||||
styles,
|
||||
wrapLogMessage,
|
||||
]
|
||||
);
|
||||
|
||||
const onItemsRendered = useCallback(
|
||||
|
||||
@@ -2,24 +2,35 @@ import { css } from '@emotion/css';
|
||||
import { CSSProperties, useEffect, useRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { LogListModel } from './processing';
|
||||
import { hasUnderOrOverflow } from './virtualization';
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
|
||||
import { LogFieldDimension, LogListModel } from './processing';
|
||||
import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow } from './virtualization';
|
||||
|
||||
interface Props {
|
||||
displayedFields: string[];
|
||||
index: number;
|
||||
log: LogListModel;
|
||||
showTime: boolean;
|
||||
style: CSSProperties;
|
||||
styles: LogLineStyles;
|
||||
onOverflow?: (index: number, id: string, height: number) => void;
|
||||
variant?: 'infinite-scroll';
|
||||
wrapLogMessage: boolean;
|
||||
}
|
||||
|
||||
export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrapLogMessage }: Props) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
export const LogLine = ({
|
||||
displayedFields,
|
||||
index,
|
||||
log,
|
||||
style,
|
||||
styles,
|
||||
onOverflow,
|
||||
showTime,
|
||||
variant,
|
||||
wrapLogMessage,
|
||||
}: Props) => {
|
||||
const logLineRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,16 +45,59 @@ export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrap
|
||||
}, [index, log.uid, onOverflow, style.height]);
|
||||
|
||||
return (
|
||||
<div style={style} className={`${styles.logLine} ${variant}`} ref={onOverflow ? logLineRef : undefined}>
|
||||
<div className={wrapLogMessage ? styles.wrappedLogLine : styles.unwrappedLogLine}>
|
||||
{showTime && <span className={`${styles.timestamp} level-${log.logLevel}`}>{log.timestamp}</span>}
|
||||
{log.logLevel && <span className={`${styles.level} level-${log.logLevel}`}>{log.logLevel}</span>}
|
||||
{log.body}
|
||||
<div style={style} className={`${styles.logLine} ${variant ?? ''}`} ref={onOverflow ? logLineRef : undefined}>
|
||||
<div className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`}`}>
|
||||
<Log displayedFields={displayedFields} log={log} showTime={showTime} styles={styles} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface LogProps {
|
||||
displayedFields: string[];
|
||||
log: LogListModel;
|
||||
showTime: boolean;
|
||||
styles: ReturnType<typeof getStyles>;
|
||||
}
|
||||
|
||||
const Log = ({ displayedFields, log, showTime, styles }: LogProps) => {
|
||||
return (
|
||||
<>
|
||||
{showTime && <span className={`${styles.timestamp} level-${log.logLevel} field`}>{log.timestamp}</span>}
|
||||
<span className={`${styles.level} level-${log.logLevel} field`}>{log.displayLevel}</span>
|
||||
{displayedFields.length > 0 ? (
|
||||
displayedFields.map((field) => (
|
||||
<span className="field" title={field}>
|
||||
{getDisplayedFieldValue(field, log)}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="field">{log.body}</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function getDisplayedFieldValue(fieldName: string, log: LogListModel): string {
|
||||
if (fieldName === LOG_LINE_BODY_FIELD_NAME) {
|
||||
return log.body;
|
||||
}
|
||||
if (log.labels[fieldName] != null) {
|
||||
return log.labels[fieldName];
|
||||
}
|
||||
const field = log.fields.find((field) => {
|
||||
return field.keys[0] === fieldName;
|
||||
});
|
||||
|
||||
return field ? field.values.toString() : '';
|
||||
}
|
||||
|
||||
export function getGridTemplateColumns(dimensions: LogFieldDimension[]) {
|
||||
const columns = dimensions.map((dimension) => dimension.width).join('px ');
|
||||
return `${columns}px 1fr`;
|
||||
}
|
||||
|
||||
export type LogLineStyles = ReturnType<typeof getStyles>;
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
const colors = {
|
||||
critical: '#B877D9',
|
||||
@@ -82,7 +136,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
timestamp: css({
|
||||
color: theme.colors.text.secondary,
|
||||
display: 'inline-block',
|
||||
marginRight: theme.spacing(1),
|
||||
'&.level-critical': {
|
||||
color: colors.critical,
|
||||
},
|
||||
@@ -103,7 +156,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
color: theme.colors.text.secondary,
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
display: 'inline-block',
|
||||
marginRight: theme.spacing(1),
|
||||
'&.level-critical': {
|
||||
color: colors.critical,
|
||||
},
|
||||
@@ -129,12 +181,20 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
outline: 'solid 1px red',
|
||||
}),
|
||||
unwrappedLogLine: css({
|
||||
display: 'grid',
|
||||
gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER),
|
||||
whiteSpace: 'pre',
|
||||
paddingBottom: theme.spacing(0.75),
|
||||
}),
|
||||
wrappedLogLine: css({
|
||||
whiteSpace: 'pre-wrap',
|
||||
paddingBottom: theme.spacing(0.75),
|
||||
'& .field': {
|
||||
marginRight: theme.spacing(FIELD_GAP_MULTIPLIER),
|
||||
},
|
||||
'& .field:last-child': {
|
||||
marginRight: 0,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { getStyles } from './LogLine';
|
||||
import { LogLineStyles } from './LogLine';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
style: CSSProperties;
|
||||
styles: LogLineStyles;
|
||||
}
|
||||
|
||||
export const LogLineMessage = ({ children, onClick, style }: Props) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
export const LogLineMessage = ({ children, onClick, style, styles }: Props) => {
|
||||
return (
|
||||
<div style={style} className={`${styles.logLine} ${styles.logLineMessage}`}>
|
||||
{onClick ? (
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
|
||||
import { AbsoluteTimeRange, CoreApp, EventBus, LogRowModel, LogsSortOrder, TimeRange } from '@grafana/data';
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
CoreApp,
|
||||
DataFrame,
|
||||
EventBus,
|
||||
Field,
|
||||
LinkModel,
|
||||
LogRowModel,
|
||||
LogsSortOrder,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { InfiniteScroll } from './InfiniteScroll';
|
||||
import { preProcessLogs, LogListModel } from './processing';
|
||||
import { getGridTemplateColumns } from './LogLine';
|
||||
import { preProcessLogs, LogListModel, calculateFieldDimensions, LogFieldDimension } from './processing';
|
||||
import {
|
||||
getLogLineSize,
|
||||
init as initVirtualization,
|
||||
@@ -15,14 +27,18 @@ import {
|
||||
storeLogLineSize,
|
||||
} from './virtualization';
|
||||
|
||||
export type GetFieldLinksFn = (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
||||
|
||||
interface Props {
|
||||
app: CoreApp;
|
||||
logs: LogRowModel[];
|
||||
containerElement: HTMLDivElement;
|
||||
displayedFields: string[];
|
||||
eventBus: EventBus;
|
||||
forceEscape?: boolean;
|
||||
getFieldLinks?: GetFieldLinksFn;
|
||||
initialScrollPosition?: 'top' | 'bottom';
|
||||
loadMore?: (range: AbsoluteTimeRange) => void;
|
||||
logs: LogRowModel[];
|
||||
showTime: boolean;
|
||||
sortOrder: LogsSortOrder;
|
||||
timeRange: TimeRange;
|
||||
@@ -33,8 +49,10 @@ interface Props {
|
||||
export const LogList = ({
|
||||
app,
|
||||
containerElement,
|
||||
displayedFields = [],
|
||||
eventBus,
|
||||
forceEscape = false,
|
||||
getFieldLinks,
|
||||
initialScrollPosition = 'top',
|
||||
loadMore,
|
||||
logs,
|
||||
@@ -52,6 +70,11 @@ export const LogList = ({
|
||||
const listRef = useRef<VariableSizeList | null>(null);
|
||||
const widthRef = useRef(containerElement.clientWidth);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const dimensions = useMemo(
|
||||
() => (wrapLogMessage ? [] : calculateFieldDimensions(processedLogs, displayedFields)),
|
||||
[displayedFields, processedLogs, wrapLogMessage]
|
||||
);
|
||||
const styles = getStyles(dimensions, { showTime });
|
||||
|
||||
useEffect(() => {
|
||||
initVirtualization(theme);
|
||||
@@ -65,9 +88,11 @@ export const LogList = ({
|
||||
}, [eventBus, logs.length]);
|
||||
|
||||
useEffect(() => {
|
||||
setProcessedLogs(preProcessLogs(logs, { wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone }));
|
||||
setProcessedLogs(
|
||||
preProcessLogs(logs, { getFieldLinks, wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone })
|
||||
);
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}, [forceEscape, logs, sortOrder, timeZone, wrapLogMessage]);
|
||||
}, [forceEscape, getFieldLinks, logs, sortOrder, timeZone, wrapLogMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = debounce(() => {
|
||||
@@ -110,6 +135,7 @@ export const LogList = ({
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
displayedFields={displayedFields}
|
||||
handleOverflow={handleOverflow}
|
||||
logs={processedLogs}
|
||||
loadMore={loadMore}
|
||||
@@ -123,9 +149,13 @@ export const LogList = ({
|
||||
>
|
||||
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
|
||||
<VariableSizeList
|
||||
className={styles.logList}
|
||||
height={listHeight}
|
||||
itemCount={itemCount}
|
||||
itemSize={getLogLineSize.bind(null, processedLogs, containerElement, { wrap: wrapLogMessage, showTime })}
|
||||
itemSize={getLogLineSize.bind(null, processedLogs, containerElement, displayedFields, {
|
||||
wrap: wrapLogMessage,
|
||||
showTime,
|
||||
})}
|
||||
itemKey={getItemKey}
|
||||
layout="vertical"
|
||||
onItemsRendered={onItemsRendered}
|
||||
@@ -141,6 +171,18 @@ export const LogList = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(dimensions: LogFieldDimension[], { showTime }: { showTime: boolean }) {
|
||||
const columns = showTime ? dimensions : dimensions.filter((_, index) => index > 0);
|
||||
return {
|
||||
logList: css({
|
||||
'& .unwrapped-log-line': {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: getGridTemplateColumns(columns),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function handleScrollToEvent(event: ScrollToLogsEvent, logsCount: number, list: VariableSizeList | null) {
|
||||
if (event.payload.scrollTo === 'top') {
|
||||
list?.scrollTo(0);
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { dateTimeFormat, LogRowModel, LogsSortOrder } from '@grafana/data';
|
||||
import { dateTimeFormat, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data';
|
||||
|
||||
import { escapeUnescapedString, sortLogRows } from '../../utils';
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { FieldDef, getAllFields } from '../logParser';
|
||||
|
||||
import { getDisplayedFieldValue } from './LogLine';
|
||||
import { GetFieldLinksFn } from './LogList';
|
||||
import { measureTextWidth } from './virtualization';
|
||||
|
||||
export interface LogListModel extends LogRowModel {
|
||||
body: string;
|
||||
displayLevel: string;
|
||||
fields: FieldDef[];
|
||||
timestamp: string;
|
||||
dimensions: LogDimensions;
|
||||
}
|
||||
|
||||
export interface LogDimensions {
|
||||
timestampWidth: number;
|
||||
levelWidth: number;
|
||||
export interface LogFieldDimension {
|
||||
field: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface PreProcessOptions {
|
||||
escape: boolean;
|
||||
getFieldLinks?: GetFieldLinksFn;
|
||||
order: LogsSortOrder;
|
||||
timeZone: string;
|
||||
wrap: boolean;
|
||||
@@ -24,19 +30,23 @@ interface PreProcessOptions {
|
||||
|
||||
export const preProcessLogs = (
|
||||
logs: LogRowModel[],
|
||||
{ escape, order, timeZone, wrap }: PreProcessOptions
|
||||
{ escape, getFieldLinks, order, timeZone, wrap }: PreProcessOptions
|
||||
): LogListModel[] => {
|
||||
const orderedLogs = sortLogRows(logs, order);
|
||||
return orderedLogs.map((log) => preProcessLog(log, { wrap, escape, timeZone, expanded: false }));
|
||||
return orderedLogs.map((log) => preProcessLog(log, { escape, expanded: false, getFieldLinks, timeZone, wrap }));
|
||||
};
|
||||
|
||||
interface PreProcessLogOptions {
|
||||
escape: boolean;
|
||||
expanded: boolean; // Not yet implemented
|
||||
getFieldLinks?: GetFieldLinksFn;
|
||||
timeZone: string;
|
||||
wrap: boolean;
|
||||
}
|
||||
const preProcessLog = (log: LogRowModel, { escape, expanded, timeZone, wrap }: PreProcessLogOptions): LogListModel => {
|
||||
const preProcessLog = (
|
||||
log: LogRowModel,
|
||||
{ escape, expanded, getFieldLinks, timeZone, wrap }: PreProcessLogOptions
|
||||
): LogListModel => {
|
||||
let body = log.entry;
|
||||
const timestamp = dateTimeFormat(log.timeEpochMs, {
|
||||
timeZone,
|
||||
@@ -54,10 +64,65 @@ const preProcessLog = (log: LogRowModel, { escape, expanded, timeZone, wrap }: P
|
||||
return {
|
||||
...log,
|
||||
body,
|
||||
displayLevel: logLevelToDisplayLevel(log.logLevel),
|
||||
fields: getAllFields(log, getFieldLinks),
|
||||
timestamp,
|
||||
dimensions: {
|
||||
timestampWidth: measureTextWidth(timestamp),
|
||||
levelWidth: measureTextWidth(log.logLevel),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function logLevelToDisplayLevel(level = '') {
|
||||
switch (level) {
|
||||
case LogLevel.critical:
|
||||
return 'crit';
|
||||
case LogLevel.warning:
|
||||
return 'warn';
|
||||
case LogLevel.unknown:
|
||||
return '';
|
||||
default:
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
export const calculateFieldDimensions = (logs: LogListModel[], displayedFields: string[] = []) => {
|
||||
if (!logs.length) {
|
||||
return [];
|
||||
}
|
||||
let timestampWidth = 0;
|
||||
let levelWidth = 0;
|
||||
const fieldWidths: Record<string, number> = {};
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
let width = measureTextWidth(logs[i].timestamp);
|
||||
if (width > timestampWidth) {
|
||||
timestampWidth = Math.round(width);
|
||||
}
|
||||
width = measureTextWidth(logs[i].displayLevel);
|
||||
if (width > levelWidth) {
|
||||
levelWidth = Math.round(width);
|
||||
}
|
||||
for (const field of displayedFields) {
|
||||
width = measureTextWidth(getDisplayedFieldValue(field, logs[i]));
|
||||
fieldWidths[field] = !fieldWidths[field] || width > fieldWidths[field] ? Math.round(width) : fieldWidths[field];
|
||||
}
|
||||
}
|
||||
const dimensions: LogFieldDimension[] = [
|
||||
{
|
||||
field: 'timestamp',
|
||||
width: timestampWidth,
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
width: levelWidth,
|
||||
},
|
||||
];
|
||||
for (const field in fieldWidths) {
|
||||
// Skip the log line when it's a displayed field
|
||||
if (field === LOG_LINE_BODY_FIELD_NAME) {
|
||||
continue;
|
||||
}
|
||||
dimensions.push({
|
||||
field,
|
||||
width: fieldWidths[field],
|
||||
});
|
||||
}
|
||||
return dimensions;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { getDisplayedFieldValue } from './LogLine';
|
||||
import { LogListModel } from './processing';
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
@@ -8,6 +9,9 @@ let paddingBottom = gridSize * 0.75;
|
||||
let lineHeight = 22;
|
||||
let measurementMode: 'canvas' | 'dom' = 'canvas';
|
||||
|
||||
// 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 function init(theme: GrafanaTheme2) {
|
||||
const font = `${theme.typography.fontSize}px ${theme.typography.fontFamilyMonospace}`;
|
||||
const letterSpacing = theme.typography.body.letterSpacing;
|
||||
@@ -146,6 +150,7 @@ interface DisplayOptions {
|
||||
export function getLogLineSize(
|
||||
logs: LogListModel[],
|
||||
container: HTMLDivElement | null,
|
||||
displayedFields: string[],
|
||||
{ wrap, showTime }: DisplayOptions,
|
||||
index: number
|
||||
) {
|
||||
@@ -160,15 +165,26 @@ export function getLogLineSize(
|
||||
if (storedSize) {
|
||||
return storedSize;
|
||||
}
|
||||
const gap = gridSize;
|
||||
|
||||
let textToMeasure = '';
|
||||
const gap = gridSize * FIELD_GAP_MULTIPLIER;
|
||||
let optionsWidth = 0;
|
||||
if (showTime) {
|
||||
optionsWidth += logs[index].dimensions.timestampWidth + gap;
|
||||
optionsWidth += gap;
|
||||
textToMeasure += logs[index].timestamp;
|
||||
}
|
||||
if (logs[index].logLevel) {
|
||||
optionsWidth += logs[index].dimensions.levelWidth + gap;
|
||||
optionsWidth += gap;
|
||||
textToMeasure += logs[index].logLevel;
|
||||
}
|
||||
const { height } = measureTextHeight(logs[index].body, getLogContainerWidth(container), optionsWidth);
|
||||
for (const field of displayedFields) {
|
||||
textToMeasure = getDisplayedFieldValue(field, logs[index]) + textToMeasure;
|
||||
}
|
||||
if (!displayedFields.length) {
|
||||
textToMeasure += logs[index].body;
|
||||
}
|
||||
|
||||
const { height } = measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth);
|
||||
return height;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ export const LogsPanel = ({
|
||||
<LogList
|
||||
app={CoreApp.Dashboard}
|
||||
containerElement={logsContainer}
|
||||
displayedFields={[]}
|
||||
eventBus={eventBus}
|
||||
initialScrollPosition={initialScrollPosition}
|
||||
logs={logs}
|
||||
|
||||
Reference in New Issue
Block a user