Logs: use displayFields for both logs panel adn table
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user