Common labels/displayed fields: Show label names with values (#86345)

* LogLabels: create specialized component for arrays of labels

* Logs: sort displayed fields when assigning to state

* LogsMetaRow: fix types and use specialized components

* LogLabels: show label and value

* LogsPanel: update common labels

* LogsMetaRow: use LogsLabelsList

* Update unit tests

* Formatting

* Update betterer

* Prettier

* Logs panel: update test

* LogLabels: add actual tooltip

* Logs: remove sorting of displayed fields
This commit is contained in:
Matias Chomicki 2024-04-18 16:25:58 +02:00 committed by GitHub
parent bbf4281d8d
commit 99f34cb1ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 36 deletions

View File

@ -3276,8 +3276,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/explore/Logs/LogsMetaRow.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/explore/Logs/LogsNavigation.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
@ -6019,9 +6018,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
],
"public/app/plugins/panel/logs/LogsPanel.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/logs/types.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./panelcfg.gen\`)", "0"]
],

View File

@ -13,13 +13,14 @@ import {
transformDataFrame,
DataTransformerConfig,
CustomTransformOperator,
Labels,
} from '@grafana/data';
import { DataFrame } from '@grafana/data/';
import { reportInteraction } from '@grafana/runtime';
import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
import { downloadDataFrameAsCsv, downloadLogsModelAsTxt } from '../../inspector/utils/download';
import { LogLabels } from '../../logs/components/LogLabels';
import { LogLabels, LogLabelsList } from '../../logs/components/LogLabels';
import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage';
import { logRowsToReadableJson } from '../../logs/utils';
import { MetaInfoText, MetaItemProps } from '../MetaInfoText';
@ -133,7 +134,7 @@ export const LogsMetaRow = React.memo(
logsMetaItem.push(
{
label: 'Showing only selected fields',
value: renderMetaItem(displayedFields, LogsMetaKind.LabelsMap),
value: <LogLabelsList labels={displayedFields} />,
},
{
label: '',
@ -195,11 +196,16 @@ export const LogsMetaRow = React.memo(
LogsMetaRow.displayName = 'LogsMetaRow';
function renderMetaItem(value: any, kind: LogsMetaKind) {
function renderMetaItem(value: string | number | Labels, kind: LogsMetaKind) {
if (typeof value === 'string' || typeof value === 'number') {
return <>{value}</>;
}
if (kind === LogsMetaKind.LabelsMap) {
return <LogLabels labels={value} />;
} else if (kind === LogsMetaKind.Error) {
return <span className="logs-meta-item__error">{value}</span>;
}
return value;
if (kind === LogsMetaKind.Error) {
return <span className="logs-meta-item__error">{value.toString()}</span>;
}
console.error(`Meta type ${typeof value} ${value} not recognized.`);
return <></>;
}

View File

@ -1,27 +1,35 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { LogLabels } from './LogLabels';
import { LogLabels, LogLabelsList } from './LogLabels';
describe('<LogLabels />', () => {
it('renders notice when no labels are found', () => {
render(<LogLabels labels={{}} />);
render(<LogLabels labels={{}} emptyMessage="(no unique labels)" />);
expect(screen.queryByText('(no unique labels)')).toBeInTheDocument();
});
it('renders labels', () => {
render(<LogLabels labels={{ foo: 'bar', baz: '42' }} />);
expect(screen.queryByText('bar')).toBeInTheDocument();
expect(screen.queryByText('42')).toBeInTheDocument();
expect(screen.queryByText('foo=bar')).toBeInTheDocument();
expect(screen.queryByText('baz=42')).toBeInTheDocument();
});
it('excludes labels with certain names or labels starting with underscore', () => {
render(<LogLabels labels={{ foo: 'bar', level: '42', _private: '13' }} />);
expect(screen.queryByText('bar')).toBeInTheDocument();
expect(screen.queryByText('42')).not.toBeInTheDocument();
expect(screen.queryByText('foo=bar')).toBeInTheDocument();
expect(screen.queryByText('level=42')).not.toBeInTheDocument();
expect(screen.queryByText('13')).not.toBeInTheDocument();
});
it('excludes labels with empty string values', () => {
render(<LogLabels labels={{ foo: 'bar', baz: '' }} />);
expect(screen.queryByText('bar')).toBeInTheDocument();
expect(screen.queryByText('baz')).not.toBeInTheDocument();
expect(screen.queryByText('foo=bar')).toBeInTheDocument();
expect(screen.queryByText(/baz/)).not.toBeInTheDocument();
});
});
describe('<LogLabelsList />', () => {
it('renders labels', () => {
render(<LogLabelsList labels={['bar', '42']} />);
expect(screen.queryByText('bar')).toBeInTheDocument();
expect(screen.queryByText('42')).toBeInTheDocument();
});
});

View File

@ -1,47 +1,90 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import React, { useMemo } from 'react';
import { GrafanaTheme2, Labels } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { Tooltip, useStyles2 } from '@grafana/ui';
// Levels are already encoded in color, filename is a Loki-ism
const HIDDEN_LABELS = ['level', 'lvl', 'filename'];
interface Props {
labels: Labels;
emptyMessage?: string;
}
export const LogLabels = ({ labels }: Props) => {
export const LogLabels = React.memo(({ labels, emptyMessage }: Props) => {
const styles = useStyles2(getStyles);
const displayLabels = Object.keys(labels).filter((label) => !label.startsWith('_') && !HIDDEN_LABELS.includes(label));
const displayLabels = useMemo(
() =>
Object.keys(labels)
.filter((label) => !label.startsWith('_') && !HIDDEN_LABELS.includes(label))
.sort(),
[labels]
);
if (displayLabels.length === 0) {
if (displayLabels.length === 0 && emptyMessage) {
return (
<span className={cx([styles.logsLabels])}>
<span className={cx([styles.logsLabel])}>(no unique labels)</span>
<span className={cx([styles.logsLabel])}>{emptyMessage}</span>
</span>
);
}
return (
<span className={cx([styles.logsLabels])}>
{displayLabels.sort().map((label) => {
{displayLabels.map((label) => {
const value = labels[label];
if (!value) {
return;
}
const tooltip = `${label}: ${value}`;
const labelValue = `${label}=${value}`;
return (
<span key={label} className={cx([styles.logsLabel])}>
<span className={cx([styles.logsLabelValue])} title={tooltip}>
{value}
</span>
</span>
<Tooltip content={labelValue} key={label} placement="top">
<LogLabel styles={styles}>{labelValue}</LogLabel>
</Tooltip>
);
})}
</span>
);
};
});
LogLabels.displayName = 'LogLabels';
interface LogLabelsArrayProps {
labels: string[];
}
export const LogLabelsList = React.memo(({ labels }: LogLabelsArrayProps) => {
const styles = useStyles2(getStyles);
return (
<span className={cx([styles.logsLabels])}>
{labels.map((label) => (
<LogLabel key={label} styles={styles} tooltip={label}>
{label}
</LogLabel>
))}
</span>
);
});
LogLabelsList.displayName = 'LogLabelsList';
interface LogLabelProps {
styles: Record<string, string>;
tooltip?: string;
children: JSX.Element | string;
}
const LogLabel = React.forwardRef<HTMLSpanElement, LogLabelProps>(
({ styles, tooltip, children }: LogLabelProps, ref) => {
return (
<span className={cx([styles.logsLabel])} ref={ref}>
<span className={cx([styles.logsLabelValue])} title={tooltip}>
{children}
</span>
</span>
);
}
);
LogLabel.displayName = 'LogLabel';
const getStyles = (theme: GrafanaTheme2) => {
return {

View File

@ -87,7 +87,7 @@ describe('LogsPanel', () => {
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Descending },
});
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
expect(container.firstChild?.childNodes[0].textContent).toMatch(/^Common labels:common_appcommon_job/);
expect(container.firstChild?.childNodes[0].textContent).toMatch(/^Common labels:app=common_appjob=common_job/);
});
it('shows common labels on bottom when ascending sort order', async () => {
const { container } = setup({
@ -95,7 +95,7 @@ describe('LogsPanel', () => {
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Ascending },
});
expect(await screen.findByText(/common labels:/i)).toBeInTheDocument();
expect(container.firstChild?.childNodes[0].textContent).toMatch(/Common labels:common_appcommon_job$/);
expect(container.firstChild?.childNodes[0].textContent).toMatch(/Common labels:app=common_appjob=common_job$/);
});
it('does not show common labels when showCommonLabels is set to false', async () => {
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: false } });

View File

@ -39,6 +39,8 @@ interface LogsPermalinkUrlState {
};
}
const noCommonLabels: Labels = {};
export const LogsPanel = ({
data,
timeZone,
@ -225,7 +227,10 @@ export const LogsPanel = ({
const renderCommonLabels = () => (
<div className={cx(style.labelContainer, isAscending && style.labelContainerAscending)}>
<span className={style.label}>Common labels:</span>
<LogLabels labels={commonLabels ? (commonLabels.value as Labels) : { labels: '(no common labels)' }} />
<LogLabels
labels={typeof commonLabels?.value === 'object' ? commonLabels?.value : noCommonLabels}
emptyMessage="(no common labels)"
/>
</div>
);