diff --git a/.betterer.results b/.betterer.results index e861dbe1415..4018a113db2 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] ], diff --git a/public/app/features/explore/Logs/LogsMetaRow.tsx b/public/app/features/explore/Logs/LogsMetaRow.tsx index 55f5bdacc02..b6608f69b20 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.tsx @@ -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 <></>; } diff --git a/public/app/features/logs/components/LogLabels.test.tsx b/public/app/features/logs/components/LogLabels.test.tsx index e76459f0b65..9bfd070d9a7 100644 --- a/public/app/features/logs/components/LogLabels.test.tsx +++ b/public/app/features/logs/components/LogLabels.test.tsx @@ -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(); }); }); diff --git a/public/app/features/logs/components/LogLabels.tsx b/public/app/features/logs/components/LogLabels.tsx index 3f76fda46dd..2305ad7e866 100644 --- a/public/app/features/logs/components/LogLabels.tsx +++ b/public/app/features/logs/components/LogLabels.tsx @@ -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 { diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx index 7c3fdbf0726..2230f70dae1 100644 --- a/public/app/plugins/panel/logs/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx @@ -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 } }); diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 8069088e533..40aa8dab3f0 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -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> );