diff --git a/public/app/features/logs/components/LogDetails.test.tsx b/public/app/features/logs/components/LogDetails.test.tsx index c69d1a6e932..89b6e6793e6 100644 --- a/public/app/features/logs/components/LogDetails.test.tsx +++ b/public/app/features/logs/components/LogDetails.test.tsx @@ -295,4 +295,89 @@ describe('LogDetails', () => { expect(screen.getByText('shouldShowLinkName')).toBeInTheDocument(); expect(screen.getByText('shouldShowLinkValue')).toBeInTheDocument(); }); + + describe('Label types', () => { + const entry = 'test'; + const labels = { + label1: 'value1', + label2: 'value2', + label3: 'value3', + }; + const dataFrame = createDataFrame({ + fields: [ + { name: 'timestamp', config: {}, type: FieldType.time, values: [1] }, + { name: 'body', type: FieldType.string, values: [entry] }, + { name: 'id', type: FieldType.string, values: ['1'] }, + { + name: 'labels', + type: FieldType.other, + values: [labels], + }, + { + name: 'labelTypes', + type: FieldType.other, + values: [ + { + label1: 'I', + label2: 'S', + label3: 'P', + }, + ], + }, + ], + meta: { + type: DataFrameType.LogLines, + }, + }); + it('should show label types if they are available and supported', () => { + setup( + {}, + { + entry, + dataFrame, + entryFieldIndex: 0, + rowIndex: 0, + labels, + datasourceType: 'loki', + rowId: '1', + } + ); + + // Show labels and links + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('label3')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + expect(screen.getByText('I')).toBeInTheDocument(); + expect(screen.getByText('S')).toBeInTheDocument(); + expect(screen.getByText('P')).toBeInTheDocument(); + }); + it('should not show label types if they are unavailable or not supported', () => { + setup( + {}, + { + entry, + dataFrame, + entryFieldIndex: 0, + rowIndex: 0, + labels, + datasourceType: 'other datasource', + rowId: '1', + } + ); + + // Show labels and links + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('label3')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + expect(screen.queryByText('I')).not.toBeInTheDocument(); + expect(screen.queryByText('S')).not.toBeInTheDocument(); + expect(screen.queryByText('P')).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index 0139ddec647..39a7795483e 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -15,9 +15,18 @@ import { LogRowModel, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { ClipboardButton, DataLinkButton, IconButton, PopoverContent, Themeable2, withTheme2 } from '@grafana/ui'; +import { + ClipboardButton, + DataLinkButton, + IconButton, + PopoverContent, + Themeable2, + Tooltip, + withTheme2, +} from '@grafana/ui'; import { logRowToSingleRowDataFrame } from '../logsModel'; +import { getLabelTypeFromRow } from '../utils'; import { LogLabelStats } from './LogLabelStats'; import { getLogRowStyles } from './getLogRowStyles'; @@ -50,6 +59,19 @@ interface State { const getStyles = memoizeOne((theme: GrafanaTheme2) => { return { + labelType: css({ + border: `solid 1px ${theme.colors.text.secondary}`, + color: theme.colors.text.secondary, + borderRadius: theme.shape.radius.circle, + fontSize: theme.spacing(1), + lineHeight: theme.spacing(1.25), + height: theme.spacing(1.5), + width: theme.spacing(1.5), + display: 'flex', + justifyContent: 'center', + verticalAlign: 'middle', + marginLeft: theme.spacing(1), + }), wordBreakAll: css({ label: 'wordBreakAll', wordBreak: 'break-all', @@ -275,6 +297,7 @@ class UnThemedLogDetailsRow extends PureComponent { const singleVal = parsedValues == null ? false : parsedValues.length === 1; const hasFilteringFunctionality = !disableActions && onClickFilterLabel && onClickFilterOutLabel; const refIdTooltip = app === CoreApp.Explore && row.dataFrame?.refId ? ` in query ${row.dataFrame?.refId}` : ''; + const labelType = singleKey ? getLabelTypeFromRow(parsedKeys[0], row) : null; const isMultiParsedValueWithNoContent = !singleVal && parsedValues != null && !parsedValues.every((val) => val === ''); @@ -321,6 +344,7 @@ class UnThemedLogDetailsRow extends PureComponent { + {labelType && } {/* Key - value columns */} {singleKey ? parsedKeys[0] : this.generateMultiVal(parsedKeys)} @@ -360,7 +384,7 @@ class UnThemedLogDetailsRow extends PureComponent { {showFieldsStats && singleKey && singleVal && ( - + { } } +function LabelTypeBadge({ type, styles }: { type: string; styles: ReturnType }) { + return ( + +
+ {type.substring(0, 1)} +
+
+ ); +} + interface AsyncIconButtonProps extends Pick, 'onClick'> { name: IconName; isActive(): Promise; diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 4d8f88c5641..e6db5c67038 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -343,3 +343,48 @@ export function createLogRowsMap() { return false; }; } + +function getLabelTypeFromFrame(labelKey: string, frame: DataFrame, index: number): null | string { + const typeField = frame.fields.find((field) => field.name === 'labelTypes')?.values[index]; + if (!typeField) { + return null; + } + return typeField[labelKey] ?? null; +} + +export function getLabelTypeFromRow(label: string, row: LogRowModel) { + if (!row.datasourceType) { + return null; + } + const idField = row.dataFrame.fields.find((field) => field.name === 'id'); + if (!idField) { + return null; + } + const rowIndex = idField.values.findIndex((id) => id === row.rowId); + if (rowIndex < 0) { + return null; + } + const labelType = getLabelTypeFromFrame(label, row.dataFrame, rowIndex); + if (!labelType) { + return null; + } + return getDataSourceLabelType(labelType, row.datasourceType); +} + +function getDataSourceLabelType(labelType: string, datasourceType: string) { + switch (datasourceType) { + case 'loki': + switch (labelType) { + case 'I': + return 'Indexed label'; + case 'S': + return 'Structured metadata'; + case 'P': + return 'Parsed label'; + default: + return null; + } + default: + return null; + } +}