mirror of https://github.com/grafana/grafana.git
				
				
				
			Logs: Add log line to content outline when clicking on datalinks (#90207)
* feat: add bg color to pinned logs, pin logs when opening datalinks
This commit is contained in:
		
							parent
							
								
									2d35b11323
								
							
						
					
					
						commit
						1367d5d721
					
				|  | @ -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}`, | ||||
|   }); | ||||
| } | ||||
|  | @ -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<ContentOutlineItemContextProps, 'id'>) => string; | ||||
| type RegisterFunction = (outlineItem: SetOptional<ContentOutlineItemContextProps, 'id'>) => string; | ||||
| 
 | ||||
| export interface ContentOutlineContextProps { | ||||
|   outlineItems: ContentOutlineItemContextProps[]; | ||||
|  | @ -44,7 +45,10 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }: | |||
|   const parentlessItemsRef = useRef<ParentlessItems>({}); | ||||
| 
 | ||||
|   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') { | ||||
|  |  | |||
|  | @ -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: Props) => { | ||||
|   const { | ||||
|  | @ -194,7 +205,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { | |||
|   const [forceEscape, setForceEscape] = useState<boolean>(false); | ||||
|   const [contextOpen, setContextOpen] = useState<boolean>(false); | ||||
|   const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined); | ||||
|   const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>('Pin to content outline'); | ||||
|   const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>(PINNED_LOGS_MESSAGE); | ||||
|   const [visualisationType, setVisualisationType] = useState<LogsVisualisationType | undefined>( | ||||
|     panelState?.logs?.visualisationType ?? getDefaultVisualisationType() | ||||
|   ); | ||||
|  | @ -215,6 +226,17 @@ const UnthemedLogs: React.FunctionComponent<Props> = (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: 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: 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: Props) => { | |||
|     } | ||||
|   }, [logsVolumeData?.data, unregisterAllChildren, logsVolumeEnabled, hiddenLogLevels, register, toggleLegendRef]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) { | ||||
|       setPinLineButtonTooltipTitle( | ||||
|         <span style={{ display: 'flex', textAlign: 'center' }}> | ||||
|           ❗️ | ||||
|           <Trans i18nKey="explore.logs.maximum-pinned-logs"> | ||||
|             Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another. | ||||
|           </Trans> | ||||
|         </span> | ||||
|       ); | ||||
|     } 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: Props) => { | |||
|     topLogsRef.current?.scrollIntoView(); | ||||
|   }, [logsContainerRef, topLogsRef]); | ||||
| 
 | ||||
|   const onPinToContentOutlineClick = (row: LogRowModel) => { | ||||
|     if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) { | ||||
|       setPinLineButtonTooltipTitle( | ||||
|         <span style={{ display: 'flex', textAlign: 'center' }}> | ||||
|           ❗️ | ||||
|           <Trans i18nKey="explore.logs.maximum-pinned-logs"> | ||||
|             Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another. | ||||
|           </Trans> | ||||
|         </span> | ||||
|       ); | ||||
| 
 | ||||
|       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: Props) => { | |||
|                 sortOrder={logsSortOrder} | ||||
|               > | ||||
|                 <LogRows | ||||
|                   pinnedLogs={pinnedLogs} | ||||
|                   logRows={logRows} | ||||
|                   deduplicatedRows={dedupedRows} | ||||
|                   dedupStrategy={dedupStrategy} | ||||
|  | @ -958,6 +970,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { | |||
|                   containerRendered={!!logsContainerRef} | ||||
|                   onClickFilterString={props.onClickFilterString} | ||||
|                   onClickFilterOutString={props.onClickFilterOutString} | ||||
|                   onUnpinLine={onPinToContentOutlineClick} | ||||
|                   onPinLine={onPinToContentOutlineClick} | ||||
|                   pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} | ||||
|                 /> | ||||
|  |  | |||
|  | @ -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<boolean>; | ||||
| 
 | ||||
|   onPinLine?: (row: LogRowModel) => void; | ||||
|   pinLineButtonTooltipTitle?: PopoverContent; | ||||
| } | ||||
| 
 | ||||
| class UnThemedLogDetails extends PureComponent<Props> { | ||||
|  | @ -46,7 +49,9 @@ class UnThemedLogDetails extends PureComponent<Props> { | |||
|       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<Props> { | |||
|                       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<Props> { | |||
|                       links={links} | ||||
|                       onClickShowField={onClickShowField} | ||||
|                       onClickHideField={onClickHideField} | ||||
|                       onPinLine={onPinLine} | ||||
|                       pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} | ||||
|                       getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values)} | ||||
|                       displayedFields={displayedFields} | ||||
|                       wrapLogMessage={wrapLogMessage} | ||||
|  |  | |||
|  | @ -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<LinkModel<Field>> = [ | ||||
|         { | ||||
|           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(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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<boolean>; | ||||
|   onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void; | ||||
|   pinLineButtonTooltipTitle?: PopoverContent; | ||||
| } | ||||
| 
 | ||||
| interface State { | ||||
|  | @ -263,6 +265,8 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> { | |||
|       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<Props, State> { | |||
|               {singleVal ? parsedValues[0] : this.generateMultiVal(parsedValues, true)} | ||||
|               {singleVal && this.generateClipboardButton(parsedValues[0])} | ||||
|               <div className={cx((singleVal || isMultiParsedValueWithNoContent) && styles.adjoiningLinkButton)}> | ||||
|                 {links?.map((link, i) => ( | ||||
|                   <span key={`${link.title}-${i}`}> | ||||
|                     <DataLinkButton link={link} /> | ||||
|                   </span> | ||||
|                 ))} | ||||
|                 {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 ( | ||||
|                     <span key={`${link.title}-${i}`}> | ||||
|                       <DataLinkButton | ||||
|                         buttonProps={{ | ||||
|                           // Show tooltip message if max number of pinned lines has been reached
 | ||||
|                           tooltip: | ||||
|                             typeof pinLineButtonTooltipTitle === 'object' && link.onClick | ||||
|                               ? pinLineButtonTooltipTitle | ||||
|                               : undefined, | ||||
|                         }} | ||||
|                         link={link} | ||||
|                       /> | ||||
|                     </span> | ||||
|                   ); | ||||
|                 })} | ||||
|               </div> | ||||
|             </div> | ||||
|           </td> | ||||
|  |  | |||
|  | @ -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<boolean>; | ||||
|   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<Props, State> { | |||
|       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<Props, State> { | |||
|         </tr> | ||||
|         {this.state.showDetails && ( | ||||
|           <LogDetails | ||||
|             onPinLine={this.props.onPinLine} | ||||
|             className={logRowDetailsBackground} | ||||
|             showDuplicates={showDuplicates} | ||||
|             getFieldLinks={getFieldLinks} | ||||
|  | @ -342,6 +345,7 @@ class UnThemedLogRow extends PureComponent<Props, State> { | |||
|             app={app} | ||||
|             styles={styles} | ||||
|             isFilterLabelActive={this.props.isFilterLabelActive} | ||||
|             pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle} | ||||
|           /> | ||||
|         )} | ||||
|       </> | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ export interface Props extends Themeable2 { | |||
|   getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>; | ||||
|   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<boolean>; | ||||
|   pinnedRowId?: string; | ||||
|   pinnedLogs?: string[]; | ||||
|   containerRendered?: boolean; | ||||
|   /** | ||||
|    * If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` for performance reasons. | ||||
|  | @ -191,7 +192,8 @@ class UnThemedLogRows extends PureComponent<Props, State> { | |||
|   ); | ||||
| 
 | ||||
|   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<Props, State> { | |||
|                   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<Props, State> { | |||
|                   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} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue