diff --git a/.betterer.results b/.betterer.results index 0dcf32bb34a..d4168a6aaa1 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3303,11 +3303,6 @@ exports[`better eslint`] = { "public/app/features/logs/components/InfiniteScroll.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/logs/components/LogDetails.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], "public/app/features/logs/components/LogLabelStats.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], diff --git a/public/app/features/logs/components/LogDetails.test.tsx b/public/app/features/logs/components/LogDetails.test.tsx index a98dbd1a9f9..c69d1a6e932 100644 --- a/public/app/features/logs/components/LogDetails.test.tsx +++ b/public/app/features/logs/components/LogDetails.test.tsx @@ -14,6 +14,7 @@ import { } from '@grafana/data'; import { LogDetails, Props } from './LogDetails'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { createLogRow } from './__mocks__/logRow'; import { getLogRowStyles } from './getLogRowStyles'; @@ -58,6 +59,33 @@ describe('LogDetails', () => { expect(screen.getByRole('cell', { name: 'key2' })).toBeInTheDocument(); expect(screen.getByRole('cell', { name: 'label2' })).toBeInTheDocument(); }); + it('should show an option to display the log line when displayed fields are used', async () => { + const onClickShowField = jest.fn(); + + setup({ displayedFields: ['key1'], onClickShowField }, { labels: { key1: 'label1' } }); + expect(screen.getByRole('cell', { name: 'key1' })).toBeInTheDocument(); + expect(screen.getByLabelText('Show log line')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Show log line')); + + expect(onClickShowField).toHaveBeenCalledTimes(1); + }); + it('should show an active option to display the log line when displayed fields are used', async () => { + const onClickHideField = jest.fn(); + + setup({ displayedFields: ['key1', LOG_LINE_BODY_FIELD_NAME], onClickHideField }, { labels: { key1: 'label1' } }); + expect(screen.getByRole('cell', { name: 'key1' })).toBeInTheDocument(); + expect(screen.getByLabelText('Hide log line')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Hide log line')); + + expect(onClickHideField).toHaveBeenCalledTimes(1); + }); + it('should not show an option to display the log line when displayed fields are not used', () => { + setup({ displayedFields: undefined }, { labels: { key1: 'label1' } }); + expect(screen.getByRole('cell', { name: 'key1' })).toBeInTheDocument(); + expect(screen.queryByLabelText('Show log line')).not.toBeInTheDocument(); + }); it('should render filter controls when the callbacks are provided', () => { setup( { diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index 4d99054d1d9..807320d5829 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -3,9 +3,11 @@ import { PureComponent } from 'react'; import { CoreApp, DataFrame, DataFrameType, Field, LinkModel, LogRowModel } from '@grafana/data'; import { PopoverContent, Themeable2, withTheme2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; import { calculateLogsLabelStats, calculateStats } from '../utils'; +import { LogDetailsBody } from './LogDetailsBody'; import { LogDetailsRow } from './LogDetailsRow'; import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles'; import { getAllFields, createLogLineLinks } from './logParser'; @@ -86,10 +88,28 @@ class UnThemedLogDetails extends PureComponent {
+ {displayedFields && displayedFields.length > 0 && ( + <> + + + + + + )} {(labelsAvailable || fieldsAvailable) && ( )} @@ -142,7 +162,7 @@ class UnThemedLogDetails extends PureComponent { {fieldsWithLinksAvailable && ( )} @@ -192,7 +212,7 @@ class UnThemedLogDetails extends PureComponent { {!fieldsAvailable && !labelsAvailable && !fieldsWithLinksAvailable && ( )} diff --git a/public/app/features/logs/components/LogDetailsBody.tsx b/public/app/features/logs/components/LogDetailsBody.tsx new file mode 100644 index 00000000000..0cc844e246d --- /dev/null +++ b/public/app/features/logs/components/LogDetailsBody.tsx @@ -0,0 +1,84 @@ +import { css } from '@emotion/css'; +import memoizeOne from 'memoize-one'; + +import { CoreApp, GrafanaTheme2, LogRowModel } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { IconButton, Themeable2 } from '@grafana/ui'; + +import { getLogRowStyles } from './getLogRowStyles'; + +export interface Props extends Themeable2 { + app?: CoreApp; + disableActions: boolean; + displayedFields?: string[]; + onClickShowField?: (key: string) => void; + onClickHideField?: (key: string) => void; + row: LogRowModel; + theme: GrafanaTheme2; +} + +const getStyles = memoizeOne((theme: GrafanaTheme2) => { + return { + buttonRow: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(0.5), + marginLeft: theme.spacing(0.5), + }), + }; +}); + +export const LOG_LINE_BODY_FIELD_NAME = '___LOG_LINE_BODY___'; + +export const LogDetailsBody = (props: Props) => { + const showField = () => { + const { onClickShowField, row } = props; + if (onClickShowField) { + onClickShowField(LOG_LINE_BODY_FIELD_NAME); + } + + reportInteraction('grafana_explore_logs_log_details_show_body_clicked', { + datasourceType: row.datasourceType, + logRowUid: row.uid, + type: 'enable', + app: props.app, + }); + }; + + const hideField = () => { + const { onClickHideField, row } = props; + if (onClickHideField) { + onClickHideField(LOG_LINE_BODY_FIELD_NAME); + } + + reportInteraction('grafana_explore_logs_log_details_show_body_clicked', { + datasourceType: row.datasourceType, + logRowUid: row.uid, + type: 'disable', + app: props.app, + }); + }; + + const { theme, displayedFields, disableActions, row } = props; + const styles = getStyles(theme); + const rowStyles = getLogRowStyles(theme); + + const toggleFieldButton = + displayedFields != null && displayedFields.includes(LOG_LINE_BODY_FIELD_NAME) ? ( + + ) : ( + + ); + + return ( + + + + + + ); +}; diff --git a/public/app/features/logs/components/LogLabels.test.tsx b/public/app/features/logs/components/LogLabels.test.tsx index af5217d110f..f736b0a595c 100644 --- a/public/app/features/logs/components/LogLabels.test.tsx +++ b/public/app/features/logs/components/LogLabels.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { LogLabels, LogLabelsList } from './LogLabels'; describe('', () => { @@ -38,8 +39,9 @@ describe('', () => { describe('', () => { it('renders labels', () => { - render(); + render(); expect(screen.queryByText('bar')).toBeInTheDocument(); expect(screen.queryByText('42')).toBeInTheDocument(); + expect(screen.queryByText('log line')).toBeInTheDocument(); }); }); diff --git a/public/app/features/logs/components/LogLabels.tsx b/public/app/features/logs/components/LogLabels.tsx index 951597c887f..fa675287a72 100644 --- a/public/app/features/logs/components/LogLabels.tsx +++ b/public/app/features/logs/components/LogLabels.tsx @@ -4,6 +4,8 @@ import { memo, forwardRef, useMemo } from 'react'; import { GrafanaTheme2, Labels } from '@grafana/data'; import { Tooltip, useStyles2 } from '@grafana/ui'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; + // Levels are already encoded in color, filename is a Loki-ism const HIDDEN_LABELS = ['detected_level', 'level', 'lvl', 'filename']; @@ -59,7 +61,7 @@ export const LogLabelsList = memo(({ labels }: LogLabelsArrayProps) => { {labels.map((label) => ( - {label} + {label === LOG_LINE_BODY_FIELD_NAME ? 'log line' : label} ))} diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx index 05b48eabff1..4e0bdfccf4a 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; import { createTheme, LogLevel } from '@grafana/data'; import { IconButton } from '@grafana/ui'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { LogRowMessageDisplayedFields, Props } from './LogRowMessageDisplayedFields'; import { createLogRow } from './__mocks__/logRow'; import { getLogRowStyles } from './getLogRowStyles'; @@ -42,7 +43,14 @@ const setup = (propOverrides: Partial = {}, detectedFields = ['place', 'p describe('LogRowMessageDisplayedFields', () => { it('renders diplayed fields from a log row', () => { setup(); - expect(screen.queryByText('Logs are wonderful')).not.toBeInTheDocument(); + expect(screen.queryByText(/Logs are wonderful/)).not.toBeInTheDocument(); + expect(screen.getByText(/place=Earth/)).toBeInTheDocument(); + expect(screen.getByText(/planet=Mars/)).toBeInTheDocument(); + }); + + it('renders diplayed fields and body from a log row', () => { + setup({}, ['place', 'planet', LOG_LINE_BODY_FIELD_NAME]); + expect(screen.queryByText(/Logs are wonderful/)).toBeInTheDocument(); expect(screen.getByText(/place=Earth/)).toBeInTheDocument(); expect(screen.getByText(/planet=Mars/)).toBeInTheDocument(); }); diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx index 363fb97bb17..2cc495148a1 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx @@ -3,6 +3,7 @@ import { memo, ReactNode, useMemo } from 'react'; import { LogRowModel, Field, LinkModel, DataFrame } from '@grafana/data'; +import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; import { LogRowMenuCell } from './LogRowMenuCell'; import { LogRowStyles } from './getLogRowStyles'; import { getAllFields } from './logParser'; @@ -45,21 +46,26 @@ export const LogRowMessageDisplayedFields = memo((props: Props) => { let line = ''; for (let i = 0; i < detectedFields.length; i++) { const parsedKey = detectedFields[i]; + + if (parsedKey === LOG_LINE_BODY_FIELD_NAME) { + line += ` ${row.entry}`; + } + const field = fields.find((field) => { const { keys } = field; return keys[0] === parsedKey; }); - if (field) { + if (field != null) { line += ` ${parsedKey}=${field.values}`; } - if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { + if (row.labels[parsedKey] != null && row.labels[parsedKey] != null) { line += ` ${parsedKey}=${row.labels[parsedKey]}`; } } return line.trimStart(); - }, [detectedFields, fields, row.labels]); + }, [detectedFields, fields, row.entry, row.labels]); const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d1b9458d42c..8482a186a35 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "Older logs" }, + "log-details": { + "fields": "Fields", + "links": "Links", + "log-line": "Log line", + "no-details": "No details available" + }, "log-row-message": { "ellipsis": "… ", "more": "more" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index aeb77531dbb..ea4bf022248 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1758,6 +1758,12 @@ "infinite-scroll": { "older-logs": "Øľđęř ľőģş" }, + "log-details": { + "fields": "Fįęľđş", + "links": "Ŀįʼnĸş", + "log-line": "Ŀőģ ľįʼnę", + "no-details": "Ńő đęŧäįľş äväįľäþľę" + }, "log-row-message": { "ellipsis": "… ", "more": "mőřę"
+ Log line +
- Fields + Fields
- Links + Links
- No details available + No details available
+
{!disableActions && displayedFields && toggleFieldButton}
+
+ {row.entry} +