From 1367d5d72134761b683c1613a6bee04d346ae8be Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:14:53 -0500 Subject: [PATCH] Logs: Add log line to content outline when clicking on datalinks (#90207) * feat: add bg color to pinned logs, pin logs when opening datalinks --- .../ContentOutlineAnalyticEvents.ts | 44 ++++++ .../ContentOutline/ContentOutlineContext.tsx | 8 +- public/app/features/explore/Logs/Logs.tsx | 133 ++++++++++-------- .../features/logs/components/LogDetails.tsx | 11 +- .../logs/components/LogDetailsRow.test.tsx | 33 ++++- .../logs/components/LogDetailsRow.tsx | 37 ++++- .../app/features/logs/components/LogRow.tsx | 24 ++-- .../app/features/logs/components/LogRows.tsx | 10 +- 8 files changed, 216 insertions(+), 84 deletions(-) create mode 100644 public/app/features/explore/ContentOutline/ContentOutlineAnalyticEvents.ts diff --git a/public/app/features/explore/ContentOutline/ContentOutlineAnalyticEvents.ts b/public/app/features/explore/ContentOutline/ContentOutlineAnalyticEvents.ts new file mode 100644 index 00000000000..308203694da --- /dev/null +++ b/public/app/features/explore/ContentOutline/ContentOutlineAnalyticEvents.ts @@ -0,0 +1,44 @@ +import { LogLevel } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; + +export function contentOutlineTrackPinAdded() { + reportInteraction('explore_toolbar_contentoutline_clicked', { + item: 'section', + type: 'Logs:pinned:pinned-log-added', + }); +} + +export function contentOutlineTrackPinRemoved() { + reportInteraction('explore_toolbar_contentoutline_clicked', { + item: 'section', + type: 'Logs:pinned:pinned-log-deleted', + }); +} + +export function contentOutlineTrackPinLimitReached() { + reportInteraction('explore_toolbar_contentoutline_clicked', { + item: 'section', + type: 'Logs:pinned:pinned-log-limit-reached', + }); +} + +export function contentOutlineTrackPinClicked() { + reportInteraction('explore_toolbar_contentoutline_clicked', { + item: 'section', + type: 'Logs:pinned:pinned-log-clicked', + }); +} + +export function contentOutlineTrackUnpinClicked() { + reportInteraction('explore_toolbar_contentoutline_clicked', { + item: 'section', + type: 'Logs:pinned:pinned-log-deleted', + }); +} + +export function contentOutlineTrackLevelFilter(level: { levelStr: string; logLevel: LogLevel }) { + reportInteraction('explore_toolbar_contentoutline_clicked', { + item: 'section', + type: `Logs:filter:${level.levelStr}`, + }); +} diff --git a/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx b/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx index d2189f88e1a..5ddbe135e9b 100644 --- a/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx +++ b/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx @@ -1,5 +1,6 @@ import { uniqueId } from 'lodash'; import { useState, useContext, createContext, ReactNode, useCallback, useRef, useEffect } from 'react'; +import { SetOptional } from 'type-fest'; import { ContentOutlineItemBaseProps, ITEM_TYPES } from './ContentOutlineItem'; @@ -10,7 +11,7 @@ export interface ContentOutlineItemContextProps extends ContentOutlineItemBasePr children?: ContentOutlineItemContextProps[]; } -type RegisterFunction = (outlineItem: Omit) => string; +type RegisterFunction = (outlineItem: SetOptional) => string; export interface ContentOutlineContextProps { outlineItems: ContentOutlineItemContextProps[]; @@ -44,7 +45,10 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }: const parentlessItemsRef = useRef({}); const register: RegisterFunction = useCallback((outlineItem) => { - const id = uniqueId(`${outlineItem.panelId}-${outlineItem.title}-${outlineItem.icon}_`); + // Allow the caller to define unique ID so the outlineItem can be differentiated + const id = outlineItem.id + ? outlineItem.id + : uniqueId(`${outlineItem.panelId}-${outlineItem.title}-${outlineItem.icon}_`); setOutlineItems((prevItems) => { if (outlineItem.level === 'root') { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 6cce1574c5c..5143293d345 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -61,6 +61,14 @@ import { getLogLevel, getLogLevelFromKey, getLogLevelInfo } from 'app/features/l import { getState } from 'app/store/store'; import { ExploreItemState, useDispatch } from 'app/types'; +import { + contentOutlineTrackLevelFilter, + contentOutlineTrackPinAdded, + contentOutlineTrackPinClicked, + contentOutlineTrackPinLimitReached, + contentOutlineTrackPinRemoved, + contentOutlineTrackUnpinClicked, +} from '../ContentOutline/ContentOutlineAnalyticEvents'; import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext'; import { getUrlStateFromPaneState } from '../hooks/useStateSync'; import { changePanelState } from '../state/explorePane'; @@ -145,7 +153,10 @@ const getDefaultVisualisationType = (): LogsVisualisationType => { return 'logs'; }; -const PINNED_LOGS_LIMIT = 3; +const PINNED_LOGS_LIMIT = 10; +const PINNED_LOGS_TITLE = 'Pinned log'; +const PINNED_LOGS_MESSAGE = 'Pin to content outline'; +const PINNED_LOGS_PANELID = 'Logs'; const UnthemedLogs: React.FunctionComponent = (props: Props) => { const { @@ -194,7 +205,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { const [forceEscape, setForceEscape] = useState(false); const [contextOpen, setContextOpen] = useState(false); const [contextRow, setContextRow] = useState(undefined); - const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState('Pin to content outline'); + const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState(PINNED_LOGS_MESSAGE); const [visualisationType, setVisualisationType] = useState( panelState?.logs?.visualisationType ?? getDefaultVisualisationType() ); @@ -215,6 +226,17 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { const hasData = logRows && logRows.length > 0; const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; + // 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 getPinnedLogsCount = useCallback(() => { + const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root'); + 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); @@ -228,7 +250,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { // clean up all current log levels if (unregisterAllChildren) { unregisterAllChildren((items) => { - const logsParent = items?.find((item) => item.panelId === 'Logs' && item.level === 'root'); + const logsParent = items?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root'); return logsParent?.id; }, 'filter'); } @@ -256,16 +278,13 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { register({ title: level.levelStr, icon: 'gf-logs', - panelId: 'Logs', + panelId: PINNED_LOGS_PANELID, level: 'child', type: 'filter', highlight: currentLevelSelected && !allLevelsSelected, onClick: (e: React.MouseEvent) => { toggleLegendRef.current?.(level.levelStr, mapMouseEventToMode(e)); - reportInteraction('explore_toolbar_contentoutline_clicked', { - item: 'section', - type: `Logs:filter:${level.levelStr}`, - }); + contentOutlineTrackLevelFilter(level); }, ref: null, color: LogLevelColor[level.logLevel], @@ -275,6 +294,21 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { } }, [logsVolumeData?.data, unregisterAllChildren, logsVolumeEnabled, hiddenLogLevels, register, toggleLegendRef]); + useEffect(() => { + if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) { + setPinLineButtonTooltipTitle( + + ❗️ + + Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another. + + + ); + } else { + setPinLineButtonTooltipTitle(PINNED_LOGS_MESSAGE); + } + }, [outlineItems, getPinnedLogsCount]); + useEffect(() => { if (loading && !previousLoading && panelState?.logs?.id) { // loading stopped, so we need to remove any permalinked log lines @@ -652,70 +686,47 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { topLogsRef.current?.scrollIntoView(); }, [logsContainerRef, topLogsRef]); - const onPinToContentOutlineClick = (row: LogRowModel) => { - if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) { - setPinLineButtonTooltipTitle( - - ❗️ - - Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another. - - - ); - - reportInteraction('explore_toolbar_contentoutline_clicked', { - item: 'section', - type: 'Logs:pinned:pinned-log-limit-reached', - }); + const onPinToContentOutlineClick = (row: LogRowModel, allowUnPin = true) => { + if (getPinnedLogsCount() === PINNED_LOGS_LIMIT && !allowUnPin) { + contentOutlineTrackPinLimitReached(); return; } // find the Logs parent item - const logsParent = outlineItems?.find((item) => item.panelId === 'Logs' && item.level === 'root'); + 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 }); } - register?.({ - icon: 'gf-logs', - title: 'Pinned log', - panelId: 'Logs', - level: 'child', - ref: null, - color: LogLevelColor[row.logLevel], - childOnTop: true, - onClick: () => { - onOpenContext(row, () => {}); - reportInteraction('explore_toolbar_contentoutline_clicked', { - item: 'section', - type: 'Logs:pinned:pinned-log-clicked', - }); - }, - onRemove: (id: string) => { - unregister?.(id); - if (getPinnedLogsCount() < PINNED_LOGS_LIMIT) { - setPinLineButtonTooltipTitle('Pin to content outline'); - } - reportInteraction('explore_toolbar_contentoutline_clicked', { - item: 'section', - type: 'Logs:pinned:pinned-log-deleted', - }); - }, - }); + 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?.(); - - reportInteraction('explore_toolbar_contentoutline_clicked', { - item: 'section', - type: 'Logs:pinned:pinned-log-added', - }); - }; - - const getPinnedLogsCount = () => { - const logsParent = outlineItems?.find((item) => item.panelId === 'Logs' && item.level === 'root'); - return logsParent?.children?.filter((child) => child.title === 'Pinned log').length ?? 0; }; const hasUnescapedContent = checkUnescapedContent(logRows); @@ -929,6 +940,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { sortOrder={logsSortOrder} > = (props: Props) => { containerRendered={!!logsContainerRef} onClickFilterString={props.onClickFilterString} onClickFilterOutString={props.onClickFilterOutString} + onUnpinLine={onPinToContentOutlineClick} onPinLine={onPinToContentOutlineClick} pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} /> diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index 9f2ebf15858..4d99054d1d9 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -2,7 +2,7 @@ import { cx } from '@emotion/css'; import { PureComponent } from 'react'; import { CoreApp, DataFrame, DataFrameType, Field, LinkModel, LogRowModel } from '@grafana/data'; -import { Themeable2, withTheme2 } from '@grafana/ui'; +import { PopoverContent, Themeable2, withTheme2 } from '@grafana/ui'; import { calculateLogsLabelStats, calculateStats } from '../utils'; @@ -27,6 +27,9 @@ export interface Props extends Themeable2 { onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; + + onPinLine?: (row: LogRowModel) => void; + pinLineButtonTooltipTitle?: PopoverContent; } class UnThemedLogDetails extends PureComponent { @@ -46,7 +49,9 @@ class UnThemedLogDetails extends PureComponent { displayedFields, getFieldLinks, wrapLogMessage, + onPinLine, styles, + pinLineButtonTooltipTitle, } = this.props; const levelStyles = getLogLevelStyles(theme, row.logLevel); const labels = row.labels ? row.labels : {}; @@ -151,6 +156,8 @@ class UnThemedLogDetails extends PureComponent { links={links} onClickShowField={onClickShowField} onClickHideField={onClickHideField} + onPinLine={onPinLine} + pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values)} displayedFields={displayedFields} wrapLogMessage={wrapLogMessage} @@ -170,6 +177,8 @@ class UnThemedLogDetails extends PureComponent { links={links} onClickShowField={onClickShowField} onClickHideField={onClickHideField} + onPinLine={onPinLine} + pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values)} displayedFields={displayedFields} wrapLogMessage={wrapLogMessage} diff --git a/public/app/features/logs/components/LogDetailsRow.test.tsx b/public/app/features/logs/components/LogDetailsRow.test.tsx index 1629efecc0e..5f3229255b3 100644 --- a/public/app/features/logs/components/LogDetailsRow.test.tsx +++ b/public/app/features/logs/components/LogDetailsRow.test.tsx @@ -1,7 +1,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { ComponentProps } from 'react'; -import { CoreApp } from '@grafana/data'; +import { CoreApp, FieldType, LinkModel } from '@grafana/data'; +import { Field } from '@grafana/data/'; import { LogDetailsRow } from './LogDetailsRow'; import { createLogRow } from './__mocks__/logRow'; @@ -147,4 +148,34 @@ describe('LogDetailsRow', () => { // Asserting visibility on mouse-over is currently not possible. }); }); + + describe('datalinks', () => { + it('datalinks should pin and call the original link click', () => { + const onLinkClick = jest.fn(); + const onPinLine = jest.fn(); + const links: Array> = [ + { + onClick: onLinkClick, + href: '#', + title: 'Hello link', + target: '_self', + origin: { + name: 'name', + type: FieldType.string, + config: {}, + values: ['string'], + }, + }, + ]; + setup({ links, onPinLine }); + + expect(onLinkClick).not.toHaveBeenCalled(); + expect(onPinLine).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole('button', { name: 'Hello link' })); + + expect(onLinkClick).toHaveBeenCalled(); + expect(onPinLine).toHaveBeenCalled(); + }); + }); }); diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index 44db6bb81ad..2bf94eabf05 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -15,7 +15,7 @@ import { LogRowModel, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { ClipboardButton, DataLinkButton, IconButton, Themeable2, withTheme2 } from '@grafana/ui'; +import { ClipboardButton, DataLinkButton, IconButton, PopoverContent, Themeable2, withTheme2 } from '@grafana/ui'; import { logRowToSingleRowDataFrame } from '../logsModel'; @@ -38,6 +38,8 @@ export interface Props extends Themeable2 { row: LogRowModel; app?: CoreApp; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; + onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void; + pinLineButtonTooltipTitle?: PopoverContent; } interface State { @@ -263,6 +265,8 @@ class UnThemedLogDetailsRow extends PureComponent { disableActions, row, app, + onPinLine, + pinLineButtonTooltipTitle, } = this.props; const { showFieldsStats, fieldStats, fieldCount } = this.state; const styles = getStyles(theme); @@ -324,11 +328,32 @@ class UnThemedLogDetailsRow extends PureComponent { {singleVal ? parsedValues[0] : this.generateMultiVal(parsedValues, true)} {singleVal && this.generateClipboardButton(parsedValues[0])}
- {links?.map((link, i) => ( - - - - ))} + {links?.map((link, i) => { + if (link.onClick && onPinLine) { + const originalOnClick = link.onClick; + link.onClick = (e, origin) => { + // Pin the line + onPinLine(row, false); + + // Execute the link onClick function + originalOnClick(e, origin); + }; + } + return ( + + + + ); + })}
diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index 79af86f3144..c8311c308d0 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -1,24 +1,24 @@ import { cx } from '@emotion/css'; import { debounce } from 'lodash'; import memoizeOne from 'memoize-one'; -import { PureComponent, MouseEvent } from 'react'; import * as React from 'react'; +import { MouseEvent, PureComponent } from 'react'; import { - Field, - LinkModel, - LogRowModel, - LogsSortOrder, - dateTimeFormat, CoreApp, DataFrame, + dateTimeFormat, + Field, + LinkModel, LogRowContextOptions, + LogRowModel, + LogsSortOrder, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; -import { withTheme2, Themeable2, Icon, Tooltip, PopoverContent } from '@grafana/ui'; +import { Icon, PopoverContent, Themeable2, Tooltip, withTheme2 } from '@grafana/ui'; -import { checkLogsError, escapeUnescapedString, checkLogsSampled } from '../utils'; +import { checkLogsError, checkLogsSampled, escapeUnescapedString } from '../utils'; import { LogDetails } from './LogDetails'; import { LogLabels } from './LogLabels'; @@ -59,7 +59,7 @@ interface Props extends Themeable2 { permalinkedRowId?: string; scrollIntoView?: (element: HTMLElement) => void; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; - onPinLine?: (row: LogRowModel) => void; + onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void; onUnpinLine?: (row: LogRowModel) => void; pinLineButtonTooltipTitle?: PopoverContent; pinned?: boolean; @@ -219,14 +219,16 @@ class UnThemedLogRow extends PureComponent { app, styles, getRowContextQuery, + pinned, } = this.props; + const { showDetails, showingContext, permalinked } = this.state; const levelStyles = getLogLevelStyles(theme, row.logLevel); const { errorMessage, hasError } = checkLogsError(row); const { sampleMessage, isSampled } = checkLogsSampled(row); const logRowBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, - [styles.highlightBackground]: showingContext || permalinked, + [styles.highlightBackground]: showingContext || permalinked || pinned, }); const logRowDetailsBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, @@ -327,6 +329,7 @@ class UnThemedLogRow extends PureComponent { {this.state.showDetails && ( { app={app} styles={styles} isFilterLabelActive={this.props.isFilterLabelActive} + pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle} /> )} diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index a267ee9ef32..6902d2175aa 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -48,7 +48,7 @@ export interface Props extends Themeable2 { getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; - onPinLine?: (row: LogRowModel) => void; + onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void; onUnpinLine?: (row: LogRowModel) => void; pinLineButtonTooltipTitle?: PopoverContent; onLogRowHover?: (row?: LogRowModel) => void; @@ -63,6 +63,7 @@ export interface Props extends Themeable2 { scrollIntoView?: (element: HTMLElement) => void; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; pinnedRowId?: string; + pinnedLogs?: string[]; containerRendered?: boolean; /** * If false or undefined, the `contain:strict` css property will be added to the wrapping `` for performance reasons. @@ -191,7 +192,8 @@ class UnThemedLogRows extends PureComponent { ); render() { - const { deduplicatedRows, logRows, dedupStrategy, theme, logsSortOrder, previewLimit, ...rest } = this.props; + const { deduplicatedRows, logRows, dedupStrategy, theme, logsSortOrder, previewLimit, pinnedLogs, ...rest } = + this.props; const { renderAll } = this.state; const styles = getLogRowStyles(theme); const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows; @@ -241,7 +243,7 @@ class UnThemedLogRows extends PureComponent { onPinLine={this.props.onPinLine} onUnpinLine={this.props.onUnpinLine} pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle} - pinned={this.props.pinnedRowId === row.uid} + pinned={this.props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)} isFilterLabelActive={this.props.isFilterLabelActive} handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined} {...rest} @@ -264,7 +266,7 @@ class UnThemedLogRows extends PureComponent { onPinLine={this.props.onPinLine} onUnpinLine={this.props.onUnpinLine} pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle} - pinned={this.props.pinnedRowId === row.uid} + pinned={this.props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)} isFilterLabelActive={this.props.isFilterLabelActive} handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined} {...rest}