diff --git a/public/app/features/explore/Graph/ExploreGraph.tsx b/public/app/features/explore/Graph/ExploreGraph.tsx index e23effa8af8..cd69a11af37 100644 --- a/public/app/features/explore/Graph/ExploreGraph.tsx +++ b/public/app/features/explore/Graph/ExploreGraph.tsx @@ -58,7 +58,7 @@ interface Props { thresholdsStyle?: GraphThresholdsStyleConfig; eventBus: EventBus; vizLegendOverrides?: Partial; - toggleLegendRef?: React.MutableRefObject<(name: string, mode: SeriesVisibilityChangeMode) => void>; + toggleLegendRef?: React.MutableRefObject<(name: string | undefined, mode: SeriesVisibilityChangeMode) => void>; } export function ExploreGraph({ @@ -141,27 +141,27 @@ export function ExploreGraph({ const structureRev = useStructureRev(dataWithConfig); - const onHiddenSeriesChangedRef = useRef(onHiddenSeriesChanged); const previousHiddenFrames = useRef(undefined); useEffect(() => { - if (onHiddenSeriesChangedRef.current) { - const hiddenFrames: string[] = []; - dataWithConfig.forEach((frame) => { - const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity); - if (allFieldsHidden) { - hiddenFrames.push(getFrameDisplayName(frame)); - } - }); - if ( - previousHiddenFrames.current === undefined || - !isEqual(sortBy(hiddenFrames), sortBy(previousHiddenFrames.current)) - ) { - previousHiddenFrames.current = hiddenFrames; - onHiddenSeriesChangedRef.current(hiddenFrames); - } + if (!onHiddenSeriesChanged) { + return; } - }, [dataWithConfig]); + const hiddenFrames: string[] = []; + dataWithConfig.forEach((frame) => { + const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity); + if (allFieldsHidden) { + hiddenFrames.push(getFrameDisplayName(frame)); + } + }); + if ( + previousHiddenFrames.current === undefined || + !isEqual(sortBy(hiddenFrames), sortBy(previousHiddenFrames.current)) + ) { + previousHiddenFrames.current = hiddenFrames; + onHiddenSeriesChanged(hiddenFrames); + } + }, [dataWithConfig, onHiddenSeriesChanged]); const panelContext: PanelContext = { eventsScope: 'explore', @@ -174,7 +174,14 @@ export function ExploreGraph({ dataLinkPostProcessor, }; - function toggleLegend(name: string, mode: SeriesVisibilityChangeMode) { + function toggleLegend(name: string | undefined, mode: SeriesVisibilityChangeMode) { + if (!name) { + setFieldConfig({ + ...fieldConfig, + overrides: [], + }); + return; + } setFieldConfig(seriesVisibilityConfigFactory(name, mode, fieldConfig, data)); } diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 65f958c71de..acaa32584c1 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -21,8 +21,6 @@ import { TimeRange, LogsDedupStrategy, LogsSortOrder, - LogLevel, - DataTopic, CoreApp, LogsDedupDescription, rangeUtil, @@ -31,10 +29,11 @@ import { DataHoverEvent, serializeStateToUrlParam, urlUtil, + LogLevel, } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema'; +import { DataQuery, DataTopic } from '@grafana/schema'; import { Button, InlineField, @@ -47,7 +46,6 @@ import { Themeable2, withTheme2, } from '@grafana/ui'; -import { mapMouseEventToMode } from '@grafana/ui/internal'; import store from 'app/core/store'; import { createAndCopyShortLink, getLogsPermalinkRange } from 'app/core/utils/shortLinks'; import { ControlledLogRows } from 'app/features/logs/components/ControlledLogRows'; @@ -56,7 +54,7 @@ import { LogRows } from 'app/features/logs/components/LogRows'; import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal'; import { LogList, LogListControlOptions } from 'app/features/logs/components/panel/LogList'; import { isDedupStrategy, isLogsSortOrder } from 'app/features/logs/components/panel/LogListContext'; -import { LogLevelColor, dedupLogRows, filterLogLevels } from 'app/features/logs/logsModel'; +import { LogLevelColor, dedupLogRows } from 'app/features/logs/logsModel'; import { getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils'; @@ -65,7 +63,6 @@ import { getState } from 'app/store/store'; import { ExploreItemState, useDispatch } from 'app/types'; import { - contentOutlineTrackLevelFilter, contentOutlineTrackPinAdded, contentOutlineTrackPinClicked, contentOutlineTrackPinLimitReached, @@ -202,7 +199,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { store.getBool(SETTINGS_KEYS.prettifyLogMessage, false) ); const [dedupStrategy, setDedupStrategy] = useState(LogsDedupStrategy.none); - const [hiddenLogLevels, setHiddenLogLevels] = useState([]); const [logsSortOrder, setLogsSortOrder] = useState( store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending ); @@ -219,12 +215,12 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { const previousLoading = usePrevious(loading); const logsVolumeEventBus = eventBus.newScopedBus('logsvolume', { onlyLocal: false }); - const { register, unregister, outlineItems, updateItem, unregisterAllChildren } = useContentOutlineContext() ?? {}; + const { register, unregister, outlineItems, updateItem } = useContentOutlineContext() ?? {}; const flipOrderTimer = useRef(undefined); const cancelFlippingTimer = useRef(undefined); - const toggleLegendRef = useRef<(name: string, mode: SeriesVisibilityChangeMode) => void>(() => {}); + const toggleLegendRef = useRef<(name: string | undefined, mode: SeriesVisibilityChangeMode) => void>(() => {}); const topLogsRef = useRef(null); - const logLevelsRef = useRef(null); + const [filterLevels, setFilterLevels] = useState(undefined); const tableHeight = getLogsTableHeight(); const setWrapperLineWrapStyles = wrapLogMessage || visualisationType === 'table'; @@ -247,62 +243,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { return logsParent?.children?.filter((child) => child.title === PINNED_LOGS_TITLE).length ?? 0; }, [outlineItems]); - const registerLogLevelsWithContentOutline = useCallback(() => { - const levelsArr = Object.keys(LogLevelColor); - const logVolumeDataFrames = new Set(logsVolumeData?.data); - // TODO remove this once filtering multiple log volumes is supported - const logVolData = logsVolumeData?.data.filter( - (frame: DataFrame) => frame.meta?.dataTopic !== DataTopic.Annotations - ); - const grouped = groupBy(logVolData, 'meta.custom.datasourceName'); - const numberOfLogVolumes = Object.keys(grouped).length; - - // clean up all current log levels - if (unregisterAllChildren) { - unregisterAllChildren((items) => { - const logsParent = items?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root'); - return logsParent?.id; - }, 'filter'); - } - - // check if we have dataFrames that return the same level - const logLevelsArray: LogLevel[] = []; - logVolumeDataFrames.forEach((dataFrame) => { - const { level } = getLogLevelInfo(dataFrame, logsVolumeData?.data ?? []); - logLevelsArray.push(getLogLevelFromKey(level)); - }); - - const sortedLLArray = logLevelsArray.sort((a: string, b: string) => - levelsArr.indexOf(a) > levelsArr.indexOf(b) ? 1 : -1 - ); - - const logLevels = new Set(sortedLLArray); - logLevelsRef.current = Array.from(logLevels); - - if (logLevels.size > 1 && logsVolumeEnabled && numberOfLogVolumes === 1) { - logLevels.forEach((level) => { - const allLevelsSelected = hiddenLogLevels.length === 0; - const currentLevelSelected = !hiddenLogLevels.find((hiddenLevel) => hiddenLevel === level); - if (register) { - register({ - title: level, - icon: 'gf-logs', - panelId: PINNED_LOGS_PANELID, - level: 'child', - type: 'filter', - highlight: currentLevelSelected && !allLevelsSelected, - onClick: (e: React.MouseEvent) => { - toggleLegendRef.current?.(level, mapMouseEventToMode(e)); - contentOutlineTrackLevelFilter(level); - }, - ref: null, - color: LogLevelColor[level], - }); - } - }); - } - }, [logsVolumeData?.data, unregisterAllChildren, logsVolumeEnabled, hiddenLogLevels, register, toggleLegendRef]); - useEffect(() => { if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) { setPinLineButtonTooltipTitle( @@ -348,10 +288,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { setDisplayedFields(displayedFields); }, [panelState?.logs?.displayedFields]); - useEffect(() => { - registerLogLevelsWithContentOutline(); - }, [logsVolumeData?.data, hiddenLogLevels, registerLogLevelsWithContentOutline]); - useUnmount(() => { if (flipOrderTimer) { window.clearTimeout(flipOrderTimer.current); @@ -564,11 +500,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { } }, []); - const onToggleLogLevel = useCallback((hiddenRawLevels: string[]) => { - const hiddenLogLevels = hiddenRawLevels.map(getLogLevelFromKey); - setHiddenLogLevels(hiddenLogLevels); - }, []); - const onToggleLogsVolumeCollapse = useCallback( (collapsed: boolean) => { props.onSetLogsVolumeEnabled(!collapsed); @@ -758,60 +689,81 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const hasUnescapedContent = useMemo(() => checkUnescapedContent(logRows), [logRows]); - const filteredLogs = useMemo(() => filterRows(logRows, hiddenLogLevels), [hiddenLogLevels, logRows]); - const { dedupedRows, dedupCount } = useMemo( - () => dedupRows(filteredLogs, dedupStrategy), - [dedupStrategy, filteredLogs] - ); + const { dedupedRows, dedupCount } = useMemo(() => dedupRows(logRows, dedupStrategy), [dedupStrategy, logRows]); const navigationRange = useMemo(() => createNavigationRange(logRows), [logRows]); const infiniteScrollAvailable = useMemo( () => !logsQueries?.some((query) => 'direction' in query && query.direction === LokiQueryDirection.Scan), [logsQueries] ); + const visibilityChangedRef = useRef(true); const onLogOptionsChange = useCallback( (option: keyof LogListControlOptions, value: string | string[] | boolean) => { if (option === 'sortOrder' && isLogsSortOrder(value)) { sortOrderChanged(value); - } else if (option === 'filterLevels' && Array.isArray(value)) { - if (value.length === 0) { - setHiddenLogLevels([]); - return; - } - const allLevels = logLevelsRef.current ?? Object.keys(LogLevelColor).map(getLogLevelFromKey); - if (!allLevels.length) { - // Logs panel is disabled - return; - } - if (hiddenLogLevels.length === 0) { - toggleLegendRef.current?.(value[0], SeriesVisibilityChangeMode.ToggleSelection); - setHiddenLogLevels(allLevels.filter((level) => level !== value[0])); - return; - } - const appendsLevel = value.find((level) => hiddenLogLevels.includes(getLogLevelFromKey(level))); - const removesLevel = allLevels.find((level) => !value.includes(level) && !hiddenLogLevels.includes(level)); - if (appendsLevel) { - toggleLegendRef.current?.(appendsLevel, SeriesVisibilityChangeMode.AppendToSelection); - setHiddenLogLevels(hiddenLogLevels.filter((hiddenLevel) => hiddenLevel === appendsLevel)); - return; - } else if (removesLevel) { - toggleLegendRef.current?.(removesLevel, SeriesVisibilityChangeMode.AppendToSelection); - setHiddenLogLevels([...hiddenLogLevels, removesLevel]); - } } else if (option === 'dedupStrategy' && isDedupStrategy(value)) { setDedupStrategy(value); + } else if (option === 'filterLevels' && Array.isArray(value) && logsVolumeEnabled) { + const logVolData = + logsVolumeData?.data.filter((frame: DataFrame) => frame.meta?.dataTopic !== DataTopic.Annotations) ?? []; + const grouped = groupBy(logVolData, 'meta.custom.datasourceName'); + const numberOfLogVolumes = Object.keys(grouped).length; + + // Not supported + if (numberOfLogVolumes > 1) { + return; + } + + const logsVolumeLevels = [ + ...new Set( + logVolData.map((dataFrame) => { + const { level } = getLogLevelInfo(dataFrame, logVolData); + return getLogLevelFromKey(level); + }) + ), + ]; + + setFilterLevels((currentFilterLevels: LogLevel[] | undefined) => { + const newLevels = value.map((level) => getLogLevelFromKey(level)); + const newLogsVolumeLevels = newLevels.filter((level) => logsVolumeLevels.includes(level)); + const prevLogsVolumeLevels = currentFilterLevels?.filter((level) => logsVolumeLevels.includes(level)) ?? []; + + if (!newLogsVolumeLevels.length) { + toggleLegendRef.current?.(undefined, SeriesVisibilityChangeMode.ToggleSelection); + return undefined; + } + + const addedLevels = newLogsVolumeLevels.filter((newLevel) => !prevLogsVolumeLevels.includes(newLevel)); + const removedLevels = prevLogsVolumeLevels.filter((prevLevel) => !newLogsVolumeLevels.includes(prevLevel)); + + addedLevels.forEach((level) => { + visibilityChangedRef.current = true; + toggleLegendRef.current?.( + level, + !prevLogsVolumeLevels.length + ? SeriesVisibilityChangeMode.ToggleSelection + : SeriesVisibilityChangeMode.AppendToSelection + ); + }); + removedLevels.forEach((level) => { + visibilityChangedRef.current = true; + toggleLegendRef.current?.(level, SeriesVisibilityChangeMode.AppendToSelection); + }); + + return newLevels; + }); } }, - [hiddenLogLevels, sortOrderChanged] + [logsVolumeData?.data, logsVolumeEnabled, sortOrderChanged] ); - const filterLevels: LogLevel[] | undefined = useMemo( - () => - !logLevelsRef.current || logLevelsRef.current.length === 0 - ? undefined - : logLevelsRef.current.filter((level) => hiddenLogLevels.length > 0 && !hiddenLogLevels.includes(level)), - [hiddenLogLevels] - ); + const onDisplayedSeriesChanged = useCallback((levels: string[]) => { + if (visibilityChangedRef.current) { + visibilityChangedRef.current = false; + return; + } + setFilterLevels(levels.map((level) => getLogLevelFromKey(level))); + }, []); return ( <> @@ -843,7 +795,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { timeZone={timeZone} splitOpen={splitOpen} onLoadLogsVolume={loadLogsVolumeData} - onHiddenSeriesChanged={onToggleLogLevel} + onDisplayedSeriesChanged={onDisplayedSeriesChanged} eventBus={logsVolumeEventBus} onClose={() => onToggleLogsVolumeCollapse(true)} /> @@ -1315,10 +1267,6 @@ const dedupRows = (logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => return { dedupedRows, dedupCount }; }; -const filterRows = (logRows: LogRowModel[], hiddenLogLevels: string[]) => { - return filterLogLevels(logRows, new Set(hiddenLogLevels)); -}; - const createNavigationRange = (logRows: LogRowModel[]): { from: number; to: number } | undefined => { if (!logRows || logRows.length === 0) { return undefined; diff --git a/public/app/features/explore/Logs/LogsVolumePanel.tsx b/public/app/features/explore/Logs/LogsVolumePanel.tsx index 4038350eb19..cc80ac9a073 100644 --- a/public/app/features/explore/Logs/LogsVolumePanel.tsx +++ b/public/app/features/explore/Logs/LogsVolumePanel.tsx @@ -31,7 +31,9 @@ type Props = { onHiddenSeriesChanged: (hiddenSeries: string[]) => void; eventBus: EventBus; annotations: DataFrame[]; - toggleLegendRef?: React.MutableRefObject<(name: string, mode: SeriesVisibilityChangeMode) => void> | undefined; + toggleLegendRef?: + | React.MutableRefObject<(name: string | undefined, mode: SeriesVisibilityChangeMode) => void> + | undefined; }; export function LogsVolumePanel(props: Props) { diff --git a/public/app/features/explore/Logs/LogsVolumePanelList.test.tsx b/public/app/features/explore/Logs/LogsVolumePanelList.test.tsx index de08a6095d3..959fc9fb6a3 100644 --- a/public/app/features/explore/Logs/LogsVolumePanelList.test.tsx +++ b/public/app/features/explore/Logs/LogsVolumePanelList.test.tsx @@ -22,7 +22,7 @@ function renderPanel(logsVolumeData?: DataQueryResponse, onLoadLogsVolume = () = onUpdateTimeRange={() => {}} logsVolumeData={logsVolumeData} onLoadLogsVolume={onLoadLogsVolume} - onHiddenSeriesChanged={() => null} + onDisplayedSeriesChanged={() => null} eventBus={new EventBusSrv()} /> ); diff --git a/public/app/features/explore/Logs/LogsVolumePanelList.tsx b/public/app/features/explore/Logs/LogsVolumePanelList.tsx index 2366b2f5a40..160cb820fe2 100644 --- a/public/app/features/explore/Logs/LogsVolumePanelList.tsx +++ b/public/app/features/explore/Logs/LogsVolumePanelList.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { flatten, groupBy, mapValues, sortBy } from 'lodash'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import * as React from 'react'; import { @@ -10,8 +10,10 @@ import { DataTopic, dateTime, EventBus, + getFrameDisplayName, GrafanaTheme2, LoadingState, + shallowCompare, SplitOpen, TimeRange, TimeZone, @@ -34,10 +36,10 @@ type Props = { width: number; onUpdateTimeRange: (timeRange: AbsoluteTimeRange) => void; onLoadLogsVolume: () => void; - onHiddenSeriesChanged: (hiddenSeries: string[]) => void; + onDisplayedSeriesChanged: (series: string[]) => void; eventBus: EventBus; onClose?(): void; - toggleLegendRef?: React.MutableRefObject<(name: string, mode: SeriesVisibilityChangeMode) => void>; + toggleLegendRef?: React.MutableRefObject<(name: string | undefined, mode: SeriesVisibilityChangeMode) => void>; }; export const LogsVolumePanelList = ({ @@ -46,7 +48,7 @@ export const LogsVolumePanelList = ({ onUpdateTimeRange, width, onLoadLogsVolume, - onHiddenSeriesChanged, + onDisplayedSeriesChanged, eventBus, splitOpen, timeZone, @@ -96,6 +98,25 @@ export const LogsVolumePanelList = ({ const to = dateTime(Math.min(absoluteRange.to, allLogsVolumeMaximumRange.to)); const visibleRange: TimeRange = { from, to, raw: { from, to } }; + const handleHiddenSeriesChanged = useCallback( + (hiddenSeries: string[]) => { + // Not supported + if (numberOfLogVolumes > 1) { + return; + } + const allLevels = [ + ...new Set( + Object.values(logVolumes) + .map((series) => series.map((dataFrame) => getFrameDisplayName(dataFrame))) + .flat() + ), + ]; + const displayedLevels = allLevels.filter((level) => !hiddenSeries.includes(level)); + onDisplayedSeriesChanged(shallowCompare(allLevels, displayedLevels) ? [] : displayedLevels); + }, + [logVolumes, numberOfLogVolumes, onDisplayedSeriesChanged] + ); + if (logsVolumeData?.state === LoadingState.Loading) { return ( @@ -185,7 +206,7 @@ export const LogsVolumePanelList = ({ splitOpen={splitOpen} onLoadLogsVolume={onLoadLogsVolume} // TODO: Support filtering level from multiple log levels - onHiddenSeriesChanged={numberOfLogVolumes > 1 ? () => {} : onHiddenSeriesChanged} + onHiddenSeriesChanged={numberOfLogVolumes > 1 ? () => {} : handleHiddenSeriesChanged} eventBus={eventBus} annotations={annotations} /> diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index 5eb6d8a53bd..c638437aa2a 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -261,10 +261,13 @@ export const LogListContextProvider = ({ if (filterLevels === undefined) { return; } - if (!shallowCompare(logListState.filterLevels, filterLevels)) { - setLogListState({ ...logListState, filterLevels }); - } - }, [filterLevels, logListState]); + setLogListState((logListState) => { + if (!shallowCompare(logListState.filterLevels, filterLevels)) { + return { ...logListState, filterLevels }; + } + return logListState; + }); + }, [filterLevels]); useEffect(() => { setLogListState((logListState) => ({ ...logListState, fontSize })); diff --git a/public/app/features/logs/logsModel.test.ts b/public/app/features/logs/logsModel.test.ts index 62f692e4846..5c62e8a07b4 100644 --- a/public/app/features/logs/logsModel.test.ts +++ b/public/app/features/logs/logsModel.test.ts @@ -31,7 +31,6 @@ import { COMMON_LABELS, dataFrameToLogsModel, dedupLogRows, - filterLogLevels, getSeriesProperties, LIMIT_LABEL, TOTAL_LABEL, @@ -169,63 +168,6 @@ describe('dedupLogRows()', () => { }); }); -describe('filterLogLevels()', () => { - test('should correctly filter out log levels', () => { - const rows = [ - { - entry: 'DEBUG 1', - logLevel: LogLevel.debug, - }, - { - entry: 'ERROR 1', - logLevel: LogLevel.error, - }, - { - entry: 'TRACE 1', - logLevel: LogLevel.trace, - }, - ] as LogRowModel[]; - const filteredLogs = filterLogLevels(rows, new Set([LogLevel.debug])); - expect(filteredLogs.length).toBe(2); - expect(filteredLogs).toEqual([ - { entry: 'ERROR 1', logLevel: 'error' }, - { entry: 'TRACE 1', logLevel: 'trace' }, - ]); - }); - test('should correctly filter out log levels and then deduplicate', () => { - const rows = [ - { - entry: 'DEBUG 1', - logLevel: LogLevel.debug, - }, - { - entry: 'DEBUG 2', - logLevel: LogLevel.debug, - }, - { - entry: 'DEBUG 2', - logLevel: LogLevel.debug, - }, - { - entry: 'ERROR 1', - logLevel: LogLevel.error, - }, - { - entry: 'TRACE 1', - logLevel: LogLevel.trace, - }, - ] as LogRowModel[]; - const filteredLogs = filterLogLevels(rows, new Set([LogLevel.error])); - const deduplicatedLogs = dedupLogRows(filteredLogs, LogsDedupStrategy.exact); - expect(deduplicatedLogs.length).toBe(3); - expect(deduplicatedLogs).toEqual([ - { duplicates: 0, entry: 'DEBUG 1', logLevel: 'debug' }, - { duplicates: 1, entry: 'DEBUG 2', logLevel: 'debug' }, - { duplicates: 0, entry: 'TRACE 1', logLevel: 'trace' }, - ]); - }); -}); - const emptyLogsModel = { hasUniqueLabels: false, rows: [], diff --git a/public/app/features/logs/logsModel.ts b/public/app/features/logs/logsModel.ts index b59479f01ca..bd724ddff16 100644 --- a/public/app/features/logs/logsModel.ts +++ b/public/app/features/logs/logsModel.ts @@ -104,16 +104,6 @@ export function dedupLogRows(rows: LogRowModel[], strategy?: LogsDedupStrategy): }, []); } -export function filterLogLevels(logRows: LogRowModel[], hiddenLogLevels: Set): LogRowModel[] { - if (hiddenLogLevels.size === 0) { - return logRows; - } - - return logRows.filter((row: LogRowModel) => { - return !hiddenLogLevels.has(row.logLevel); - }); -} - interface Series { lastTs: number | null; datapoints: Array<[number, number]>;