New Logs Panel: Support multiple virtualization instances for different font sizes (#107023)
* LogList: memoize styles * virtualization: refactor to support multiple instances * LogList: refactor to support multiple virtualization instances * Update tests * LogLine: unfocus test * virtualization: refactor class to use the provided font size * LogLine: split component to reduce re-renders
This commit is contained in:
@@ -45,6 +45,7 @@ export const createLogLine = (
|
||||
escape: false,
|
||||
order: LogsSortOrder.Descending,
|
||||
timeZone: 'browser',
|
||||
virtualization: undefined,
|
||||
}
|
||||
): LogListModel => {
|
||||
const logs = preProcessLogs([createLogRow(overrides)], processOptions);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } fro
|
||||
import { getStyles, LogLine } from './LogLine';
|
||||
import { LogLineMessage } from './LogLineMessage';
|
||||
import { LogListModel } from './processing';
|
||||
import { LogLineVirtualization } from './virtualization';
|
||||
|
||||
interface ChildrenProps {
|
||||
itemCount: number;
|
||||
@@ -33,6 +34,7 @@ interface Props {
|
||||
sortOrder: LogsSortOrder;
|
||||
timeRange: TimeRange;
|
||||
timeZone: string;
|
||||
virtualization: LogLineVirtualization;
|
||||
wrapLogMessage: boolean;
|
||||
}
|
||||
|
||||
@@ -51,6 +53,7 @@ export const InfiniteScroll = ({
|
||||
sortOrder,
|
||||
timeRange,
|
||||
timeZone,
|
||||
virtualization,
|
||||
wrapLogMessage,
|
||||
}: Props) => {
|
||||
const [infiniteLoaderState, setInfiniteLoaderState] = useState<InfiniteLoaderState>('idle');
|
||||
@@ -61,7 +64,7 @@ export const InfiniteScroll = ({
|
||||
const lastEvent = useRef<Event | WheelEvent | null>(null);
|
||||
const countRef = useRef(0);
|
||||
const lastLogOfPage = useRef<string[]>([]);
|
||||
const styles = useStyles2(getStyles);
|
||||
const styles = useStyles2(getStyles, virtualization);
|
||||
|
||||
useEffect(() => {
|
||||
// Logs have not changed, ignore effect
|
||||
@@ -156,6 +159,7 @@ export const InfiniteScroll = ({
|
||||
style={style}
|
||||
styles={styles}
|
||||
variant={getLogLineVariant(logs, index, lastLogOfPage.current)}
|
||||
virtualization={virtualization}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
onOverflow={handleOverflow}
|
||||
/>
|
||||
@@ -171,6 +175,7 @@ export const InfiniteScroll = ({
|
||||
showTime,
|
||||
sortOrder,
|
||||
styles,
|
||||
virtualization,
|
||||
wrapLogMessage,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -12,13 +12,13 @@ import { LogListContextProvider } from './LogListContext';
|
||||
import { LogListSearchContext } from './LogListSearchContext';
|
||||
import { defaultProps } from './__mocks__/LogListContext';
|
||||
import { LogListModel } from './processing';
|
||||
import { getTruncationLength } from './virtualization';
|
||||
import { LogLineVirtualization } from './virtualization';
|
||||
|
||||
jest.mock('./LogListContext');
|
||||
jest.mock('./virtualization');
|
||||
|
||||
const theme = createTheme();
|
||||
const styles = getStyles(theme);
|
||||
const virtualization = new LogLineVirtualization(theme, 'default');
|
||||
const styles = getStyles(theme, virtualization);
|
||||
const contextProps = {
|
||||
...defaultProps,
|
||||
app: CoreApp.Unknown,
|
||||
@@ -34,7 +34,10 @@ const fontSizes: LogListFontSize[] = ['default', 'small'];
|
||||
describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => {
|
||||
let log: LogListModel, defaultProps: Props;
|
||||
beforeEach(() => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: `log message 1` });
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: `log message 1` },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
contextProps.logs = [log];
|
||||
contextProps.fontSize = fontSize;
|
||||
defaultProps = {
|
||||
@@ -219,8 +222,12 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => {
|
||||
|
||||
describe('Collapsible log lines', () => {
|
||||
beforeEach(() => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: `log message 1` });
|
||||
jest.mocked(getTruncationLength).mockReturnValue(5);
|
||||
const virtualization = new LogLineVirtualization(theme, 'default');
|
||||
jest.spyOn(virtualization, 'getTruncationLength').mockReturnValue(5);
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: `log message 1` },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
});
|
||||
|
||||
test('Logs are not collapsed by default', () => {
|
||||
|
||||
@@ -17,9 +17,9 @@ import { LogListModel } from './processing';
|
||||
import {
|
||||
FIELD_GAP_MULTIPLIER,
|
||||
hasUnderOrOverflow,
|
||||
getLineHeight,
|
||||
LogFieldDimension,
|
||||
getTruncationLineCount,
|
||||
LogLineVirtualization,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
} from './virtualization';
|
||||
|
||||
export interface Props {
|
||||
@@ -32,6 +32,7 @@ export interface Props {
|
||||
onClick: (e: MouseEvent<HTMLElement>, log: LogListModel) => void;
|
||||
onOverflow?: (index: number, id: string, height?: number) => void;
|
||||
variant?: 'infinite-scroll';
|
||||
virtualization?: LogLineVirtualization;
|
||||
wrapLogMessage: boolean;
|
||||
}
|
||||
|
||||
@@ -45,157 +46,201 @@ export const LogLine = ({
|
||||
onOverflow,
|
||||
showTime,
|
||||
variant,
|
||||
virtualization,
|
||||
wrapLogMessage,
|
||||
}: Props) => {
|
||||
const {
|
||||
detailsDisplayed,
|
||||
dedupStrategy,
|
||||
enableLogDetails,
|
||||
fontSize,
|
||||
hasLogsWithErrors,
|
||||
hasSampledLogs,
|
||||
onLogLineHover,
|
||||
} = useLogListContext();
|
||||
const [collapsed, setCollapsed] = useState<boolean | undefined>(
|
||||
wrapLogMessage && log.collapsed !== undefined ? log.collapsed : undefined
|
||||
);
|
||||
const logLineRef = useRef<HTMLDivElement | null>(null);
|
||||
const pinned = useLogIsPinned(log);
|
||||
const permalinked = useLogIsPermalinked(log);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onOverflow || !logLineRef.current) {
|
||||
return;
|
||||
}
|
||||
const calculatedHeight = typeof style.height === 'number' ? style.height : undefined;
|
||||
const actualHeight = hasUnderOrOverflow(logLineRef.current, calculatedHeight, log.collapsed);
|
||||
if (actualHeight) {
|
||||
onOverflow(index, log.uid, actualHeight);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!wrapLogMessage) {
|
||||
setCollapsed(undefined);
|
||||
} else if (collapsed === undefined && log.collapsed !== undefined) {
|
||||
setCollapsed(log.collapsed);
|
||||
} else if (collapsed !== undefined && log.collapsed === undefined) {
|
||||
setCollapsed(log.collapsed);
|
||||
}
|
||||
}, [collapsed, log.collapsed, wrapLogMessage]);
|
||||
|
||||
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]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLElement>) => {
|
||||
onClick(e, log);
|
||||
},
|
||||
[log, onClick]
|
||||
);
|
||||
|
||||
const detailsShown = detailsDisplayed(log);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div
|
||||
className={`${styles.logLine} ${variant ?? ''} ${pinned ? styles.pinnedLogLine : ''} ${permalinked ? styles.permalinkedLogLine : ''} ${detailsShown ? styles.detailsDisplayed : ''} ${fontSize === 'small' ? styles.fontSizeSmall : ''}`}
|
||||
ref={onOverflow ? logLineRef : undefined}
|
||||
onMouseEnter={handleMouseOver}
|
||||
onFocus={handleMouseOver}
|
||||
>
|
||||
<LogLineMenu styles={styles} log={log} />
|
||||
{dedupStrategy !== LogsDedupStrategy.none && (
|
||||
<div className={`${styles.duplicates}`}>
|
||||
{log.duplicates && log.duplicates > 0 ? `${log.duplicates + 1}x` : null}
|
||||
</div>
|
||||
)}
|
||||
{hasLogsWithErrors && (
|
||||
<div className={`${styles.hasError}`}>
|
||||
{log.hasError && (
|
||||
<Tooltip
|
||||
content={t('logs.log-line.tooltip-error', 'Error: {{errorMessage}}', {
|
||||
errorMessage: log.errorMessage,
|
||||
})}
|
||||
placement="right"
|
||||
theme="error"
|
||||
>
|
||||
<Icon
|
||||
className={styles.logIconError}
|
||||
name="exclamation-triangle"
|
||||
aria-label={t('logs.log-line.has-error', 'Has errors')}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasSampledLogs && (
|
||||
<div className={`${styles.isSampled}`}>
|
||||
{log.isSampled && (
|
||||
<Tooltip content={log.sampledMessage ?? ''} placement="right" theme="info">
|
||||
<Icon
|
||||
className={styles.logIconInfo}
|
||||
name="info-circle"
|
||||
size="xs"
|
||||
aria-label={t('logs.log-line.is-sampled', 'Is sampled')}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* A button element could be used but in Safari it prevents text selection. Fallback available for a11y in LogLineMenu */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
|
||||
<div
|
||||
className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`} ${collapsed === true ? styles.collapsedLogLine : ''} ${enableLogDetails ? styles.clickable : ''}`}
|
||||
style={collapsed ? { maxHeight: `${getTruncationLineCount() * getLineHeight()}px` } : undefined}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Log
|
||||
displayedFields={displayedFields}
|
||||
log={log}
|
||||
showTime={showTime}
|
||||
styles={styles}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
<LogLineComponent
|
||||
displayedFields={displayedFields}
|
||||
height={style.height}
|
||||
index={index}
|
||||
log={log}
|
||||
styles={styles}
|
||||
onClick={onClick}
|
||||
onOverflow={onOverflow}
|
||||
showTime={showTime}
|
||||
variant={variant}
|
||||
virtualization={virtualization}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface LogLineComponentProps extends Omit<Props, 'style'> {
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
const LogLineComponent = memo(
|
||||
({
|
||||
displayedFields,
|
||||
height,
|
||||
index,
|
||||
log,
|
||||
styles,
|
||||
onClick,
|
||||
onOverflow,
|
||||
showTime,
|
||||
variant,
|
||||
virtualization,
|
||||
wrapLogMessage,
|
||||
}: LogLineComponentProps) => {
|
||||
const {
|
||||
detailsDisplayed,
|
||||
dedupStrategy,
|
||||
enableLogDetails,
|
||||
fontSize,
|
||||
hasLogsWithErrors,
|
||||
hasSampledLogs,
|
||||
onLogLineHover,
|
||||
} = useLogListContext();
|
||||
const [collapsed, setCollapsed] = useState<boolean | undefined>(
|
||||
wrapLogMessage && log.collapsed !== undefined ? log.collapsed : undefined
|
||||
);
|
||||
const logLineRef = useRef<HTMLDivElement | null>(null);
|
||||
const pinned = useLogIsPinned(log);
|
||||
const permalinked = useLogIsPermalinked(log);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onOverflow || !logLineRef.current || !virtualization || !height) {
|
||||
return;
|
||||
}
|
||||
const calculatedHeight = typeof height === 'number' ? height : undefined;
|
||||
const actualHeight = hasUnderOrOverflow(virtualization, logLineRef.current, calculatedHeight, log.collapsed);
|
||||
if (actualHeight) {
|
||||
onOverflow(index, log.uid, actualHeight);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!wrapLogMessage) {
|
||||
setCollapsed(undefined);
|
||||
} else if (collapsed === undefined && log.collapsed !== undefined) {
|
||||
setCollapsed(log.collapsed);
|
||||
} else if (collapsed !== undefined && log.collapsed === undefined) {
|
||||
setCollapsed(log.collapsed);
|
||||
}
|
||||
}, [collapsed, log.collapsed, wrapLogMessage]);
|
||||
|
||||
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]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLElement>) => {
|
||||
onClick(e, log);
|
||||
},
|
||||
[log, onClick]
|
||||
);
|
||||
|
||||
const detailsShown = detailsDisplayed(log);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${styles.logLine} ${variant ?? ''} ${pinned ? styles.pinnedLogLine : ''} ${permalinked ? styles.permalinkedLogLine : ''} ${detailsShown ? styles.detailsDisplayed : ''} ${fontSize === 'small' ? styles.fontSizeSmall : ''}`}
|
||||
ref={onOverflow ? logLineRef : undefined}
|
||||
onMouseEnter={handleMouseOver}
|
||||
onFocus={handleMouseOver}
|
||||
>
|
||||
<LogLineMenu styles={styles} log={log} />
|
||||
{dedupStrategy !== LogsDedupStrategy.none && (
|
||||
<div className={`${styles.duplicates}`}>
|
||||
{log.duplicates && log.duplicates > 0 ? `${log.duplicates + 1}x` : null}
|
||||
</div>
|
||||
)}
|
||||
{hasLogsWithErrors && (
|
||||
<div className={`${styles.hasError}`}>
|
||||
{log.hasError && (
|
||||
<Tooltip
|
||||
content={t('logs.log-line.tooltip-error', 'Error: {{errorMessage}}', {
|
||||
errorMessage: log.errorMessage,
|
||||
})}
|
||||
placement="right"
|
||||
theme="error"
|
||||
>
|
||||
<Icon
|
||||
className={styles.logIconError}
|
||||
name="exclamation-triangle"
|
||||
aria-label={t('logs.log-line.has-error', 'Has errors')}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasSampledLogs && (
|
||||
<div className={`${styles.isSampled}`}>
|
||||
{log.isSampled && (
|
||||
<Tooltip content={log.sampledMessage ?? ''} placement="right" theme="info">
|
||||
<Icon
|
||||
className={styles.logIconInfo}
|
||||
name="info-circle"
|
||||
size="xs"
|
||||
aria-label={t('logs.log-line.is-sampled', 'Is sampled')}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* A button element could be used but in Safari it prevents text selection. Fallback available for a11y in LogLineMenu */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
|
||||
<div
|
||||
className={`${wrapLogMessage ? styles.wrappedLogLine : `${styles.unwrappedLogLine} unwrapped-log-line`} ${collapsed === true ? styles.collapsedLogLine : ''} ${enableLogDetails ? styles.clickable : ''}`}
|
||||
style={
|
||||
collapsed && virtualization
|
||||
? { maxHeight: `${virtualization.getTruncationLineCount() * virtualization.getLineHeight()}px` }
|
||||
: undefined
|
||||
}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Log
|
||||
displayedFields={displayedFields}
|
||||
log={log}
|
||||
showTime={showTime}
|
||||
styles={styles}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
LogLineComponent.displayName = 'LogLineComponent';
|
||||
|
||||
interface LogProps {
|
||||
displayedFields: string[];
|
||||
log: LogListModel;
|
||||
@@ -311,7 +356,7 @@ export function getGridTemplateColumns(dimensions: LogFieldDimension[]) {
|
||||
}
|
||||
|
||||
export type LogLineStyles = ReturnType<typeof getStyles>;
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtualization) => {
|
||||
const colors = {
|
||||
critical: '#B877D9',
|
||||
error: theme.colors.error.text,
|
||||
@@ -404,7 +449,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
backgroundColor: tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString(),
|
||||
}),
|
||||
menuIcon: css({
|
||||
height: getLineHeight(),
|
||||
height: virtualization?.getLineHeight() ?? DEFAULT_LINE_HEIGHT,
|
||||
margin: 0,
|
||||
padding: theme.spacing(0, 0, 0, 0.5),
|
||||
}),
|
||||
@@ -501,7 +546,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
expandCollapseControlButton: css({
|
||||
fontWeight: theme.typography.fontWeightLight,
|
||||
height: getLineHeight(),
|
||||
height: virtualization?.getLineHeight() ?? DEFAULT_LINE_HEIGHT,
|
||||
margin: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { ConfirmModal, Icon, PopoverContent, useTheme2 } from '@grafana/ui';
|
||||
import { ConfirmModal, Icon, PopoverContent, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { PopoverMenu } from 'app/features/explore/Logs/PopoverMenu';
|
||||
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
||||
|
||||
@@ -35,15 +35,7 @@ import { LogListSearchContextProvider, useLogListSearchContext } from './LogList
|
||||
import { preProcessLogs, LogListModel } from './processing';
|
||||
import { useKeyBindings } from './useKeyBindings';
|
||||
import { usePopoverMenu } from './usePopoverMenu';
|
||||
import {
|
||||
calculateFieldDimensions,
|
||||
getLogLineSize,
|
||||
init as initVirtualization,
|
||||
LogFieldDimension,
|
||||
resetLogLineSizes,
|
||||
ScrollToLogsEvent,
|
||||
storeLogLineSize,
|
||||
} from './virtualization';
|
||||
import { LogLineVirtualization, getLogLineSize, LogFieldDimension, ScrollToLogsEvent } from './virtualization';
|
||||
|
||||
export interface Props {
|
||||
app: CoreApp;
|
||||
@@ -251,11 +243,12 @@ const LogListComponent = ({
|
||||
const widthRef = useRef(containerElement.clientWidth);
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const virtualization = useMemo(() => new LogLineVirtualization(theme, fontSize), [theme, fontSize]);
|
||||
const dimensions = useMemo(
|
||||
() => (wrapLogMessage ? [] : calculateFieldDimensions(processedLogs, displayedFields)),
|
||||
[displayedFields, processedLogs, wrapLogMessage]
|
||||
() => (wrapLogMessage ? [] : virtualization.calculateFieldDimensions(processedLogs, displayedFields)),
|
||||
[displayedFields, processedLogs, virtualization, wrapLogMessage]
|
||||
);
|
||||
const styles = getStyles(dimensions, { showTime }, theme);
|
||||
const styles = useStyles2(getStyles, dimensions, { showTime });
|
||||
const widthContainer = wrapperRef.current ?? containerElement;
|
||||
const {
|
||||
closePopoverMenu,
|
||||
@@ -276,10 +269,6 @@ const LogListComponent = ({
|
||||
}, 25);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
initVirtualization(theme, fontSize);
|
||||
}, [fontSize, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
|
||||
handleScrollToEvent(e, logs.length, listRef.current)
|
||||
@@ -292,11 +281,15 @@ const LogListComponent = ({
|
||||
return;
|
||||
}
|
||||
setProcessedLogs(
|
||||
preProcessLogs(logs, { getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone }, grammar)
|
||||
preProcessLogs(
|
||||
logs,
|
||||
{ getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone, virtualization },
|
||||
grammar
|
||||
)
|
||||
);
|
||||
resetLogLineSizes();
|
||||
virtualization.resetLogLineSizes();
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}, [forceEscape, getFieldLinks, grammar, loading, logs, sortOrder, timeZone]);
|
||||
}, [forceEscape, getFieldLinks, grammar, loading, logs, sortOrder, timeZone, virtualization]);
|
||||
|
||||
useEffect(() => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
@@ -329,7 +322,7 @@ const LogListComponent = ({
|
||||
const handleOverflow = useCallback(
|
||||
(index: number, id: string, height?: number) => {
|
||||
if (height !== undefined) {
|
||||
storeLogLineSize(id, widthContainer, height, fontSize);
|
||||
virtualization.storeLogLineSize(id, widthContainer, height);
|
||||
}
|
||||
if (index === overflowIndexRef.current) {
|
||||
return;
|
||||
@@ -337,7 +330,7 @@ const LogListComponent = ({
|
||||
overflowIndexRef.current = index < overflowIndexRef.current ? index : overflowIndexRef.current;
|
||||
debouncedResetAfterIndex(overflowIndexRef.current);
|
||||
},
|
||||
[debouncedResetAfterIndex, fontSize, widthContainer]
|
||||
[debouncedResetAfterIndex, virtualization, widthContainer]
|
||||
);
|
||||
|
||||
const handleScrollPosition = useCallback(() => {
|
||||
@@ -434,6 +427,7 @@ const LogListComponent = ({
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
setInitialScrollPosition={handleScrollPosition}
|
||||
virtualization={virtualization}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
>
|
||||
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
|
||||
@@ -441,8 +435,7 @@ const LogListComponent = ({
|
||||
className={styles.logList}
|
||||
height={listHeight}
|
||||
itemCount={itemCount}
|
||||
itemSize={getLogLineSize.bind(null, filteredLogs, widthContainer, displayedFields, {
|
||||
fontSize,
|
||||
itemSize={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, {
|
||||
hasLogsWithErrors,
|
||||
hasSampledLogs,
|
||||
showDuplicates: dedupStrategy !== LogsDedupStrategy.none,
|
||||
@@ -476,7 +469,7 @@ const LogListComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(dimensions: LogFieldDimension[], { showTime }: { showTime: boolean }, theme: GrafanaTheme2) {
|
||||
function getStyles(theme: GrafanaTheme2, dimensions: LogFieldDimension[], { showTime }: { showTime: boolean }) {
|
||||
const columns = showTime ? dimensions : dimensions.filter((_, index) => index > 0);
|
||||
return {
|
||||
logList: css({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createLogLine, createLogRow } from '../__mocks__/logRow';
|
||||
|
||||
import { LogListFontSize } from './LogList';
|
||||
import { LogListModel, preProcessLogs } from './processing';
|
||||
import { getTruncationLength, init } from './virtualization';
|
||||
import { LogLineVirtualization } from './virtualization';
|
||||
|
||||
describe('preProcessLogs', () => {
|
||||
let logFmtLog: LogRowModel, nginxLog: LogRowModel, jsonLog: LogRowModel;
|
||||
@@ -168,13 +168,16 @@ describe('preProcessLogs', () => {
|
||||
});
|
||||
|
||||
describe.each(fontSizes)('Collapsible log lines', (fontSize: LogListFontSize) => {
|
||||
let longLog: LogListModel, entry: string, container: HTMLDivElement;
|
||||
let longLog: LogListModel, entry: string, container: HTMLDivElement, virtualization: LogLineVirtualization;
|
||||
beforeEach(() => {
|
||||
init(createTheme(), fontSize);
|
||||
virtualization = new LogLineVirtualization(createTheme(), fontSize);
|
||||
container = document.createElement('div');
|
||||
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(200);
|
||||
entry = new Array(2 * getTruncationLength(null)).fill('e').join('');
|
||||
longLog = createLogLine({ entry, labels: { field: 'value' } });
|
||||
entry = new Array(2 * virtualization.getTruncationLength(null)).fill('e').join('');
|
||||
longLog = createLogLine(
|
||||
{ entry, labels: { field: 'value' } },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
});
|
||||
|
||||
test('Long lines that are not truncated are not modified', () => {
|
||||
|
||||
@@ -8,7 +8,9 @@ import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { FieldDef, getAllFields } from '../logParser';
|
||||
|
||||
import { generateLogGrammar, generateTextMatchGrammar } from './grammar';
|
||||
import { getTruncationLength } from './virtualization';
|
||||
import { LogLineVirtualization } from './virtualization';
|
||||
|
||||
const TRUNCATION_DEFAULT_LENGTH = 50000;
|
||||
|
||||
export class LogListModel implements LogRowModel {
|
||||
collapsed: boolean | undefined = undefined;
|
||||
@@ -44,11 +46,13 @@ export class LogListModel implements LogRowModel {
|
||||
private _highlightedBody: string | undefined = undefined;
|
||||
private _fields: FieldDef[] | undefined = undefined;
|
||||
private _getFieldLinks: GetFieldLinksFn | undefined = undefined;
|
||||
private _virtualization?: LogLineVirtualization;
|
||||
|
||||
constructor(log: LogRowModel, { escape, getFieldLinks, grammar, timeZone }: PreProcessLogOptions) {
|
||||
constructor(log: LogRowModel, { escape, getFieldLinks, grammar, timeZone, virtualization }: PreProcessLogOptions) {
|
||||
// LogRowModel
|
||||
this.datasourceType = log.datasourceType;
|
||||
this.dataFrame = log.dataFrame;
|
||||
this.datasourceUid = log.datasourceUid;
|
||||
this.duplicates = log.duplicates;
|
||||
this.entry = log.entry;
|
||||
this.entryFieldIndex = log.entryFieldIndex;
|
||||
@@ -68,7 +72,6 @@ export class LogListModel implements LogRowModel {
|
||||
this.timeUtc = log.timeUtc;
|
||||
this.uid = log.uid;
|
||||
this.uniqueLabels = log.uniqueLabels;
|
||||
this.datasourceUid = log.datasourceUid;
|
||||
|
||||
// LogListModel
|
||||
this.displayLevel = logLevelToDisplayLevel(log.logLevel);
|
||||
@@ -78,6 +81,7 @@ export class LogListModel implements LogRowModel {
|
||||
timeZone,
|
||||
defaultWithMS: true,
|
||||
});
|
||||
this._virtualization = virtualization;
|
||||
|
||||
let raw = log.raw;
|
||||
if (escape && log.hasUnescapedContent) {
|
||||
@@ -88,7 +92,9 @@ export class LogListModel implements LogRowModel {
|
||||
|
||||
get body(): string {
|
||||
if (this._body === undefined) {
|
||||
this._body = this.collapsed ? this.raw.substring(0, getTruncationLength(null)) : this.raw;
|
||||
this._body = this.collapsed
|
||||
? this.raw.substring(0, this._virtualization?.getTruncationLength(null) ?? TRUNCATION_DEFAULT_LENGTH)
|
||||
: this.raw;
|
||||
}
|
||||
return this._body;
|
||||
}
|
||||
@@ -136,7 +142,10 @@ export class LogListModel implements LogRowModel {
|
||||
displayedFields.length > 0
|
||||
? displayedFields.map((field) => this.getDisplayedFieldValue(field)).join('').length
|
||||
: this.raw.length;
|
||||
const collapsed = lineLength >= getTruncationLength(container) ? true : undefined;
|
||||
const collapsed =
|
||||
lineLength >= (this._virtualization?.getTruncationLength(container) ?? TRUNCATION_DEFAULT_LENGTH)
|
||||
? true
|
||||
: undefined;
|
||||
if (this.collapsed === undefined || collapsed === undefined) {
|
||||
this.collapsed = collapsed;
|
||||
}
|
||||
@@ -162,15 +171,16 @@ export interface PreProcessOptions {
|
||||
getFieldLinks?: GetFieldLinksFn;
|
||||
order: LogsSortOrder;
|
||||
timeZone: string;
|
||||
virtualization?: LogLineVirtualization;
|
||||
}
|
||||
|
||||
export const preProcessLogs = (
|
||||
logs: LogRowModel[],
|
||||
{ escape, getFieldLinks, order, timeZone }: PreProcessOptions,
|
||||
{ escape, getFieldLinks, order, timeZone, virtualization }: PreProcessOptions,
|
||||
grammar?: Grammar
|
||||
): LogListModel[] => {
|
||||
const orderedLogs = sortLogRows(logs, order);
|
||||
return orderedLogs.map((log) => preProcessLog(log, { escape, getFieldLinks, grammar, timeZone }));
|
||||
return orderedLogs.map((log) => preProcessLog(log, { escape, getFieldLinks, grammar, timeZone, virtualization }));
|
||||
};
|
||||
|
||||
interface PreProcessLogOptions {
|
||||
@@ -178,6 +188,7 @@ interface PreProcessLogOptions {
|
||||
getFieldLinks?: GetFieldLinksFn;
|
||||
grammar?: Grammar;
|
||||
timeZone: string;
|
||||
virtualization?: LogLineVirtualization;
|
||||
}
|
||||
const preProcessLog = (log: LogRowModel, options: PreProcessLogOptions): LogListModel => {
|
||||
return new LogListModel(log, options);
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
import { createTheme } from '@grafana/data';
|
||||
import { createTheme, LogsSortOrder } 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,
|
||||
getTruncationLineCount,
|
||||
DisplayOptions,
|
||||
} 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;
|
||||
let TWO_LINES_OF_CHARACTERS: number;
|
||||
const defaultOptions: DisplayOptions = {
|
||||
wrap: false,
|
||||
showTime: false,
|
||||
showDuplicates: false,
|
||||
hasLogsWithErrors: false,
|
||||
hasSampledLogs: false,
|
||||
fontSize: 'default',
|
||||
};
|
||||
import { LogLineVirtualization, getLogLineSize, DisplayOptions } from './virtualization';
|
||||
|
||||
describe('Virtualization', () => {
|
||||
let log: LogListModel, container: HTMLDivElement;
|
||||
|
||||
let virtualization = new LogLineVirtualization(createTheme(), 'default');
|
||||
|
||||
const PADDING_BOTTOM = 6;
|
||||
const LINE_HEIGHT = virtualization.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;
|
||||
let TWO_LINES_OF_CHARACTERS: number;
|
||||
|
||||
const defaultOptions: DisplayOptions = {
|
||||
wrap: false,
|
||||
showTime: false,
|
||||
showDuplicates: false,
|
||||
hasLogsWithErrors: false,
|
||||
hasSampledLogs: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: `log message 1` });
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: `log message 1` },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
//virtualization = new LogLineVirtualization(createTheme(), 'default');
|
||||
container = document.createElement('div');
|
||||
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(CONTAINER_SIZE);
|
||||
init(createTheme(), 'default');
|
||||
LETTER_WIDTH = measureTextWidth('e');
|
||||
LETTER_WIDTH = virtualization.measureTextWidth('e');
|
||||
TWO_LINES_OF_CHARACTERS = (CONTAINER_SIZE / LETTER_WIDTH) * 1.5;
|
||||
});
|
||||
|
||||
describe('getLogLineSize', () => {
|
||||
test('Returns the a single line if the display mode is unwrapped', () => {
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, showTime: true }, 0);
|
||||
const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, 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(
|
||||
virtualization,
|
||||
logs,
|
||||
container,
|
||||
[],
|
||||
@@ -63,42 +63,66 @@ describe('Virtualization', () => {
|
||||
// Very small container
|
||||
log.collapsed = true;
|
||||
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(10);
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, showTime: true }, 0);
|
||||
expect(size).toBe((getTruncationLineCount() + 1) * LINE_HEIGHT);
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
[],
|
||||
{ ...defaultOptions, wrap: true, showTime: true },
|
||||
0
|
||||
);
|
||||
expect(size).toBe((virtualization.getTruncationLineCount() + 1) * LINE_HEIGHT);
|
||||
});
|
||||
|
||||
test.each([true, false])('Measures a log line with controls %s and displayed time %s', (showTime: boolean) => {
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, showTime }, 0);
|
||||
const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, wrap: true, showTime }, 0);
|
||||
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line log line with no displayed time', () => {
|
||||
log = createLogLine({
|
||||
labels: { place: 'luna' },
|
||||
entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join(''),
|
||||
logLevel: undefined,
|
||||
});
|
||||
log = createLogLine(
|
||||
{
|
||||
labels: { place: 'luna' },
|
||||
entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join(''),
|
||||
logLevel: undefined,
|
||||
},
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true }, 0);
|
||||
const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, wrap: true }, 0);
|
||||
expect(size).toBe(TWO_LINES_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line log line with level, controls, and displayed time', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, showTime: true }, 0);
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
[],
|
||||
{ ...defaultOptions, wrap: 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', () => {
|
||||
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,
|
||||
});
|
||||
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,
|
||||
},
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
['place', LOG_LINE_BODY_FIELD_NAME],
|
||||
@@ -110,34 +134,74 @@ describe('Virtualization', () => {
|
||||
});
|
||||
|
||||
test('Measures displayed fields in a log line with level, controls, and displayed time', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
|
||||
const size = getLogLineSize([log], container, ['place'], { ...defaultOptions, wrap: true, showTime: true }, 0);
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
['place'],
|
||||
{ ...defaultOptions, wrap: true, showTime: true },
|
||||
0
|
||||
);
|
||||
// Only renders a short displayed field, so a single line
|
||||
expect(size).toBe(SINGLE_LINE_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line log line with duplicates', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
log.duplicates = 1;
|
||||
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, showDuplicates: true }, 0);
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
[],
|
||||
{ ...defaultOptions, wrap: true, showDuplicates: true },
|
||||
0
|
||||
);
|
||||
// Two lines for the log and one extra for duplicates
|
||||
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line log line with errors', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, hasLogsWithErrors: true }, 0);
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
[],
|
||||
{ ...defaultOptions, wrap: true, hasLogsWithErrors: true },
|
||||
0
|
||||
);
|
||||
// Two lines for the log and one extra for the error icon
|
||||
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||
});
|
||||
|
||||
test('Measures a multi-line sampled log line', () => {
|
||||
log = createLogLine({ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') });
|
||||
log = createLogLine(
|
||||
{ labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') },
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
|
||||
const size = getLogLineSize([log], container, [], { ...defaultOptions, wrap: true, hasSampledLogs: true }, 0);
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
[],
|
||||
{ ...defaultOptions, wrap: true, hasSampledLogs: true },
|
||||
0
|
||||
);
|
||||
// Two lines for the log and one extra for the sampled icon
|
||||
expect(size).toBe(THREE_LINES_HEIGHT);
|
||||
});
|
||||
@@ -145,29 +209,34 @@ describe('Virtualization', () => {
|
||||
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, [], { ...defaultOptions, wrap: true }, 0);
|
||||
const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, wrap: true }, 0);
|
||||
expect(size).toBe(TWO_LINES_HEIGHT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With small font size', () => {
|
||||
const virtualization = new LogLineVirtualization(createTheme(), 'small');
|
||||
|
||||
beforeEach(() => {
|
||||
init(createTheme(), 'small');
|
||||
LETTER_WIDTH = measureTextWidth('e');
|
||||
LETTER_WIDTH = virtualization.measureTextWidth('e');
|
||||
TWO_LINES_OF_CHARACTERS = (CONTAINER_SIZE / LETTER_WIDTH) * 1.5;
|
||||
});
|
||||
|
||||
test('Measures a multi-line log line with displayed fields', () => {
|
||||
const SMALL_LINE_HEIGHT = getLineHeight();
|
||||
const SMALL_LINE_HEIGHT = virtualization.getLineHeight();
|
||||
const SMALL_THREE_LINES_HEIGHT = 3 * SMALL_LINE_HEIGHT + PADDING_BOTTOM;
|
||||
|
||||
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,
|
||||
});
|
||||
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,
|
||||
},
|
||||
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization }
|
||||
);
|
||||
|
||||
const size = getLogLineSize(
|
||||
virtualization,
|
||||
[log],
|
||||
container,
|
||||
['place', LOG_LINE_BODY_FIELD_NAME],
|
||||
|
||||
@@ -7,167 +7,237 @@ import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { LogListFontSize } from './LogList';
|
||||
import { LogListModel } from './processing';
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let gridSize = 8;
|
||||
let paddingBottom = gridSize * 0.75;
|
||||
let lineHeight = 22;
|
||||
let measurementMode: 'canvas' | 'dom' = 'canvas';
|
||||
const iconWidth = 24;
|
||||
|
||||
export const LOG_LIST_MIN_WIDTH = 35 * gridSize;
|
||||
|
||||
export const LOG_LIST_MIN_WIDTH = 35 * 8;
|
||||
// Controls the space between fields in the log line, timestamp, level, displayed fields, and log line body
|
||||
export const FIELD_GAP_MULTIPLIER = 1.5;
|
||||
|
||||
export const getLineHeight = () => lineHeight;
|
||||
export const DEFAULT_LINE_HEIGHT = 22;
|
||||
|
||||
export function init(theme: GrafanaTheme2, fontSize: LogListFontSize) {
|
||||
let fontSizePx = theme.typography.fontSize;
|
||||
export class LogLineVirtualization {
|
||||
private ctx: CanvasRenderingContext2D | null = null;
|
||||
private gridSize;
|
||||
private paddingBottom;
|
||||
private lineHeight;
|
||||
private measurementMode: 'canvas' | 'dom' = 'canvas';
|
||||
private textWidthMap: Map<number, number>;
|
||||
private logLineSizesMap: Map<string, number>;
|
||||
private spanElement = document.createElement('span');
|
||||
readonly fontSize: LogListFontSize;
|
||||
|
||||
if (fontSize === 'default') {
|
||||
lineHeight = theme.typography.fontSize * theme.typography.body.lineHeight;
|
||||
} else {
|
||||
fontSizePx =
|
||||
typeof theme.typography.bodySmall.fontSize === 'string' && theme.typography.bodySmall.fontSize.includes('rem')
|
||||
? theme.typography.fontSize * parseFloat(theme.typography.bodySmall.fontSize)
|
||||
: parseInt(theme.typography.bodySmall.fontSize, 10);
|
||||
lineHeight = fontSizePx * theme.typography.bodySmall.lineHeight;
|
||||
}
|
||||
constructor(theme: GrafanaTheme2, fontSize: LogListFontSize) {
|
||||
this.fontSize = fontSize;
|
||||
|
||||
const font = `${fontSizePx}px ${theme.typography.fontFamilyMonospace}`;
|
||||
const letterSpacing = theme.typography.body.letterSpacing;
|
||||
|
||||
initDOMmeasurement(font, letterSpacing);
|
||||
initCanvasMeasurement(font, letterSpacing);
|
||||
|
||||
gridSize = theme.spacing.gridSize;
|
||||
paddingBottom = gridSize * 0.75;
|
||||
|
||||
widthMap = new Map<number, number>();
|
||||
resetLogLineSizes();
|
||||
|
||||
determineMeasurementMode();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function determineMeasurementMode() {
|
||||
if (!ctx) {
|
||||
measurementMode = 'dom';
|
||||
return;
|
||||
}
|
||||
const canvasCharWidth = ctx.measureText('e').width;
|
||||
const domCharWidth = measureTextWidthWithDOM('e');
|
||||
const diff = domCharWidth - canvasCharWidth;
|
||||
if (diff >= 0.1) {
|
||||
console.warn('Virtualized log list: falling back to DOM for measurement');
|
||||
measurementMode = 'dom';
|
||||
}
|
||||
}
|
||||
|
||||
function initCanvasMeasurement(font: string, letterSpacing: string | undefined) {
|
||||
const canvas = document.createElement('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
ctx.font = font;
|
||||
ctx.fontKerning = 'normal';
|
||||
ctx.fontStretch = 'normal';
|
||||
ctx.fontVariantCaps = 'normal';
|
||||
ctx.textRendering = 'optimizeLegibility';
|
||||
if (letterSpacing) {
|
||||
ctx.letterSpacing = letterSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
const span = document.createElement('span');
|
||||
function initDOMmeasurement(font: string, letterSpacing: string | undefined) {
|
||||
span.style.font = font;
|
||||
span.style.visibility = 'hidden';
|
||||
span.style.position = 'absolute';
|
||||
span.style.wordBreak = 'break-all';
|
||||
if (letterSpacing) {
|
||||
span.style.letterSpacing = letterSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
let widthMap = new Map<number, number>();
|
||||
export function measureTextWidth(text: string): number {
|
||||
if (!ctx) {
|
||||
throw new Error(`Measuring context canvas is not initialized. Call init() before.`);
|
||||
}
|
||||
const key = text.length;
|
||||
|
||||
const storedWidth = widthMap.get(key);
|
||||
if (storedWidth) {
|
||||
return storedWidth;
|
||||
}
|
||||
|
||||
const width = measurementMode === 'canvas' ? ctx.measureText(text).width : measureTextWidthWithDOM(text);
|
||||
widthMap.set(key, width);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
function measureTextWidthWithDOM(text: string) {
|
||||
span.textContent = text;
|
||||
|
||||
document.body.appendChild(span);
|
||||
const width = span.getBoundingClientRect().width;
|
||||
document.body.removeChild(span);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
export function measureTextHeight(text: string, maxWidth: number, beforeWidth = 0) {
|
||||
let logLines = 0;
|
||||
const charWidth = measureTextWidth('e');
|
||||
let logLineCharsLength = Math.round(maxWidth / charWidth);
|
||||
const firstLineCharsLength = Math.floor((maxWidth - beforeWidth) / charWidth) - 2 * charWidth;
|
||||
const textLines = text.split('\n');
|
||||
|
||||
// Skip unnecessary measurements
|
||||
if (textLines.length === 1 && text.length < firstLineCharsLength) {
|
||||
return {
|
||||
lines: 1,
|
||||
height: getLineHeight() + paddingBottom,
|
||||
};
|
||||
}
|
||||
|
||||
const availableWidth = maxWidth - beforeWidth;
|
||||
for (const textLine of textLines) {
|
||||
for (let start = 0; start < textLine.length; ) {
|
||||
let testLogLine: string;
|
||||
let width = 0;
|
||||
let delta = 0;
|
||||
do {
|
||||
testLogLine = textLine.substring(start, start + logLineCharsLength - delta);
|
||||
let measuredLine = testLogLine;
|
||||
if (logLines > 0) {
|
||||
measuredLine.trimStart();
|
||||
}
|
||||
width = measureTextWidth(measuredLine);
|
||||
delta += 1;
|
||||
} while (width >= availableWidth);
|
||||
if (beforeWidth) {
|
||||
beforeWidth = 0;
|
||||
}
|
||||
logLines += 1;
|
||||
start += testLogLine.length;
|
||||
let fontSizePx;
|
||||
if (fontSize === 'default') {
|
||||
fontSizePx = theme.typography.fontSize;
|
||||
this.lineHeight = theme.typography.fontSize * theme.typography.body.lineHeight;
|
||||
} else {
|
||||
fontSizePx =
|
||||
typeof theme.typography.bodySmall.fontSize === 'string' && theme.typography.bodySmall.fontSize.includes('rem')
|
||||
? theme.typography.fontSize * parseFloat(theme.typography.bodySmall.fontSize)
|
||||
: parseInt(theme.typography.bodySmall.fontSize, 10);
|
||||
this.lineHeight = fontSizePx * theme.typography.bodySmall.lineHeight;
|
||||
}
|
||||
|
||||
this.gridSize = theme.spacing.gridSize;
|
||||
this.paddingBottom = this.gridSize * 0.75;
|
||||
this.logLineSizesMap = new Map<string, number>();
|
||||
this.textWidthMap = new Map<number, number>();
|
||||
|
||||
const font = `${fontSizePx}px ${theme.typography.fontFamilyMonospace}`;
|
||||
const letterSpacing = theme.typography.body.letterSpacing;
|
||||
|
||||
this.initDOMmeasurement(font, letterSpacing);
|
||||
this.initCanvasMeasurement(font, letterSpacing);
|
||||
this.determineMeasurementMode();
|
||||
}
|
||||
|
||||
const height = logLines * getLineHeight() + paddingBottom;
|
||||
getLineHeight = () => this.lineHeight;
|
||||
getGridSize = () => this.gridSize;
|
||||
getPaddingBottom = () => this.paddingBottom;
|
||||
|
||||
return {
|
||||
lines: logLines,
|
||||
height,
|
||||
// 2/3 of the viewport height
|
||||
getTruncationLineCount = () => Math.round(window.innerHeight / this.getLineHeight() / 1.5);
|
||||
|
||||
getTruncationLength = (container: HTMLDivElement | null) => {
|
||||
const availableWidth = container ? getLogContainerWidth(container) : window.innerWidth;
|
||||
return (availableWidth / this.measureTextWidth('e')) * this.getTruncationLineCount();
|
||||
};
|
||||
|
||||
determineMeasurementMode = () => {
|
||||
if (!this.ctx) {
|
||||
this.measurementMode = 'dom';
|
||||
return;
|
||||
}
|
||||
const canvasCharWidth = this.ctx.measureText('e').width;
|
||||
const domCharWidth = this.measureTextWidthWithDOM('e');
|
||||
const diff = domCharWidth - canvasCharWidth;
|
||||
if (diff >= 0.1) {
|
||||
console.warn('Virtualized log list: falling back to DOM for measurement');
|
||||
this.measurementMode = 'dom';
|
||||
}
|
||||
};
|
||||
|
||||
initCanvasMeasurement = (font: string, letterSpacing: string | undefined) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
this.ctx = canvas.getContext('2d');
|
||||
if (!this.ctx) {
|
||||
return;
|
||||
}
|
||||
this.ctx.font = font;
|
||||
this.ctx.fontKerning = 'normal';
|
||||
this.ctx.fontStretch = 'normal';
|
||||
this.ctx.fontVariantCaps = 'normal';
|
||||
this.ctx.textRendering = 'optimizeLegibility';
|
||||
if (letterSpacing) {
|
||||
this.ctx.letterSpacing = letterSpacing;
|
||||
}
|
||||
};
|
||||
|
||||
initDOMmeasurement = (font: string, letterSpacing: string | undefined) => {
|
||||
this.spanElement.style.font = font;
|
||||
this.spanElement.style.visibility = 'hidden';
|
||||
this.spanElement.style.position = 'absolute';
|
||||
this.spanElement.style.wordBreak = 'break-all';
|
||||
if (letterSpacing) {
|
||||
this.spanElement.style.letterSpacing = letterSpacing;
|
||||
}
|
||||
};
|
||||
|
||||
measureTextWidth = (text: string): number => {
|
||||
if (!this.ctx) {
|
||||
throw new Error(`Measuring context canvas is not initialized. Call init() before.`);
|
||||
}
|
||||
const key = text.length;
|
||||
|
||||
const storedWidth = this.textWidthMap.get(key);
|
||||
if (storedWidth) {
|
||||
return storedWidth;
|
||||
}
|
||||
|
||||
const width =
|
||||
this.measurementMode === 'canvas' ? this.ctx.measureText(text).width : this.measureTextWidthWithDOM(text);
|
||||
this.textWidthMap.set(key, width);
|
||||
|
||||
return width;
|
||||
};
|
||||
|
||||
measureTextWidthWithDOM = (text: string) => {
|
||||
this.spanElement.textContent = text;
|
||||
|
||||
document.body.appendChild(this.spanElement);
|
||||
const width = this.spanElement.getBoundingClientRect().width;
|
||||
document.body.removeChild(this.spanElement);
|
||||
|
||||
return width;
|
||||
};
|
||||
|
||||
measureTextHeight = (text: string, maxWidth: number, beforeWidth = 0) => {
|
||||
let logLines = 0;
|
||||
const charWidth = this.measureTextWidth('e');
|
||||
let logLineCharsLength = Math.round(maxWidth / charWidth);
|
||||
const firstLineCharsLength = Math.floor((maxWidth - beforeWidth) / charWidth) - 2 * charWidth;
|
||||
const textLines = text.split('\n');
|
||||
|
||||
// Skip unnecessary measurements
|
||||
if (textLines.length === 1 && text.length < firstLineCharsLength) {
|
||||
return {
|
||||
lines: 1,
|
||||
height: this.getLineHeight() + this.paddingBottom,
|
||||
};
|
||||
}
|
||||
|
||||
const availableWidth = maxWidth - beforeWidth;
|
||||
for (const textLine of textLines) {
|
||||
for (let start = 0; start < textLine.length; ) {
|
||||
let testLogLine: string;
|
||||
let width = 0;
|
||||
let delta = 0;
|
||||
do {
|
||||
testLogLine = textLine.substring(start, start + logLineCharsLength - delta);
|
||||
let measuredLine = testLogLine;
|
||||
if (logLines > 0) {
|
||||
measuredLine.trimStart();
|
||||
}
|
||||
width = this.measureTextWidth(measuredLine);
|
||||
delta += 1;
|
||||
} while (width >= availableWidth);
|
||||
if (beforeWidth) {
|
||||
beforeWidth = 0;
|
||||
}
|
||||
logLines += 1;
|
||||
start += testLogLine.length;
|
||||
}
|
||||
}
|
||||
|
||||
const height = logLines * this.getLineHeight() + this.paddingBottom;
|
||||
|
||||
return {
|
||||
lines: logLines,
|
||||
height,
|
||||
};
|
||||
};
|
||||
|
||||
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 = this.measureTextWidth(logs[i].timestamp);
|
||||
if (width > timestampWidth) {
|
||||
timestampWidth = Math.round(width);
|
||||
}
|
||||
width = this.measureTextWidth(logs[i].displayLevel);
|
||||
if (width > levelWidth) {
|
||||
levelWidth = Math.round(width);
|
||||
}
|
||||
for (const field of displayedFields) {
|
||||
width = this.measureTextWidth(logs[i].getDisplayedFieldValue(field));
|
||||
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;
|
||||
};
|
||||
|
||||
resetLogLineSizes = () => {
|
||||
this.logLineSizesMap = new Map<string, number>();
|
||||
};
|
||||
|
||||
storeLogLineSize = (id: string, container: HTMLDivElement, height: number) => {
|
||||
const key = `${id}_${getLogContainerWidth(container)}_${this.fontSize}`;
|
||||
this.logLineSizesMap.set(key, height);
|
||||
};
|
||||
|
||||
retrieveLogLineSize = (id: string, container: HTMLDivElement) => {
|
||||
const key = `${id}_${getLogContainerWidth(container)}_${this.fontSize}`;
|
||||
return this.logLineSizesMap.get(key);
|
||||
};
|
||||
}
|
||||
|
||||
export interface DisplayOptions {
|
||||
fontSize: LogListFontSize;
|
||||
hasLogsWithErrors?: boolean;
|
||||
hasSampledLogs?: boolean;
|
||||
showDuplicates: boolean;
|
||||
@@ -176,10 +246,11 @@ export interface DisplayOptions {
|
||||
}
|
||||
|
||||
export function getLogLineSize(
|
||||
virtualization: LogLineVirtualization,
|
||||
logs: LogListModel[],
|
||||
container: HTMLDivElement | null,
|
||||
displayedFields: string[],
|
||||
{ fontSize, hasLogsWithErrors, hasSampledLogs, showDuplicates, showTime, wrap }: DisplayOptions,
|
||||
{ hasLogsWithErrors, hasSampledLogs, showDuplicates, showTime, wrap }: DisplayOptions,
|
||||
index: number
|
||||
) {
|
||||
if (!container) {
|
||||
@@ -187,31 +258,31 @@ export function getLogLineSize(
|
||||
}
|
||||
// !logs[index] means the line is not yet loaded by infinite scrolling
|
||||
if (!wrap || !logs[index]) {
|
||||
return getLineHeight() + paddingBottom;
|
||||
return virtualization.getLineHeight() + virtualization.getPaddingBottom();
|
||||
}
|
||||
// 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 (getTruncationLineCount() + 1) * getLineHeight();
|
||||
return (virtualization.getTruncationLineCount() + 1) * virtualization.getLineHeight();
|
||||
}
|
||||
|
||||
const storedSize = retrieveLogLineSize(logs[index].uid, container, fontSize);
|
||||
const storedSize = virtualization.retrieveLogLineSize(logs[index].uid, container);
|
||||
if (storedSize) {
|
||||
return storedSize;
|
||||
}
|
||||
|
||||
let textToMeasure = '';
|
||||
const gap = gridSize * FIELD_GAP_MULTIPLIER;
|
||||
const iconsGap = gridSize * 0.5;
|
||||
const gap = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER;
|
||||
const iconsGap = virtualization.getGridSize() * 0.5;
|
||||
let optionsWidth = 0;
|
||||
if (showDuplicates) {
|
||||
optionsWidth += gridSize * 4.5 + iconsGap;
|
||||
optionsWidth += virtualization.getGridSize() * 4.5 + iconsGap;
|
||||
}
|
||||
if (hasLogsWithErrors) {
|
||||
optionsWidth += gridSize * 2 + iconsGap;
|
||||
optionsWidth += virtualization.getGridSize() * 2 + iconsGap;
|
||||
}
|
||||
if (hasSampledLogs) {
|
||||
optionsWidth += gridSize * 2 + iconsGap;
|
||||
optionsWidth += virtualization.getGridSize() * 2 + iconsGap;
|
||||
}
|
||||
if (showTime) {
|
||||
optionsWidth += gap;
|
||||
@@ -229,9 +300,9 @@ export function getLogLineSize(
|
||||
textToMeasure += ansicolor.strip(logs[index].body);
|
||||
}
|
||||
|
||||
const { height } = measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth);
|
||||
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 + getLineHeight() : height;
|
||||
return logs[index].collapsed === false ? height + virtualization.getLineHeight() : height;
|
||||
}
|
||||
|
||||
export interface LogFieldDimension {
|
||||
@@ -239,80 +310,31 @@ export interface LogFieldDimension {
|
||||
width: number;
|
||||
}
|
||||
|
||||
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(logs[i].getDisplayedFieldValue(field));
|
||||
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;
|
||||
};
|
||||
|
||||
// 2/3 of the viewport height
|
||||
export const getTruncationLineCount = () => Math.round(window.innerHeight / getLineHeight() / 1.5);
|
||||
export function getTruncationLength(container: HTMLDivElement | null) {
|
||||
const availableWidth = container ? getLogContainerWidth(container) : window.innerWidth;
|
||||
return (availableWidth / measureTextWidth('e')) * getTruncationLineCount();
|
||||
}
|
||||
|
||||
export function hasUnderOrOverflow(
|
||||
virtualization: LogLineVirtualization,
|
||||
element: HTMLDivElement,
|
||||
calculatedHeight?: number,
|
||||
collapsed?: boolean
|
||||
): number | null {
|
||||
if (collapsed !== undefined && calculatedHeight) {
|
||||
calculatedHeight -= getLineHeight();
|
||||
calculatedHeight -= virtualization.getLineHeight();
|
||||
}
|
||||
const height = calculatedHeight ?? element.clientHeight;
|
||||
if (element.scrollHeight > height) {
|
||||
return collapsed !== undefined ? element.scrollHeight + getLineHeight() : element.scrollHeight;
|
||||
return collapsed !== undefined ? element.scrollHeight + virtualization.getLineHeight() : element.scrollHeight;
|
||||
}
|
||||
const child = element.children[1];
|
||||
if (child instanceof HTMLDivElement && child.clientHeight < height) {
|
||||
return collapsed !== undefined ? child.clientHeight + getLineHeight() : child.clientHeight;
|
||||
return collapsed !== undefined ? child.clientHeight + virtualization.getLineHeight() : child.clientHeight;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const logLineMenuIconWidth = 24;
|
||||
const scrollBarWidth = getScrollbarWidth();
|
||||
|
||||
export function getLogContainerWidth(container: HTMLDivElement) {
|
||||
return container.clientWidth - scrollBarWidth - iconWidth;
|
||||
return container.clientWidth - scrollBarWidth - logLineMenuIconWidth;
|
||||
}
|
||||
|
||||
export function getScrollbarWidth() {
|
||||
@@ -331,21 +353,6 @@ export function getScrollbarWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
let logLineSizesMap = new Map<string, number>();
|
||||
export function resetLogLineSizes() {
|
||||
logLineSizesMap = new Map<string, number>();
|
||||
}
|
||||
|
||||
export function storeLogLineSize(id: string, container: HTMLDivElement, height: number, fontSize: LogListFontSize) {
|
||||
const key = `${id}_${getLogContainerWidth(container)}_${fontSize}`;
|
||||
logLineSizesMap.set(key, height);
|
||||
}
|
||||
|
||||
export function retrieveLogLineSize(id: string, container: HTMLDivElement, fontSize: LogListFontSize) {
|
||||
const key = `${id}_${getLogContainerWidth(container)}_${fontSize}`;
|
||||
return logLineSizesMap.get(key);
|
||||
}
|
||||
|
||||
export interface ScrollToLogsEventPayload {
|
||||
scrollTo: 'top' | 'bottom';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user