chore: wip - support onCellFilterAdded

This commit is contained in:
Galen
2026-01-14 10:27:26 -06:00
parent 857f9b3d26
commit 3a39e927a2
6 changed files with 208 additions and 105 deletions
+92 -43
View File
@@ -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}
@@ -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 = {
@@ -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}
@@ -5,6 +5,7 @@ import { lastValueFrom } from 'rxjs';
import {
applyFieldOverrides,
DataFrame,
Field,
FieldConfigSource,
GrafanaTheme2,
PanelProps,
@@ -25,13 +26,7 @@ import {
parseLogsFrame,
} from '../../../features/logs/logsFrame';
import { isSetDisplayedFields } from '../logs/types';
import {
AdHocFilterItem,
FILTER_FOR_OPERATOR,
FILTER_OUT_OPERATOR,
TableFilterActionCallback,
TablePanel,
} from '../table/TablePanel';
import { TablePanel } from '../table/TablePanel';
import type { Options as TableOptions } from '../table/panelcfg.gen';
import type { Options as LogsTableOptions } from './panelcfg.gen';
@@ -125,39 +120,11 @@ export const LogsTable = ({
const handleTableOnFieldConfigChange = useCallback(
(fieldConfig: FieldConfigSource) => {
console.log('onTableOnFieldConfigChange', fieldConfig);
onFieldConfigChange(fieldConfig);
},
[onFieldConfigChange]
);
const handleOnCellFilterAdded: TableFilterActionCallback = (filter: AdHocFilterItem) => {
const { value, key, operator } = filter;
if (operator === FILTER_FOR_OPERATOR) {
onClickFilterLabel(key, value);
}
if (operator === FILTER_OUT_OPERATOR) {
onClickFilterOutLabel(key, value);
}
};
/**
* @todo
* Used by Logs details.
*/
const onClickFilterLabel = (key: string, value: string | number, frame?: DataFrame) => {
console.log('onClickFilterLabel', key, value, frame);
};
/**
* @todo
* Used by Logs details.
*/
const onClickFilterOutLabel = (key: string, value: string | number, frame?: DataFrame) => {
console.log('onClickFilterOutLabel', key, value, frame);
};
/**
* Extract fields transform
*/
@@ -169,18 +136,32 @@ export const LogsTable = ({
};
extractFields().then((frame) => {
setExtractedFrame(
applyFieldOverrides({
data: frame,
fieldConfig,
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
theme: config.theme2,
timeZone: timeZone,
dataLinkPostProcessor: dataLinksContext.dataLinkPostProcessor,
})
);
const extractedFrames = applyFieldOverrides({
data: frame,
fieldConfig: fieldConfig,
replaceVariables: replaceVariables ?? getTemplateSrv().replace.bind(getTemplateSrv()),
theme: config.theme2,
timeZone: timeZone,
dataLinkPostProcessor: dataLinksContext.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, timeZone, unTransformedDataFrame]);
}, [dataLinksContext.dataLinkPostProcessor, fieldConfig, replaceVariables, timeZone, unTransformedDataFrame]);
/**
* Organize fields transform
@@ -267,7 +248,6 @@ export const LogsTable = ({
onFieldConfigChange={handleTableOnFieldConfigChange}
replaceVariables={replaceVariables}
onChangeTimeRange={onChangeTimeRange}
onCellFilterAdded={handleOnCellFilterAdded}
/>
</div>
</div>
@@ -293,6 +273,10 @@ function displayedFieldsToColumns(displayedFields: string[], logsFrame: LogsFram
return columns;
}
function doesFieldSupportAdHocFiltering(field: Field): boolean {
return true;
}
const getStyles = (theme: GrafanaTheme2, sidebarWidth: number, height: number, width: number) => {
return {
tableWrapper: css({
+2 -11
View File
@@ -24,16 +24,7 @@ import { getActions } from '../../../features/actions/utils';
import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames } from './migrations';
import { Options } from './panelcfg.gen';
/** @todo get yelled at by dataviz */
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 type TableFilterActionCallback = (item: AdHocFilterItem) => void;
interface Props extends PanelProps<Options> {
onCellFilterAdded?: TableFilterActionCallback;
}
interface Props extends PanelProps<Options> {}
export function TablePanel(props: Props) {
const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables, transparent } = props;
@@ -86,7 +77,7 @@ export function TablePanel(props: Props) {
initialSortBy={options.sortBy}
onSortByChange={(sortBy) => onSortByChange(sortBy, props)}
onColumnResize={(displayName, resizedWidth) => onColumnResize(displayName, resizedWidth, props)}
onCellFilterAdded={props.onCellFilterAdded ?? panelContext.onAddAdHocFilter}
onCellFilterAdded={panelContext.onAddAdHocFilter}
frozenColumns={options.frozenColumns?.left}
enablePagination={options.enablePagination}
cellHeight={options.cellHeight}