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';
}