From ae63c67bc6b2c94bdd8f6450f56d4bcef91e95f4 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 16 Jun 2023 14:07:51 +0200 Subject: [PATCH] Logs: Add permalink to log lines (#69464) * create explore panel state for logs * add props to LogRows and unify * pass properties from explore to logs * add css * implement button and scrolling * export and use `getUrlStateFromPaneState` * make `scrollIntoView` optional * change state handling for permalinks * change link icon * removed unused state * add tests for `LogRowMessage` * remove unused prop * fix name * reorg component * add `LogRow` tests * add test for `Logs` * Update public/app/features/logs/components/LogRow.test.tsx Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * Update public/app/features/explore/Logs/Logs.test.tsx Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * improve types in test * fix props export in Logs.tsx * fix props export in LogRowMessage.tsx * fix props export in LogRow.tsx * fixed import * fix theme import * remove hidden style * add better test names * change to `log line` rather logline Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * fix tooltips * remove unused css --------- Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> --- packages/grafana-data/src/types/explore.ts | 5 + public/app/features/explore/Explore.tsx | 1 + .../app/features/explore/Logs/Logs.test.tsx | 62 +++++++++++- public/app/features/explore/Logs/Logs.tsx | 51 ++++++++++ .../features/explore/Logs/LogsContainer.tsx | 6 ++ .../features/explore/hooks/useStateSync.ts | 2 +- .../features/logs/components/LogRow.test.tsx | 97 +++++++++++++++++++ .../app/features/logs/components/LogRow.tsx | 55 +++++++++-- .../logs/components/LogRowMessage.test.tsx | 86 ++++++++++++++++ .../logs/components/LogRowMessage.tsx | 16 ++- .../app/features/logs/components/LogRows.tsx | 91 +++++++---------- .../logs/components/getLogRowStyles.ts | 22 ++++- 12 files changed, 419 insertions(+), 75 deletions(-) create mode 100644 public/app/features/logs/components/LogRow.test.tsx create mode 100644 public/app/features/logs/components/LogRowMessage.test.tsx diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index 0bb4e8fdf0ba5..136d57ffaa147 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -15,12 +15,17 @@ export interface ExploreUrlState { export interface ExplorePanelsState extends Partial> { trace?: ExploreTracePanelState; + logs?: ExploreLogsPanelState; } export interface ExploreTracePanelState { spanId?: string; } +export interface ExploreLogsPanelState { + id?: string; +} + export interface SplitOpenOptions { datasourceUid: string; /** @deprecated Will be removed in a future version. Use queries instead. */ diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 543e7feb58e33..1c728611220f8 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -347,6 +347,7 @@ export class Explore extends React.PureComponent { onStopScanning={this.onStopScanning} eventBus={this.logsEventBus} splitOpenFn={this.onSplitOpen('logs')} + scrollElement={this.scrollElement} /> ); } diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index f7295da241df9..e79059e8c9ca0 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -1,21 +1,41 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React from 'react'; +import React, { ComponentProps } from 'react'; -import { LoadingState, LogLevel, LogRowModel, MutableDataFrame, toUtc, EventBusSrv } from '@grafana/data'; +import { + EventBusSrv, + ExploreLogsPanelState, + LoadingState, + LogLevel, + LogRowModel, + MutableDataFrame, + toUtc, +} from '@grafana/data'; import { ExploreId } from 'app/types'; import { Logs } from './Logs'; +const changePanelState = jest.fn(); +jest.mock('../state/explorePane', () => ({ + ...jest.requireActual('../state/explorePane'), + changePanelState: (exploreId: ExploreId, panel: 'logs', panelState: {} | ExploreLogsPanelState) => { + return changePanelState(exploreId, panel, panelState); + }, +})); + describe('Logs', () => { - const setup = (logs?: LogRowModel[]) => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const getComponent = (partialProps?: Partial>, logs?: LogRowModel[]) => { const rows = [ makeLog({ uid: '1', timeEpochMs: 1 }), makeLog({ uid: '2', timeEpochMs: 2 }), makeLog({ uid: '3', timeEpochMs: 3 }), ]; - return render( + return ( undefined} @@ -41,9 +61,13 @@ describe('Logs', () => { return []; }} eventBus={new EventBusSrv()} + {...partialProps} /> ); }; + const setup = (partialProps?: Partial>, logs?: LogRowModel[]) => { + return render(getComponent(partialProps, logs)); + }; it('should render logs', () => { setup(); @@ -55,7 +79,7 @@ describe('Logs', () => { }); it('should render no logs found', () => { - setup([]); + setup({}, []); expect(screen.getByText(/no logs found\./i)).toBeInTheDocument(); expect( @@ -192,6 +216,34 @@ describe('Logs', () => { expect(logRows[0].textContent).toContain('log message 1'); expect(logRows[2].textContent).toContain('log message 3'); }); + + describe('for permalinking', () => { + it('should dispatch a `changePanelState` event without the id', () => { + const panelState = { logs: { id: '1' } }; + const { rerender } = setup({ loading: false, panelState }); + + rerender(getComponent({ loading: true, exploreId: ExploreId.right, panelState })); + rerender(getComponent({ loading: false, exploreId: ExploreId.right, panelState })); + + expect(changePanelState).toHaveBeenCalledWith(ExploreId.right, 'logs', { logs: {} }); + }); + + it('should scroll the scrollElement into view if rows contain id', () => { + const panelState = { logs: { id: '3' } }; + const scrollElementMock = { scroll: jest.fn() }; + setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState }); + + expect(scrollElementMock.scroll).toHaveBeenCalled(); + }); + + it('should not scroll the scrollElement into view if rows does not contain id', () => { + const panelState = { logs: { id: 'not-included' } }; + const scrollElementMock = { scroll: jest.fn() }; + setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState }); + + expect(scrollElementMock.scroll).not.toHaveBeenCalled(); + }); + }); }); const makeLog = (overrides: Partial): LogRowModel => { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 7782d901dc7a2..2867ef4f52239 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -26,6 +26,9 @@ import { DataHoverClearEvent, EventBus, LogRowContextOptions, + ExplorePanelsState, + serializeStateToUrlParam, + urlUtil, } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; @@ -41,10 +44,14 @@ import { } from '@grafana/ui'; import { dedupLogRows, filterLogLevels } from 'app/core/logsModel'; import store from 'app/core/store'; +import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; +import { getState, dispatch } from 'app/store/store'; import { ExploreId } from 'app/types/explore'; import { LogRows } from '../../logs/components/LogRows'; import { LogRowContextModal } from '../../logs/components/log-context/LogRowContextModal'; +import { getUrlStateFromPaneState } from '../hooks/useStateSync'; +import { changePanelState } from '../state/explorePane'; import { LogsMetaRow } from './LogsMetaRow'; import LogsNavigation from './LogsNavigation'; @@ -85,6 +92,8 @@ interface Props extends Themeable2 { addResultsToCache: () => void; clearCache: () => void; eventBus: EventBus; + panelState?: ExplorePanelsState; + scrollElement?: HTMLDivElement; } interface State { @@ -158,6 +167,18 @@ class UnthemedLogs extends PureComponent { } } + componentDidUpdate(prevProps: Readonly): void { + if (this.props.loading && !prevProps.loading && this.props.panelState?.logs?.id) { + // loading stopped, so we need to remove any permalinked log lines + delete this.props.panelState.logs.id; + dispatch( + changePanelState(this.props.exploreId, 'logs', { + ...this.props.panelState, + }) + ); + } + } + onLogRowHover = (row?: LogRowModel) => { if (!row) { this.props.eventBus.publish(new DataHoverClearEvent()); @@ -332,6 +353,33 @@ class UnthemedLogs extends PureComponent { }; }; + onPermalinkClick = async (row: LogRowModel) => { + // get explore state, add log-row-id and make timerange absolute + const urlState = getUrlStateFromPaneState(getState().explore.panes[this.props.exploreId]!); + urlState.panelsState = { ...this.props.panelState, logs: { id: row.uid } }; + urlState.range = { + from: new Date(this.props.absoluteRange.from).toISOString(), + to: new Date(this.props.absoluteRange.to).toISOString(), + }; + + // 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); + }; + + scrollIntoView = (element: HTMLElement) => { + const { scrollElement } = this.props; + + if (scrollElement) { + scrollElement.scroll({ + behavior: 'smooth', + top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, + }); + } + }; + checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => { return !!logRows.some((r) => r.hasUnescapedContent); }); @@ -557,6 +605,9 @@ class UnthemedLogs extends PureComponent { app={CoreApp.Explore} onLogRowHover={this.onLogRowHover} onOpenContext={this.onOpenContext} + onPermalinkClick={this.onPermalinkClick} + permalinkedRowId={this.props.panelState?.logs?.id} + scrollIntoView={this.scrollIntoView} /> {!loading && !hasData && !scanning && (
diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index c91410d29fec4..b39c4df0a23d7 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -50,6 +50,7 @@ interface LogsContainerProps extends PropsFromRedux { onStopScanning: () => void; eventBus: EventBus; splitOpenFn: SplitOpen; + scrollElement?: HTMLDivElement; } class LogsContainer extends PureComponent { @@ -144,6 +145,7 @@ class LogsContainer extends PureComponent { addResultsToCache, clearCache, logsVolume, + scrollElement, } = this.props; if (!logRows) { @@ -206,6 +208,8 @@ class LogsContainer extends PureComponent { addResultsToCache={() => addResultsToCache(exploreId)} clearCache={() => clearCache(exploreId)} eventBus={this.props.eventBus} + panelState={this.props.panelState} + scrollElement={scrollElement} /> @@ -228,6 +232,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI absoluteRange, supplementaryQueries, } = item; + const panelState = item.panelsState; const timeZone = getTimeZone(state.user); const logsVolume = supplementaryQueries[SupplementaryQueryType.LogsVolume]; @@ -247,6 +252,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI range, absoluteRange, logsVolume, + panelState, }; } diff --git a/public/app/features/explore/hooks/useStateSync.ts b/public/app/features/explore/hooks/useStateSync.ts index 40c5bbb0f7232..bb622b37b94d9 100644 --- a/public/app/features/explore/hooks/useStateSync.ts +++ b/public/app/features/explore/hooks/useStateSync.ts @@ -366,7 +366,7 @@ const urlDiff = ( }; }; -function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState { +export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState { return { // datasourceInstance should not be undefined anymore here but in case there is some path for it to be undefined // lets just fallback instead of crashing. diff --git a/public/app/features/logs/components/LogRow.test.tsx b/public/app/features/logs/components/LogRow.test.tsx new file mode 100644 index 0000000000000..71af96525b841 --- /dev/null +++ b/public/app/features/logs/components/LogRow.test.tsx @@ -0,0 +1,97 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React, { ComponentProps } from 'react'; +import tinycolor from 'tinycolor2'; + +import { CoreApp, createTheme, LogLevel, LogRowModel } from '@grafana/data'; + +import { LogRow } from './LogRow'; +import { createLogRow } from './__mocks__/logRow'; +import { getLogRowStyles } from './getLogRowStyles'; + +const theme = createTheme(); +const styles = getLogRowStyles(theme); +const setup = (propOverrides?: Partial>, rowOverrides?: Partial) => { + const props: ComponentProps = { + row: createLogRow({ + entry: 'test123', + uid: 'log-row-id', + logLevel: LogLevel.error, + timeEpochMs: 1546297200000, + ...rowOverrides, + }), + enableLogDetails: false, + getRows: () => [], + onOpenContext: () => {}, + prettifyLogMessage: false, + app: CoreApp.Explore, + showDuplicates: false, + showLabels: false, + showTime: false, + wrapLogMessage: false, + timeZone: 'utc', + styles, + ...(propOverrides || {}), + }; + + const { container } = render( + + + + +
+ ); + + return { props, container }; +}; + +describe('LogRow', () => { + it('renders row entry', () => { + setup(); + expect(screen.queryByText('test123')).toBeInTheDocument(); + }); + + describe('with permalinking', () => { + it('highlights row with same permalink-id', () => { + const { container } = setup({ permalinkedRowId: 'log-row-id' }); + const row = container.querySelector('tr'); + expect(row).toHaveStyle( + `background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}` + ); + }); + + it('does not highlight row details with different permalink-id', async () => { + const { container } = setup({ permalinkedRowId: 'log-row-id', enableLogDetails: true }); + const row = container.querySelector('tr'); + await userEvent.click(row!); + const allRows = container.querySelectorAll('tr'); + + expect(row).toHaveStyle( + `background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}` + ); + expect(allRows[allRows.length - 1]).not.toHaveStyle( + `background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}` + ); + }); + + it('not highlights row with different permalink-id', () => { + const { container } = setup({ permalinkedRowId: 'wrong-log-row-id' }); + const row = container.querySelector('tr'); + expect(row).not.toHaveStyle( + `background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}` + ); + }); + + it('calls `scrollIntoView` if permalink matches', () => { + const scrollIntoView = jest.fn(); + setup({ permalinkedRowId: 'log-row-id', scrollIntoView }); + expect(scrollIntoView).toHaveBeenCalled(); + }); + + it('not calls `scrollIntoView` if permalink does not match', () => { + const scrollIntoView = jest.fn(); + setup({ permalinkedRowId: 'wrong-log-row-id', scrollIntoView }); + expect(scrollIntoView).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index cd57a02aa00ed..f270e5f4c7ac4 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -38,11 +38,14 @@ interface Props extends Themeable2 { onClickHideField?: (key: string) => void; onLogRowHover?: (row?: LogRowModel) => void; onOpenContext: (row: LogRowModel, onClose: () => void) => void; + onPermalinkClick?: (row: LogRowModel) => Promise; styles: LogRowStyles; + permalinkedRowId?: string; + scrollIntoView?: (element: HTMLElement) => void; } interface State { - showContext: boolean; + highlightBackround: boolean; showDetails: boolean; } @@ -55,17 +58,23 @@ interface State { */ class UnThemedLogRow extends PureComponent { state: State = { - showContext: false, + highlightBackround: false, showDetails: false, }; + logLineRef: React.RefObject; + + constructor(props: Props) { + super(props); + this.logLineRef = React.createRef(); + } // we are debouncing the state change by 3 seconds to highlight the logline after the context closed. debouncedContextClose = debounce(() => { - this.setState({ showContext: false }); + this.setState({ highlightBackround: false }); }, 3000); onOpenContext = (row: LogRowModel) => { - this.setState({ showContext: true }); + this.setState({ highlightBackround: true }); this.props.onOpenContext(row, this.debouncedContextClose); }; @@ -107,6 +116,32 @@ class UnThemedLogRow extends PureComponent { } }; + componentDidMount() { + this.scrollToLogRow(this.state, true); + } + + componentDidUpdate(_: Props, prevState: State) { + this.scrollToLogRow(prevState); + } + + scrollToLogRow = (prevState: State, mounted = false) => { + if (this.props.permalinkedRowId !== this.props.row.uid) { + // only set the new state if the row is not permalinked anymore or if the component was mounted. + if (prevState.highlightBackround || mounted) { + this.setState({ highlightBackround: false }); + } + return; + } + + // at this point this row is the permalinked row, so we need to scroll to it and highlight it if possible. + if (this.logLineRef.current && this.props.scrollIntoView) { + this.props.scrollIntoView(this.logLineRef.current); + } + if (!this.state.highlightBackround) { + this.setState({ highlightBackround: true }); + } + }; + render() { const { getRows, @@ -129,12 +164,16 @@ class UnThemedLogRow extends PureComponent { app, styles, } = this.props; - const { showDetails, showContext } = this.state; + const { showDetails, highlightBackround } = this.state; const levelStyles = getLogLevelStyles(theme, row.logLevel); const { errorMessage, hasError } = checkLogsError(row); const logRowBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, - [styles.contextBackground]: showContext, + [styles.highlightBackground]: highlightBackround, + }); + const logRowDetailsBackground = cx(styles.logsRow, { + [styles.errorLogRow]: hasError, + [styles.highlightBackground]: highlightBackround && !this.state.showDetails, }); const processedRow = @@ -145,6 +184,7 @@ class UnThemedLogRow extends PureComponent { return ( <> { wrapLogMessage={wrapLogMessage} prettifyLogMessage={prettifyLogMessage} onOpenContext={this.onOpenContext} + onPermalinkClick={this.props.onPermalinkClick} app={app} styles={styles} /> @@ -194,7 +235,7 @@ class UnThemedLogRow extends PureComponent { {this.state.showDetails && ( >, rowOverrides?: Partial) => { + const theme = createTheme(); + const styles = getLogRowStyles(theme); + const props: ComponentProps = { + wrapLogMessage: false, + row: createLogRow({ entry: 'test123', logLevel: LogLevel.error, timeEpochMs: 1546297200000, ...rowOverrides }), + onOpenContext: () => {}, + prettifyLogMessage: false, + app: CoreApp.Explore, + styles, + ...(propOverrides || {}), + }; + + render( + + + + + + +
+ ); + + return props; +}; + +describe('LogRowMessage', () => { + it('renders row entry', () => { + setup(); + expect(screen.queryByText('test123')).toBeInTheDocument(); + }); + + describe('with show context', () => { + it('should show context button', () => { + setup({ showContextToggle: () => true }); + expect(screen.queryByLabelText('Show context')).toBeInTheDocument(); + }); + + it('should not show context button', () => { + setup({ showContextToggle: () => false }); + expect(screen.queryByLabelText('Show context')).not.toBeInTheDocument(); + }); + + it('should call `onOpenContext` with row on click', async () => { + const showContextToggle = jest.fn(); + const props = setup({ showContextToggle: () => true, onOpenContext: showContextToggle }); + const button = screen.getByLabelText('Show context'); + + await userEvent.click(button); + + expect(showContextToggle).toHaveBeenCalledWith(props.row); + }); + }); + + describe('with permalinking', () => { + it('should show permalinking button when no `onPermalinkClick` is defined', () => { + setup({ onPermalinkClick: jest.fn() }); + expect(screen.queryByLabelText('Copy shortlink')).toBeInTheDocument(); + }); + + it('should not show permalinking button when `onPermalinkClick` is defined', () => { + setup(); + expect(screen.queryByLabelText('Copy shortlink')).not.toBeInTheDocument(); + }); + + it('should call `onPermalinkClick` with row on click', async () => { + const permalinkClick = jest.fn(); + const props = setup({ onPermalinkClick: permalinkClick }); + const button = screen.getByLabelText('Copy shortlink'); + + await userEvent.click(button); + + expect(permalinkClick).toHaveBeenCalledWith(props.row); + }); + }); +}); diff --git a/public/app/features/logs/components/LogRowMessage.tsx b/public/app/features/logs/components/LogRowMessage.tsx index e5014e1c36535..5f1213354d7a7 100644 --- a/public/app/features/logs/components/LogRowMessage.tsx +++ b/public/app/features/logs/components/LogRowMessage.tsx @@ -18,6 +18,7 @@ interface Props { app?: CoreApp; showContextToggle?: (row?: LogRowModel) => boolean; onOpenContext: (row: LogRowModel) => void; + onPermalinkClick?: (row: LogRowModel) => Promise; styles: LogRowStyles; } @@ -76,7 +77,7 @@ export class LogRowMessage extends PureComponent { }; render() { - const { row, wrapLogMessage, prettifyLogMessage, showContextToggle, styles } = this.props; + const { row, wrapLogMessage, prettifyLogMessage, showContextToggle, styles, onPermalinkClick } = this.props; const { hasAnsi, raw } = row; const restructuredEntry = restructureLog(raw, prettifyLogMessage); const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false; @@ -108,6 +109,7 @@ export class LogRowMessage extends PureComponent { onClick={this.onShowContextClick} tooltip="Show context" tooltipPlacement="top" + aria-label="Show context" /> )} { fill="text" size="md" getText={this.getLogText} - tooltip="Copy" + tooltip="Copy to clipboard" tooltipPlacement="top" /> + {onPermalinkClick && row.uid && ( + onPermalinkClick(row)} + /> + )} diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 9cc934fcd4ee0..67497540bc7be 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -44,6 +44,9 @@ export interface Props extends Themeable2 { onClickHideField?: (key: string) => void; onLogRowHover?: (row?: LogRowModel) => void; onOpenContext?: (row: LogRowModel, onClose: () => void) => void; + onPermalinkClick?: (row: LogRowModel) => Promise; + permalinkedRowId?: string; + scrollIntoView?: (element: HTMLElement) => void; } interface State { @@ -139,66 +142,40 @@ class UnThemedLogRows extends PureComponent { // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead const getRows = this.makeGetRows(orderedRows); + const getLogRowProperties = (row: LogRowModel) => { + return { + getRows: getRows, + row: row, + showContextToggle: showContextToggle, + showDuplicates: showDuplicates, + showLabels: showLabels, + showTime: showTime, + displayedFields: displayedFields, + wrapLogMessage: wrapLogMessage, + prettifyLogMessage: prettifyLogMessage, + timeZone: timeZone, + enableLogDetails: enableLogDetails, + onClickFilterLabel: onClickFilterLabel, + onClickFilterOutLabel: onClickFilterOutLabel, + onClickShowField: onClickShowField, + onClickHideField: onClickHideField, + getFieldLinks: getFieldLinks, + logsSortOrder: logsSortOrder, + forceEscape: forceEscape, + onOpenContext: this.openContext, + onLogRowHover: onLogRowHover, + app: app, + styles: styles, + onPermalinkClick: this.props.onPermalinkClick, + scrollIntoView: this.props.scrollIntoView, + permalinkedRowId: this.props.permalinkedRowId, + }; + }; return ( - {hasData && - firstRows.map((row, index) => ( - - ))} - {hasData && - renderAll && - lastRows.map((row, index) => ( - - ))} + {hasData && firstRows.map((row) => )} + {hasData && renderAll && lastRows.map((row) => )} {hasData && !renderAll && ( diff --git a/public/app/features/logs/components/getLogRowStyles.ts b/public/app/features/logs/components/getLogRowStyles.ts index b35ac2738d6a1..f83afd0a4a03a 100644 --- a/public/app/features/logs/components/getLogRowStyles.ts +++ b/public/app/features/logs/components/getLogRowStyles.ts @@ -74,8 +74,8 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { width: 100%; ${!scrollableLogsContainer && `margin-bottom: ${theme.spacing(2.25)};`} `, - contextBackground: css` - background: ${hoverBgColor}; + highlightBackground: css` + background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()}; `, logsRow: css` label: logs-row; @@ -234,6 +234,7 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { margin-left: 0px; `, rowMenu: css` + label: rowMenu; display: flex; flex-wrap: nowrap; flex-direction: row; @@ -245,10 +246,14 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { bottom: auto; background: ${theme.colors.background.primary}; box-shadow: ${theme.shadows.z3}; - padding: ${theme.spacing(0.5, 0.5, 0.5, 1)}; + padding: ${theme.spacing(0.5, 1, 0.5, 1)}; z-index: 100; visibility: hidden; gap: ${theme.spacing(0.5)}; + + & > button { + margin: 0; + } `, logRowMenuCell: css` position: sticky; @@ -286,6 +291,17 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { background-color: ${theme.colors.background.primary}; } `, + visibleRowMenu: css` + label: visibleRowMenu; + aspect-ratio: 1/1; + z-index: 90; + `, + linkButton: css` + label: linkButton; + > button { + padding-top: ${theme.spacing(0.5)}; + } + `, }; });
Rendering {orderedRows.length - previewLimit!} rows...