mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
Logs Panel: Table UI - Add time and body fields to column selection in logs table visualization (#77468)
* add the ability to set visibility of time and body fields, set default of time and body fields when no other fields are selected
This commit is contained in:
@@ -48,6 +48,9 @@ export function LogsTable(props: Props) {
|
|||||||
|
|
||||||
const prepareTableFrame = useCallback(
|
const prepareTableFrame = useCallback(
|
||||||
(frame: DataFrame): DataFrame => {
|
(frame: DataFrame): DataFrame => {
|
||||||
|
if (!frame.length) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
// Parse the dataframe to a logFrame
|
// Parse the dataframe to a logFrame
|
||||||
const logsFrame = parseLogsFrame(frame);
|
const logsFrame = parseLogsFrame(frame);
|
||||||
const timeIndex = logsFrame?.timeField.index;
|
const timeIndex = logsFrame?.timeField.index;
|
||||||
|
|||||||
@@ -33,6 +33,65 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortLabels(labels: Record<string, fieldNameMeta>) {
|
||||||
|
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: {
|
export const LogsTableNavColumn = (props: {
|
||||||
labels: Record<string, fieldNameMeta>;
|
labels: Record<string, fieldNameMeta>;
|
||||||
valueFilter: (value: number) => boolean;
|
valueFilter: (value: number) => boolean;
|
||||||
@@ -45,7 +104,7 @@ export const LogsTableNavColumn = (props: {
|
|||||||
if (labelKeys.length) {
|
if (labelKeys.length) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.columnWrapper}>
|
<div className={styles.columnWrapper}>
|
||||||
{labelKeys.map((labelName) => (
|
{labelKeys.sort(sortLabels(labels)).map((labelName) => (
|
||||||
<div className={styles.wrap} key={labelName}>
|
<div className={styles.wrap} key={labelName}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className={styles.checkbox}
|
className={styles.checkbox}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ describe('LogsTableWrap', () => {
|
|||||||
checkboxLabel.click();
|
checkboxLabel.click();
|
||||||
expect(updatePanelState).toBeCalledWith({
|
expect(updatePanelState).toBeCalledWith({
|
||||||
visualisationType: 'table',
|
visualisationType: 'table',
|
||||||
columns: { 0: 'app' },
|
columns: { 0: 'app', 1: 'Line', 2: 'Time' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ describe('LogsTableWrap', () => {
|
|||||||
checkboxLabel.click();
|
checkboxLabel.click();
|
||||||
expect(updatePanelState).toBeCalledWith({
|
expect(updatePanelState).toBeCalledWith({
|
||||||
visualisationType: 'table',
|
visualisationType: 'table',
|
||||||
columns: {},
|
columns: { 0: 'Line', 1: 'Time' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ interface Props extends Themeable2 {
|
|||||||
onClickFilterOutLabel?: (key: string, value: string, refId?: string) => void;
|
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 fieldName = string;
|
||||||
type fieldNameMetaStore = Record<fieldName, fieldNameMeta>;
|
type fieldNameMetaStore = Record<fieldName, fieldNameMeta>;
|
||||||
|
|
||||||
@@ -96,14 +100,28 @@ export function LogsTableWrap(props: Props) {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
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 numberOfLogLines = dataFrame ? dataFrame.length : 0;
|
||||||
const logsFrame = parseLogsFrame(dataFrame);
|
const logsFrame = parseLogsFrame(dataFrame);
|
||||||
const labels = logsFrame?.getLogFrameLabelsAsLabels();
|
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) {
|
if (logsFrame?.severityField) {
|
||||||
otherFields.push(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
|
// Use a map to dedupe labels and count their occurrences in the logs
|
||||||
const labelCardinality = new Map<fieldName, fieldNameMeta>();
|
const labelCardinality = new Map<fieldName, fieldNameMeta>();
|
||||||
@@ -159,6 +177,24 @@ export function LogsTableWrap(props: Props) {
|
|||||||
|
|
||||||
pendingLabelState = getColumnsFromProps(pendingLabelState);
|
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);
|
setColumnsWithMeta(pendingLabelState);
|
||||||
|
|
||||||
// The panel state is updated when the user interacts with the multi-select sidebar
|
// The panel state is updated when the user interacts with the multi-select sidebar
|
||||||
|
|||||||
Reference in New Issue
Block a user