From b059f5ce3214dd7eec1d13540ee1f5664b31a3a9 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 14 Jan 2026 16:39:20 -0600 Subject: [PATCH] chore: wip --- .../panelcfg/x/LogsTablePanelCfg_types.gen.ts | 1 + .../explore/Logs/LogsTableActionButtons.tsx | 5 + .../panel/logstable/CustomCellRenderer.tsx | 41 ++++++ .../logstable/LogsNGTableRowActionButtons.tsx | 132 ++++++++++++++++++ .../app/plugins/panel/logstable/LogsTable.tsx | 110 +++++++++------ .../app/plugins/panel/logstable/panelcfg.cue | 67 ++++----- .../plugins/panel/logstable/panelcfg.gen.ts | 1 + public/app/plugins/panel/logstable/types.ts | 12 +- 8 files changed, 292 insertions(+), 77 deletions(-) create mode 100644 public/app/plugins/panel/logstable/CustomCellRenderer.tsx create mode 100644 public/app/plugins/panel/logstable/LogsNGTableRowActionButtons.tsx diff --git a/packages/grafana-schema/src/raw/composable/logstable/panelcfg/x/LogsTablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logstable/panelcfg/x/LogsTablePanelCfg_types.gen.ts index c0ef0805e13..9f3c396ed87 100644 --- a/packages/grafana-schema/src/raw/composable/logstable/panelcfg/x/LogsTablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logstable/panelcfg/x/LogsTablePanelCfg_types.gen.ts @@ -11,6 +11,7 @@ export const pluginVersion = "12.4.0-pre"; export interface Options { + buildLinkToLogLine?: unknown; controlsStorageKey?: string; /** * isFilterLabelActive?: _ diff --git a/public/app/features/explore/Logs/LogsTableActionButtons.tsx b/public/app/features/explore/Logs/LogsTableActionButtons.tsx index 88e3c1db4ed..594a86d0d4b 100644 --- a/public/app/features/explore/Logs/LogsTableActionButtons.tsx +++ b/public/app/features/explore/Logs/LogsTableActionButtons.tsx @@ -28,6 +28,11 @@ interface Props extends CustomCellRendererProps { index?: number; } +/** + * @deprecated + * @param props + * @constructor + */ export function LogsTableActionButtons(props: Props) { const { exploreId, absoluteRange, logRows, rowIndex, panelState, displayedFields, logsFrame, frame } = props; diff --git a/public/app/plugins/panel/logstable/CustomCellRenderer.tsx b/public/app/plugins/panel/logstable/CustomCellRenderer.tsx new file mode 100644 index 00000000000..4415280e030 --- /dev/null +++ b/public/app/plugins/panel/logstable/CustomCellRenderer.tsx @@ -0,0 +1,41 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { CustomCellRendererProps, useStyles2 } from '@grafana/ui'; + +import { LogsFrame } from '../../../features/logs/logsFrame'; + +import { LogsNGTableRowActionButtons } from './LogsNGTableRowActionButtons'; +import { BuildLinkToLogLine } from './types'; + +export function LogsTableCustomCellRenderer(props: { + cellProps: CustomCellRendererProps; + logsFrame: LogsFrame; + buildLinkToLog?: BuildLinkToLogLine; +}) { + const styles = useStyles2(getStyles); + return ( + <> + + + {props.cellProps.field.display?.(props.cellProps.value).text ?? String(props.cellProps.value)} + + + ); +} + +const buildLinkToLog: BuildLinkToLogLine = (logsFrame, rowIndex, field) => { + return '@todo'; +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + firstColumnCell: css({ + paddingLeft: theme.spacing(7), + }), + }; +}; diff --git a/public/app/plugins/panel/logstable/LogsNGTableRowActionButtons.tsx b/public/app/plugins/panel/logstable/LogsNGTableRowActionButtons.tsx new file mode 100644 index 00000000000..8b71259a053 --- /dev/null +++ b/public/app/plugins/panel/logstable/LogsNGTableRowActionButtons.tsx @@ -0,0 +1,132 @@ +import { css } from '@emotion/css'; +import { memoize } from 'lodash'; +import { useState } from 'react'; + +import { DataFrame, GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { ClipboardButton, CustomCellRendererProps, IconButton, Modal, useTheme2 } from '@grafana/ui'; +import { LogsFrame } from 'app/features/logs/logsFrame'; + +import { BuildLinkToLogLine } from './types'; + +interface Props extends CustomCellRendererProps { + logsFrame: LogsFrame; + buildLinkToLog?: BuildLinkToLogLine; +} + +/** + * Logs row actions buttons + * @todo use new inspector and default to code mode + * @param props + * @constructor + */ +export function LogsNGTableRowActionButtons(props: Props) { + const { rowIndex, logsFrame, field, frame, buildLinkToLog } = props; + const theme = useTheme2(); + const [isInspecting, setIsInspecting] = useState(false); + const styles = getStyles(theme); + + const handleViewClick = () => { + setIsInspecting(true); + }; + + return ( + <> +
+
+ +
+ {buildLinkToLog && ( +
+ buildLinkToLog(logsFrame, rowIndex, field)} + /> +
+ )} +
+ {isInspecting && ( + setIsInspecting(false)} + isOpen={true} + title={t('explore.logs-table.action-buttons.inspect-value', 'Inspect value')} + > +
{getLineValue(logsFrame, frame, rowIndex)}
+ + getLineValue(logsFrame, frame, rowIndex)}> + {t('explore.logs-table.action-buttons.copy-to-clipboard', 'Copy to Clipboard')} + + +
+ )} + + ); +} + +const getLineValue = memoize((logsFrame: LogsFrame, frame: DataFrame, rowIndex: number) => { + const bodyFieldName = logsFrame?.bodyField?.name; + const bodyField = bodyFieldName + ? frame.fields.find((field) => field.name === bodyFieldName) + : frame.fields.find((field) => field.type === 'string'); + return bodyField?.values[rowIndex]; +}); + +export const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + background: theme.colors.background.secondary, + boxShadow: theme.shadows.z2, + display: 'flex', + flexDirection: 'row', + height: '100%', + left: 0, + top: 0, + padding: `0 ${theme.spacing(0.5)}`, + position: 'absolute', + zIndex: 1, + }), + buttonWrapper: css({ + height: '100%', + '& button svg': { + marginRight: 'auto', + }, + '&:hover': { + color: theme.colors.text.link, + }, + padding: theme.spacing(0, 1), + display: 'flex', + alignItems: 'center', + }), + inspectButton: css({ + borderRadius: theme.shape.radius.default, + display: 'inline-flex', + margin: 0, + overflow: 'hidden', + verticalAlign: 'middle', + cursor: 'pointer', + }), + clipboardButton: css({ + height: 30, + lineHeight: '1', + padding: 0, + width: '20px', + cursor: 'pointer', + }), +}); diff --git a/public/app/plugins/panel/logstable/LogsTable.tsx b/public/app/plugins/panel/logstable/LogsTable.tsx index 9add863aade..ce735c8740b 100644 --- a/public/app/plugins/panel/logstable/LogsTable.tsx +++ b/public/app/plugins/panel/logstable/LogsTable.tsx @@ -13,7 +13,7 @@ import { useDataLinksContext, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; -import { useStyles2 } from '@grafana/ui'; +import { CustomCellRendererProps, TableCellDisplayMode, useStyles2 } from '@grafana/ui'; import { config } from '../../../core/config'; import { getLogsExtractFields } from '../../../features/explore/Logs/LogsTable'; @@ -29,20 +29,17 @@ import { isSetDisplayedFields } from '../logs/types'; import { TablePanel } from '../table/TablePanel'; import type { Options as TableOptions } from '../table/panelcfg.gen'; +import { LogsTableCustomCellRenderer } from './CustomCellRenderer'; import type { Options as LogsTableOptions } from './panelcfg.gen'; -import { isOnLogsTableOptionsChange, onLogsTableOptionsChangeType } from './types'; +import { isBuildLinkToLogLine, isOnLogsTableOptionsChange, OnLogsTableOptionsChange } from './types'; interface LogsTablePanelProps extends PanelProps { frameIndex?: number; showHeader?: boolean; } -// keeping alias @todo remove -//@ts-expect-error -const a: TableOptions = {}; -console.log('a', a); - -const sidebarWidth = 200; +// Defaults +const DEFAULT_SIDEBAR_WIDTH = 200; export const LogsTable = ({ data, @@ -53,7 +50,7 @@ export const LogsTable = ({ options, eventBus, frameIndex = 0, - showHeader = true, + showHeader = true, // @todo not pulling from panel settings onOptionsChange, onFieldConfigChange, replaceVariables, @@ -65,10 +62,10 @@ export const LogsTable = ({ renderCounter, }: LogsTablePanelProps) => { // Variables - const dataFrame = data.series[frameIndex]; + const unTransformedDataFrame = data.series[frameIndex]; // Hooks - const logsFrame: LogsFrame | null = useMemo(() => parseLogsFrame(dataFrame), [dataFrame]); + const logsFrame: LogsFrame | null = useMemo(() => parseLogsFrame(unTransformedDataFrame), [unTransformedDataFrame]); const defaultDisplayedFields = useMemo( () => [ logsFrame?.timeField.name ?? LOGS_DATAPLANE_TIMESTAMP_NAME, @@ -81,12 +78,12 @@ export const LogsTable = ({ 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 styles = useStyles2(getStyles, DEFAULT_SIDEBAR_WIDTH, height, width); const dataLinksContext = useDataLinksContext(); - const unTransformedDataFrame = data.series[frameIndex]; + const dataLinkPostProcessor = dataLinksContext.dataLinkPostProcessor; // Methods - const onLogsTableOptionsChange: onLogsTableOptionsChangeType | undefined = isOnLogsTableOptionsChange(onOptionsChange) + const onLogsTableOptionsChange: OnLogsTableOptionsChange | undefined = isOnLogsTableOptionsChange(onOptionsChange) ? onOptionsChange : undefined; @@ -97,7 +94,6 @@ export const LogsTable = ({ // Callbacks const onTableOptionsChange = useCallback( (options: TableOptions) => { - console.log('onTableOptionsChange', options); onLogsTableOptionsChange?.(options); }, [onLogsTableOptionsChange] @@ -135,33 +131,18 @@ export const LogsTable = ({ ); }; - extractFields().then((frame) => { + extractFields().then((data) => { const extractedFrames = applyFieldOverrides({ - data: frame, - fieldConfig: fieldConfig, + data, + fieldConfig, replaceVariables: replaceVariables ?? getTemplateSrv().replace.bind(getTemplateSrv()), theme: config.theme2, timeZone: timeZone, - dataLinkPostProcessor: dataLinksContext.dataLinkPostProcessor, + dataLinkPostProcessor, }); - - for (let frameIndex = 0; frameIndex < extractedFrames.length; frameIndex++) { - const frame = extractedFrames[frameIndex]; - for (const [, field] of frame.fields.entries()) { - field.config = { - ...field.config, - filterable: field.config?.filterable ?? doesFieldSupportAdHocFiltering(field), - custom: { - ...field.config.custom, - inspect: field.config?.custom?.inspect ?? true, - }, - }; - } - } - setExtractedFrame(extractedFrames); }); - }, [dataLinksContext.dataLinkPostProcessor, fieldConfig, replaceVariables, timeZone, unTransformedDataFrame]); + }, [dataLinkPostProcessor, fieldConfig, replaceVariables, timeZone, unTransformedDataFrame]); /** * Organize fields transform @@ -181,7 +162,7 @@ export const LogsTable = ({ } } - return await lastValueFrom( + const organizedFrame = await lastValueFrom( transformDataFrame( [ { @@ -195,6 +176,44 @@ export const LogsTable = ({ extractedFrame ) ); + + if (!logsFrame) { + throw new Error('missing logsFrame'); + } + + for (let frameIndex = 0; frameIndex < organizedFrame.length; frameIndex++) { + const frame = organizedFrame[frameIndex]; + for (const [fieldIndex, field] of frame.fields.entries()) { + const isFirstField = fieldIndex === 0; + + field.config = { + ...field.config, + filterable: field.config?.filterable ?? doesFieldSupportAdHocFiltering(field, logsFrame), + custom: { + ...field.config.custom, + inspect: field.config?.custom?.inspect ?? doesFieldSupportInspector(field, logsFrame), + // @todo add row actions panel option + cellOptions: + isFirstField && logsFrame + ? { + type: TableCellDisplayMode.Custom, + cellComponent: (cellProps: CustomCellRendererProps) => ( + + ), + } + : field.config.custom?.cellOptions, + }, + }; + } + } + + return organizedFrame; }; organizeFields(displayedFields).then((frame) => { @@ -202,13 +221,13 @@ export const LogsTable = ({ setOrganizedFrame(frame); } }); - }, [extractedFrame, displayedFields]); + }, [extractedFrame, displayedFields, logsFrame, options.buildLinkToLogLine]); if (extractedFrame === null || organizedFrame === null || logsFrame === null) { return; } - console.log('render::LogsTable', { extractedFrame }); + console.log('render::LogsTable', { extractedFrame, organizedFrame }); return (
@@ -220,7 +239,7 @@ export const LogsTable = ({ logs={[]} reorder={(columns: string[]) => {}} setSidebarWidth={(width) => {}} - sidebarWidth={sidebarWidth} + sidebarWidth={DEFAULT_SIDEBAR_WIDTH} toggle={(key: string) => { if (displayedFields.includes(key)) { handleSetDisplayedFields(displayedFields.filter((f) => f !== key)); @@ -233,7 +252,7 @@ export const LogsTable = ({
{ diff --git a/public/app/plugins/panel/logstable/panelcfg.cue b/public/app/plugins/panel/logstable/panelcfg.cue index f8de532f0fd..af5936ff780 100644 --- a/public/app/plugins/panel/logstable/panelcfg.cue +++ b/public/app/plugins/panel/logstable/panelcfg.cue @@ -26,40 +26,41 @@ composableKinds: PanelCfg: { version: [0, 0] schema: { Options: { -// showLabels: bool -// showCommonLabels: bool -// showFieldSelector?: bool -// showTime: bool -// showLogContextToggle: bool - showControls?: bool - controlsStorageKey?: string -// wrapLogMessage: bool -// prettifyLogMessage: bool -// enableLogDetails: bool -// syntaxHighlighting?: bool -// sortOrder: common.LogsSortOrder -// dedupStrategy: common.LogsDedupStrategy -// enableInfiniteScrolling?: bool -// noInteractions?: bool -// showLogAttributes?: bool -// fontSize?: "default" | "small" @cuetsy(kind="enum", memberNames="default|small") -// detailsMode?: "inline" | "sidebar" @cuetsy(kind="enum", memberNames="inline|sidebar") -// timestampResolution?: "ms" | "ns" @cuetsy(kind="enum", memberNames="ms|ns") - // TODO: figure out how to define callbacks - onClickFilterLabel?: _ - onClickFilterOutLabel?: _ -// isFilterLabelActive?: _ -// onClickFilterString?: _ -// onClickFilterOutString?: _ -// onClickShowField?: _ -// onClickHideField?: _ -// onLogOptionsChange?: _ -// logRowMenuIconsBefore?: _ -// logRowMenuIconsAfter?: _ -// logLineMenuCustomItems?: _ -// onNewLogsReceived?: _ + // showLabels: bool + // showCommonLabels: bool + // showFieldSelector?: bool + // showTime: bool + // showLogContextToggle: bool + showControls?: bool + controlsStorageKey?: string + // wrapLogMessage: bool + // prettifyLogMessage: bool + // enableLogDetails: bool + // syntaxHighlighting?: bool + // sortOrder: common.LogsSortOrder + // dedupStrategy: common.LogsDedupStrategy + // enableInfiniteScrolling?: bool + // noInteractions?: bool + // showLogAttributes?: bool + // fontSize?: "default" | "small" @cuetsy(kind="enum", memberNames="default|small") + // detailsMode?: "inline" | "sidebar" @cuetsy(kind="enum", memberNames="inline|sidebar") + // timestampResolution?: "ms" | "ns" @cuetsy(kind="enum", memberNames="ms|ns") + // @todo filter methods no longer needed as props since these are defined by context? + // onClickFilterLabel?: _ + // onClickFilterOutLabel?: _ + // isFilterLabelActive?: _ + // onClickFilterString?: _ + // onClickFilterOutString?: _ + // onClickShowField?: _ + // onClickHideField?: _ + // onLogOptionsChange?: _ + // logRowMenuIconsBefore?: _ + // logRowMenuIconsAfter?: _ + // logLineMenuCustomItems?: _ + // onNewLogsReceived?: _ displayedFields?: [...string] - setDisplayedFields?: _ + setDisplayedFields?: _ + buildLinkToLogLine?: _ } @cuetsy(kind="interface") } }] diff --git a/public/app/plugins/panel/logstable/panelcfg.gen.ts b/public/app/plugins/panel/logstable/panelcfg.gen.ts index 1c7cb3025b2..bc6731f65fd 100644 --- a/public/app/plugins/panel/logstable/panelcfg.gen.ts +++ b/public/app/plugins/panel/logstable/panelcfg.gen.ts @@ -9,6 +9,7 @@ // Run 'make gen-cue' from repository root to regenerate. export interface Options { + buildLinkToLogLine?: unknown; controlsStorageKey?: string; /** * isFilterLabelActive?: _ diff --git a/public/app/plugins/panel/logstable/types.ts b/public/app/plugins/panel/logstable/types.ts index 19929075381..4f90da41412 100644 --- a/public/app/plugins/panel/logstable/types.ts +++ b/public/app/plugins/panel/logstable/types.ts @@ -1,8 +1,16 @@ +import { Field } from '@grafana/data'; + +import { LogsFrame } from '../../../features/logs/logsFrame'; import type { Options as TableOptions } from '../table/panelcfg.gen'; import type { Options as LogsTableOptions } from './panelcfg.gen'; -export type onLogsTableOptionsChangeType = (option: LogsTableOptions & TableOptions) => void; -export function isOnLogsTableOptionsChange(callback: unknown): callback is onLogsTableOptionsChangeType { +export type OnLogsTableOptionsChange = (option: LogsTableOptions & TableOptions) => void; +export type BuildLinkToLogLine = (logsFrame: LogsFrame, rowIndex: number, field: Field) => string; +export function isOnLogsTableOptionsChange(callback: unknown): callback is OnLogsTableOptionsChange { + return typeof callback === 'function'; +} + +export function isBuildLinkToLogLine(callback: unknown): callback is BuildLinkToLogLine { return typeof callback === 'function'; }