diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index 1eb255aa7c3..aed4e3dc1f5 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -78,7 +78,6 @@ export interface ExploreTracePanelState { export interface ExploreLogsPanelState { id?: string; - columns?: Record; visualisationType?: 'table' | 'logs'; labelFieldName?: string; // Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 1ff012f3fee..2b61b5fd590 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -201,8 +201,9 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { panelState?.logs?.sortOrder ?? store.get(SETTINGS_KEYS.logsSortOrder) ?? LogsSortOrder.Descending ); const [isFlipping, setIsFlipping] = useState(false); - const [displayedFields, setDisplayedFields] = useState(panelState?.logs?.displayedFields ?? []); const [defaultDisplayedFields, setDefaultDisplayedFields] = useState([]); + // Use Redux state as single source of truth + const displayedFields = useMemo(() => panelState?.logs?.displayedFields ?? [], [panelState?.logs?.displayedFields]); const [contextOpen, setContextOpen] = useState(false); const [contextRow, setContextRow] = useState(undefined); const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState(PINNED_LOGS_MESSAGE); @@ -322,7 +323,6 @@ const UnthemedLogs: React.FunctionComponent = (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) => { [ dispatch, exploreId, - panelState?.logs?.columns, panelState?.logs?.displayedFields, panelState?.logs?.refId, panelState?.logs?.tableSortBy, @@ -345,14 +344,7 @@ const UnthemedLogs: React.FunctionComponent = (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) => { 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>(() => {}); diff --git a/public/app/features/explore/Logs/LogsTable.tsx b/public/app/features/explore/Logs/LogsTable.tsx index 9a5245cd7a0..73fd7d6b4ee 100644 --- a/public/app/features/explore/Logs/LogsTable.tsx +++ b/public/app/features/explore/Logs/LogsTable.tsx @@ -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) { // 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 = {}; 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 diff --git a/public/app/features/explore/Logs/LogsTableWrap.tsx b/public/app/features/explore/Logs/LogsTableWrap.tsx index cde546d92ba..3ad3279fac5 100644 --- a/public/app/features/explore/Logs/LogsTableWrap.tsx +++ b/public/app/features/explore/Logs/LogsTableWrap.tsx @@ -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; 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(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 = 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) { { 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; diff --git a/public/app/features/logs/components/fieldSelector/ActiveFields.tsx b/public/app/features/logs/components/fieldSelector/ActiveFields.tsx index b7b2053a603..5e160f8c22b 100644 --- a/public/app/features/logs/components/fieldSelector/ActiveFields.tsx +++ b/public/app/features/logs/components/fieldSelector/ActiveFields.tsx @@ -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] diff --git a/public/app/features/logs/components/fieldSelector/FieldSelector.tsx b/public/app/features/logs/components/fieldSelector/FieldSelector.tsx index d68cf5340c9..ef3930b9940 100644 --- a/public/app/features/logs/components/fieldSelector/FieldSelector.tsx +++ b/public/app/features/logs/components/fieldSelector/FieldSelector.tsx @@ -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 ? ( { 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(() => {