mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
bbf4281d8d
commit
99f34cb1ed
@ -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"]
|
||||
],
|
||||
|
@ -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 <></>;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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 } });
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user