chore: render transformed default displayed fields

This commit is contained in:
Galen
2026-01-07 17:00:08 -06:00
parent 3914147c86
commit b87da74c1e
6 changed files with 187 additions and 74 deletions
@@ -43,14 +43,14 @@ export interface Options {
onClickFilterLabel?: unknown;
onClickFilterOutLabel?: unknown;
setDisplayedFields?: unknown;
showControls?: boolean;
/**
* showLabels: bool
* showCommonLabels: bool
* showFieldSelector?: bool
* showTime: bool
* showLogContextToggle: bool
*/
showLogContextToggle: boolean;
showControls?: boolean;
}
export const defaultOptions: Partial<Options> = {
@@ -367,8 +367,7 @@ const isFieldFilterable = (field: Field, bodyName: string, timeName: string) =>
return true;
};
// TODO: explore if `logsFrame.ts` can help us with getting the right fields
// TODO Why is typeInfo not defined on the Field interface?
// @todo move to logsFrame
export function getLogsExtractFields(dataFrame: DataFrame) {
return dataFrame.fields
.filter((field: Field & { typeInfo?: { frame: string } }) => {
@@ -1,4 +1,134 @@
export const LogsTable = () => {
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
return <div>Hello plugin</div>;
import { useEffect, useState } from 'react';
import { lastValueFrom } from 'rxjs';
import { DataFrame, PanelProps, transformDataFrame } from '@grafana/data';
import { getLogsExtractFields } from '../../../features/explore/Logs/LogsTable';
import { LogsFrame, parseLogsFrame } from '../../../features/logs/logsFrame';
import { TablePanel } from '../table/TablePanel';
import type { Options as TableOptions } from '../table/panelcfg.gen';
import type { Options } from './panelcfg.gen';
interface LogsTablePanelProps extends PanelProps<Options> {
frameIndex?: number;
showHeader?: boolean;
}
export const LogsTable = ({
data,
width,
height,
timeRange,
fieldConfig,
options,
eventBus,
frameIndex = 0,
showHeader = true,
onOptionsChange,
onFieldConfigChange,
replaceVariables,
onChangeTimeRange,
title,
transparent,
timeZone,
id,
renderCounter,
}: LogsTablePanelProps) => {
const logsFrame: LogsFrame | null = parseLogsFrame(data.series[frameIndex]);
if (!logsFrame?.timeField.name || !logsFrame?.bodyField.name) {
throw new Error(`Invalid logsFrame frame name: ${frameIndex}`);
}
const [extractedFrame, setExtractedFrame] = useState<DataFrame | null>(null);
const [organizedFrame, setOrganizedFrame] = useState<DataFrame | null>(null);
const onTableOptionsChange = (options: TableOptions) => {
onOptionsChange({});
};
const unTransformedDataFrame = data.series[frameIndex];
/**
* Extract fields transform
*/
useEffect(() => {
const extractFields = async () => {
return await lastValueFrom(
transformDataFrame(getLogsExtractFields(unTransformedDataFrame), [unTransformedDataFrame])
);
};
extractFields().then((frame) => {
setExtractedFrame(frame[0]);
});
}, [unTransformedDataFrame]);
/**
* Organize fields transform
*/
useEffect(() => {
const organizeFields = async (displayedFields: string[]) => {
if (!extractedFrame) {
return Promise.resolve(null);
}
let indexByName: Record<string, number> = {};
let includeByName: Record<string, boolean> = {};
if (displayedFields) {
for (const [idx, field] of displayedFields.entries()) {
indexByName[field] = idx;
includeByName[field] = true;
}
}
return await lastValueFrom(
transformDataFrame(
[
{
id: 'organize',
options: {
indexByName,
includeByName,
},
},
],
[extractedFrame]
)
);
};
organizeFields(options.displayedFields ?? [logsFrame.timeField.name, logsFrame.bodyField.name]).then((frame) => {
if (frame) {
setOrganizedFrame(frame[0]);
}
});
}, [extractedFrame, options.displayedFields, logsFrame.timeField.name, logsFrame.bodyField.name]);
if (extractedFrame === null || organizedFrame === null) {
return;
}
console.log('render::LogsTable', { extractedFrame });
return (
<TablePanel
data={{ ...data, series: [organizedFrame] }}
width={width}
height={height}
id={id}
timeRange={timeRange}
timeZone={timeZone}
options={{ ...options, frameIndex: 0, showHeader }}
transparent={transparent}
fieldConfig={fieldConfig}
renderCounter={renderCounter}
title={title}
eventBus={eventBus}
onOptionsChange={onTableOptionsChange}
onFieldConfigChange={onFieldConfigChange}
replaceVariables={replaceVariables}
onChangeTimeRange={onChangeTimeRange}
/>
);
};
+48 -64
View File
@@ -1,70 +1,54 @@
import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data';
import { PanelPlugin } from '@grafana/data';
import { LogsTable } from './LogsTable';
import { Options } from './panelcfg.gen';
export const plugin = new PanelPlugin<Options>(LogsTable).setPanelOptions((builder) => {
builder
.addBooleanSwitch({
path: 'showTime',
name: 'Time',
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'wrapLogMessage',
name: 'Wrap lines',
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'enableLogDetails',
name: 'Enable log details',
description: '',
defaultValue: true,
})
.addBooleanSwitch({
path: 'enableInfiniteScrolling',
name: 'Enable infinite scrolling',
description: 'Experimental. Request more results by scrolling to the bottom of the logs list.',
defaultValue: false,
})
.addRadio({
path: 'dedupStrategy',
name: 'Deduplication',
description: '',
settings: {
options: [
{ value: LogsDedupStrategy.none, label: 'None', description: LogsDedupDescription[LogsDedupStrategy.none] },
{
value: LogsDedupStrategy.exact,
label: 'Exact',
description: LogsDedupDescription[LogsDedupStrategy.exact],
},
{
value: LogsDedupStrategy.numbers,
label: 'Numbers',
description: LogsDedupDescription[LogsDedupStrategy.numbers],
},
{
value: LogsDedupStrategy.signature,
label: 'Signature',
description: LogsDedupDescription[LogsDedupStrategy.signature],
},
],
},
defaultValue: LogsDedupStrategy.none,
})
.addRadio({
path: 'sortOrder',
name: 'Order',
description: '',
settings: {
options: [
{ value: LogsSortOrder.Descending, label: 'Newest first' },
{ value: LogsSortOrder.Ascending, label: 'Oldest first' },
],
},
defaultValue: LogsSortOrder.Descending,
});
builder.addBooleanSwitch({
path: 'wrapLogMessage',
name: 'Wrap lines',
description: '',
defaultValue: false,
});
// @todo add table options?
//
// .addRadio({
// path: 'dedupStrategy',
// name: 'Deduplication',
// description: '',
// settings: {
// options: [
// { value: LogsDedupStrategy.none, label: 'None', description: LogsDedupDescription[LogsDedupStrategy.none] },
// {
// value: LogsDedupStrategy.exact,
// label: 'Exact',
// description: LogsDedupDescription[LogsDedupStrategy.exact],
// },
// {
// value: LogsDedupStrategy.numbers,
// label: 'Numbers',
// description: LogsDedupDescription[LogsDedupStrategy.numbers],
// },
// {
// value: LogsDedupStrategy.signature,
// label: 'Signature',
// description: LogsDedupDescription[LogsDedupStrategy.signature],
// },
// ],
// },
// defaultValue: LogsDedupStrategy.none,
// })
// .addRadio({
// path: 'sortOrder',
// name: 'Order',
// description: '',
// settings: {
// options: [
// { value: LogsSortOrder.Descending, label: 'Newest first' },
// { value: LogsSortOrder.Ascending, label: 'Oldest first' },
// ],
// },
// defaultValue: LogsSortOrder.Descending,
// });
});
@@ -30,7 +30,7 @@ composableKinds: PanelCfg: {
// showCommonLabels: bool
// showFieldSelector?: bool
// showTime: bool
showLogContextToggle: bool
// showLogContextToggle: bool
showControls?: bool
controlsStorageKey?: string
// wrapLogMessage: bool
+2 -2
View File
@@ -41,14 +41,14 @@ export interface Options {
onClickFilterLabel?: unknown;
onClickFilterOutLabel?: unknown;
setDisplayedFields?: unknown;
showControls?: boolean;
/**
* showLabels: bool
* showCommonLabels: bool
* showFieldSelector?: bool
* showTime: bool
* showLogContextToggle: bool
*/
showLogContextToggle: boolean;
showControls?: boolean;
}
export const defaultOptions: Partial<Options> = {