diff --git a/public/app/features/logs/logsFrame.ts b/public/app/features/logs/logsFrame.ts index 5a9938b6e2b..0a6ea9af4ff 100644 --- a/public/app/features/logs/logsFrame.ts +++ b/public/app/features/logs/logsFrame.ts @@ -29,11 +29,11 @@ function getField(cache: FieldCache, name: string, fieldType: FieldType): FieldW return field.type === fieldType ? field : undefined; } -const DATAPLANE_TIMESTAMP_NAME = 'timestamp'; -const DATAPLANE_BODY_NAME = 'body'; -const DATAPLANE_SEVERITY_NAME = 'severity'; -const DATAPLANE_ID_NAME = 'id'; -const DATAPLANE_LABELS_NAME = 'labels'; +export const LOGS_DATAPLANE_TIMESTAMP_NAME = 'timestamp'; +export const LOGS_DATAPLANE_BODY_NAME = 'body'; +const LOGS_DATAPLANE_SEVERITY_NAME = 'severity'; +const LOGS_DATAPLANE_ID_NAME = 'id'; +const LOGS_DATAPLANE_LABELS_NAME = 'labels'; // NOTE: this is a hot fn, we need to avoid allocating new objects here export function logFrameLabelsToLabels(logFrameLabels: LogFrameLabels): Labels { @@ -66,17 +66,17 @@ export function logFrameLabelsToLabels(logFrameLabels: LogFrameLabels): Labels { export function parseDataplaneLogsFrame(frame: DataFrame): LogsFrame | null { const cache = new FieldCache(frame); - const timestampField = getField(cache, DATAPLANE_TIMESTAMP_NAME, FieldType.time); - const bodyField = getField(cache, DATAPLANE_BODY_NAME, FieldType.string); + const timestampField = getField(cache, LOGS_DATAPLANE_TIMESTAMP_NAME, FieldType.time); + const bodyField = getField(cache, LOGS_DATAPLANE_BODY_NAME, FieldType.string); // these two are mandatory if (timestampField === undefined || bodyField === undefined) { return null; } - const severityField = getField(cache, DATAPLANE_SEVERITY_NAME, FieldType.string) ?? null; - const idField = getField(cache, DATAPLANE_ID_NAME, FieldType.string) ?? null; - const labelsField = getField(cache, DATAPLANE_LABELS_NAME, FieldType.other) ?? null; + const severityField = getField(cache, LOGS_DATAPLANE_SEVERITY_NAME, FieldType.string) ?? null; + const idField = getField(cache, LOGS_DATAPLANE_ID_NAME, FieldType.string) ?? null; + const labelsField = getField(cache, LOGS_DATAPLANE_LABELS_NAME, FieldType.other) ?? null; const labels = labelsField === null ? null : labelsField.values; diff --git a/public/app/plugins/panel/logstable/LogsTable.tsx b/public/app/plugins/panel/logstable/LogsTable.tsx index b9f9f7c864c..f8fca1576d6 100644 --- a/public/app/plugins/panel/logstable/LogsTable.tsx +++ b/public/app/plugins/panel/logstable/LogsTable.tsx @@ -1,10 +1,28 @@ -import { useEffect, useState } from 'react'; +import { css } from '@emotion/css'; +import { useEffect, useState, useMemo } from 'react'; import { lastValueFrom } from 'rxjs'; -import { DataFrame, PanelProps, transformDataFrame } from '@grafana/data'; +import { + applyFieldOverrides, + DataFrame, + GrafanaTheme2, + PanelProps, + transformDataFrame, + useDataLinksContext, +} from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; +import { useStyles2 } from '@grafana/ui'; +import { config } from '../../../core/config'; import { getLogsExtractFields } from '../../../features/explore/Logs/LogsTable'; -import { LogsFrame, parseLogsFrame } from '../../../features/logs/logsFrame'; +import { FieldNameMetaStore } from '../../../features/explore/Logs/LogsTableWrap'; +import { LogsTableFieldSelector } from '../../../features/logs/components/fieldSelector/FieldSelector'; +import { + LOGS_DATAPLANE_BODY_NAME, + LOGS_DATAPLANE_TIMESTAMP_NAME, + LogsFrame, + parseLogsFrame, +} from '../../../features/logs/logsFrame'; import { TablePanel } from '../table/TablePanel'; import type { Options as TableOptions } from '../table/panelcfg.gen'; @@ -15,6 +33,8 @@ interface LogsTablePanelProps extends PanelProps { showHeader?: boolean; } +const sidebarWidth = 200; + export const LogsTable = ({ data, width, @@ -35,13 +55,21 @@ export const LogsTable = ({ 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 dataFrame = data.series[frameIndex]; + const logsFrame: LogsFrame | null = useMemo(() => parseLogsFrame(dataFrame), [dataFrame]); + const defaultDisplayedFields = useMemo( + () => [ + logsFrame?.timeField.name ?? LOGS_DATAPLANE_TIMESTAMP_NAME, + logsFrame?.bodyField.name ?? LOGS_DATAPLANE_BODY_NAME, + ], + [logsFrame?.timeField.name, logsFrame?.bodyField.name] + ); - const [extractedFrame, setExtractedFrame] = useState(null); - const [organizedFrame, setOrganizedFrame] = useState(null); + const [extractedFrame, setExtractedFrame] = useState(null); + const [organizedFrame, setOrganizedFrame] = useState(null); + const [displayedFields, setDisplayedFields] = useState(options.displayedFields ?? defaultDisplayedFields); + const styles = useStyles2(getStyles, sidebarWidth, height, width); + const dataLinksContext = useDataLinksContext(); const onTableOptionsChange = (options: TableOptions) => { onOptionsChange({}); @@ -60,9 +88,21 @@ export const LogsTable = ({ }; extractFields().then((frame) => { - setExtractedFrame(frame[0]); + setExtractedFrame( + applyFieldOverrides({ + data: frame, + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), + theme: config.theme2, + timeZone: timeZone, + dataLinkPostProcessor: dataLinksContext.dataLinkPostProcessor, + }) + ); }); - }, [unTransformedDataFrame]); + }, [dataLinksContext.dataLinkPostProcessor, timeZone, unTransformedDataFrame]); /** * Organize fields transform @@ -93,42 +133,102 @@ export const LogsTable = ({ }, }, ], - [extractedFrame] + extractedFrame ) ); }; - organizeFields(options.displayedFields ?? [logsFrame.timeField.name, logsFrame.bodyField.name]).then((frame) => { + organizeFields(displayedFields).then((frame) => { if (frame) { - setOrganizedFrame(frame[0]); + setOrganizedFrame(frame); } }); - }, [extractedFrame, options.displayedFields, logsFrame.timeField.name, logsFrame.bodyField.name]); + }, [extractedFrame, displayedFields]); - if (extractedFrame === null || organizedFrame === null) { + if (extractedFrame === null || organizedFrame === null || logsFrame === null) { return; } console.log('render::LogsTable', { extractedFrame }); return ( - +
+
+ {}} + columnsWithMeta={displayedFieldsToColumns(displayedFields, logsFrame)} + dataFrames={extractedFrame} + logs={[]} + reorder={(columns: string[]) => {}} + setSidebarWidth={(width) => {}} + sidebarWidth={sidebarWidth} + toggle={(key: string) => { + if (displayedFields.includes(key)) { + setDisplayedFields(displayedFields.filter((f) => f !== key)); + } else { + setDisplayedFields([...displayedFields, key]); + } + }} + /> +
+
+ +
+
); }; + +function displayedFieldsToColumns(displayedFields: string[], logsFrame: LogsFrame): FieldNameMetaStore { + const columns: FieldNameMetaStore = {}; + for (const [idx, field] of displayedFields.entries()) { + columns[field] = { + percentOfLinesWithLabel: 0, + type: + field === logsFrame.bodyField.name + ? 'BODY_FIELD' + : field === logsFrame.timeField.name + ? 'TIME_FIELD' + : undefined, + active: true, + index: idx, + }; + } + + return columns; +} + +const getStyles = (theme: GrafanaTheme2, sidebarWidth: number, height: number, width: number) => { + return { + tableWrapper: css({ + paddingLeft: sidebarWidth, + height, + width, + }), + sidebarWrapper: css({ + position: 'absolute', + height: height, + width: sidebarWidth, + }), + wrapper: css({ + height, + width, + }), + }; +};