New Logs Panel: Handle long lines (#103171)
* LogLine: introduce truncation limit * Processing: turn LogListModel into a class * LogLine: introduce collapsed/expanded state and read from model * Virtualization: correct extra line for controls in size * LogLine: fix collapsed state initialization * Add new criteria to define long lines to truncate * Virtualization: better truncation limit * Check collapsed state based on container size * Update function name * Extract translations * LogLine: update unit test * virtualization: add unit test * processing: update unit test * Fix focused test * processing: process fields on demand * Logs: consider loading state from explore * LogList: improve resize and recalculation * chore: build?
This commit is contained in:
@@ -1151,6 +1151,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||||||
filterLevels={filterLevels}
|
filterLevels={filterLevels}
|
||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
getRowContextQuery={getRowContextQuery}
|
getRowContextQuery={getRowContextQuery}
|
||||||
|
loading={loading}
|
||||||
loadMore={loadMoreLogs}
|
loadMore={loadMoreLogs}
|
||||||
logOptionsStorageKey={SETTING_KEY_ROOT}
|
logOptionsStorageKey={SETTING_KEY_ROOT}
|
||||||
logs={dedupedRows}
|
logs={dedupedRows}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface ChildrenProps {
|
|||||||
interface Props {
|
interface Props {
|
||||||
children: (props: ChildrenProps) => ReactNode;
|
children: (props: ChildrenProps) => ReactNode;
|
||||||
displayedFields: string[];
|
displayedFields: string[];
|
||||||
handleOverflow: (index: number, id: string, height: number) => void;
|
handleOverflow: (index: number, id: string, height?: number) => void;
|
||||||
loadMore?: (range: AbsoluteTimeRange) => void;
|
loadMore?: (range: AbsoluteTimeRange) => void;
|
||||||
logs: LogListModel[];
|
logs: LogListModel[];
|
||||||
scrollElement: HTMLDivElement | null;
|
scrollElement: HTMLDivElement | null;
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { createLogLine } from '../__mocks__/logRow';
|
|||||||
import { getStyles, LogLine } from './LogLine';
|
import { getStyles, LogLine } from './LogLine';
|
||||||
import { LogListContextProvider } from './LogListContext';
|
import { LogListContextProvider } from './LogListContext';
|
||||||
import { LogListModel } from './processing';
|
import { LogListModel } from './processing';
|
||||||
|
import { getTruncationLength } from './virtualization';
|
||||||
|
|
||||||
jest.mock('./LogListContext');
|
jest.mock('./LogListContext');
|
||||||
|
jest.mock('./virtualization');
|
||||||
|
|
||||||
const theme = createTheme();
|
const theme = createTheme();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
@@ -97,6 +99,25 @@ describe('LogLine', () => {
|
|||||||
expect(screen.getByText('luna')).toBeInTheDocument();
|
expect(screen.getByText('luna')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Reports mouse over events', async () => {
|
||||||
|
const onLogLineHover = jest.fn();
|
||||||
|
render(
|
||||||
|
<LogListContextProvider {...contextProps} onLogLineHover={onLogLineHover}>
|
||||||
|
<LogLine
|
||||||
|
displayedFields={[]}
|
||||||
|
index={0}
|
||||||
|
log={log}
|
||||||
|
showTime={true}
|
||||||
|
style={{}}
|
||||||
|
styles={styles}
|
||||||
|
wrapLogMessage={false}
|
||||||
|
/>
|
||||||
|
</LogListContextProvider>
|
||||||
|
);
|
||||||
|
await userEvent.hover(screen.getByText('log message 1'));
|
||||||
|
expect(onLogLineHover).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
describe('Log line menu', () => {
|
describe('Log line menu', () => {
|
||||||
test('Renders a log line menu', async () => {
|
test('Renders a log line menu', async () => {
|
||||||
render(
|
render(
|
||||||
@@ -180,4 +201,85 @@ describe('LogLine', () => {
|
|||||||
expect(screen.queryByText(log.entry)).not.toBeInTheDocument();
|
expect(screen.queryByText(log.entry)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Collapsible log lines', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
log = createLogLine({ labels: { place: 'luna' }, entry: `log message 1` });
|
||||||
|
jest.mocked(getTruncationLength).mockReturnValue(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Logs are not collapsed by default', () => {
|
||||||
|
render(
|
||||||
|
<LogLine
|
||||||
|
displayedFields={[]}
|
||||||
|
index={0}
|
||||||
|
log={log}
|
||||||
|
showTime={true}
|
||||||
|
style={{}}
|
||||||
|
styles={styles}
|
||||||
|
wrapLogMessage={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('show less')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('show more')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Logs are not collapsible when unwrapped', () => {
|
||||||
|
log.collapsed = true;
|
||||||
|
render(
|
||||||
|
<LogLine
|
||||||
|
displayedFields={[]}
|
||||||
|
index={0}
|
||||||
|
log={log}
|
||||||
|
showTime={true}
|
||||||
|
style={{}}
|
||||||
|
styles={styles}
|
||||||
|
// Unwrapped logs
|
||||||
|
wrapLogMessage={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('show less')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('show more')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Long logs can be collapsed and expanded', async () => {
|
||||||
|
log.collapsed = true;
|
||||||
|
render(
|
||||||
|
<LogLine
|
||||||
|
displayedFields={[]}
|
||||||
|
index={0}
|
||||||
|
log={log}
|
||||||
|
showTime={true}
|
||||||
|
style={{}}
|
||||||
|
styles={styles}
|
||||||
|
wrapLogMessage={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('show more')).toBeVisible();
|
||||||
|
await userEvent.click(screen.getByText('show more'));
|
||||||
|
expect(await screen.findByText('show less')).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText('show less'));
|
||||||
|
expect(await screen.findByText('show more')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('When the collapsed state changes invokes a callback to update virtualized sizes', async () => {
|
||||||
|
log.collapsed = true;
|
||||||
|
const onOverflow = jest.fn();
|
||||||
|
render(
|
||||||
|
<LogLine
|
||||||
|
displayedFields={[]}
|
||||||
|
index={0}
|
||||||
|
log={log}
|
||||||
|
onOverflow={onOverflow}
|
||||||
|
showTime={true}
|
||||||
|
style={{}}
|
||||||
|
styles={styles}
|
||||||
|
wrapLogMessage={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(await screen.findByText('show more'));
|
||||||
|
await userEvent.click(await screen.findByText('show less'));
|
||||||
|
expect(onOverflow).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { CSSProperties, useCallback, useEffect, useRef } from 'react';
|
import { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Button } from '@grafana/ui';
|
||||||
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||||
import { LogMessageAnsi } from '../LogMessageAnsi';
|
import { LogMessageAnsi } from '../LogMessageAnsi';
|
||||||
@@ -10,7 +12,13 @@ import { LogMessageAnsi } from '../LogMessageAnsi';
|
|||||||
import { LogLineMenu } from './LogLineMenu';
|
import { LogLineMenu } from './LogLineMenu';
|
||||||
import { useLogIsPinned, useLogListContext } from './LogListContext';
|
import { useLogIsPinned, useLogListContext } from './LogListContext';
|
||||||
import { LogListModel } from './processing';
|
import { LogListModel } from './processing';
|
||||||
import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow, getLineHeight, LogFieldDimension } from './virtualization';
|
import {
|
||||||
|
FIELD_GAP_MULTIPLIER,
|
||||||
|
hasUnderOrOverflow,
|
||||||
|
getLineHeight,
|
||||||
|
LogFieldDimension,
|
||||||
|
TRUNCATION_LINE_COUNT,
|
||||||
|
} from './virtualization';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
displayedFields: string[];
|
displayedFields: string[];
|
||||||
@@ -19,7 +27,7 @@ interface Props {
|
|||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
styles: LogLineStyles;
|
styles: LogLineStyles;
|
||||||
onOverflow?: (index: number, id: string, height: number) => void;
|
onOverflow?: (index: number, id: string, height?: number) => void;
|
||||||
variant?: 'infinite-scroll';
|
variant?: 'infinite-scroll';
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
}
|
}
|
||||||
@@ -36,41 +44,78 @@ export const LogLine = ({
|
|||||||
wrapLogMessage,
|
wrapLogMessage,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { onLogLineHover } = useLogListContext();
|
const { onLogLineHover } = useLogListContext();
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean | undefined>(
|
||||||
|
wrapLogMessage && log.collapsed !== undefined ? log.collapsed : undefined
|
||||||
|
);
|
||||||
const logLineRef = useRef<HTMLDivElement | null>(null);
|
const logLineRef = useRef<HTMLDivElement | null>(null);
|
||||||
const pinned = useLogIsPinned(log);
|
const pinned = useLogIsPinned(log);
|
||||||
|
|
||||||
const handleMouseOver = useCallback(() => {
|
|
||||||
onLogLineHover?.(log);
|
|
||||||
}, [log, onLogLineHover]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onOverflow || !logLineRef.current) {
|
if (!onOverflow || !logLineRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const calculatedHeight = typeof style.height === 'number' ? style.height : undefined;
|
const calculatedHeight = typeof style.height === 'number' ? style.height : undefined;
|
||||||
const actualHeight = hasUnderOrOverflow(logLineRef.current, calculatedHeight);
|
const actualHeight = hasUnderOrOverflow(logLineRef.current, calculatedHeight, log.collapsed);
|
||||||
if (actualHeight) {
|
if (actualHeight) {
|
||||||
onOverflow(index, log.uid, actualHeight);
|
onOverflow(index, log.uid, actualHeight);
|
||||||
}
|
}
|
||||||
}, [index, log.uid, onOverflow, style.height]);
|
}, [index, log.collapsed, log.uid, onOverflow, style.height]);
|
||||||
|
|
||||||
|
const handleMouseOver = useCallback(() => onLogLineHover?.(log), [log, onLogLineHover]);
|
||||||
|
|
||||||
|
const handleExpandCollapse = useCallback(() => {
|
||||||
|
const newState = !collapsed;
|
||||||
|
setCollapsed(newState);
|
||||||
|
log.setCollapsedState(newState);
|
||||||
|
onOverflow?.(index, log.uid);
|
||||||
|
}, [collapsed, index, log, onOverflow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={style}>
|
||||||
style={style}
|
<div
|
||||||
className={`${styles.logLine} ${variant ?? ''} ${pinned ? styles.pinnedLogLine : ''}`}
|
className={`${styles.logLine} ${variant ?? ''} ${pinned ? styles.pinnedLogLine : ''}`}
|
||||||
ref={onOverflow ? logLineRef : undefined}
|
ref={onOverflow ? logLineRef : undefined}
|
||||||
onMouseOver={handleMouseOver}
|
onMouseEnter={handleMouseOver}
|
||||||
>
|
>
|
||||||
<LogLineMenu styles={styles} log={log} />
|
<LogLineMenu styles={styles} log={log} />
|
||||||
<div className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`}`}>
|
<div
|
||||||
<Log
|
className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`} ${collapsed === true ? styles.collapsedLogLine : ''}`}
|
||||||
displayedFields={displayedFields}
|
>
|
||||||
log={log}
|
<Log
|
||||||
showTime={showTime}
|
displayedFields={displayedFields}
|
||||||
styles={styles}
|
log={log}
|
||||||
wrapLogMessage={wrapLogMessage}
|
showTime={showTime}
|
||||||
/>
|
styles={styles}
|
||||||
|
wrapLogMessage={wrapLogMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{collapsed === true && (
|
||||||
|
<div className={styles.expandCollapseControl}>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
fill="text"
|
||||||
|
size="sm"
|
||||||
|
className={styles.expandCollapseControlButton}
|
||||||
|
onClick={handleExpandCollapse}
|
||||||
|
>
|
||||||
|
{t('logs.log-line.show-more', 'show more')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{collapsed === false && (
|
||||||
|
<div className={styles.expandCollapseControl}>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
fill="text"
|
||||||
|
size="sm"
|
||||||
|
className={styles.expandCollapseControlButton}
|
||||||
|
onClick={handleExpandCollapse}
|
||||||
|
>
|
||||||
|
{t('logs.log-line.show-less', 'show less')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -99,7 +144,7 @@ const Log = ({ displayedFields, log, showTime, styles, wrapLogMessage }: LogProp
|
|||||||
<LogLineBody log={log} key={field} />
|
<LogLineBody log={log} key={field} />
|
||||||
) : (
|
) : (
|
||||||
<span className="field" title={field} key={field}>
|
<span className="field" title={field} key={field}>
|
||||||
{getDisplayedFieldValue(field, log)}
|
{log.getDisplayedFieldValue(field)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -131,20 +176,6 @@ const LogLineBody = ({ log }: { log: LogListModel }) => {
|
|||||||
return <span className="field log-syntax-highlight" dangerouslySetInnerHTML={{ __html: log.highlightedBody }} />;
|
return <span className="field log-syntax-highlight" dangerouslySetInnerHTML={{ __html: log.highlightedBody }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
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[]) {
|
export function getGridTemplateColumns(dimensions: LogFieldDimension[]) {
|
||||||
const columns = dimensions.map((dimension) => dimension.width).join('px ');
|
const columns = dimensions.map((dimension) => dimension.width).join('px ');
|
||||||
return `${columns}px 1fr`;
|
return `${columns}px 1fr`;
|
||||||
@@ -172,6 +203,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
fontFamily: theme.typography.fontFamilyMonospace,
|
fontFamily: theme.typography.fontFamilyMonospace,
|
||||||
fontSize: theme.typography.fontSize,
|
fontSize: theme.typography.fontSize,
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
|
cursor: 'pointer',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: `hsla(0, 0%, 0%, 0.2)`,
|
background: `hsla(0, 0%, 0%, 0.2)`,
|
||||||
},
|
},
|
||||||
@@ -283,5 +315,18 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
marginRight: 0,
|
marginRight: 0,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
collapsedLogLine: css({
|
||||||
|
maxHeight: `${TRUNCATION_LINE_COUNT * getLineHeight()}px`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
|
expandCollapseControl: css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}),
|
||||||
|
expandCollapseControlButton: css({
|
||||||
|
fontWeight: theme.typography.fontWeightLight,
|
||||||
|
height: getLineHeight(),
|
||||||
|
margin: 0,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ interface Props {
|
|||||||
getRowContextQuery?: GetRowContextQueryFn;
|
getRowContextQuery?: GetRowContextQueryFn;
|
||||||
grammar?: Grammar;
|
grammar?: Grammar;
|
||||||
initialScrollPosition?: 'top' | 'bottom';
|
initialScrollPosition?: 'top' | 'bottom';
|
||||||
|
loading?: boolean;
|
||||||
loadMore?: (range: AbsoluteTimeRange) => void;
|
loadMore?: (range: AbsoluteTimeRange) => void;
|
||||||
logOptionsStorageKey?: string;
|
logOptionsStorageKey?: string;
|
||||||
logs: LogRowModel[];
|
logs: LogRowModel[];
|
||||||
@@ -88,6 +89,7 @@ export const LogList = ({
|
|||||||
getRowContextQuery,
|
getRowContextQuery,
|
||||||
grammar,
|
grammar,
|
||||||
initialScrollPosition = 'top',
|
initialScrollPosition = 'top',
|
||||||
|
loading,
|
||||||
loadMore,
|
loadMore,
|
||||||
logOptionsStorageKey,
|
logOptionsStorageKey,
|
||||||
logs,
|
logs,
|
||||||
@@ -140,6 +142,7 @@ export const LogList = ({
|
|||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
grammar={grammar}
|
grammar={grammar}
|
||||||
initialScrollPosition={initialScrollPosition}
|
initialScrollPosition={initialScrollPosition}
|
||||||
|
loading={loading}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
logs={logs}
|
logs={logs}
|
||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
@@ -156,6 +159,7 @@ const LogListComponent = ({
|
|||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
grammar,
|
grammar,
|
||||||
initialScrollPosition = 'top',
|
initialScrollPosition = 'top',
|
||||||
|
loading,
|
||||||
loadMore,
|
loadMore,
|
||||||
logs,
|
logs,
|
||||||
showControls,
|
showControls,
|
||||||
@@ -189,15 +193,19 @@ const LogListComponent = ({
|
|||||||
}, [eventBus, logs.length]);
|
}, [eventBus, logs.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setProcessedLogs(
|
setProcessedLogs(
|
||||||
preProcessLogs(logs, { getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone }, grammar)
|
preProcessLogs(logs, { getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone }, grammar)
|
||||||
);
|
);
|
||||||
}, [forceEscape, getFieldLinks, grammar, logs, sortOrder, timeZone]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resetLogLineSizes();
|
resetLogLineSizes();
|
||||||
listRef.current?.resetAfterIndex(0);
|
listRef.current?.resetAfterIndex(0);
|
||||||
}, [wrapLogMessage, processedLogs]);
|
}, [forceEscape, getFieldLinks, grammar, loading, logs, sortOrder, timeZone]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listRef.current?.resetAfterIndex(0);
|
||||||
|
}, [wrapLogMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = debounce(() => {
|
const handleResize = debounce(() => {
|
||||||
@@ -214,17 +222,16 @@ const LogListComponent = ({
|
|||||||
if (widthRef.current === containerElement.clientWidth) {
|
if (widthRef.current === containerElement.clientWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetLogLineSizes();
|
|
||||||
listRef.current?.resetAfterIndex(0);
|
|
||||||
widthRef.current = containerElement.clientWidth;
|
widthRef.current = containerElement.clientWidth;
|
||||||
|
listRef.current?.resetAfterIndex(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOverflow = useCallback(
|
const handleOverflow = useCallback(
|
||||||
(index: number, id: string, height: number) => {
|
(index: number, id: string, height?: number) => {
|
||||||
if (containerElement) {
|
if (containerElement && height !== undefined) {
|
||||||
storeLogLineSize(id, containerElement, height);
|
storeLogLineSize(id, containerElement, height);
|
||||||
listRef.current?.resetAfterIndex(index);
|
|
||||||
}
|
}
|
||||||
|
listRef.current?.resetAfterIndex(index);
|
||||||
},
|
},
|
||||||
[containerElement]
|
[containerElement]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export const LogListContextProvider = ({
|
|||||||
filterLevels = [],
|
filterLevels = [],
|
||||||
getRowContextQuery = jest.fn(),
|
getRowContextQuery = jest.fn(),
|
||||||
logSupportsContext = jest.fn(),
|
logSupportsContext = jest.fn(),
|
||||||
|
onLogLineHover,
|
||||||
onPermalinkClick = jest.fn(),
|
onPermalinkClick = jest.fn(),
|
||||||
onPinLine = jest.fn(),
|
onPinLine = jest.fn(),
|
||||||
onOpenContext = jest.fn(),
|
onOpenContext = jest.fn(),
|
||||||
@@ -102,6 +103,7 @@ export const LogListContextProvider = ({
|
|||||||
filterLevels,
|
filterLevels,
|
||||||
getRowContextQuery,
|
getRowContextQuery,
|
||||||
logSupportsContext,
|
logSupportsContext,
|
||||||
|
onLogLineHover,
|
||||||
onPermalinkClick,
|
onPermalinkClick,
|
||||||
onPinLine,
|
onPinLine,
|
||||||
onOpenContext,
|
onOpenContext,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Field, FieldType, LogLevel, LogRowModel, LogsSortOrder, toDataFrame } from '@grafana/data';
|
import { createTheme, Field, FieldType, LogLevel, LogRowModel, LogsSortOrder, toDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { createLogRow } from '../__mocks__/logRow';
|
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||||
|
import { createLogLine, createLogRow } from '../__mocks__/logRow';
|
||||||
|
|
||||||
import { LogListModel, preProcessLogs } from './processing';
|
import { LogListModel, preProcessLogs } from './processing';
|
||||||
|
import { getTruncationLength, init } from './virtualization';
|
||||||
|
|
||||||
describe('preProcessLogs', () => {
|
describe('preProcessLogs', () => {
|
||||||
let logFmtLog: LogRowModel, nginxLog: LogRowModel, jsonLog: LogRowModel;
|
let logFmtLog: LogRowModel, nginxLog: LogRowModel, jsonLog: LogRowModel;
|
||||||
@@ -138,4 +140,58 @@ describe('preProcessLogs', () => {
|
|||||||
expect(processedLogs[2].highlightedBody).toContain('log-token-string');
|
expect(processedLogs[2].highlightedBody).toContain('log-token-string');
|
||||||
expect(processedLogs[2].highlightedBody).not.toContain('log-token-method');
|
expect(processedLogs[2].highlightedBody).not.toContain('log-token-method');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Returns displayed field values', () => {
|
||||||
|
expect(processedLogs[0].getDisplayedFieldValue('logger')).toBe('interceptor');
|
||||||
|
expect(processedLogs[1].getDisplayedFieldValue('method')).toBe('POST');
|
||||||
|
expect(processedLogs[2].getDisplayedFieldValue('kind')).toBe('Event');
|
||||||
|
expect(processedLogs[0].getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME)).toBe(processedLogs[0].body);
|
||||||
|
expect(processedLogs[1].getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME)).toBe(processedLogs[1].body);
|
||||||
|
expect(processedLogs[2].getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME)).toBe(processedLogs[2].body);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Collapsible log lines', () => {
|
||||||
|
let longLog: LogListModel, entry: string, container: HTMLDivElement;
|
||||||
|
beforeEach(() => {
|
||||||
|
init(createTheme());
|
||||||
|
container = document.createElement('div');
|
||||||
|
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(200);
|
||||||
|
entry = new Array(2 * getTruncationLength(null)).fill('e').join('');
|
||||||
|
longLog = createLogLine({ entry });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Long lines that are not truncated are not modified', () => {
|
||||||
|
expect(longLog.body).toBe(entry);
|
||||||
|
expect(longLog.highlightedBody).toBe(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sets the collapsed state based on the container size', () => {
|
||||||
|
// Make container half of the size
|
||||||
|
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(100);
|
||||||
|
|
||||||
|
expect(longLog.collapsed).toBeUndefined();
|
||||||
|
|
||||||
|
longLog.updateCollapsedState([], container);
|
||||||
|
|
||||||
|
expect(longLog.collapsed).toBe(true);
|
||||||
|
expect(longLog.body).not.toBe(entry);
|
||||||
|
expect(entry).toContain(longLog.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Updates the body based on the collapsed state', () => {
|
||||||
|
expect(longLog.collapsed).toBeUndefined();
|
||||||
|
expect(longLog.body).toBe(entry);
|
||||||
|
|
||||||
|
longLog.setCollapsedState(true);
|
||||||
|
|
||||||
|
expect(longLog.collapsed).toBe(true);
|
||||||
|
expect(longLog.body).not.toBe(entry);
|
||||||
|
expect(entry).toContain(longLog.body);
|
||||||
|
|
||||||
|
longLog.setCollapsedState(false);
|
||||||
|
|
||||||
|
expect(longLog.collapsed).toBe(false);
|
||||||
|
expect(longLog.body).toBe(entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,139 @@
|
|||||||
import Prism, { Grammar } from 'prismjs';
|
import Prism, { Grammar } from 'prismjs';
|
||||||
|
|
||||||
import { dateTimeFormat, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data';
|
import { DataFrame, dateTimeFormat, Labels, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data';
|
||||||
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
||||||
|
|
||||||
import { escapeUnescapedString, sortLogRows } from '../../utils';
|
import { escapeUnescapedString, sortLogRows } from '../../utils';
|
||||||
|
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||||
import { FieldDef, getAllFields } from '../logParser';
|
import { FieldDef, getAllFields } from '../logParser';
|
||||||
|
|
||||||
import { generateLogGrammar } from './grammar';
|
import { generateLogGrammar } from './grammar';
|
||||||
|
import { getTruncationLength } from './virtualization';
|
||||||
|
|
||||||
export interface LogListModel extends LogRowModel {
|
export class LogListModel implements LogRowModel {
|
||||||
body: string;
|
collapsed: boolean | undefined = undefined;
|
||||||
_highlightedBody: string;
|
datasourceType: string | undefined;
|
||||||
highlightedBody: string;
|
dataFrame: DataFrame;
|
||||||
displayLevel: string;
|
displayLevel: string;
|
||||||
fields: FieldDef[];
|
duplicates: number | undefined;
|
||||||
|
entry: string;
|
||||||
|
entryFieldIndex: number;
|
||||||
|
hasAnsi: boolean;
|
||||||
|
hasUnescapedContent: boolean;
|
||||||
|
labels: Labels;
|
||||||
|
logLevel: LogLevel;
|
||||||
|
raw: string;
|
||||||
|
rowIndex: number;
|
||||||
|
rowId?: string | undefined;
|
||||||
|
searchWords: string[] | undefined;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
timeFromNow: string;
|
||||||
|
timeEpochMs: number;
|
||||||
|
timeEpochNs: string;
|
||||||
|
timeLocal: string;
|
||||||
|
timeUtc: string;
|
||||||
|
uid: string;
|
||||||
|
uniqueLabels: Labels | undefined;
|
||||||
|
|
||||||
|
private _body: string | undefined = undefined;
|
||||||
|
private _grammar?: Grammar;
|
||||||
|
private _highlightedBody: string | undefined = undefined;
|
||||||
|
private _fields: FieldDef[] | undefined = undefined;
|
||||||
|
private _getFieldLinks: GetFieldLinksFn | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(log: LogRowModel, { escape, getFieldLinks, grammar, timeZone }: PreProcessLogOptions) {
|
||||||
|
// LogRowModel
|
||||||
|
this.datasourceType = log.datasourceType;
|
||||||
|
this.dataFrame = log.dataFrame;
|
||||||
|
this.duplicates = log.duplicates;
|
||||||
|
this.entry = log.entry;
|
||||||
|
this.entryFieldIndex = log.entryFieldIndex;
|
||||||
|
this.hasAnsi = log.hasAnsi;
|
||||||
|
this.hasUnescapedContent = log.hasUnescapedContent;
|
||||||
|
this.labels = log.labels;
|
||||||
|
this.logLevel = log.logLevel;
|
||||||
|
this.rowIndex = log.rowIndex;
|
||||||
|
this.rowId = log.rowId;
|
||||||
|
this.searchWords = log.searchWords;
|
||||||
|
this.timeFromNow = log.timeFromNow;
|
||||||
|
this.timeEpochMs = log.timeEpochMs;
|
||||||
|
this.timeEpochNs = log.timeEpochNs;
|
||||||
|
this.timeLocal = log.timeLocal;
|
||||||
|
this.timeUtc = log.timeUtc;
|
||||||
|
this.uid = log.uid;
|
||||||
|
this.uniqueLabels = log.uniqueLabels;
|
||||||
|
|
||||||
|
// LogListModel
|
||||||
|
this.displayLevel = logLevelToDisplayLevel(log.logLevel);
|
||||||
|
this._getFieldLinks = getFieldLinks;
|
||||||
|
this._grammar = grammar;
|
||||||
|
this.timestamp = dateTimeFormat(log.timeEpochMs, {
|
||||||
|
timeZone,
|
||||||
|
defaultWithMS: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let raw = log.raw;
|
||||||
|
if (escape && log.hasUnescapedContent) {
|
||||||
|
raw = escapeUnescapedString(raw);
|
||||||
|
}
|
||||||
|
this.raw = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
get body(): string {
|
||||||
|
if (this._body === undefined) {
|
||||||
|
let body = this.collapsed ? this.raw.substring(0, getTruncationLength(null)) : this.raw;
|
||||||
|
// Turn it into a single-line log entry for the list
|
||||||
|
this._body = body.replace(/(\r\n|\n|\r)/g, '');
|
||||||
|
}
|
||||||
|
return this._body;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fields(): FieldDef[] {
|
||||||
|
if (this._fields === undefined) {
|
||||||
|
this._fields = getAllFields(this, this._getFieldLinks);
|
||||||
|
}
|
||||||
|
return this._fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
get highlightedBody() {
|
||||||
|
if (this._highlightedBody === undefined) {
|
||||||
|
this._grammar = this._grammar ?? generateLogGrammar(this);
|
||||||
|
this._highlightedBody = Prism.highlight(this.body, this._grammar, 'lokiql');
|
||||||
|
}
|
||||||
|
return this._highlightedBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayedFieldValue(fieldName: string): string {
|
||||||
|
if (fieldName === LOG_LINE_BODY_FIELD_NAME) {
|
||||||
|
return this.body;
|
||||||
|
}
|
||||||
|
if (this.labels[fieldName] != null) {
|
||||||
|
return this.labels[fieldName];
|
||||||
|
}
|
||||||
|
const field = this.fields.find((field) => {
|
||||||
|
return field.keys[0] === fieldName;
|
||||||
|
});
|
||||||
|
|
||||||
|
return field ? field.values.toString() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCollapsedState(displayedFields: string[], container: HTMLDivElement | null) {
|
||||||
|
const lineLength =
|
||||||
|
displayedFields.map((field) => this.getDisplayedFieldValue(field)).join('').length + this.raw.length;
|
||||||
|
const collapsed = lineLength >= getTruncationLength(container) ? true : undefined;
|
||||||
|
if (this.collapsed === undefined || collapsed === undefined) {
|
||||||
|
this.collapsed = collapsed;
|
||||||
|
}
|
||||||
|
return this.collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCollapsedState(collapsed: boolean) {
|
||||||
|
if (this.collapsed !== collapsed) {
|
||||||
|
this._body = undefined;
|
||||||
|
this._highlightedBody = undefined;
|
||||||
|
}
|
||||||
|
this.collapsed = collapsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreProcessOptions {
|
export interface PreProcessOptions {
|
||||||
@@ -30,45 +149,17 @@ export const preProcessLogs = (
|
|||||||
grammar?: Grammar
|
grammar?: Grammar
|
||||||
): LogListModel[] => {
|
): LogListModel[] => {
|
||||||
const orderedLogs = sortLogRows(logs, order);
|
const orderedLogs = sortLogRows(logs, order);
|
||||||
return orderedLogs.map((log) => preProcessLog(log, { escape, getFieldLinks, timeZone }, grammar));
|
return orderedLogs.map((log) => preProcessLog(log, { escape, getFieldLinks, grammar, timeZone }));
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PreProcessLogOptions {
|
interface PreProcessLogOptions {
|
||||||
escape: boolean;
|
escape: boolean;
|
||||||
getFieldLinks?: GetFieldLinksFn;
|
getFieldLinks?: GetFieldLinksFn;
|
||||||
|
grammar?: Grammar;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
}
|
}
|
||||||
const preProcessLog = (
|
const preProcessLog = (log: LogRowModel, options: PreProcessLogOptions): LogListModel => {
|
||||||
log: LogRowModel,
|
return new LogListModel(log, options);
|
||||||
{ escape, getFieldLinks, timeZone }: PreProcessLogOptions,
|
|
||||||
grammar?: Grammar
|
|
||||||
): LogListModel => {
|
|
||||||
let body = log.raw;
|
|
||||||
const timestamp = dateTimeFormat(log.timeEpochMs, {
|
|
||||||
timeZone,
|
|
||||||
defaultWithMS: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (escape && log.hasUnescapedContent) {
|
|
||||||
body = escapeUnescapedString(body);
|
|
||||||
}
|
|
||||||
// Turn it into a single-line log entry for the list
|
|
||||||
body = body.replace(/(\r\n|\n|\r)/g, '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
...log,
|
|
||||||
body,
|
|
||||||
_highlightedBody: '',
|
|
||||||
get highlightedBody() {
|
|
||||||
if (!this._highlightedBody) {
|
|
||||||
this._highlightedBody = Prism.highlight(body, grammar ? grammar : generateLogGrammar(this), 'lokiql');
|
|
||||||
}
|
|
||||||
return this._highlightedBody;
|
|
||||||
},
|
|
||||||
displayLevel: logLevelToDisplayLevel(log.logLevel),
|
|
||||||
fields: getAllFields(log, getFieldLinks),
|
|
||||||
timestamp,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function logLevelToDisplayLevel(level = '') {
|
function logLevelToDisplayLevel(level = '') {
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { createTheme } from '@grafana/data';
|
||||||
|
|
||||||
|
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||||
|
import { createLogLine } from '../__mocks__/logRow';
|
||||||
|
|
||||||
|
import { LogListModel } from './processing';
|
||||||
|
import { getLineHeight, getLogLineSize, init, measureTextWidth, TRUNCATION_LINE_COUNT } from './virtualization';
|
||||||
|
|
||||||
|
const PADDING_BOTTOM = 6;
|
||||||
|
const LINE_HEIGHT = getLineHeight();
|
||||||
|
const SINGLE_LINE_HEIGHT = LINE_HEIGHT + PADDING_BOTTOM;
|
||||||
|
const TWO_LINES_HEIGHT = 2 * LINE_HEIGHT + PADDING_BOTTOM;
|
||||||
|
const THREE_LINES_HEIGHT = 3 * LINE_HEIGHT + PADDING_BOTTOM;
|
||||||
|
let LETTER_WIDTH: number;
|
||||||
|
let CONTAINER_SIZE = 200;
|
||||||
|
|
||||||
|
describe('Virtualization', () => {
|
||||||
|
let log: LogListModel, container: HTMLDivElement;
|
||||||
|
beforeEach(() => {
|
||||||
|
log = createLogLine({ labels: { place: 'luna' }, entry: `log message 1` });
|
||||||
|
container = document.createElement('div');
|
||||||
|
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(CONTAINER_SIZE);
|
||||||
|
init(createTheme());
|
||||||
|
LETTER_WIDTH = measureTextWidth('e');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogLineSize', () => {
|
||||||
|
test('Returns the a single line if the display mode is unwrapped', () => {
|
||||||
|
const size = getLogLineSize([log], container, [], { wrap: false, showControls: true, showTime: true }, 0);
|
||||||
|
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Returns the a single line if the line is not loaded yet', () => {
|
||||||
|
const logs = [log];
|
||||||
|
const size = getLogLineSize(
|
||||||
|
logs,
|
||||||
|
container,
|
||||||
|
[],
|
||||||
|
{ wrap: true, showControls: true, showTime: true },
|
||||||
|
logs.length + 1
|
||||||
|
);
|
||||||
|
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Returns the size of a truncated long line', () => {
|
||||||
|
// Very small container
|
||||||
|
log.collapsed = true;
|
||||||
|
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(10);
|
||||||
|
const size = getLogLineSize([log], container, [], { wrap: true, showControls: true, showTime: true }, 0);
|
||||||
|
expect(size).toBe((TRUNCATION_LINE_COUNT + 1) * LINE_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[false, false],
|
||||||
|
[true, false],
|
||||||
|
[false, true],
|
||||||
|
[true, true],
|
||||||
|
])('Measures a log line with controls %s and displayed time %s', (showControls: boolean, showTime: boolean) => {
|
||||||
|
const size = getLogLineSize([log], container, [], { wrap: true, showControls, showTime }, 0);
|
||||||
|
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Measures a multi-line log line with no controls and no displayed time', () => {
|
||||||
|
const TWO_LINES_OF_CHARACTERS = (CONTAINER_SIZE / LETTER_WIDTH) * 1.5;
|
||||||
|
log = createLogLine({
|
||||||
|
labels: { place: 'luna' },
|
||||||
|
entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join(''),
|
||||||
|
logLevel: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const size = getLogLineSize([log], container, [], { wrap: true, showControls: false, showTime: false }, 0);
|
||||||
|
expect(size).toBe(TWO_LINES_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Measures a multi-line log line with level, controls, and displayed time', () => {
|
||||||
|
const TWO_LINES_OF_CHARACTERS = (CONTAINER_SIZE / LETTER_WIDTH) * 1.5;
|
||||||
|
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||||
|
|
||||||
|
const size = getLogLineSize([log], container, [], { wrap: true, showControls: true, showTime: true }, 0);
|
||||||
|
// Two lines for the log and one extra for level and time
|
||||||
|
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Measures a multi-line log line with displayed fields', () => {
|
||||||
|
const TWO_LINES_OF_CHARACTERS = (CONTAINER_SIZE / LETTER_WIDTH) * 1.5;
|
||||||
|
log = createLogLine({
|
||||||
|
labels: { place: 'very very long value for the displayed field that causes a new line' },
|
||||||
|
entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join(''),
|
||||||
|
logLevel: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const size = getLogLineSize(
|
||||||
|
[log],
|
||||||
|
container,
|
||||||
|
['place', LOG_LINE_BODY_FIELD_NAME],
|
||||||
|
{ wrap: true, showControls: false, showTime: false },
|
||||||
|
0
|
||||||
|
);
|
||||||
|
// Two lines for the log and one extra for the displayed fields
|
||||||
|
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Measures displayed fields in a log line with level, controls, and displayed time', () => {
|
||||||
|
const TWO_LINES_OF_CHARACTERS = (CONTAINER_SIZE / LETTER_WIDTH) * 2;
|
||||||
|
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||||
|
|
||||||
|
const size = getLogLineSize([log], container, ['place'], { wrap: true, showControls: true, showTime: true }, 0);
|
||||||
|
// Only renders a short displayed field, so a single line
|
||||||
|
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Adds an extra line for the expand/collapse controls if present', () => {
|
||||||
|
jest.spyOn(log, 'updateCollapsedState').mockImplementation(() => undefined);
|
||||||
|
log.collapsed = false;
|
||||||
|
const size = getLogLineSize([log], container, [], { wrap: true, showControls: false, showTime: false }, 0);
|
||||||
|
expect(size).toBe(TWO_LINES_HEIGHT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,6 @@ import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data';
|
|||||||
|
|
||||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||||
|
|
||||||
import { getDisplayedFieldValue } from './LogLine';
|
|
||||||
import { LogListModel } from './processing';
|
import { LogListModel } from './processing';
|
||||||
|
|
||||||
let ctx: CanvasRenderingContext2D | null = null;
|
let ctx: CanvasRenderingContext2D | null = null;
|
||||||
@@ -168,6 +167,13 @@ export function getLogLineSize(
|
|||||||
if (!wrap || !logs[index]) {
|
if (!wrap || !logs[index]) {
|
||||||
return lineHeight + paddingBottom;
|
return lineHeight + paddingBottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (TRUNCATION_LINE_COUNT + 1) * lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const storedSize = retrieveLogLineSize(logs[index].uid, container);
|
const storedSize = retrieveLogLineSize(logs[index].uid, container);
|
||||||
if (storedSize) {
|
if (storedSize) {
|
||||||
return storedSize;
|
return storedSize;
|
||||||
@@ -189,14 +195,15 @@ export function getLogLineSize(
|
|||||||
textToMeasure += logs[index].displayLevel ?? '';
|
textToMeasure += logs[index].displayLevel ?? '';
|
||||||
}
|
}
|
||||||
for (const field of displayedFields) {
|
for (const field of displayedFields) {
|
||||||
textToMeasure = getDisplayedFieldValue(field, logs[index]) + textToMeasure;
|
textToMeasure = logs[index].getDisplayedFieldValue(field) + textToMeasure;
|
||||||
}
|
}
|
||||||
if (!displayedFields.length) {
|
if (!displayedFields.length) {
|
||||||
textToMeasure += logs[index].body;
|
textToMeasure += logs[index].body;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { height } = measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth);
|
const { height } = measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth);
|
||||||
return height;
|
// When the log is collapsed, add an extra line for the expand/collapse control
|
||||||
|
return logs[index].collapsed === false ? height + lineHeight : height;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogFieldDimension {
|
export interface LogFieldDimension {
|
||||||
@@ -221,7 +228,7 @@ export const calculateFieldDimensions = (logs: LogListModel[], displayedFields:
|
|||||||
levelWidth = Math.round(width);
|
levelWidth = Math.round(width);
|
||||||
}
|
}
|
||||||
for (const field of displayedFields) {
|
for (const field of displayedFields) {
|
||||||
width = measureTextWidth(getDisplayedFieldValue(field, logs[i]));
|
width = measureTextWidth(logs[i].getDisplayedFieldValue(field));
|
||||||
fieldWidths[field] = !fieldWidths[field] || width > fieldWidths[field] ? Math.round(width) : fieldWidths[field];
|
fieldWidths[field] = !fieldWidths[field] || width > fieldWidths[field] ? Math.round(width) : fieldWidths[field];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,14 +255,28 @@ export const calculateFieldDimensions = (logs: LogListModel[], displayedFields:
|
|||||||
return dimensions;
|
return dimensions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function hasUnderOrOverflow(element: HTMLDivElement, calculatedHeight?: number): number | null {
|
// 2/3 of the viewport height
|
||||||
|
export const TRUNCATION_LINE_COUNT = Math.round(window.innerHeight / getLineHeight() / 1.5);
|
||||||
|
export function getTruncationLength(container: HTMLDivElement | null) {
|
||||||
|
const availableWidth = container ? getLogContainerWidth(container) : window.innerWidth;
|
||||||
|
return (availableWidth / measureTextWidth('e')) * TRUNCATION_LINE_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasUnderOrOverflow(
|
||||||
|
element: HTMLDivElement,
|
||||||
|
calculatedHeight?: number,
|
||||||
|
collapsed?: boolean
|
||||||
|
): number | null {
|
||||||
|
if (collapsed !== undefined && calculatedHeight) {
|
||||||
|
calculatedHeight -= getLineHeight();
|
||||||
|
}
|
||||||
const height = calculatedHeight ?? element.clientHeight;
|
const height = calculatedHeight ?? element.clientHeight;
|
||||||
if (element.scrollHeight > height) {
|
if (element.scrollHeight > height) {
|
||||||
return element.scrollHeight;
|
return collapsed !== undefined ? element.scrollHeight + getLineHeight() : element.scrollHeight;
|
||||||
}
|
}
|
||||||
const child = element.children[1];
|
const child = element.children[1];
|
||||||
if (child instanceof HTMLDivElement && child.clientHeight < height) {
|
if (child instanceof HTMLDivElement && child.clientHeight < height) {
|
||||||
return child.clientHeight;
|
return collapsed !== undefined ? child.clientHeight + getLineHeight() : child.clientHeight;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,7 +404,7 @@
|
|||||||
},
|
},
|
||||||
"alert-labels": {
|
"alert-labels": {
|
||||||
"aria-label-labels": "Labels",
|
"aria-label-labels": "Labels",
|
||||||
"common-labels-count_one": "+{{count}} common label",
|
"common-labels-count_one": "+{{count}} common labels",
|
||||||
"common-labels-count_other": "+{{count}} common labels"
|
"common-labels-count_other": "+{{count}} common labels"
|
||||||
},
|
},
|
||||||
"alert-manager-manual-routing": {
|
"alert-manager-manual-routing": {
|
||||||
@@ -1384,7 +1384,7 @@
|
|||||||
"new-recording-rule": "New recording rule",
|
"new-recording-rule": "New recording rule",
|
||||||
"title": "Grafana-managed"
|
"title": "Grafana-managed"
|
||||||
},
|
},
|
||||||
"loading-rules_one": "Loading rules from {{count}} source",
|
"loading-rules_one": "Loading rules from {{count}} sources",
|
||||||
"loading-rules_other": "Loading rules from {{count}} sources"
|
"loading-rules_other": "Loading rules from {{count}} sources"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2065,14 +2065,14 @@
|
|||||||
"unknown-rule-type": "Unknown rule type"
|
"unknown-rule-type": "Unknown rule type"
|
||||||
},
|
},
|
||||||
"rule-list-errors": {
|
"rule-list-errors": {
|
||||||
"button-errors_one": "{{count}} error",
|
"button-errors_one": "{{count}} errors",
|
||||||
"button-errors_other": "{{count}} errors",
|
"button-errors_other": "{{count}} errors",
|
||||||
"cloud-rulessource-errors-title-errors-loading-rules": "Errors loading rules",
|
"cloud-rulessource-errors-title-errors-loading-rules": "Errors loading rules",
|
||||||
"failed-to-load-grafana-rules-config": "Failed to load Grafana rules config:",
|
"failed-to-load-grafana-rules-config": "Failed to load Grafana rules config:",
|
||||||
"failed-to-load-grafana-rules-state": "Failed to load Grafana rules state:",
|
"failed-to-load-grafana-rules-state": "Failed to load Grafana rules state:",
|
||||||
"failed-to-load-rules-config": "Failed to load rules config from <2>{{dataSource}}</2>",
|
"failed-to-load-rules-config": "Failed to load rules config from <2>{{dataSource}}</2>",
|
||||||
"failed-to-load-rules-state": "Failed to load rules state from <2>{{dataSource}}</2>",
|
"failed-to-load-rules-state": "Failed to load rules state from <2>{{dataSource}}</2>",
|
||||||
"more-errors_one": "{{count}} more error",
|
"more-errors_one": "{{count}} more errors",
|
||||||
"more-errors_other": "{{count}} more errors",
|
"more-errors_other": "{{count}} more errors",
|
||||||
"unknown-error": "Unknown error."
|
"unknown-error": "Unknown error."
|
||||||
},
|
},
|
||||||
@@ -2099,7 +2099,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rule-stats": {
|
"rule-stats": {
|
||||||
"error_one": "{{count}} error",
|
"error_one": "{{count}} errors",
|
||||||
"error_other": "{{count}} errors",
|
"error_other": "{{count}} errors",
|
||||||
"firing": "{{alertingStats}} firing",
|
"firing": "{{alertingStats}} firing",
|
||||||
"inactive": "{{normalStats}} normal",
|
"inactive": "{{normalStats}} normal",
|
||||||
@@ -4033,7 +4033,7 @@
|
|||||||
},
|
},
|
||||||
"library-viz-panel-info": {
|
"library-viz-panel-info": {
|
||||||
"last-edited": "{{timeAgo}} by<person />",
|
"last-edited": "{{timeAgo}} by<person />",
|
||||||
"usage-count_one": "Used on {{count}} dashboard",
|
"usage-count_one": "Used on {{count}} dashboards",
|
||||||
"usage-count_other": "Used on {{count}} dashboards"
|
"usage-count_other": "Used on {{count}} dashboards"
|
||||||
},
|
},
|
||||||
"name-already-exists-error": {
|
"name-already-exists-error": {
|
||||||
@@ -4152,7 +4152,7 @@
|
|||||||
"save-variables-label-update-default-variable-values": "Update default variable values"
|
"save-variables-label-update-default-variable-values": "Update default variable values"
|
||||||
},
|
},
|
||||||
"save-library-viz-panel-modal": {
|
"save-library-viz-panel-modal": {
|
||||||
"affected-dashboards_one": "This update will affect <1>{{count}} dashboard.</1> The following dashboard using the panel will be affected:",
|
"affected-dashboards_one": "This update will affect <1>{{count}} dashboards.</1> The following dashboards using the panel will be affected:",
|
||||||
"affected-dashboards_other": "This update will affect <1>{{count}} dashboards.</1> The following dashboards using the panel will be affected:",
|
"affected-dashboards_other": "This update will affect <1>{{count}} dashboards.</1> The following dashboards using the panel will be affected:",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"dashboard-name": "Dashboard name",
|
"dashboard-name": "Dashboard name",
|
||||||
@@ -5666,9 +5666,9 @@
|
|||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"query-inspector": {
|
"query-inspector": {
|
||||||
"count-frames_one": "{{count}} frame, ",
|
"count-frames_one": "{{count}} frames, ",
|
||||||
"count-frames_other": "{{count}} frames, ",
|
"count-frames_other": "{{count}} frames, ",
|
||||||
"count-rows_one": "{{count}} row",
|
"count-rows_one": "{{count}} rows",
|
||||||
"count-rows_other": "{{count}} rows",
|
"count-rows_other": "{{count}} rows",
|
||||||
"query-inspector": "Query inspector",
|
"query-inspector": "Query inspector",
|
||||||
"text-loading-query-inspector": "Loading query inspector..."
|
"text-loading-query-inspector": "Loading query inspector..."
|
||||||
@@ -5885,7 +5885,7 @@
|
|||||||
},
|
},
|
||||||
"library-panel-info": {
|
"library-panel-info": {
|
||||||
"last-edited": "Last edited on {{timeAgo}} by<person />",
|
"last-edited": "Last edited on {{timeAgo}} by<person />",
|
||||||
"usage-count_one": "Used on {{count}} dashboard",
|
"usage-count_one": "Used on {{count}} dashboards",
|
||||||
"usage-count_other": "Used on {{count}} dashboards"
|
"usage-count_other": "Used on {{count}} dashboards"
|
||||||
},
|
},
|
||||||
"library-panels-search": {
|
"library-panels-search": {
|
||||||
@@ -5919,7 +5919,7 @@
|
|||||||
"dashboard-name": "Dashboard name",
|
"dashboard-name": "Dashboard name",
|
||||||
"discard": "Discard",
|
"discard": "Discard",
|
||||||
"loading-connected-dashboards": "Loading connected dashboards...",
|
"loading-connected-dashboards": "Loading connected dashboards...",
|
||||||
"num-affected_one": "This update will affect <1>{{count}} dashboard.</1>",
|
"num-affected_one": "This update will affect <1>{{count}} dashboards.</1>",
|
||||||
"num-affected_other": "This update will affect <1>{{count}} dashboards.</1>",
|
"num-affected_other": "This update will affect <1>{{count}} dashboards.</1>",
|
||||||
"placeholder-search-affected-dashboards": "Search affected dashboards",
|
"placeholder-search-affected-dashboards": "Search affected dashboards",
|
||||||
"update-all": "Update all"
|
"update-all": "Update all"
|
||||||
@@ -6029,6 +6029,10 @@
|
|||||||
"log-labels-list": {
|
"log-labels-list": {
|
||||||
"log-line": "log line"
|
"log-line": "log line"
|
||||||
},
|
},
|
||||||
|
"log-line": {
|
||||||
|
"show-less": "show less",
|
||||||
|
"show-more": "show more"
|
||||||
|
},
|
||||||
"log-line-menu": {
|
"log-line-menu": {
|
||||||
"copy-link": "Copy link to log line",
|
"copy-link": "Copy link to log line",
|
||||||
"copy-log": "Copy log line",
|
"copy-log": "Copy log line",
|
||||||
|
|||||||
Reference in New Issue
Block a user