Logs: use displayFields for both logs panel adn table

This commit is contained in:
L2D2Grafana
2026-01-07 07:42:56 -08:00
parent 2efcc88e62
commit 1e841eaba8
8 changed files with 272 additions and 83 deletions
@@ -78,7 +78,6 @@ export interface ExploreTracePanelState {
export interface ExploreLogsPanelState {
id?: string;
columns?: Record<number, string>;
visualisationType?: 'table' | 'logs';
labelFieldName?: string;
// Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized
+31 -21
View File
@@ -201,8 +201,9 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
panelState?.logs?.sortOrder ?? store.get(SETTINGS_KEYS.logsSortOrder) ?? LogsSortOrder.Descending
);
const [isFlipping, setIsFlipping] = useState<boolean>(false);
const [displayedFields, setDisplayedFields] = useState<string[]>(panelState?.logs?.displayedFields ?? []);
const [defaultDisplayedFields, setDefaultDisplayedFields] = useState<string[]>([]);
// Use Redux state as single source of truth
const displayedFields = useMemo(() => panelState?.logs?.displayedFields ?? [], [panelState?.logs?.displayedFields]);
const [contextOpen, setContextOpen] = useState<boolean>(false);
const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined);
const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>(PINNED_LOGS_MESSAGE);
@@ -322,7 +323,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
dispatch(
changePanelState(exploreId, 'logs', {
...state.panelsState.logs,
columns: logsPanelState.columns ?? panelState?.logs?.columns,
visualisationType: logsPanelState.visualisationType ?? visualisationType,
labelFieldName: logsPanelState.labelFieldName,
refId: logsPanelState.refId ?? panelState?.logs?.refId,
@@ -336,7 +336,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
[
dispatch,
exploreId,
panelState?.logs?.columns,
panelState?.logs?.displayedFields,
panelState?.logs?.refId,
panelState?.logs?.tableSortBy,
@@ -345,14 +344,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
]
);
useEffect(() => {
if (!shallowCompare(displayedFields, panelState?.logs?.displayedFields ?? [])) {
updatePanelState({
...panelState?.logs,
displayedFields,
});
}
}, [displayedFields, panelState?.logs, updatePanelState]);
// No longer needed - displayedFields is read directly from Redux state
// actions
const onLogRowHover = useCallback(
@@ -541,30 +533,48 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
const showField = useCallback(
(key: string) => {
const index = displayedFields.indexOf(key);
const currentFields = panelState?.logs?.displayedFields ?? [];
const index = currentFields.indexOf(key);
if (index === -1) {
const updatedDisplayedFields = displayedFields.concat(key);
setDisplayedFields(updatedDisplayedFields);
const updatedDisplayedFields = currentFields.concat(key);
updatePanelState({
displayedFields: updatedDisplayedFields,
});
}
},
[displayedFields]
[panelState?.logs?.displayedFields, updatePanelState]
);
const hideField = useCallback(
(key: string) => {
const index = displayedFields.indexOf(key);
const currentFields = panelState?.logs?.displayedFields ?? [];
const index = currentFields.indexOf(key);
if (index > -1) {
const updatedDisplayedFields = displayedFields.filter((k) => key !== k);
setDisplayedFields(updatedDisplayedFields);
const updatedDisplayedFields = currentFields.filter((k) => key !== k);
updatePanelState({
displayedFields: updatedDisplayedFields,
});
}
},
[displayedFields]
[panelState?.logs?.displayedFields, updatePanelState]
);
const clearDisplayedFields = useCallback(() => {
setDisplayedFields([]);
}, []);
updatePanelState({
displayedFields: [],
});
}, [updatePanelState]);
// Wrapper function for setDisplayedFields prop - updates Redux directly
const setDisplayedFields = useCallback(
(fields: string[]) => {
updatePanelState({
displayedFields: fields,
});
},
[updatePanelState]
);
const onCloseCallbackRef = useRef<() => void>(() => {});
@@ -33,6 +33,7 @@ import {
useStyles2,
} from '@grafana/ui';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/internal';
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from 'app/features/logs/components/otel/formats';
import { DATAPLANE_ID_NAME, LogsFrame } from 'app/features/logs/logsFrame';
import { getFieldLinksForExplore } from '../utils/links';
@@ -396,9 +397,10 @@ export function getLogsExtractFields(dataFrame: DataFrame) {
function buildLabelFilters(columnsWithMeta: Record<string, FieldNameMeta>) {
// Create object of label filters to include columns selected by the user
// Exclude OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME from table view
let labelFilters: Record<string, number> = {};
Object.keys(columnsWithMeta)
.filter((key) => columnsWithMeta[key].active)
.filter((key) => columnsWithMeta[key].active && key !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME)
.forEach((key) => {
const index = columnsWithMeta[key].index;
// Index should always be defined for any active column
@@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { Resizable, ResizeCallback } from 're-resizable';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
DataFrame,
@@ -14,15 +14,24 @@ import {
store,
TimeRange,
AbsoluteTimeRange,
shallowCompare,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { getDragStyles, InlineField, Select, useStyles2 } from '@grafana/ui';
import {
TABLE_TIME_FIELD_NAME,
TABLE_LINE_FIELD_NAME,
TABLE_DETECTED_LEVEL_FIELD_NAME,
LOG_LINE_BODY_FIELD_NAME,
} from 'app/features/logs/components/LogDetailsBody';
import {
getFieldSelectorWidth,
getSidebarWidth,
LogsTableFieldSelector,
MIN_WIDTH,
} from 'app/features/logs/components/fieldSelector/FieldSelector';
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from 'app/features/logs/components/otel/formats';
import { reportInteractionOnce } from 'app/features/logs/components/panel/analytics';
import { parseLogsFrame } from '../../logs/logsFrame';
@@ -70,9 +79,14 @@ export type FieldNameMetaStore = Record<FieldName, FieldNameMeta>;
export function LogsTableWrap(props: Props) {
const { logsFrames, updatePanelState, panelState } = props;
const propsColumns = panelState?.columns;
const propsColumns = panelState?.displayedFields;
// Save the normalized cardinality of each label
const [columnsWithMeta, setColumnsWithMeta] = useState<FieldNameMetaStore | undefined>(undefined);
// Use ref to access columnsWithMeta in useEffect without causing infinite loops
const columnsWithMetaRef = useRef(columnsWithMeta);
useEffect(() => {
columnsWithMetaRef.current = columnsWithMeta;
}, [columnsWithMeta]);
const dragStyles = useStyles2(getDragStyles);
// Filtered copy of columnsWithMeta that only includes matching results
@@ -86,29 +100,33 @@ export function LogsTableWrap(props: Props) {
logsFrames.find((f) => f.refId === panelStateRefId) ?? logsFrames[0]
);
const logsFrame = useMemo(() => parseLogsFrame(currentDataFrame), [currentDataFrame]);
const getColumnsFromProps = useCallback(
(fieldNames: FieldNameMetaStore) => {
const previouslySelected = props.panelState?.columns;
const previouslySelected = props.panelState?.displayedFields;
if (previouslySelected) {
Object.values(previouslySelected).forEach((key, index) => {
if (fieldNames[key]) {
fieldNames[key].active = true;
fieldNames[key].index = index;
// Map LOG_LINE_BODY_FIELD_NAME to actual body field name
const mappedKey =
key === LOG_LINE_BODY_FIELD_NAME ? (logsFrame?.bodyField?.name ?? TABLE_LINE_FIELD_NAME) : key;
if (fieldNames[mappedKey]) {
fieldNames[mappedKey].active = true;
fieldNames[mappedKey].index = index;
}
});
}
return fieldNames;
},
[props.panelState?.columns]
[props.panelState?.displayedFields, logsFrame?.bodyField?.name]
);
const logsFrame = useMemo(() => parseLogsFrame(currentDataFrame), [currentDataFrame]);
useEffect(() => {
if (logsFrame?.timeField.name && logsFrame?.bodyField.name && !propsColumns) {
const defaultColumns = { 0: logsFrame?.timeField.name ?? '', 1: logsFrame?.bodyField.name ?? '' };
const defaultColumns = [logsFrame?.timeField.name, logsFrame?.bodyField.name];
updatePanelState({
columns: Object.values(defaultColumns),
displayedFields: defaultColumns,
visualisationType: 'table',
labelFieldName: logsFrame?.getLabelFieldName() ?? undefined,
});
@@ -187,6 +205,7 @@ export function LogsTableWrap(props: Props) {
// If we have labels and log lines
if (labels?.length && numberOfLogLines) {
const displayedFields = props.panelState?.displayedFields ?? [];
// Iterate through all of Labels
labels.forEach((labels: Labels) => {
const labelsArray = Object.keys(labels);
@@ -196,11 +215,19 @@ export function LogsTableWrap(props: Props) {
if (labelCardinality.has(label)) {
const value = labelCardinality.get(label);
if (value) {
if (value?.active) {
// Check displayedFields first, then fall back to current value
const isActiveInDisplayedFields = displayedFields.includes(label);
const currentMeta = columnsWithMetaRef.current?.[label];
const shouldBeActive = isActiveInDisplayedFields || currentMeta?.active || value.active || false;
const index = isActiveInDisplayedFields
? displayedFields.indexOf(label)
: (currentMeta?.index ?? value.index);
if (shouldBeActive && index !== undefined) {
labelCardinality.set(label, {
percentOfLinesWithLabel: value.percentOfLinesWithLabel + 1,
active: true,
index: value.index,
index: index,
});
} else {
labelCardinality.set(label, {
@@ -212,7 +239,25 @@ export function LogsTableWrap(props: Props) {
}
// Otherwise add it
} else {
labelCardinality.set(label, { percentOfLinesWithLabel: 1, active: false, index: undefined });
// Check if this label is in displayedFields
const isActiveInDisplayedFields = displayedFields.includes(label);
const currentMeta = columnsWithMetaRef.current?.[label];
const shouldBeActive = isActiveInDisplayedFields || currentMeta?.active || false;
const index = isActiveInDisplayedFields ? displayedFields.indexOf(label) : currentMeta?.index;
if (shouldBeActive && index !== undefined) {
labelCardinality.set(label, {
percentOfLinesWithLabel: 1,
active: true,
index: index,
});
} else {
labelCardinality.set(label, {
percentOfLinesWithLabel: 1,
active: false,
index: undefined,
});
}
}
});
});
@@ -230,9 +275,14 @@ export function LogsTableWrap(props: Props) {
}
// Normalize the other fields
const displayedFields = props.panelState?.displayedFields ?? [];
otherFields.forEach((field) => {
const isActive = pendingLabelState[field.name]?.active;
const index = pendingLabelState[field.name]?.index;
// Check displayedFields first, then fall back to current columnsWithMeta
const isActiveInDisplayedFields = displayedFields.includes(field.name);
const currentMeta = columnsWithMetaRef.current?.[field.name];
const isActive = isActiveInDisplayedFields || currentMeta?.active || false;
const index = isActiveInDisplayedFields ? displayedFields.indexOf(field.name) : currentMeta?.index;
if (isActive && index !== undefined) {
pendingLabelState[field.name] = {
percentOfLinesWithLabel: normalize(
@@ -274,10 +324,13 @@ export function LogsTableWrap(props: Props) {
pendingLabelState[logsFrame.timeField.name].type = 'TIME_FIELD';
}
setColumnsWithMeta(pendingLabelState);
// Only update if the state actually changed to prevent infinite loops
if (!columnsWithMetaRef.current || !shallowCompare(columnsWithMetaRef.current, pendingLabelState)) {
setColumnsWithMeta(pendingLabelState);
}
// The panel state is updated when the user interacts with the multi-select sidebar
}, [currentDataFrame, getColumnsFromProps]);
}, [currentDataFrame, getColumnsFromProps, props.panelState?.displayedFields]);
const [sidebarWidth, setSidebarWidth] = useState(getFieldSelectorWidth(SETTING_KEY_ROOT));
const tableWidth = props.width - sidebarWidth;
@@ -323,17 +376,33 @@ export function LogsTableWrap(props: Props) {
const clearSelection = () => {
const pendingLabelState = { ...columnsWithMeta };
Object.keys(pendingLabelState).forEach((key) => {
const isDefaultField = !!pendingLabelState[key].type;
// after reset the only active fields are the special time and body fields
pendingLabelState[key].active = isDefaultField ? true : false;
// reset the index
if (pendingLabelState[key].type === 'TIME_FIELD') {
pendingLabelState[key].index = 0;
const field = pendingLabelState[key];
const isTimeField = field.type === 'TIME_FIELD' || key === TABLE_TIME_FIELD_NAME;
const isBodyField = field.type === 'BODY_FIELD' || key === TABLE_LINE_FIELD_NAME;
const isDetectedLevel = key === TABLE_DETECTED_LEVEL_FIELD_NAME;
// After reset, only active fields are Time, detected_level, and Line
if (isTimeField || isBodyField || isDetectedLevel) {
pendingLabelState[key].active = true;
// Set indices: Time at 0, detected_level at 1, Line at 2
if (isTimeField) {
pendingLabelState[key].index = 0;
} else if (isDetectedLevel) {
pendingLabelState[key].index = 1;
} else if (isBodyField) {
pendingLabelState[key].index = 2;
}
} else {
pendingLabelState[key].index = pendingLabelState[key].type === 'BODY_FIELD' ? 1 : undefined;
pendingLabelState[key].active = false;
pendingLabelState[key].index = undefined;
}
});
setColumnsWithMeta(pendingLabelState);
// Reset displayedFields to empty array to trigger defaults logic
updatePanelState({
displayedFields: [],
});
};
const reorderColumn = (newColumns: string[]) => {
@@ -364,17 +433,29 @@ export function LogsTableWrap(props: Props) {
return 0;
});
const newColumns: Record<number, string> = Object.assign(
{},
// Get the keys of the object as an array
newColumnsArray
);
// Map body field name to LOG_LINE_BODY_FIELD_NAME
const bodyFieldName = logsFrame?.bodyField?.name ?? TABLE_LINE_FIELD_NAME;
const bodyFieldIndex = newColumnsArray.indexOf(bodyFieldName);
if (bodyFieldIndex !== -1) {
// Replace body field name with LOG_LINE_BODY_FIELD_NAME
newColumnsArray[bodyFieldIndex] = LOG_LINE_BODY_FIELD_NAME;
}
const defaultColumns = { 0: logsFrame?.timeField.name ?? '', 1: logsFrame?.bodyField.name ?? '' };
// Preserve ___OTEL_LOG_ATTRIBUTES___ from displayedFields if it exists
const currentDisplayedFields = props.panelState?.displayedFields ?? [];
const otelAttributesIndex = currentDisplayedFields.indexOf(OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME);
if (otelAttributesIndex !== -1 && !newColumnsArray.includes(OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME)) {
// Insert at original position if it was in displayedFields
newColumnsArray.splice(otelAttributesIndex, 0, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME);
}
const defaultColumns: string[] = [logsFrame?.timeField.name, logsFrame?.bodyField.name].filter(
(name): name is string => name !== undefined
);
const newPanelState: ExploreLogsPanelState = {
...props.panelState,
// URL format requires our array of values be an object, so we convert it using object.assign
columns: Object.keys(newColumns).length ? newColumns : defaultColumns,
displayedFields: newColumnsArray.length ? newColumnsArray : defaultColumns,
refId: currentDataFrame.refId,
visualisationType: 'table',
labelFieldName: logsFrame?.getLabelFieldName() ?? undefined,
@@ -447,7 +528,6 @@ export function LogsTableWrap(props: Props) {
setFilteredColumnsWithMeta(pendingFilteredLabelState);
}
updateExploreState(pendingLabelState);
};
@@ -515,7 +595,7 @@ export function LogsTableWrap(props: Props) {
<LogsTableFieldSelector
clear={clearSelection}
columnsWithMeta={columnsWithMeta}
dataFrames={[currentDataFrame]}
dataFrames={memoizedDataFrames}
logs={[]}
reorder={reorderColumn}
setSidebarWidth={setSidebarWidth}
@@ -31,6 +31,11 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => {
export const LOG_LINE_BODY_FIELD_NAME = '___LOG_LINE_BODY___';
// Table view field constants
export const TABLE_TIME_FIELD_NAME = 'Time';
export const TABLE_LINE_FIELD_NAME = 'Line';
export const TABLE_DETECTED_LEVEL_FIELD_NAME = 'detected_level';
export const LogDetailsBody = (props: Props) => {
const showField = () => {
const { onClickShowField, row } = props;
@@ -21,22 +21,6 @@ interface Props {
export const ActiveFields = ({ activeFields, clear, fields, reorder, suggestedFields, toggle }: Props) => {
const styles = useStyles2(getLogsFieldsStyles);
const onDragEnd = useCallback(
(result: DropResult) => {
if (!result.destination) {
return;
}
const newActiveFields = [...activeFields];
const element = activeFields[result.source.index];
newActiveFields.splice(result.source.index, 1);
newActiveFields.splice(result.destination.index, 0, element);
reorder(newActiveFields);
},
[activeFields, reorder]
);
const active = useMemo(
() => [
...activeFields
@@ -48,6 +32,55 @@ export const ActiveFields = ({ activeFields, clear, fields, reorder, suggestedFi
[activeFields, fields, suggestedFields]
);
const onDragEnd = useCallback(
(result: DropResult) => {
if (!result.destination) {
return;
}
// Get the field names from the active array (what's actually rendered)
// The indices in result are based on the active array, not activeFields
const sourceFieldName = active[result.source.index]?.name;
if (!sourceFieldName) {
return;
}
// Create a new array with the reordered fields
const newActiveFields = [...activeFields];
// Find the actual index of the source field in activeFields
const sourceIndexInActiveFields = newActiveFields.indexOf(sourceFieldName);
if (sourceIndexInActiveFields === -1) {
return;
}
// Remove the source field from its current position
const [movedField] = newActiveFields.splice(sourceIndexInActiveFields, 1);
// Find where to insert it based on the destination in the active array
const destFieldName = active[result.destination.index]?.name;
if (destFieldName) {
// Find the destination field in activeFields
const destIndexInActiveFields = newActiveFields.indexOf(destFieldName);
if (destIndexInActiveFields !== -1) {
// If dragging down, insert after; if dragging up, insert before
const insertIndex =
result.source.index < result.destination.index ? destIndexInActiveFields + 1 : destIndexInActiveFields;
newActiveFields.splice(insertIndex, 0, movedField);
} else {
// Destination field not found in activeFields (shouldn't happen), append
newActiveFields.push(movedField);
}
} else {
// No destination field, append
newActiveFields.push(movedField);
}
reorder(newActiveFields);
},
[activeFields, active, reorder]
);
const suggested = useMemo(
() => suggestedFields.filter((suggestedField) => !activeFields.includes(suggestedField.name)),
[activeFields, suggestedFields]
@@ -10,8 +10,8 @@ import { FieldNameMetaStore } from 'app/features/explore/Logs/LogsTableWrap';
import { SETTING_KEY_ROOT } from 'app/features/explore/Logs/utils/logs';
import { parseLogsFrame } from 'app/features/logs/logsFrame';
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
import { getSuggestedFieldsForLogs } from '../otel/formats';
import { LOG_LINE_BODY_FIELD_NAME, TABLE_DETECTED_LEVEL_FIELD_NAME } from '../LogDetailsBody';
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, getSuggestedFieldsForLogs } from '../otel/formats';
import { useLogListContext } from '../panel/LogListContext';
import { reportInteractionOnce } from '../panel/analytics';
import { LogListModel } from '../panel/processing';
@@ -103,7 +103,10 @@ export const LogListFieldSelector = ({ containerElement, dataFrames, logs }: Log
);
const suggestedFields = useMemo(() => getSuggestedFields(logs, displayedFields), [displayedFields, logs]);
const fields = useMemo(() => getFieldsWithStats(dataFrames), [dataFrames]);
const fields = useMemo(
() => getFieldsWithStats(dataFrames).filter((field) => field.name !== TABLE_DETECTED_LEVEL_FIELD_NAME),
[dataFrames]
);
if (!onClickShowField || !onClickHideField || !setDisplayedFields) {
console.warn(
@@ -215,7 +218,7 @@ export const LogsTableFieldSelector = ({
const displayedColumns = useMemo(
() =>
Object.keys(columnsWithMeta)
.filter((column) => columnsWithMeta[column].active)
.filter((column) => columnsWithMeta[column].active && column !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME)
.sort((a, b) =>
columnsWithMeta[a].index !== undefined && columnsWithMeta[b].index !== undefined
? columnsWithMeta[a].index - columnsWithMeta[b].index
@@ -250,7 +253,10 @@ export const LogsTableFieldSelector = ({
() => getSuggestedFields(logs, displayedColumns, defaultColumns),
[defaultColumns, displayedColumns, logs]
);
const fields = useMemo(() => getFieldsWithStats(dataFrames), [dataFrames]);
const fields = useMemo(
() => getFieldsWithStats(dataFrames).filter((field) => field.name !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME),
[dataFrames]
);
return sidebarWidth > MIN_WIDTH * 2 ? (
<FieldSelector
@@ -27,8 +27,9 @@ import { config, getDataSourceSrv } from '@grafana/runtime';
import { PopoverContent } from '@grafana/ui';
import { checkLogsError, checkLogsSampled, downloadLogs as download, DownloadFormat } from '../../utils';
import { LOG_LINE_BODY_FIELD_NAME, TABLE_TIME_FIELD_NAME, TABLE_DETECTED_LEVEL_FIELD_NAME } from '../LogDetailsBody';
import { getFieldSelectorState } from '../fieldSelector/FieldSelector';
import { getDisplayedFieldsForLogs } from '../otel/formats';
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, getDisplayedFieldsForLogs } from '../otel/formats';
import { getDefaultDetailsMode, getDetailsWidth } from './LogDetailsContext';
import { LogLineTimestampResolution } from './LogLine';
@@ -118,6 +119,15 @@ export const useLogIsPermalinked = (log: LogListModel) => {
return permalinkedLogId && permalinkedLogId === log.uid;
};
/**
* Get default table fields.
* Always returns the table field constants: Time and detected_level (excluding Line).
*/
function getTableDefaultFields(logs: LogRowModel[]): string[] {
// Always return the table field constants (Time and detected_level, excluding Line)
return [TABLE_TIME_FIELD_NAME, TABLE_DETECTED_LEVEL_FIELD_NAME];
}
export type LogListState = Pick<
LogListContextData,
| 'dedupStrategy'
@@ -269,6 +279,48 @@ export const LogListContextProvider = ({
return getDisplayedFieldsForLogs(logs);
}, [logs, setDisplayedFields, showLogAttributes]);
// Get table default fields
const tableDefaultFields = useMemo(() => {
return getTableDefaultFields(logs);
}, [logs]);
// Combine table defaults with OTel defaults in specific order:
// ['Time', 'detected_level', '___LOG_LINE_BODY___', '___OTEL_LOG_ATTRIBUTES___']
const defaultDisplayedFields = useMemo(() => {
const orderedFields: string[] = [];
// 1. Add Time from table defaults
if (tableDefaultFields.includes(TABLE_TIME_FIELD_NAME)) {
orderedFields.push(TABLE_TIME_FIELD_NAME);
}
// 2. Add detected_level from table defaults
if (tableDefaultFields.includes(TABLE_DETECTED_LEVEL_FIELD_NAME)) {
orderedFields.push(TABLE_DETECTED_LEVEL_FIELD_NAME);
}
// 3. Always add LOG_LINE_BODY
orderedFields.push(LOG_LINE_BODY_FIELD_NAME);
// 4. Add OTEL_LOG_ATTRIBUTES if it's in OTel fields
if (otelDisplayedFields.includes(OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME)) {
orderedFields.push(OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME);
}
// 5. Add any other OTel fields that aren't already included
otelDisplayedFields.forEach((field) => {
if (
field !== LOG_LINE_BODY_FIELD_NAME &&
field !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME &&
!orderedFields.includes(field)
) {
orderedFields.push(field);
}
});
return orderedFields;
}, [tableDefaultFields, otelDisplayedFields]);
// OTel displayed fields
useEffect(() => {
if (config.featureToggles.otelLogsFormatting && showLogAttributes !== false) {
@@ -276,14 +328,16 @@ export const LogListContextProvider = ({
}
}, [onLogOptionsChange, otelDisplayedFields, showLogAttributes]);
// Set default displayed fields (table defaults + OTel defaults) when displayedFields is empty or missing table defaults
useEffect(() => {
if (displayedFields.length > 0 || !setDisplayedFields) {
if (!setDisplayedFields || defaultDisplayedFields.length === 0) {
return;
}
if (otelDisplayedFields.length) {
setDisplayedFields(otelDisplayedFields);
if (displayedFields.length === 0) {
setDisplayedFields(defaultDisplayedFields);
}
}, [displayedFields.length, otelDisplayedFields, setDisplayedFields]);
}, [displayedFields, defaultDisplayedFields, tableDefaultFields, setDisplayedFields]);
// Sync state
useEffect(() => {