diff --git a/public/app/features/explore/Logs/LogsTable.tsx b/public/app/features/explore/Logs/LogsTable.tsx index aafbf21cb32..f482d33b311 100644 --- a/public/app/features/explore/Logs/LogsTable.tsx +++ b/public/app/features/explore/Logs/LogsTable.tsx @@ -48,6 +48,9 @@ export function LogsTable(props: Props) { const prepareTableFrame = useCallback( (frame: DataFrame): DataFrame => { + if (!frame.length) { + return frame; + } // Parse the dataframe to a logFrame const logsFrame = parseLogsFrame(frame); const timeIndex = logsFrame?.timeField.index; diff --git a/public/app/features/explore/Logs/LogsTableNavColumn.tsx b/public/app/features/explore/Logs/LogsTableNavColumn.tsx index 3540f54f8a9..242af7a0213 100644 --- a/public/app/features/explore/Logs/LogsTableNavColumn.tsx +++ b/public/app/features/explore/Logs/LogsTableNavColumn.tsx @@ -33,6 +33,65 @@ function getStyles(theme: GrafanaTheme2) { }; } +function sortLabels(labels: Record) { + return (a: string, b: string) => { + // First sort by active + if (labels[a].active && labels[b].active) { + // If both fields are active, sort time first + if (labels[a]?.type === 'TIME_FIELD') { + return -1; + } + if (labels[b]?.type === 'TIME_FIELD') { + return 1; + } + // And then line second + if (labels[a]?.type === 'BODY_FIELD') { + return -1; + } + // special fields are next + if (labels[b]?.type === 'BODY_FIELD') { + return 1; + } + } + + // If just one label is active, sort it first + if (labels[b].active) { + return 1; + } + if (labels[a].active) { + return -1; + } + + // If both fields are special, and not selected, sort time first + if (labels[a]?.type && labels[b]?.type) { + if (labels[a]?.type === 'TIME_FIELD') { + return -1; + } + return 0; + } + + // If only one special field, stick to the top of inactive fields + if (labels[a]?.type && !labels[b]?.type) { + return -1; + } + // if the b field is special, sort it first + if (!labels[a]?.type && labels[b]?.type) { + return 1; + } + + // Finally sort by percent enabled, this could have conflicts with the special fields above, except they are always on 100% of logs + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + + // otherwise do not sort + return 0; + }; +} + export const LogsTableNavColumn = (props: { labels: Record; valueFilter: (value: number) => boolean; @@ -45,7 +104,7 @@ export const LogsTableNavColumn = (props: { if (labelKeys.length) { return (
- {labelKeys.map((labelName) => ( + {labelKeys.sort(sortLabels(labels)).map((labelName) => (
{ checkboxLabel.click(); expect(updatePanelState).toBeCalledWith({ visualisationType: 'table', - columns: { 0: 'app' }, + columns: { 0: 'app', 1: 'Line', 2: 'Time' }, }); }); @@ -109,7 +109,7 @@ describe('LogsTableWrap', () => { checkboxLabel.click(); expect(updatePanelState).toBeCalledWith({ visualisationType: 'table', - columns: {}, + columns: { 0: 'Line', 1: 'Time' }, }); }); }); diff --git a/public/app/features/explore/Logs/LogsTableWrap.tsx b/public/app/features/explore/Logs/LogsTableWrap.tsx index aa3a9b2baf0..33eab4d23db 100644 --- a/public/app/features/explore/Logs/LogsTableWrap.tsx +++ b/public/app/features/explore/Logs/LogsTableWrap.tsx @@ -34,7 +34,11 @@ interface Props extends Themeable2 { onClickFilterOutLabel?: (key: string, value: string, refId?: string) => void; } -export type fieldNameMeta = { percentOfLinesWithLabel: number; active: boolean | undefined }; +export type fieldNameMeta = { + percentOfLinesWithLabel: number; + active: boolean | undefined; + type?: 'BODY_FIELD' | 'TIME_FIELD'; +}; type fieldName = string; type fieldNameMetaStore = Record; @@ -96,14 +100,28 @@ export function LogsTableWrap(props: Props) { * */ useEffect(() => { + // If the data frame is empty, there's nothing to viz, it could mean the user has unselected all columns + if (!dataFrame.length) { + return; + } const numberOfLogLines = dataFrame ? dataFrame.length : 0; const logsFrame = parseLogsFrame(dataFrame); const labels = logsFrame?.getLogFrameLabelsAsLabels(); - const otherFields = logsFrame ? logsFrame.extraFields.filter((field) => !field?.config?.custom?.hidden) : []; + const otherFields = []; + + if (logsFrame) { + otherFields.push(...logsFrame.extraFields.filter((field) => !field?.config?.custom?.hidden)); + } if (logsFrame?.severityField) { otherFields.push(logsFrame?.severityField); } + if (logsFrame?.bodyField) { + otherFields.push(logsFrame?.bodyField); + } + if (logsFrame?.timeField) { + otherFields.push(logsFrame?.timeField); + } // Use a map to dedupe labels and count their occurrences in the logs const labelCardinality = new Map(); @@ -159,6 +177,24 @@ export function LogsTableWrap(props: Props) { pendingLabelState = getColumnsFromProps(pendingLabelState); + // Get all active columns + const active = Object.keys(pendingLabelState).filter((key) => pendingLabelState[key].active); + + // If nothing is selected, then select the default columns + if (active.length === 0) { + if (logsFrame?.bodyField?.name) { + pendingLabelState[logsFrame.bodyField.name].active = true; + } + if (logsFrame?.timeField?.name) { + pendingLabelState[logsFrame.timeField.name].active = true; + } + } + + if (logsFrame?.bodyField?.name && logsFrame?.timeField?.name) { + pendingLabelState[logsFrame.bodyField.name].type = 'BODY_FIELD'; + pendingLabelState[logsFrame.timeField.name].type = 'TIME_FIELD'; + } + setColumnsWithMeta(pendingLabelState); // The panel state is updated when the user interacts with the multi-select sidebar