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>
This commit is contained in:
Sven Grossmann
2023-06-16 14:07:51 +02:00
committed by GitHub
parent c1fe8e8927
commit 68637059c4
12 changed files with 419 additions and 75 deletions

View File

@@ -15,12 +15,17 @@ export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
export interface ExplorePanelsState extends Partial<Record<PreferredVisualisationType, {}>> {
trace?: ExploreTracePanelState;
logs?: ExploreLogsPanelState;
}
export interface ExploreTracePanelState {
spanId?: string;
}
export interface ExploreLogsPanelState {
id?: string;
}
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
datasourceUid: string;
/** @deprecated Will be removed in a future version. Use queries instead. */

View File

@@ -347,6 +347,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
onStopScanning={this.onStopScanning}
eventBus={this.logsEventBus}
splitOpenFn={this.onSplitOpen('logs')}
scrollElement={this.scrollElement}
/>
);
}

View File

@@ -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<ComponentProps<typeof Logs>>, logs?: LogRowModel[]) => {
const rows = [
makeLog({ uid: '1', timeEpochMs: 1 }),
makeLog({ uid: '2', timeEpochMs: 2 }),
makeLog({ uid: '3', timeEpochMs: 3 }),
];
return render(
return (
<Logs
exploreId={ExploreId.left}
splitOpen={() => undefined}
@@ -41,9 +61,13 @@ describe('Logs', () => {
return [];
}}
eventBus={new EventBusSrv()}
{...partialProps}
/>
);
};
const setup = (partialProps?: Partial<ComponentProps<typeof Logs>>, 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>): LogRowModel => {

View File

@@ -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<Props, State> {
}
}
componentDidUpdate(prevProps: Readonly<Props>): 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<Props, State> {
};
};
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<Props, State> {
app={CoreApp.Explore}
onLogRowHover={this.onLogRowHover}
onOpenContext={this.onOpenContext}
onPermalinkClick={this.onPermalinkClick}
permalinkedRowId={this.props.panelState?.logs?.id}
scrollIntoView={this.scrollIntoView}
/>
{!loading && !hasData && !scanning && (
<div className={styles.noData}>

View File

@@ -50,6 +50,7 @@ interface LogsContainerProps extends PropsFromRedux {
onStopScanning: () => void;
eventBus: EventBus;
splitOpenFn: SplitOpen;
scrollElement?: HTMLDivElement;
}
class LogsContainer extends PureComponent<LogsContainerProps> {
@@ -144,6 +145,7 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
addResultsToCache,
clearCache,
logsVolume,
scrollElement,
} = this.props;
if (!logRows) {
@@ -206,6 +208,8 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
addResultsToCache={() => addResultsToCache(exploreId)}
clearCache={() => clearCache(exploreId)}
eventBus={this.props.eventBus}
panelState={this.props.panelState}
scrollElement={scrollElement}
/>
</LogsCrossFadeTransition>
</>
@@ -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,
};
}

View File

@@ -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.

View File

@@ -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<ComponentProps<typeof LogRow>>, rowOverrides?: Partial<LogRowModel>) => {
const props: ComponentProps<typeof LogRow> = {
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(
<table>
<tbody>
<LogRow {...props} />
</tbody>
</table>
);
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();
});
});
});

View File

@@ -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<void>;
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<Props, State> {
state: State = {
showContext: false,
highlightBackround: false,
showDetails: false,
};
logLineRef: React.RefObject<HTMLTableRowElement>;
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<Props, State> {
}
};
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<Props, State> {
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<Props, State> {
return (
<>
<tr
ref={this.logLineRef}
className={logRowBackground}
onClick={this.toggleDetails}
onMouseEnter={this.onMouseEnter}
@@ -187,6 +227,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
wrapLogMessage={wrapLogMessage}
prettifyLogMessage={prettifyLogMessage}
onOpenContext={this.onOpenContext}
onPermalinkClick={this.props.onPermalinkClick}
app={app}
styles={styles}
/>
@@ -194,7 +235,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
</tr>
{this.state.showDetails && (
<LogDetails
className={logRowBackground}
className={logRowDetailsBackground}
showDuplicates={showDuplicates}
getFieldLinks={getFieldLinks}
onClickFilterLabel={onClickFilterLabel}

View File

@@ -0,0 +1,86 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { ComponentProps } from 'react';
import { CoreApp, createTheme, LogLevel, LogRowModel } from '@grafana/data';
import { LogRowMessage } from './LogRowMessage';
import { createLogRow } from './__mocks__/logRow';
import { getLogRowStyles } from './getLogRowStyles';
const setup = (propOverrides?: Partial<ComponentProps<typeof LogRowMessage>>, rowOverrides?: Partial<LogRowModel>) => {
const theme = createTheme();
const styles = getLogRowStyles(theme);
const props: ComponentProps<typeof LogRowMessage> = {
wrapLogMessage: false,
row: createLogRow({ entry: 'test123', logLevel: LogLevel.error, timeEpochMs: 1546297200000, ...rowOverrides }),
onOpenContext: () => {},
prettifyLogMessage: false,
app: CoreApp.Explore,
styles,
...(propOverrides || {}),
};
render(
<table>
<tbody>
<tr>
<LogRowMessage {...props} />
</tr>
</tbody>
</table>
);
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);
});
});
});

View File

@@ -18,6 +18,7 @@ interface Props {
app?: CoreApp;
showContextToggle?: (row?: LogRowModel) => boolean;
onOpenContext: (row: LogRowModel) => void;
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
styles: LogRowStyles;
}
@@ -76,7 +77,7 @@ export class LogRowMessage extends PureComponent<Props> {
};
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<Props> {
onClick={this.onShowContextClick}
tooltip="Show context"
tooltipPlacement="top"
aria-label="Show context"
/>
)}
<ClipboardButton
@@ -117,9 +119,19 @@ export class LogRowMessage extends PureComponent<Props> {
fill="text"
size="md"
getText={this.getLogText}
tooltip="Copy"
tooltip="Copy to clipboard"
tooltipPlacement="top"
/>
{onPermalinkClick && row.uid && (
<IconButton
tooltip="Copy shortlink"
aria-label="Copy shortlink"
tooltipPlacement="top"
size="md"
name="share-alt"
onClick={() => onPermalinkClick(row)}
/>
)}
</span>
</td>
</>

View File

@@ -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<void>;
permalinkedRowId?: string;
scrollIntoView?: (element: HTMLElement) => void;
}
interface State {
@@ -139,66 +142,40 @@ class UnThemedLogRows extends PureComponent<Props, State> {
// 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 (
<table className={styles.logsRowsTable}>
<tbody>
{hasData &&
firstRows.map((row, index) => (
<LogRow
key={row.uid}
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}
/>
))}
{hasData &&
renderAll &&
lastRows.map((row, index) => (
<LogRow
key={row.uid}
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}
/>
))}
{hasData && firstRows.map((row) => <LogRow key={row.uid} {...getLogRowProperties(row)} />)}
{hasData && renderAll && lastRows.map((row) => <LogRow key={row.uid} {...getLogRowProperties(row)} />)}
{hasData && !renderAll && (
<tr>
<td colSpan={5}>Rendering {orderedRows.length - previewLimit!} rows...</td>

View File

@@ -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)};
}
`,
};
});