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:
@@ -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',
|
||||
}),
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user