|
|
|
@ -1,7 +1,6 @@ |
|
|
|
|
import { css, cx } from '@emotion/css'; |
|
|
|
|
import { capitalize, groupBy } from 'lodash'; |
|
|
|
|
import memoizeOne from 'memoize-one'; |
|
|
|
|
import { useCallback, useEffect, useState, useRef } from 'react'; |
|
|
|
|
import { useCallback, useEffect, useState, useRef, useMemo } from 'react'; |
|
|
|
|
import * as React from 'react'; |
|
|
|
|
import { usePrevious, useUnmount } from 'react-use'; |
|
|
|
|
|
|
|
|
@ -189,6 +188,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
loadMoreLogs, |
|
|
|
|
panelState, |
|
|
|
|
eventBus, |
|
|
|
|
onPinLineCallback, |
|
|
|
|
scrollElement, |
|
|
|
|
} = props; |
|
|
|
|
const [showLabels, setShowLabels] = useState<boolean>(store.getBool(SETTINGS_KEYS.showLabels, false)); |
|
|
|
|
const [showTime, setShowTime] = useState<boolean>(store.getBool(SETTINGS_KEYS.showTime, true)); |
|
|
|
@ -210,8 +211,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
const [visualisationType, setVisualisationType] = useState<LogsVisualisationType | undefined>( |
|
|
|
|
panelState?.logs?.visualisationType ?? getDefaultVisualisationType() |
|
|
|
|
); |
|
|
|
|
const [scrollIntoView, setScrollIntoView] = useState<((element: HTMLElement) => void) | undefined>(undefined); |
|
|
|
|
const logsContainerRef = useRef<HTMLDivElement | undefined>(undefined); |
|
|
|
|
const logsContainerRef = useRef<HTMLDivElement | null>(null); |
|
|
|
|
const dispatch = useDispatch(); |
|
|
|
|
const previousLoading = usePrevious(loading); |
|
|
|
|
|
|
|
|
@ -230,9 +230,13 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
|
|
|
|
|
// Get pinned log lines
|
|
|
|
|
const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root'); |
|
|
|
|
const pinnedLogs = logsParent?.children |
|
|
|
|
?.filter((outlines) => outlines.title === PINNED_LOGS_TITLE) |
|
|
|
|
.map((pinnedLogs) => pinnedLogs.id); |
|
|
|
|
const pinnedLogs = useMemo( |
|
|
|
|
() => |
|
|
|
|
logsParent?.children |
|
|
|
|
?.filter((outlines) => outlines.title === PINNED_LOGS_TITLE) |
|
|
|
|
.map((pinnedLogs) => pinnedLogs.id), |
|
|
|
|
[logsParent?.children] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const getPinnedLogsCount = useCallback(() => { |
|
|
|
|
const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root'); |
|
|
|
@ -433,39 +437,28 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
[props.eventBus] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const onLogsContainerRef = useCallback( |
|
|
|
|
(node: HTMLDivElement) => { |
|
|
|
|
logsContainerRef.current = node; |
|
|
|
|
|
|
|
|
|
// In theory this should be just a function passed down to LogRows but:
|
|
|
|
|
// - LogRow.componentDidMount which calls scrollIntoView is called BEFORE the logsContainerRef is set
|
|
|
|
|
// - the if check below if (logsContainerRef.current) was falsy and scrolling doesn't happen
|
|
|
|
|
// - and LogRow.scrollToLogRow marks the line as scrolled anyway (and won't perform scrolling when the ref is set)
|
|
|
|
|
// - see more details in https://github.com/facebook/react/issues/29897
|
|
|
|
|
// We can change it once LogRow is converted into a functional component
|
|
|
|
|
setScrollIntoView(() => (element: HTMLElement) => { |
|
|
|
|
if (config.featureToggles.logsInfiniteScrolling) { |
|
|
|
|
if (logsContainerRef.current) { |
|
|
|
|
topLogsRef.current?.scrollIntoView(); |
|
|
|
|
logsContainerRef.current.scroll({ |
|
|
|
|
behavior: 'smooth', |
|
|
|
|
top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const scrollElement = props.scrollElement; |
|
|
|
|
|
|
|
|
|
if (scrollElement) { |
|
|
|
|
scrollElement.scroll({ |
|
|
|
|
const scrollIntoView = useCallback( |
|
|
|
|
(element: HTMLElement) => { |
|
|
|
|
if (config.featureToggles.logsInfiniteScrolling) { |
|
|
|
|
if (logsContainerRef.current) { |
|
|
|
|
topLogsRef.current?.scrollIntoView(); |
|
|
|
|
logsContainerRef.current.scroll({ |
|
|
|
|
behavior: 'smooth', |
|
|
|
|
top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, |
|
|
|
|
top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (scrollElement) { |
|
|
|
|
scrollElement.scroll({ |
|
|
|
|
behavior: 'smooth', |
|
|
|
|
top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
[props.scrollElement] |
|
|
|
|
[scrollElement] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const onChangeLogsSortOrder = () => { |
|
|
|
@ -644,7 +637,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
onCloseCallbackRef?.current(); |
|
|
|
|
}, [contextRow?.datasourceType, contextRow?.uid, onCloseCallbackRef]); |
|
|
|
|
|
|
|
|
|
const onOpenContext = (row: LogRowModel, onClose: () => void) => { |
|
|
|
|
const onOpenContext = useCallback((row: LogRowModel, onClose: () => void) => { |
|
|
|
|
// we are setting the `contextOpen` open state and passing it down to the `LogRow` in order to highlight the row when a LogContext is open
|
|
|
|
|
setContextOpen(true); |
|
|
|
|
setContextRow(row); |
|
|
|
@ -653,37 +646,40 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
logRowUid: row.uid, |
|
|
|
|
}); |
|
|
|
|
onCloseCallbackRef.current = onClose; |
|
|
|
|
}; |
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
const onPermalinkClick = async (row: LogRowModel) => { |
|
|
|
|
// this is an extra check, to be sure that we are not
|
|
|
|
|
// creating permalinks for logs without an id-field.
|
|
|
|
|
// normally it should never happen, because we do not
|
|
|
|
|
// display the permalink button in such cases.
|
|
|
|
|
if (row.rowId === undefined) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const onPermalinkClick = useCallback( |
|
|
|
|
async (row: LogRowModel) => { |
|
|
|
|
// this is an extra check, to be sure that we are not
|
|
|
|
|
// creating permalinks for logs without an id-field.
|
|
|
|
|
// normally it should never happen, because we do not
|
|
|
|
|
// display the permalink button in such cases.
|
|
|
|
|
if (row.rowId === undefined) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// get explore state, add log-row-id and make timerange absolute
|
|
|
|
|
const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!); |
|
|
|
|
urlState.panelsState = { |
|
|
|
|
...panelState, |
|
|
|
|
logs: { id: row.uid, visualisationType: visualisationType ?? getDefaultVisualisationType(), displayedFields }, |
|
|
|
|
}; |
|
|
|
|
urlState.range = getLogsPermalinkRange(row, logRows, absoluteRange); |
|
|
|
|
|
|
|
|
|
// append changed urlState to baseUrl
|
|
|
|
|
const serializedState = serializeStateToUrlParam(urlState); |
|
|
|
|
const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)![0]; |
|
|
|
|
const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState }); |
|
|
|
|
await createAndCopyShortLink(url); |
|
|
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_permalink_clicked', { |
|
|
|
|
datasourceType: row.datasourceType ?? 'unknown', |
|
|
|
|
logRowUid: row.uid, |
|
|
|
|
logRowLevel: row.logLevel, |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
// get explore state, add log-row-id and make timerange absolute
|
|
|
|
|
const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!); |
|
|
|
|
urlState.panelsState = { |
|
|
|
|
...panelState, |
|
|
|
|
logs: { id: row.uid, visualisationType: visualisationType ?? getDefaultVisualisationType(), displayedFields }, |
|
|
|
|
}; |
|
|
|
|
urlState.range = getLogsPermalinkRange(row, logRows, absoluteRange); |
|
|
|
|
|
|
|
|
|
// append changed urlState to baseUrl
|
|
|
|
|
const serializedState = serializeStateToUrlParam(urlState); |
|
|
|
|
const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)![0]; |
|
|
|
|
const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState }); |
|
|
|
|
await createAndCopyShortLink(url); |
|
|
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_permalink_clicked', { |
|
|
|
|
datasourceType: row.datasourceType ?? 'unknown', |
|
|
|
|
logRowUid: row.uid, |
|
|
|
|
logRowLevel: row.logLevel, |
|
|
|
|
}); |
|
|
|
|
}, |
|
|
|
|
[absoluteRange, displayedFields, exploreId, logRows, panelState, visualisationType] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const scrollToTopLogs = useCallback(() => { |
|
|
|
|
if (config.featureToggles.logsInfiniteScrolling) { |
|
|
|
@ -697,55 +693,62 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
topLogsRef.current?.scrollIntoView(); |
|
|
|
|
}, [logsContainerRef, topLogsRef]); |
|
|
|
|
|
|
|
|
|
const onPinToContentOutlineClick = (row: LogRowModel, allowUnPin = true) => { |
|
|
|
|
if (getPinnedLogsCount() === PINNED_LOGS_LIMIT && !allowUnPin) { |
|
|
|
|
contentOutlineTrackPinLimitReached(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const onPinToContentOutlineClick = useCallback( |
|
|
|
|
(row: LogRowModel, allowUnPin = true) => { |
|
|
|
|
if (getPinnedLogsCount() === PINNED_LOGS_LIMIT && !allowUnPin) { |
|
|
|
|
contentOutlineTrackPinLimitReached(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// find the Logs parent item
|
|
|
|
|
const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root'); |
|
|
|
|
// find the Logs parent item
|
|
|
|
|
const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root'); |
|
|
|
|
|
|
|
|
|
//update the parent's expanded state
|
|
|
|
|
if (logsParent && updateItem) { |
|
|
|
|
updateItem(logsParent.id, { expanded: true }); |
|
|
|
|
} |
|
|
|
|
//update the parent's expanded state
|
|
|
|
|
if (logsParent && updateItem) { |
|
|
|
|
updateItem(logsParent.id, { expanded: true }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const alreadyPinned = pinnedLogs?.find((pin) => pin === row.rowId); |
|
|
|
|
if (alreadyPinned && row.rowId && allowUnPin) { |
|
|
|
|
unregister?.(row.rowId); |
|
|
|
|
contentOutlineTrackPinRemoved(); |
|
|
|
|
} else if (getPinnedLogsCount() !== PINNED_LOGS_LIMIT && !alreadyPinned) { |
|
|
|
|
register?.({ |
|
|
|
|
id: row.rowId, |
|
|
|
|
icon: 'gf-logs', |
|
|
|
|
title: PINNED_LOGS_TITLE, |
|
|
|
|
panelId: PINNED_LOGS_PANELID, |
|
|
|
|
level: 'child', |
|
|
|
|
ref: null, |
|
|
|
|
color: LogLevelColor[row.logLevel], |
|
|
|
|
childOnTop: true, |
|
|
|
|
onClick: () => { |
|
|
|
|
onOpenContext(row, () => {}); |
|
|
|
|
contentOutlineTrackPinClicked(); |
|
|
|
|
}, |
|
|
|
|
onRemove: (id: string) => { |
|
|
|
|
unregister?.(id); |
|
|
|
|
contentOutlineTrackUnpinClicked(); |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
contentOutlineTrackPinAdded(); |
|
|
|
|
} |
|
|
|
|
const alreadyPinned = pinnedLogs?.find((pin) => pin === row.rowId); |
|
|
|
|
if (alreadyPinned && row.rowId && allowUnPin) { |
|
|
|
|
unregister?.(row.rowId); |
|
|
|
|
contentOutlineTrackPinRemoved(); |
|
|
|
|
} else if (getPinnedLogsCount() !== PINNED_LOGS_LIMIT && !alreadyPinned) { |
|
|
|
|
register?.({ |
|
|
|
|
id: row.rowId, |
|
|
|
|
icon: 'gf-logs', |
|
|
|
|
title: PINNED_LOGS_TITLE, |
|
|
|
|
panelId: PINNED_LOGS_PANELID, |
|
|
|
|
level: 'child', |
|
|
|
|
ref: null, |
|
|
|
|
color: LogLevelColor[row.logLevel], |
|
|
|
|
childOnTop: true, |
|
|
|
|
onClick: () => { |
|
|
|
|
onOpenContext(row, () => {}); |
|
|
|
|
contentOutlineTrackPinClicked(); |
|
|
|
|
}, |
|
|
|
|
onRemove: (id: string) => { |
|
|
|
|
unregister?.(id); |
|
|
|
|
contentOutlineTrackUnpinClicked(); |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
contentOutlineTrackPinAdded(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
props.onPinLineCallback?.(); |
|
|
|
|
}; |
|
|
|
|
onPinLineCallback?.(); |
|
|
|
|
}, |
|
|
|
|
[getPinnedLogsCount, onOpenContext, onPinLineCallback, outlineItems, pinnedLogs, register, unregister, updateItem] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const hasUnescapedContent = checkUnescapedContent(logRows); |
|
|
|
|
const filteredLogs = filterRows(logRows, hiddenLogLevels); |
|
|
|
|
const { dedupedRows, dedupCount } = dedupRows(filteredLogs, dedupStrategy); |
|
|
|
|
const navigationRange = createNavigationRange(logRows); |
|
|
|
|
const infiniteScrollAvailable = !logsQueries?.some( |
|
|
|
|
(query) => 'direction' in query && query.direction === LokiQueryDirection.Scan |
|
|
|
|
const hasUnescapedContent = useMemo(() => checkUnescapedContent(logRows), [logRows]); |
|
|
|
|
const filteredLogs = useMemo(() => filterRows(logRows, hiddenLogLevels), [hiddenLogLevels, logRows]); |
|
|
|
|
const { dedupedRows, dedupCount } = useMemo( |
|
|
|
|
() => dedupRows(filteredLogs, dedupStrategy), |
|
|
|
|
[dedupStrategy, filteredLogs] |
|
|
|
|
); |
|
|
|
|
const navigationRange = useMemo(() => createNavigationRange(logRows), [logRows]); |
|
|
|
|
const infiniteScrollAvailable = useMemo( |
|
|
|
|
() => !logsQueries?.some((query) => 'direction' in query && query.direction === LokiQueryDirection.Scan), |
|
|
|
|
[logsQueries] |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
@ -938,58 +941,61 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
{visualisationType === 'logs' && hasData && ( |
|
|
|
|
{visualisationType === 'logs' && ( |
|
|
|
|
<div |
|
|
|
|
className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows} |
|
|
|
|
data-testid="logRows" |
|
|
|
|
ref={onLogsContainerRef} |
|
|
|
|
ref={logsContainerRef} |
|
|
|
|
> |
|
|
|
|
<InfiniteScroll |
|
|
|
|
loading={loading} |
|
|
|
|
loadMoreLogs={infiniteScrollAvailable ? loadMoreLogs : undefined} |
|
|
|
|
range={props.range} |
|
|
|
|
timeZone={timeZone} |
|
|
|
|
rows={logRows} |
|
|
|
|
scrollElement={logsContainerRef.current} |
|
|
|
|
sortOrder={logsSortOrder} |
|
|
|
|
app={CoreApp.Explore} |
|
|
|
|
> |
|
|
|
|
<LogRows |
|
|
|
|
pinnedLogs={pinnedLogs} |
|
|
|
|
logRows={logRows} |
|
|
|
|
deduplicatedRows={dedupedRows} |
|
|
|
|
dedupStrategy={dedupStrategy} |
|
|
|
|
onClickFilterLabel={onClickFilterLabel} |
|
|
|
|
onClickFilterOutLabel={onClickFilterOutLabel} |
|
|
|
|
showContextToggle={showContextToggle} |
|
|
|
|
getRowContextQuery={getRowContextQuery} |
|
|
|
|
showLabels={showLabels} |
|
|
|
|
showTime={showTime} |
|
|
|
|
enableLogDetails={true} |
|
|
|
|
forceEscape={forceEscape} |
|
|
|
|
wrapLogMessage={wrapLogMessage} |
|
|
|
|
prettifyLogMessage={prettifyLogMessage} |
|
|
|
|
{hasData && ( |
|
|
|
|
<InfiniteScroll |
|
|
|
|
loading={loading} |
|
|
|
|
loadMoreLogs={infiniteScrollAvailable ? loadMoreLogs : undefined} |
|
|
|
|
range={props.range} |
|
|
|
|
timeZone={timeZone} |
|
|
|
|
getFieldLinks={getFieldLinks} |
|
|
|
|
logsSortOrder={logsSortOrder} |
|
|
|
|
displayedFields={displayedFields} |
|
|
|
|
onClickShowField={showField} |
|
|
|
|
onClickHideField={hideField} |
|
|
|
|
rows={logRows} |
|
|
|
|
scrollElement={logsContainerRef.current} |
|
|
|
|
sortOrder={logsSortOrder} |
|
|
|
|
app={CoreApp.Explore} |
|
|
|
|
onLogRowHover={onLogRowHover} |
|
|
|
|
onOpenContext={onOpenContext} |
|
|
|
|
onPermalinkClick={onPermalinkClick} |
|
|
|
|
permalinkedRowId={panelState?.logs?.id} |
|
|
|
|
scrollIntoView={scrollIntoView} |
|
|
|
|
isFilterLabelActive={props.isFilterLabelActive} |
|
|
|
|
containerRendered={!!logsContainerRef} |
|
|
|
|
onClickFilterString={props.onClickFilterString} |
|
|
|
|
onClickFilterOutString={props.onClickFilterOutString} |
|
|
|
|
onUnpinLine={onPinToContentOutlineClick} |
|
|
|
|
onPinLine={onPinToContentOutlineClick} |
|
|
|
|
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} |
|
|
|
|
/> |
|
|
|
|
</InfiniteScroll> |
|
|
|
|
> |
|
|
|
|
<LogRows |
|
|
|
|
pinnedLogs={pinnedLogs} |
|
|
|
|
logRows={logRows} |
|
|
|
|
deduplicatedRows={dedupedRows} |
|
|
|
|
dedupStrategy={dedupStrategy} |
|
|
|
|
onClickFilterLabel={onClickFilterLabel} |
|
|
|
|
onClickFilterOutLabel={onClickFilterOutLabel} |
|
|
|
|
showContextToggle={showContextToggle} |
|
|
|
|
getRowContextQuery={getRowContextQuery} |
|
|
|
|
showLabels={showLabels} |
|
|
|
|
showTime={showTime} |
|
|
|
|
enableLogDetails={true} |
|
|
|
|
forceEscape={forceEscape} |
|
|
|
|
wrapLogMessage={wrapLogMessage} |
|
|
|
|
prettifyLogMessage={prettifyLogMessage} |
|
|
|
|
timeZone={timeZone} |
|
|
|
|
getFieldLinks={getFieldLinks} |
|
|
|
|
logsSortOrder={logsSortOrder} |
|
|
|
|
displayedFields={displayedFields} |
|
|
|
|
onClickShowField={showField} |
|
|
|
|
onClickHideField={hideField} |
|
|
|
|
app={CoreApp.Explore} |
|
|
|
|
onLogRowHover={onLogRowHover} |
|
|
|
|
onOpenContext={onOpenContext} |
|
|
|
|
onPermalinkClick={onPermalinkClick} |
|
|
|
|
permalinkedRowId={panelState?.logs?.id} |
|
|
|
|
scrollIntoView={scrollIntoView} |
|
|
|
|
isFilterLabelActive={props.isFilterLabelActive} |
|
|
|
|
scrollElement={logsContainerRef.current} |
|
|
|
|
onClickFilterString={props.onClickFilterString} |
|
|
|
|
onClickFilterOutString={props.onClickFilterOutString} |
|
|
|
|
onUnpinLine={onPinToContentOutlineClick} |
|
|
|
|
onPinLine={onPinToContentOutlineClick} |
|
|
|
|
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} |
|
|
|
|
renderPreview |
|
|
|
|
/> |
|
|
|
|
</InfiniteScroll> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
{!loading && !hasData && !scanning && ( |
|
|
|
@ -1098,21 +1104,21 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n |
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => { |
|
|
|
|
const checkUnescapedContent = (logRows: LogRowModel[]) => { |
|
|
|
|
return logRows.some((r) => r.hasUnescapedContent); |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const dedupRows = memoizeOne((logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => { |
|
|
|
|
const dedupRows = (logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => { |
|
|
|
|
const dedupedRows = dedupLogRows(logRows, dedupStrategy); |
|
|
|
|
const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0); |
|
|
|
|
return { dedupedRows, dedupCount }; |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const filterRows = memoizeOne((logRows: LogRowModel[], hiddenLogLevels: LogLevel[]) => { |
|
|
|
|
const filterRows = (logRows: LogRowModel[], hiddenLogLevels: LogLevel[]) => { |
|
|
|
|
return filterLogLevels(logRows, new Set(hiddenLogLevels)); |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const createNavigationRange = memoizeOne((logRows: LogRowModel[]): { from: number; to: number } | undefined => { |
|
|
|
|
const createNavigationRange = (logRows: LogRowModel[]): { from: number; to: number } | undefined => { |
|
|
|
|
if (!logRows || logRows.length === 0) { |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
@ -1124,4 +1130,4 @@ const createNavigationRange = memoizeOne((logRows: LogRowModel[]): { from: numbe |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return { from: firstTimeStamp, to: lastTimeStamp }; |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|