chore: wip

This commit is contained in:
Galen
2026-01-14 16:39:20 -06:00
parent 3a39e927a2
commit b059f5ce32
8 changed files with 292 additions and 77 deletions
@@ -11,6 +11,7 @@
export const pluginVersion = "12.4.0-pre";
export interface Options {
buildLinkToLogLine?: unknown;
controlsStorageKey?: string;
/**
* isFilterLabelActive?: _
@@ -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;
@@ -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 (
<>
<LogsNGTableRowActionButtons
{...props.cellProps}
buildLinkToLog={props.buildLinkToLog ?? buildLinkToLog}
logsFrame={props.logsFrame}
/>
<span className={styles.firstColumnCell}>
{props.cellProps.field.display?.(props.cellProps.value).text ?? String(props.cellProps.value)}
</span>
</>
);
}
const buildLinkToLog: BuildLinkToLogLine = (logsFrame, rowIndex, field) => {
return '@todo';
};
const getStyles = (theme: GrafanaTheme2) => {
return {
firstColumnCell: css({
paddingLeft: theme.spacing(7),
}),
};
};
@@ -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 (
<>
<div className={styles.container}>
<div className={styles.buttonWrapper}>
<IconButton
className={styles.inspectButton}
tooltip={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
variant="secondary"
aria-label={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
tooltipPlacement="top"
size="md"
name="eye"
onClick={handleViewClick}
tabIndex={0}
/>
</div>
{buildLinkToLog && (
<div className={styles.buttonWrapper}>
<ClipboardButton
className={styles.clipboardButton}
icon="share-alt"
variant="secondary"
fill="text"
size="md"
tooltip={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
tooltipPlacement="top"
tabIndex={0}
aria-label={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
getText={() => buildLinkToLog(logsFrame, rowIndex, field)}
/>
</div>
)}
</div>
{isInspecting && (
<Modal
onDismiss={() => setIsInspecting(false)}
isOpen={true}
title={t('explore.logs-table.action-buttons.inspect-value', 'Inspect value')}
>
<pre>{getLineValue(logsFrame, frame, rowIndex)}</pre>
<Modal.ButtonRow>
<ClipboardButton icon="copy" getText={() => getLineValue(logsFrame, frame, rowIndex)}>
{t('explore.logs-table.action-buttons.copy-to-clipboard', 'Copy to Clipboard')}
</ClipboardButton>
</Modal.ButtonRow>
</Modal>
)}
</>
);
}
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',
}),
});
@@ -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<LogsTableOptions> {
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<DataFrame[] | null>(null);
const [organizedFrame, setOrganizedFrame] = useState<DataFrame[] | null>(null);
const [displayedFields, setDisplayedFields] = useState<string[]>(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) => (
<LogsTableCustomCellRenderer
cellProps={cellProps}
logsFrame={logsFrame}
buildLinkToLog={
isBuildLinkToLogLine(options.buildLinkToLogLine) ? options.buildLinkToLogLine : undefined
}
/>
),
}
: 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 (
<div className={styles.wrapper}>
@@ -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 = ({
<div className={styles.tableWrapper}>
<TablePanel
data={{ ...data, series: organizedFrame }}
width={width - sidebarWidth}
width={width - DEFAULT_SIDEBAR_WIDTH}
height={height}
id={id}
timeRange={timeRange}
@@ -273,8 +292,15 @@ function displayedFieldsToColumns(displayedFields: string[], logsFrame: LogsFram
return columns;
}
function doesFieldSupportAdHocFiltering(field: Field): boolean {
return true;
function doesFieldSupportInspector(field: Field, logsFrame: LogsFrame) {
// const unsupportedFields = [logsFrame.bodyField.name]
// return !unsupportedFields.includes(field.name);
return false;
}
function doesFieldSupportAdHocFiltering(field: Field, logsFrame: LogsFrame): boolean {
const unsupportedFields = [logsFrame.timeField.name, logsFrame.bodyField.name];
return !unsupportedFields.includes(field.name);
}
const getStyles = (theme: GrafanaTheme2, sidebarWidth: number, height: number, width: number) => {
+34 -33
View File
@@ -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")
}
}]
+1
View File
@@ -9,6 +9,7 @@
// Run 'make gen-cue' from repository root to regenerate.
export interface Options {
buildLinkToLogLine?: unknown;
controlsStorageKey?: string;
/**
* isFilterLabelActive?: _
+10 -2
View File
@@ -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';
}