Log Details: Update links UI (#110412)

* LogLineDetailsLinks: create component

* Label: add space

* Comment

* LogLineDetailsLinks: show value in a toggletip

* LogLineDetailsLinks: add label

* Update tests
This commit is contained in:
Matias Chomicki
2025-09-02 15:06:57 +00:00
committed by GitHub
parent a058665bbc
commit 9b57d9616a
5 changed files with 181 additions and 21 deletions
@@ -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', () => {
@@ -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(
<LogLineDetailsDisplayedFields />
</ControlledCollapse>
)}
{hasLinks && (
{allLinks.length > 0 && (
<ControlledCollapse
className={styles.collapsable}
label={t('logs.log-line-details.links-section', 'Links')}
@@ -151,20 +155,7 @@ export const LogLineDetailsComponent = memo(
isOpen={linksOpen}
onToggle={(isOpen: boolean) => handleToggle('linksOpen', isOpen)}
>
<LogLineDetailsFields
disableActions
log={log}
logs={logs}
fields={fieldsWithLinks.links}
search={search}
/>
<LogLineDetailsFields
disableActions
log={log}
logs={logs}
fields={fieldsWithLinks.linksFromVariableMap}
search={search}
/>
<LogLineDetailsLinks log={log} logs={logs} fields={allLinks} search={search} />
</ControlledCollapse>
)}
{trace && (
@@ -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 <IconButton {...rest} variant={active ? 'primary' : undefined} tooltip={tooltip + tooltipSuffix} />;
};
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(' '));
@@ -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 (
<div className={styles.linksTable}>
{filteredFields.map((field, i) => (
<LogLineDetailsField key={`${field.keys[0]}=${field.values[0]}-${i}`} field={field} log={log} />
))}
</div>
);
});
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(
() => (
<div className={styles.value}>
<div className={styles.valueContainer}>
{singleValue ? (
<SingleValue value={field.values[0]} syntaxHighlighting={syntaxHighlighting} />
) : (
<MultipleValue showCopy={true} values={field.values} />
)}
</div>
</div>
),
[field.values, singleValue, styles.value, styles.valueContainer, syntaxHighlighting]
);
return (
<>
<div className={styles.label}>
{singleKey ? field.keys[0] : <MultipleValue values={field.keys} />}
<Toggletip fitContent content={tooltip}>
<Icon
aria-label={t('logs.log-line-details.link-value-tooltip', 'Link value')}
className={styles.labelIcon}
name="info-circle"
/>
</Toggletip>
</div>
<div className={styles.links}>
{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 (
<span key={`${link.title}-${i}`} className={styles.link}>
<DataLinkButton
buttonProps={{
// Show tooltip message if max number of pinned lines has been reached
tooltip:
typeof pinLineButtonTooltipTitle === 'object' && link.onClick
? pinLineButtonTooltipTitle
: undefined,
variant: 'secondary',
fill: 'outline',
}}
link={link}
/>
</span>
);
})}
</div>
</>
);
};
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',
}),
});
+1
View File
@@ -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",