diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx index 085b4e2c8cb..d2938119d0e 100644 --- a/public/app/features/logs/components/panel/LogLineDetails.test.tsx +++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx @@ -228,7 +228,7 @@ describe('LogLineDetails', () => { expect(screen.queryByText('Structured metadata')).not.toBeInTheDocument(); }); }); - test('should render fields from the dataframe with links', () => { + test('should render fields from the dataframe with links', async () => { const entry = 'traceId=1234 msg="some message"'; const dataFrame = toDataFrame({ fields: [ @@ -273,6 +273,10 @@ describe('LogLineDetails', () => { expect(screen.getByText('Links')).toBeInTheDocument(); expect(screen.getByText('traceId')).toBeInTheDocument(); expect(screen.getByText('link title')).toBeInTheDocument(); + expect(screen.queryByText('1234')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Link value')); + expect(screen.getByText('1234')).toBeInTheDocument(); }); @@ -346,7 +350,6 @@ describe('LogLineDetails', () => { expect(screen.getByText('label1')).toBeInTheDocument(); expect(screen.getByText('value1')).toBeInTheDocument(); expect(screen.getByText('shouldShowLinkName')).toBeInTheDocument(); - expect(screen.getByText('shouldShowLinkValue')).toBeInTheDocument(); }); test('should load plugin links for logs view resource attributes extension point', () => { diff --git a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx index af307f85ac9..0f274e4ed6d 100644 --- a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx +++ b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx @@ -14,6 +14,7 @@ import { createLogLineLinks } from '../logParser'; import { LogLineDetailsDisplayedFields } from './LogLineDetailsDisplayedFields'; import { LabelWithLinks, LogLineDetailsFields, LogLineDetailsLabelFields } from './LogLineDetailsFields'; import { LogLineDetailsHeader } from './LogLineDetailsHeader'; +import { LogLineDetailsLinks } from './LogLineDetailsLinks'; import { LogLineDetailsLog } from './LogLineDetailsLog'; import { LogLineDetailsTrace } from './LogLineDetailsTrace'; import { useLogListContext } from './LogListContext'; @@ -118,7 +119,10 @@ export const LogLineDetailsComponent = memo( !labelGroups.length && !fieldsWithoutLinks.length; - const hasLinks = fieldsWithLinks.links.length > 0 || fieldsWithLinks.linksFromVariableMap.length > 0; + const allLinks = useMemo( + () => [...fieldsWithLinks.links, ...fieldsWithLinks.linksFromVariableMap], + [fieldsWithLinks.links, fieldsWithLinks.linksFromVariableMap] + ); return ( <> @@ -143,7 +147,7 @@ export const LogLineDetailsComponent = memo( )} - {hasLinks && ( + {allLinks.length > 0 && ( handleToggle('linksOpen', isOpen)} > - - + )} {trace && ( diff --git a/public/app/features/logs/components/panel/LogLineDetailsFields.tsx b/public/app/features/logs/components/panel/LogLineDetailsFields.tsx index fbd561d7900..3a7d01d10ff 100644 --- a/public/app/features/logs/components/panel/LogLineDetailsFields.tsx +++ b/public/app/features/logs/components/panel/LogLineDetailsFields.tsx @@ -464,7 +464,7 @@ const getClipboardButtonStyles = (theme: GrafanaTheme2) => ({ }), }); -const MultipleValue = ({ showCopy, values = [] }: { showCopy?: boolean; values: string[] }) => { +export const MultipleValue = ({ showCopy, values = [] }: { showCopy?: boolean; values: string[] }) => { if (values.every((val) => val === '')) { return null; } @@ -484,7 +484,13 @@ const MultipleValue = ({ showCopy, values = [] }: { showCopy?: boolean; values: ); }; -const SingleValue = ({ value: originalValue, syntaxHighlighting }: { value: string; syntaxHighlighting?: boolean }) => { +export const SingleValue = ({ + value: originalValue, + syntaxHighlighting, +}: { + value: string; + syntaxHighlighting?: boolean; +}) => { const value = useMemo(() => { if (!syntaxHighlighting) { return originalValue; @@ -523,7 +529,7 @@ const AsyncIconButton = ({ isActive, tooltipSuffix, ...rest }: AsyncIconButtonPr return ; }; -function filterFields(fields: FieldDef[], search: string) { +export function filterFields(fields: FieldDef[], search: string) { const keys = fields.map((field) => field.keys.join(' ')); const keysIdx = fuzzySearch(keys, search); const values = fields.map((field) => field.values.join(' ')); diff --git a/public/app/features/logs/components/panel/LogLineDetailsLinks.tsx b/public/app/features/logs/components/panel/LogLineDetailsLinks.tsx new file mode 100644 index 00000000000..f33b21fc76d --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetailsLinks.tsx @@ -0,0 +1,159 @@ +import { css } from '@emotion/css'; +import { memo, useMemo } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { DataLinkButton, Icon, Toggletip, useStyles2 } from '@grafana/ui'; + +import { FieldDef } from '../logParser'; + +import { filterFields, MultipleValue, SingleValue } from './LogLineDetailsFields'; +import { useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +interface LogLineDetailsLinksProps { + fields: FieldDef[]; + log: LogListModel; + logs: LogListModel[]; + search?: string; +} + +export const LogLineDetailsLinks = memo(({ fields, log, search }: LogLineDetailsLinksProps) => { + const styles = useStyles2(getFieldsStyles); + const filteredFields = useMemo(() => (search ? filterFields(fields, search) : fields), [fields, search]); + + if (!fields.length) { + return null; + } else if (filteredFields.length === 0) { + return t('logs.log-line-details.search.no-results', 'No results to display.'); + } + + return ( +
+ {filteredFields.map((field, i) => ( + + ))} +
+ ); +}); +LogLineDetailsLinks.displayName = 'LogLineDetailsLinks'; + +const getFieldsStyles = (theme: GrafanaTheme2) => ({ + linksTable: css({ + display: 'grid', + gap: theme.spacing(1), + gridTemplateColumns: `minmax(auto, 40%) 1fr`, + marginBottom: theme.spacing(1), + }), +}); + +interface LogLineDetailsFieldProps { + field: FieldDef; + log: LogListModel; +} + +export const LogLineDetailsField = ({ field, log }: LogLineDetailsFieldProps) => { + const { closeDetails, onPinLine, pinLineButtonTooltipTitle, syntaxHighlighting } = useLogListContext(); + + const styles = useStyles2(getFieldStyles); + + const singleKey = field.keys.length === 1; + const singleValue = field.values.length === 1; + + const tooltip = useMemo( + () => ( +
+
+ {singleValue ? ( + + ) : ( + + )} +
+
+ ), + [field.values, singleValue, styles.value, styles.valueContainer, syntaxHighlighting] + ); + + return ( + <> +
+ {singleKey ? field.keys[0] : } + + + +
+
+ {field.links?.map((link, i) => { + if (link.onClick && onPinLine) { + const originalOnClick = link.onClick; + link.onClick = (e, origin) => { + // Pin the line + onPinLine(log); + + // Execute the link onClick function + originalOnClick(e, origin); + + closeDetails(); + }; + } + return ( + + + + ); + })} +
+ + ); +}; + +const getFieldStyles = (theme: GrafanaTheme2) => ({ + label: css({ + overflowWrap: 'break-word', + wordBreak: 'break-word', + paddingRight: theme.spacing(1), + }), + labelIcon: css({ + marginLeft: theme.spacing(1), + }), + value: css({ + button: { + visibility: 'hidden', + }, + '&:hover': { + button: { + visibility: 'visible', + }, + }, + }), + links: css({ + paddingBottom: theme.spacing(0.5), + }), + link: css({ + marginRight: theme.spacing(0.5), + }), + valueContainer: css({ + display: 'flex', + lineHeight: theme.typography.body.lineHeight, + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + maxHeight: '50vh', + overflow: 'auto', + }), +}); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a0a0c87740b..34866699b8c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9535,6 +9535,7 @@ "fields-section": "Fields", "hide-log-line": "Hide log line", "inline-mode": "Display inline", + "link-value-tooltip": "Link value", "links-section": "Links", "log-line-field": "Log line", "log-line-section": "Log line",