Compare commits
7 Commits
sriram/SQL
...
gtk-grafan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b059f5ce32 | ||
|
|
3a39e927a2 | ||
|
|
857f9b3d26 | ||
|
|
a3f9ad5c0d | ||
|
|
c16ab6dd21 | ||
|
|
b87da74c1e | ||
|
|
3914147c86 |
59
packages/grafana-schema/src/raw/composable/logstable/panelcfg/x/LogsTablePanelCfg_types.gen.ts
generated
Normal file
59
packages/grafana-schema/src/raw/composable/logstable/panelcfg/x/LogsTablePanelCfg_types.gen.ts
generated
Normal file
@@ -0,0 +1,59 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
//
|
||||
// Generated by:
|
||||
// public/app/plugins/gen.go
|
||||
// Using jennies:
|
||||
// TSTypesJenny
|
||||
// PluginTsTypesJenny
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
export const pluginVersion = "12.4.0-pre";
|
||||
|
||||
export interface Options {
|
||||
buildLinkToLogLine?: unknown;
|
||||
controlsStorageKey?: string;
|
||||
/**
|
||||
* isFilterLabelActive?: _
|
||||
* onClickFilterString?: _
|
||||
* onClickFilterOutString?: _
|
||||
* onClickShowField?: _
|
||||
* onClickHideField?: _
|
||||
* onLogOptionsChange?: _
|
||||
* logRowMenuIconsBefore?: _
|
||||
* logRowMenuIconsAfter?: _
|
||||
* logLineMenuCustomItems?: _
|
||||
* onNewLogsReceived?: _
|
||||
*/
|
||||
displayedFields?: Array<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?: unknown;
|
||||
onClickFilterOutLabel?: unknown;
|
||||
setDisplayedFields?: unknown;
|
||||
/**
|
||||
* showLabels: bool
|
||||
* showCommonLabels: bool
|
||||
* showFieldSelector?: bool
|
||||
* showTime: bool
|
||||
* showLogContextToggle: bool
|
||||
*/
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export const defaultOptions: Partial<Options> = {
|
||||
displayedFields: [],
|
||||
};
|
||||
@@ -248,6 +248,16 @@ func GetComposableKinds() ([]ComposableKind, error) {
|
||||
CueFile: logsCue,
|
||||
})
|
||||
|
||||
logstableCue, err := loadCueFileWithCommon(root, filepath.Join(root, "./public/app/plugins/panel/logstable/panelcfg.cue"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kinds = append(kinds, ComposableKind{
|
||||
Name: "logstable",
|
||||
Filename: "panelcfg.cue",
|
||||
CueFile: logstableCue,
|
||||
})
|
||||
|
||||
newsCue, err := loadCueFileWithCommon(root, filepath.Join(root, "./public/app/plugins/panel/news/panelcfg.cue"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { capitalize, groupBy } from 'lodash';
|
||||
import { useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { usePrevious, useUnmount } from 'react-use';
|
||||
|
||||
import {
|
||||
SplitOpen,
|
||||
LogRowModel,
|
||||
LogsMetaItem,
|
||||
DataFrame,
|
||||
AbsoluteTimeRange,
|
||||
GrafanaTheme2,
|
||||
LoadingState,
|
||||
TimeZone,
|
||||
RawTimeRange,
|
||||
DataQueryResponse,
|
||||
LogRowContextOptions,
|
||||
EventBus,
|
||||
ExplorePanelsState,
|
||||
TimeRange,
|
||||
LogsDedupStrategy,
|
||||
LogsSortOrder,
|
||||
CoreApp,
|
||||
LogsDedupDescription,
|
||||
rangeUtil,
|
||||
ExploreLogsPanelState,
|
||||
DataFrame,
|
||||
DataHoverClearEvent,
|
||||
DataHoverEvent,
|
||||
serializeStateToUrlParam,
|
||||
urlUtil,
|
||||
DataQueryResponse,
|
||||
EventBus,
|
||||
ExploreLogsPanelState,
|
||||
ExplorePanelsState,
|
||||
FieldConfigSource,
|
||||
GrafanaTheme2,
|
||||
LoadingState,
|
||||
LogLevel,
|
||||
LogRowContextOptions,
|
||||
LogRowModel,
|
||||
LogsDedupDescription,
|
||||
LogsDedupStrategy,
|
||||
LogsMetaItem,
|
||||
LogsSortOrder,
|
||||
PanelData,
|
||||
rangeUtil,
|
||||
RawTimeRange,
|
||||
serializeStateToUrlParam,
|
||||
shallowCompare,
|
||||
SplitOpen,
|
||||
TimeRange,
|
||||
TimeZone,
|
||||
urlUtil,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { DataQuery, DataTopic } from '@grafana/schema';
|
||||
import {
|
||||
@@ -47,6 +49,7 @@ import {
|
||||
Themeable2,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { replaceVariables } from '@grafana-plugins/loki/querybuilder/parsingUtils';
|
||||
import store from 'app/core/store';
|
||||
import { createAndCopyShortLink, getLogsPermalinkRange } from 'app/core/utils/shortLinks';
|
||||
import { ControlledLogRows } from 'app/features/logs/components/ControlledLogRows';
|
||||
@@ -56,15 +59,17 @@ import { LogRowContextModal } from 'app/features/logs/components/log-context/Log
|
||||
import { LogLineContext } from 'app/features/logs/components/panel/LogLineContext';
|
||||
import { LogList, LogListOptions } from 'app/features/logs/components/panel/LogList';
|
||||
import { isDedupStrategy, isLogsSortOrder } from 'app/features/logs/components/panel/LogListContext';
|
||||
import { LogLevelColor, dedupLogRows } from 'app/features/logs/logsModel';
|
||||
import { dedupLogRows, LogLevelColor } from 'app/features/logs/logsModel';
|
||||
import { getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils';
|
||||
import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen';
|
||||
import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils';
|
||||
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
||||
import { Options } from 'app/plugins/panel/logstable/panelcfg.gen';
|
||||
import { getState } from 'app/store/store';
|
||||
import { ExploreItemState } from 'app/types/explore';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { LogsTable } from '../../../plugins/panel/logstable/LogsTable';
|
||||
import {
|
||||
contentOutlineTrackPinAdded,
|
||||
contentOutlineTrackPinClicked,
|
||||
@@ -80,7 +85,7 @@ import { changeQueries, runQueries } from '../state/query';
|
||||
import { LogsFeedback } from './LogsFeedback';
|
||||
import { LogsMetaRow } from './LogsMetaRow';
|
||||
import LogsNavigation from './LogsNavigation';
|
||||
import { LogsTableWrap, getLogsTableHeight } from './LogsTableWrap';
|
||||
import { getLogsTableHeight, LogsTableWrap } from './LogsTableWrap';
|
||||
import { LogsVolumePanelList } from './LogsVolumePanelList';
|
||||
import { SETTING_KEY_ROOT, SETTINGS_KEYS, visualisationTypeKey } from './utils/logs';
|
||||
import { getExploreBaseUrl } from './utils/url';
|
||||
@@ -127,8 +132,9 @@ interface Props extends Themeable2 {
|
||||
range: TimeRange;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
loadMoreLogs?(range: AbsoluteTimeRange): void;
|
||||
onPinLineCallback?: () => void;
|
||||
|
||||
loadMoreLogs?(range: AbsoluteTimeRange): void;
|
||||
}
|
||||
|
||||
export type LogsVisualisationType = 'table' | 'logs';
|
||||
@@ -764,6 +770,14 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
setFilterLevels(levels.map((level) => getLogLevelFromKey(level)));
|
||||
}, []);
|
||||
|
||||
// @todo ff
|
||||
const enableNewLogsTable = true;
|
||||
const panelData: PanelData = {
|
||||
state: loading ? LoadingState.Loading : LoadingState.Done,
|
||||
series: props.logsFrames ?? [],
|
||||
timeRange: props.range,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{(!config.featureToggles.newLogsPanel || !config.featureToggles.newLogContext) && getRowContext && contextRow && (
|
||||
@@ -989,24 +1003,55 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
<div className={cx(styles.logsSection, visualisationType === 'table' ? styles.logsTable : undefined)}>
|
||||
{!config.featureToggles.logsPanelControls && visualisationType === 'table' && hasData && (
|
||||
<div className={styles.logRows} data-testid="logRowsTable">
|
||||
{/* Width should be full width minus logs navigation and padding */}
|
||||
<LogsTableWrap
|
||||
logsSortOrder={logsSortOrder}
|
||||
range={props.range}
|
||||
splitOpen={splitOpen}
|
||||
timeZone={timeZone}
|
||||
width={width - 80}
|
||||
logsFrames={props.logsFrames ?? []}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
panelState={panelState?.logs}
|
||||
updatePanelState={updatePanelState}
|
||||
datasourceType={props.datasourceType}
|
||||
displayedFields={displayedFields}
|
||||
exploreId={props.exploreId}
|
||||
absoluteRange={props.absoluteRange}
|
||||
logRows={props.logRows}
|
||||
/>
|
||||
{/* @todo add flag*/}
|
||||
{enableNewLogsTable && (
|
||||
<LogsTable
|
||||
id={0}
|
||||
data={panelData}
|
||||
timeRange={props.range}
|
||||
timeZone={timeZone}
|
||||
options={{}}
|
||||
transparent={false}
|
||||
width={width - 80}
|
||||
height={tableHeight}
|
||||
fieldConfig={{
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
}}
|
||||
renderCounter={0}
|
||||
title={''}
|
||||
eventBus={props.eventBus}
|
||||
onOptionsChange={function (options: Options): void {
|
||||
console.log('onOptionsChange NOT IMP:', options);
|
||||
// throw new Error('Function not implemented.');
|
||||
}}
|
||||
onFieldConfigChange={function (config: FieldConfigSource): void {
|
||||
console.log('onFieldConfigChange NOT IMP:', config);
|
||||
}}
|
||||
replaceVariables={replaceVariables}
|
||||
onChangeTimeRange={onChangeTime}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!enableNewLogsTable && (
|
||||
<LogsTableWrap
|
||||
logsSortOrder={logsSortOrder}
|
||||
range={props.range}
|
||||
splitOpen={splitOpen}
|
||||
timeZone={timeZone}
|
||||
width={width - 80}
|
||||
logsFrames={props.logsFrames ?? []}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
panelState={panelState?.logs}
|
||||
updatePanelState={updatePanelState}
|
||||
datasourceType={props.datasourceType}
|
||||
displayedFields={displayedFields}
|
||||
exploreId={props.exploreId}
|
||||
absoluteRange={props.absoluteRange}
|
||||
logRows={props.logRows}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(!config.featureToggles.newLogsPanel || visualisationType === 'table') &&
|
||||
@@ -1014,6 +1059,10 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
hasData && (
|
||||
<div className={styles.controlledLogRowsWrapper} data-testid="logRows">
|
||||
<ControlledLogRows
|
||||
fieldConfig={{
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
}}
|
||||
ref={logsContainerRef}
|
||||
logsTableFrames={props.logsFrames}
|
||||
width={width}
|
||||
|
||||
@@ -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 } }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useMemo, useRef, forwardRef, useImperativeHandle, useCallback } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
@@ -7,17 +7,23 @@ import {
|
||||
DataFrame,
|
||||
EventBusSrv,
|
||||
ExploreLogsPanelState,
|
||||
FieldConfigSource,
|
||||
LoadingState,
|
||||
LogLevel,
|
||||
LogRowModel,
|
||||
LogsMetaItem,
|
||||
LogsSortOrder,
|
||||
PanelData,
|
||||
SplitOpen,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { getAppEvents, getTemplateSrv } from '@grafana/runtime';
|
||||
import { PanelContextProvider } from '@grafana/ui';
|
||||
import { Options } from 'app/plugins/panel/logstable/panelcfg.gen';
|
||||
|
||||
import { LogsTable } from '../../../plugins/panel/logstable/LogsTable';
|
||||
import { LogsVisualisationType } from '../../explore/Logs/Logs';
|
||||
|
||||
import { ControlledLogsTable } from './ControlledLogsTable';
|
||||
import { InfiniteScroll } from './InfiniteScroll';
|
||||
import { LogRows, Props } from './LogRows';
|
||||
import { LogListOptions } from './panel/LogList';
|
||||
@@ -25,6 +31,12 @@ import { LogListContextProvider, useLogListContext } from './panel/LogListContex
|
||||
import { LogListControls } from './panel/LogListControls';
|
||||
import { ScrollToLogsEvent } from './panel/virtualization';
|
||||
|
||||
// @todo
|
||||
export const FILTER_FOR_OPERATOR = '=';
|
||||
export const FILTER_OUT_OPERATOR = '!=';
|
||||
export type AdHocFilterOperator = typeof FILTER_FOR_OPERATOR | typeof FILTER_OUT_OPERATOR;
|
||||
export type AdHocFilterItem = { key: string; value: string; operator: AdHocFilterOperator };
|
||||
|
||||
export interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
|
||||
loading: boolean;
|
||||
logsMeta?: LogsMetaItem[];
|
||||
@@ -33,6 +45,7 @@ export interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
|
||||
onLogOptionsChange?: (option: LogListOptions, value: string | boolean | string[]) => void;
|
||||
range: TimeRange;
|
||||
filterLevels?: LogLevel[];
|
||||
fieldConfig: FieldConfigSource;
|
||||
|
||||
/** Props added for Table **/
|
||||
visualisationType: LogsVisualisationType;
|
||||
@@ -74,10 +87,35 @@ export const ControlledLogRows = forwardRef<HTMLDivElement | null, ControlledLog
|
||||
prettifyLogMessage,
|
||||
onLogOptionsChange,
|
||||
wrapLogMessage,
|
||||
fieldConfig,
|
||||
...rest
|
||||
}: ControlledLogRowsProps,
|
||||
ref
|
||||
) => {
|
||||
const dataFrames = rest.logsTableFrames ?? [];
|
||||
const panelData: PanelData = {
|
||||
state: rest.loading ? LoadingState.Loading : LoadingState.Done,
|
||||
series: dataFrames,
|
||||
timeRange: rest.timeRange,
|
||||
};
|
||||
|
||||
const eventBus = getAppEvents();
|
||||
|
||||
const onCellFilterAdded = (filter: AdHocFilterItem) => {
|
||||
const { value, key, operator } = filter;
|
||||
const { onClickFilterLabel, onClickFilterOutLabel } = rest;
|
||||
if (!onClickFilterLabel || !onClickFilterOutLabel) {
|
||||
return;
|
||||
}
|
||||
if (operator === FILTER_FOR_OPERATOR) {
|
||||
onClickFilterLabel(key, value, dataFrames[0]);
|
||||
}
|
||||
|
||||
if (operator === FILTER_OUT_OPERATOR) {
|
||||
onClickFilterOutLabel(key, value, dataFrames[0]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LogListContextProvider
|
||||
app={rest.app || CoreApp.Unknown}
|
||||
@@ -97,9 +135,46 @@ export const ControlledLogRows = forwardRef<HTMLDivElement | null, ControlledLog
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
>
|
||||
{rest.visualisationType === 'logs' && (
|
||||
<LogRowsComponent ref={ref} {...rest} deduplicatedRows={deduplicatedRows} />
|
||||
<LogRowsComponent
|
||||
ref={ref}
|
||||
{...rest}
|
||||
deduplicatedRows={deduplicatedRows}
|
||||
fieldConfig={{ defaults: {}, overrides: [] }}
|
||||
/>
|
||||
)}
|
||||
{rest.visualisationType === 'table' && rest.updatePanelState && (
|
||||
<PanelContextProvider
|
||||
value={{
|
||||
eventsScope: 'explore',
|
||||
eventBus: eventBus ?? new EventBusSrv(),
|
||||
onAddAdHocFilter: onCellFilterAdded,
|
||||
}}
|
||||
>
|
||||
<LogsTable
|
||||
id={0}
|
||||
width={rest.width ?? 0}
|
||||
data={panelData}
|
||||
options={{}}
|
||||
transparent={false}
|
||||
height={800}
|
||||
fieldConfig={fieldConfig}
|
||||
renderCounter={0}
|
||||
title={''}
|
||||
eventBus={eventBus}
|
||||
onOptionsChange={function (options: Options): void {
|
||||
console.log('onOptionsChange not implemented');
|
||||
}}
|
||||
onFieldConfigChange={function (config: FieldConfigSource): void {
|
||||
console.log('onFieldConfigChange not implemented');
|
||||
}}
|
||||
replaceVariables={getTemplateSrv().replace.bind(getTemplateSrv())}
|
||||
onChangeTimeRange={function (timeRange: AbsoluteTimeRange): void {
|
||||
console.log('onChangeTimeRange not implemented');
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
</PanelContextProvider>
|
||||
)}
|
||||
{rest.visualisationType === 'table' && rest.updatePanelState && <ControlledLogsTable {...rest} />}
|
||||
</LogListContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DataFrame,
|
||||
LogRowContextOptions,
|
||||
TimeRange,
|
||||
FieldConfigSource,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
@@ -73,6 +74,7 @@ export interface Props {
|
||||
logRowMenuIconsAfter?: ReactNode[];
|
||||
scrollElement: HTMLDivElement | null;
|
||||
renderPreview?: boolean;
|
||||
fieldConfig?: FieldConfigSource;
|
||||
}
|
||||
|
||||
export type PopoverStateType = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ const histogramPanel = async () =>
|
||||
await import(/* webpackChunkName: "histogramPanel" */ 'app/plugins/panel/histogram/module');
|
||||
const livePanel = async () => await import(/* webpackChunkName: "livePanel" */ 'app/plugins/panel/live/module');
|
||||
const logsPanel = async () => await import(/* webpackChunkName: "logsPanel" */ 'app/plugins/panel/logs/module');
|
||||
const logsTablePanel = async () =>
|
||||
await import(/* webpackChunkName: "logsPanel" */ 'app/plugins/panel/logstable/module');
|
||||
const newsPanel = async () => await import(/* webpackChunkName: "newsPanel" */ 'app/plugins/panel/news/module');
|
||||
const pieChartPanel = async () =>
|
||||
await import(/* webpackChunkName: "pieChartPanel" */ 'app/plugins/panel/piechart/module');
|
||||
@@ -108,6 +110,7 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
|
||||
'core:plugin/bargauge': barGaugePanel,
|
||||
'core:plugin/barchart': barChartPanel,
|
||||
'core:plugin/logs': logsPanel,
|
||||
'core:plugin/logstable': logsTablePanel,
|
||||
'core:plugin/traces': tracesPanel,
|
||||
'core:plugin/welcome': welcomeBanner,
|
||||
'core:plugin/nodeGraph': nodeGraph,
|
||||
|
||||
@@ -653,6 +653,7 @@ export const LogsPanel = ({
|
||||
sortOrder={sortOrder}
|
||||
>
|
||||
<LogRows
|
||||
fieldConfig={fieldConfig}
|
||||
scrollElement={scrollElement}
|
||||
scrollIntoView={scrollIntoView}
|
||||
permalinkedRowId={getLogsPanelState()?.logs?.id ?? undefined}
|
||||
@@ -705,6 +706,7 @@ export const LogsPanel = ({
|
||||
ref={(scrollElement: HTMLDivElement | null) => {
|
||||
setScrollElement(scrollElement);
|
||||
}}
|
||||
fieldConfig={fieldConfig}
|
||||
visualisationType="logs"
|
||||
loading={infiniteScrolling}
|
||||
loadMoreLogs={enableInfiniteScrolling ? loadMoreLogs : undefined}
|
||||
|
||||
41
public/app/plugins/panel/logstable/CustomCellRenderer.tsx
Normal file
41
public/app/plugins/panel/logstable/CustomCellRenderer.tsx
Normal file
@@ -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',
|
||||
}),
|
||||
});
|
||||
323
public/app/plugins/panel/logstable/LogsTable.tsx
Normal file
323
public/app/plugins/panel/logstable/LogsTable.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import {
|
||||
applyFieldOverrides,
|
||||
DataFrame,
|
||||
Field,
|
||||
FieldConfigSource,
|
||||
GrafanaTheme2,
|
||||
PanelProps,
|
||||
transformDataFrame,
|
||||
useDataLinksContext,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { CustomCellRendererProps, TableCellDisplayMode, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { config } from '../../../core/config';
|
||||
import { getLogsExtractFields } from '../../../features/explore/Logs/LogsTable';
|
||||
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 { 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 { isBuildLinkToLogLine, isOnLogsTableOptionsChange, OnLogsTableOptionsChange } from './types';
|
||||
|
||||
interface LogsTablePanelProps extends PanelProps<LogsTableOptions> {
|
||||
frameIndex?: number;
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
// Defaults
|
||||
const DEFAULT_SIDEBAR_WIDTH = 200;
|
||||
|
||||
export const LogsTable = ({
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
timeRange,
|
||||
fieldConfig,
|
||||
options,
|
||||
eventBus,
|
||||
frameIndex = 0,
|
||||
showHeader = true, // @todo not pulling from panel settings
|
||||
onOptionsChange,
|
||||
onFieldConfigChange,
|
||||
replaceVariables,
|
||||
onChangeTimeRange,
|
||||
title,
|
||||
transparent,
|
||||
timeZone,
|
||||
id,
|
||||
renderCounter,
|
||||
}: LogsTablePanelProps) => {
|
||||
// Variables
|
||||
const unTransformedDataFrame = data.series[frameIndex];
|
||||
|
||||
// Hooks
|
||||
const logsFrame: LogsFrame | null = useMemo(() => parseLogsFrame(unTransformedDataFrame), [unTransformedDataFrame]);
|
||||
const defaultDisplayedFields = useMemo(
|
||||
() => [
|
||||
logsFrame?.timeField.name ?? LOGS_DATAPLANE_TIMESTAMP_NAME,
|
||||
logsFrame?.bodyField.name ?? LOGS_DATAPLANE_BODY_NAME,
|
||||
],
|
||||
[logsFrame?.timeField.name, logsFrame?.bodyField.name]
|
||||
);
|
||||
|
||||
// State
|
||||
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, DEFAULT_SIDEBAR_WIDTH, height, width);
|
||||
const dataLinksContext = useDataLinksContext();
|
||||
const dataLinkPostProcessor = dataLinksContext.dataLinkPostProcessor;
|
||||
|
||||
// Methods
|
||||
const onLogsTableOptionsChange: OnLogsTableOptionsChange | undefined = isOnLogsTableOptionsChange(onOptionsChange)
|
||||
? onOptionsChange
|
||||
: undefined;
|
||||
|
||||
const setDisplayedFieldsFn = isSetDisplayedFields(options.setDisplayedFields)
|
||||
? options.setDisplayedFields
|
||||
: setDisplayedFields;
|
||||
|
||||
// Callbacks
|
||||
const onTableOptionsChange = useCallback(
|
||||
(options: TableOptions) => {
|
||||
onLogsTableOptionsChange?.(options);
|
||||
},
|
||||
[onLogsTableOptionsChange]
|
||||
);
|
||||
|
||||
const handleLogsTableOptionsChange = useCallback(
|
||||
(options: LogsTableOptions) => {
|
||||
onOptionsChange(options);
|
||||
},
|
||||
[onOptionsChange]
|
||||
);
|
||||
|
||||
const handleSetDisplayedFields = useCallback(
|
||||
(displayedFields: string[]) => {
|
||||
setDisplayedFieldsFn(displayedFields);
|
||||
handleLogsTableOptionsChange({ ...options, displayedFields: displayedFields });
|
||||
},
|
||||
[handleLogsTableOptionsChange, options, setDisplayedFieldsFn]
|
||||
);
|
||||
|
||||
const handleTableOnFieldConfigChange = useCallback(
|
||||
(fieldConfig: FieldConfigSource) => {
|
||||
onFieldConfigChange(fieldConfig);
|
||||
},
|
||||
[onFieldConfigChange]
|
||||
);
|
||||
|
||||
/**
|
||||
* Extract fields transform
|
||||
*/
|
||||
useEffect(() => {
|
||||
const extractFields = async () => {
|
||||
return await lastValueFrom(
|
||||
transformDataFrame(getLogsExtractFields(unTransformedDataFrame), [unTransformedDataFrame])
|
||||
);
|
||||
};
|
||||
|
||||
extractFields().then((data) => {
|
||||
const extractedFrames = applyFieldOverrides({
|
||||
data,
|
||||
fieldConfig,
|
||||
replaceVariables: replaceVariables ?? getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||
theme: config.theme2,
|
||||
timeZone: timeZone,
|
||||
dataLinkPostProcessor,
|
||||
});
|
||||
setExtractedFrame(extractedFrames);
|
||||
});
|
||||
}, [dataLinkPostProcessor, fieldConfig, replaceVariables, timeZone, 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;
|
||||
}
|
||||
}
|
||||
|
||||
const organizedFrame = await lastValueFrom(
|
||||
transformDataFrame(
|
||||
[
|
||||
{
|
||||
id: 'organize',
|
||||
options: {
|
||||
indexByName,
|
||||
includeByName,
|
||||
},
|
||||
},
|
||||
],
|
||||
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) => {
|
||||
if (frame) {
|
||||
setOrganizedFrame(frame);
|
||||
}
|
||||
});
|
||||
}, [extractedFrame, displayedFields, logsFrame, options.buildLinkToLogLine]);
|
||||
|
||||
if (extractedFrame === null || organizedFrame === null || logsFrame === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('render::LogsTable', { extractedFrame, organizedFrame });
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.sidebarWrapper}>
|
||||
<LogsTableFieldSelector
|
||||
clear={() => {}}
|
||||
columnsWithMeta={displayedFieldsToColumns(displayedFields, logsFrame)}
|
||||
dataFrames={extractedFrame}
|
||||
logs={[]}
|
||||
reorder={(columns: string[]) => {}}
|
||||
setSidebarWidth={(width) => {}}
|
||||
sidebarWidth={DEFAULT_SIDEBAR_WIDTH}
|
||||
toggle={(key: string) => {
|
||||
if (displayedFields.includes(key)) {
|
||||
handleSetDisplayedFields(displayedFields.filter((f) => f !== key));
|
||||
} else {
|
||||
handleSetDisplayedFields([...displayedFields, key]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.tableWrapper}>
|
||||
<TablePanel
|
||||
data={{ ...data, series: organizedFrame }}
|
||||
width={width - DEFAULT_SIDEBAR_WIDTH}
|
||||
height={height}
|
||||
id={id}
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
options={{ ...options, frameIndex, showHeader }}
|
||||
transparent={transparent}
|
||||
fieldConfig={fieldConfig}
|
||||
renderCounter={renderCounter}
|
||||
title={title}
|
||||
eventBus={eventBus}
|
||||
onOptionsChange={onTableOptionsChange}
|
||||
onFieldConfigChange={handleTableOnFieldConfigChange}
|
||||
replaceVariables={replaceVariables}
|
||||
onChangeTimeRange={onChangeTimeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) => {
|
||||
return {
|
||||
tableWrapper: css({
|
||||
paddingLeft: sidebarWidth,
|
||||
height,
|
||||
width,
|
||||
}),
|
||||
sidebarWrapper: css({
|
||||
position: 'absolute',
|
||||
height: height,
|
||||
width: sidebarWidth,
|
||||
}),
|
||||
wrapper: css({
|
||||
height,
|
||||
width,
|
||||
}),
|
||||
};
|
||||
};
|
||||
234
public/app/plugins/panel/logstable/module.tsx
Normal file
234
public/app/plugins/panel/logstable/module.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { FieldConfigProperty, identityOverrideProcessor, PanelPlugin, standardEditorsRegistry } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import {
|
||||
TableCellDisplayMode,
|
||||
TableCellHeight,
|
||||
TableCellOptions,
|
||||
TableCellTooltipPlacement,
|
||||
} from '@grafana/schema/dist/esm/common/common.gen';
|
||||
import { defaultTableFieldOptions } from '@grafana/schema/dist/esm/veneer/common.types';
|
||||
|
||||
import { PaginationEditor } from '../table/PaginationEditor';
|
||||
import { TableCellOptionEditor } from '../table/TableCellOptionEditor';
|
||||
import { tablePanelChangedHandler } from '../table/migrations';
|
||||
import { defaultOptions, FieldConfig as TableFieldConfig } from '../table/panelcfg.gen';
|
||||
import { tableSuggestionsSupplier } from '../table/suggestions';
|
||||
|
||||
import { LogsTable } from './LogsTable';
|
||||
import { Options } from './panelcfg.gen';
|
||||
|
||||
// @todo can we pull stuff from table module instead of manually adding?
|
||||
export const plugin = new PanelPlugin<Options, TableFieldConfig>(LogsTable)
|
||||
.setPanelChangeHandler(tablePanelChangedHandler)
|
||||
.useFieldConfig({
|
||||
standardOptions: {
|
||||
[FieldConfigProperty.Actions]: {
|
||||
hideFromDefaults: false,
|
||||
},
|
||||
},
|
||||
useCustomConfig: (builder) => {
|
||||
const category = [t('table.category-logs-table', 'Logs Table')];
|
||||
const cellCategory = [t('table.category-cell-options', 'Cell options')];
|
||||
builder
|
||||
.addNumberInput({
|
||||
path: 'minWidth',
|
||||
name: t('table.name-min-column-width', 'Minimum column width'),
|
||||
category,
|
||||
description: t('table.description-min-column-width', 'The minimum width for column auto resizing'),
|
||||
settings: {
|
||||
placeholder: '150',
|
||||
min: 50,
|
||||
max: 500,
|
||||
},
|
||||
shouldApply: () => true,
|
||||
defaultValue: defaultTableFieldOptions.minWidth,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'width',
|
||||
name: t('table.name-column-width', 'Column width'),
|
||||
category,
|
||||
settings: {
|
||||
placeholder: t('table.placeholder-column-width', 'auto'),
|
||||
min: 20,
|
||||
},
|
||||
shouldApply: () => true,
|
||||
defaultValue: defaultTableFieldOptions.width,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'align',
|
||||
name: t('table.name-column-alignment', 'Column alignment'),
|
||||
category,
|
||||
settings: {
|
||||
options: [
|
||||
{ label: t('table.column-alignment-options.label-auto', 'Auto'), value: 'auto' },
|
||||
{ label: t('table.column-alignment-options.label-left', 'Left'), value: 'left' },
|
||||
{ label: t('table.column-alignment-options.label-center', 'Center'), value: 'center' },
|
||||
{ label: t('table.column-alignment-options.label-right', 'Right'), value: 'right' },
|
||||
],
|
||||
},
|
||||
defaultValue: defaultTableFieldOptions.align,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'filterable',
|
||||
name: t('table.name-column-filter', 'Column filter'),
|
||||
category,
|
||||
description: t('table.description-column-filter', 'Enables/disables field filters in table'),
|
||||
defaultValue: defaultTableFieldOptions.filterable,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapText',
|
||||
name: t('table.name-wrap-text', 'Wrap text'),
|
||||
category,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapHeaderText',
|
||||
name: t('table.name-wrap-header-text', 'Wrap header text'),
|
||||
category,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'hideFrom.viz',
|
||||
name: t('table.name-hide-in-table', 'Hide in table'),
|
||||
category,
|
||||
defaultValue: undefined,
|
||||
hideFromDefaults: true,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'footer.reducers',
|
||||
category: [t('table.category-table-footer', 'Table footer')],
|
||||
path: 'footer.reducers',
|
||||
name: t('table.name-calculation', 'Calculation'),
|
||||
description: t('table.description-calculation', 'Choose a reducer function / calculation'),
|
||||
editor: standardEditorsRegistry.get('stats-picker').editor,
|
||||
override: standardEditorsRegistry.get('stats-picker').editor,
|
||||
defaultValue: [],
|
||||
process: identityOverrideProcessor,
|
||||
shouldApply: () => true,
|
||||
settings: {
|
||||
allowMultiple: true,
|
||||
},
|
||||
})
|
||||
.addCustomEditor<void, TableCellOptions>({
|
||||
id: 'cellOptions',
|
||||
path: 'cellOptions',
|
||||
name: t('table.name-cell-type', 'Cell type'),
|
||||
editor: TableCellOptionEditor,
|
||||
override: TableCellOptionEditor,
|
||||
defaultValue: defaultTableFieldOptions.cellOptions,
|
||||
process: identityOverrideProcessor,
|
||||
category: cellCategory,
|
||||
shouldApply: () => true,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'inspect',
|
||||
name: t('table.name-cell-value-inspect', 'Cell value inspect'),
|
||||
description: t('table.description-cell-value-inspect', 'Enable cell value inspection in a modal window'),
|
||||
defaultValue: false,
|
||||
category: cellCategory,
|
||||
showIf: (cfg) => {
|
||||
return (
|
||||
cfg.cellOptions.type === TableCellDisplayMode.Auto ||
|
||||
cfg.cellOptions.type === TableCellDisplayMode.JSONView ||
|
||||
cfg.cellOptions.type === TableCellDisplayMode.ColorText ||
|
||||
cfg.cellOptions.type === TableCellDisplayMode.ColorBackground
|
||||
);
|
||||
},
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'tooltip.field',
|
||||
name: t('table.name-tooltip-from-field', 'Tooltip from field'),
|
||||
description: t(
|
||||
'table.description-tooltip-from-field',
|
||||
'Render a cell from a field (hidden or visible) in a tooltip'
|
||||
),
|
||||
category: cellCategory,
|
||||
})
|
||||
.addSelect({
|
||||
path: 'tooltip.placement',
|
||||
name: t('table.name-tooltip-placement', 'Tooltip placement'),
|
||||
category: cellCategory,
|
||||
settings: {
|
||||
options: [
|
||||
{
|
||||
label: t('table.tooltip-placement-options.label-auto', 'Auto'),
|
||||
value: TableCellTooltipPlacement.Auto,
|
||||
},
|
||||
{
|
||||
label: t('table.tooltip-placement-options.label-top', 'Top'),
|
||||
value: TableCellTooltipPlacement.Top,
|
||||
},
|
||||
{
|
||||
label: t('table.tooltip-placement-options.label-right', 'Right'),
|
||||
value: TableCellTooltipPlacement.Right,
|
||||
},
|
||||
{
|
||||
label: t('table.tooltip-placement-options.label-bottom', 'Bottom'),
|
||||
value: TableCellTooltipPlacement.Bottom,
|
||||
},
|
||||
{
|
||||
label: t('table.tooltip-placement-options.label-left', 'Left'),
|
||||
value: TableCellTooltipPlacement.Left,
|
||||
},
|
||||
],
|
||||
},
|
||||
showIf: (cfg) => cfg.tooltip?.field !== undefined,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'styleField',
|
||||
name: t('table.name-styling-from-field', 'Styling from field'),
|
||||
description: t('table.description-styling-from-field', 'A field containing JSON objects with CSS properties'),
|
||||
category: cellCategory,
|
||||
});
|
||||
},
|
||||
})
|
||||
.setPanelOptions((builder) => {
|
||||
const category = [t('table.category-table', 'Table')];
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
path: 'showHeader',
|
||||
name: t('table.name-show-table-header', 'Show table header'),
|
||||
category,
|
||||
defaultValue: defaultOptions.showHeader,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'frozenColumns.left',
|
||||
name: t('table.name-frozen-columns', 'Frozen columns'),
|
||||
description: t('table.description-frozen-columns', 'Columns are frozen from the left side of the table'),
|
||||
settings: {
|
||||
placeholder: 'none',
|
||||
},
|
||||
category,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'cellHeight',
|
||||
name: t('table.name-cell-height', 'Cell height'),
|
||||
category,
|
||||
defaultValue: defaultOptions.cellHeight,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: TableCellHeight.Sm, label: t('table.cell-height-options.label-small', 'Small') },
|
||||
{ value: TableCellHeight.Md, label: t('table.cell-height-options.label-medium', 'Medium') },
|
||||
{ value: TableCellHeight.Lg, label: t('table.cell-height-options.label-large', 'Large') },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'maxRowHeight',
|
||||
name: t('table.name-max-height', 'Max row height'),
|
||||
category,
|
||||
settings: {
|
||||
placeholder: t('table.placeholder-max-height', 'none'),
|
||||
min: 0,
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'enablePagination',
|
||||
path: 'enablePagination',
|
||||
name: t('table.name-enable-pagination', 'Enable pagination'),
|
||||
category,
|
||||
editor: PaginationEditor,
|
||||
defaultValue: defaultOptions?.enablePagination,
|
||||
});
|
||||
})
|
||||
// @todo
|
||||
//@ts-expect-error
|
||||
.setSuggestionsSupplier(tableSuggestionsSupplier);
|
||||
69
public/app/plugins/panel/logstable/panelcfg.cue
Normal file
69
public/app/plugins/panel/logstable/panelcfg.cue
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2026 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License")
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
//import (
|
||||
// "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
//)
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
lineage: {
|
||||
schemas: [{
|
||||
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 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?: _
|
||||
buildLinkToLogLine?: _
|
||||
} @cuetsy(kind="interface")
|
||||
}
|
||||
}]
|
||||
lenses: []
|
||||
}
|
||||
}
|
||||
57
public/app/plugins/panel/logstable/panelcfg.gen.ts
generated
Normal file
57
public/app/plugins/panel/logstable/panelcfg.gen.ts
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
//
|
||||
// Generated by:
|
||||
// public/app/plugins/gen.go
|
||||
// Using jennies:
|
||||
// TSTypesJenny
|
||||
// PluginTsTypesJenny
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
export interface Options {
|
||||
buildLinkToLogLine?: unknown;
|
||||
controlsStorageKey?: string;
|
||||
/**
|
||||
* isFilterLabelActive?: _
|
||||
* onClickFilterString?: _
|
||||
* onClickFilterOutString?: _
|
||||
* onClickShowField?: _
|
||||
* onClickHideField?: _
|
||||
* onLogOptionsChange?: _
|
||||
* logRowMenuIconsBefore?: _
|
||||
* logRowMenuIconsAfter?: _
|
||||
* logLineMenuCustomItems?: _
|
||||
* onNewLogsReceived?: _
|
||||
*/
|
||||
displayedFields?: Array<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?: unknown;
|
||||
onClickFilterOutLabel?: unknown;
|
||||
setDisplayedFields?: unknown;
|
||||
/**
|
||||
* showLabels: bool
|
||||
* showCommonLabels: bool
|
||||
* showFieldSelector?: bool
|
||||
* showTime: bool
|
||||
* showLogContextToggle: bool
|
||||
*/
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export const defaultOptions: Partial<Options> = {
|
||||
displayedFields: [],
|
||||
};
|
||||
23
public/app/plugins/panel/logstable/plugin.json
Normal file
23
public/app/plugins/panel/logstable/plugin.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Logs Table",
|
||||
"id": "logs_table",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icn-logs-panel.svg",
|
||||
"large": "img/icn-logs-panel.svg"
|
||||
},
|
||||
"links": [
|
||||
{ "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" },
|
||||
{
|
||||
"name": "Documentation",
|
||||
"url": "https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/@todo/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
16
public/app/plugins/panel/logstable/types.ts
Normal file
16
public/app/plugins/panel/logstable/types.ts
Normal file
@@ -0,0 +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 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';
|
||||
}
|
||||
Reference in New Issue
Block a user