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:
Galen Kistler
2023-11-07 08:00:06 -06:00
committed by GitHub
parent 0b03344baa
commit 9e54012407
4 changed files with 103 additions and 5 deletions

View File

@@ -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;

View File

@@ -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: {
labels: Record<string, fieldNameMeta>;
valueFilter: (value: number) => boolean;
@@ -45,7 +104,7 @@ export const LogsTableNavColumn = (props: {
if (labelKeys.length) {
return (
<div className={styles.columnWrapper}>
{labelKeys.map((labelName) => (
{labelKeys.sort(sortLabels(labels)).map((labelName) => (
<div className={styles.wrap} key={labelName}>
<Checkbox
className={styles.checkbox}

View File

@@ -100,7 +100,7 @@ describe('LogsTableWrap', () => {
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' },
});
});
});

View File

@@ -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<fieldName, fieldNameMeta>;
@@ -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<fieldName, fieldNameMeta>();
@@ -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