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",