diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index f438ea1f8aa..6fec58a2af1 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -580,8 +580,9 @@ export interface QueryFix { action?: QueryFixAction; } +export type QueryFixType = 'ADD_FILTER' | 'ADD_FILTER_OUT' | 'ADD_STRING_FILTER' | 'ADD_STRING_FILTER_OUT'; export interface QueryFixAction { - type: string; + type: QueryFixType | string; query?: string; preventSubmit?: boolean; options?: KeyValue; diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index fe8dd249022..33560345c67 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -4,7 +4,7 @@ import { DataQuery } from '@grafana/schema'; import { KeyValue, Labels } from './data'; import { DataFrame } from './dataFrame'; -import { DataQueryRequest, DataQueryResponse } from './datasource'; +import { DataQueryRequest, DataQueryResponse, QueryFixAction, QueryFixType } from './datasource'; import { AbsoluteTimeRange } from './time'; export { LogsDedupStrategy, LogsSortOrder } from '@grafana/schema'; @@ -292,9 +292,46 @@ export const hasToggleableQueryFiltersSupport = ( datasource: unknown ): datasource is DataSourceWithToggleableQueryFiltersSupport => { return ( - datasource !== null && + datasource != null && typeof datasource === 'object' && 'toggleQueryFilter' in datasource && 'queryHasFilter' in datasource ); }; + +/** + * Data sources that support query modification actions from Log Details (ADD_FILTER, ADD_FILTER_OUT), + * and Popover Menu (ADD_STRING_FILTER, ADD_STRING_FILTER_OUT) in Explore. + * @internal + * @alpha + */ +export interface DataSourceWithQueryModificationSupport { + /** + * Given a query, applies a query modification `action`, returning the updated query. + * Explore currently supports the following action types: + * - ADD_FILTER: adds a filter to the query. + * - ADD_FILTER_OUT: adds a negative filter to the query. + * - ADD_STRING_FILTER: adds a string filter to the query. + * - ADD_STRING_FILTER_OUT: adds a negative string filter to the query. + */ + modifyQuery(query: TQuery, action: QueryFixAction): TQuery; + + /** + * Returns a list of supported action types for `modifyQuery()`. + */ + getSupportedQueryModifications(): Array; +} + +/** + * @internal + */ +export const hasQueryModificationSupport = ( + datasource: unknown +): datasource is DataSourceWithQueryModificationSupport => { + return ( + datasource != null && + typeof datasource === 'object' && + 'modifyQuery' in datasource && + 'getSupportedQueryModifications' in datasource + ); +}; diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index 96d46b37c0d..a1d66e5d7aa 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -18,6 +18,8 @@ import { DataSourceWithLogsContextSupport, DataSourceApi, hasToggleableQueryFiltersSupport, + DataSourceWithQueryModificationSupport, + hasQueryModificationSupport, } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; @@ -60,45 +62,44 @@ interface LogsContainerProps extends PropsFromRedux { onClickFilterOutValue: (value: string, refId?: string) => void; } +type DataSourceInstance = + | DataSourceApi + | (DataSourceApi & DataSourceWithLogsContextSupport) + | (DataSourceApi & DataSourceWithQueryModificationSupport); + interface LogsContainerState { - logDetailsFilterAvailable: boolean; - logContextSupport: Record & DataSourceWithLogsContextSupport>; + dsInstances: Record; } class LogsContainer extends PureComponent { state: LogsContainerState = { - logDetailsFilterAvailable: false, - logContextSupport: {}, + dsInstances: {}, }; componentDidMount() { - this.checkDataSourcesFeatures(); + this.updateDataSourceInstances(); } componentDidUpdate(prevProps: LogsContainerProps) { - this.checkDataSourcesFeatures(); + if (prevProps.logsQueries !== this.props.logsQueries) { + this.updateDataSourceInstances(); + } } - private checkDataSourcesFeatures() { + private updateDataSourceInstances() { const { logsQueries, datasourceInstance } = this.props; - if (!logsQueries || !datasourceInstance) { return; } - let newState: LogsContainerState = { ...this.state, logDetailsFilterAvailable: false }; + const dsInstances: Record = {}; // Not in mixed mode. if (datasourceInstance.uid !== MIXED_DATASOURCE_NAME) { - if (datasourceInstance?.modifyQuery || hasToggleableQueryFiltersSupport(datasourceInstance)) { - newState.logDetailsFilterAvailable = true; - } - if (hasLogsContextSupport(datasourceInstance)) { - logsQueries.forEach(({ refId }) => { - newState.logContextSupport[refId] = datasourceInstance; - }); - } - this.setState(newState); + logsQueries.forEach(({ refId }) => { + dsInstances[refId] = datasourceInstance; + }); + this.setState({ dsInstances }); return; } @@ -108,9 +109,7 @@ class LogsContainer extends PureComponent { @@ -128,18 +127,11 @@ class LogsContainer extends PureComponent { - dsInstances.forEach(({ ds, refId }) => { - newState.logDetailsFilterAvailable = - newState.logDetailsFilterAvailable || Boolean(ds.modifyQuery) || hasToggleableQueryFiltersSupport(ds); - if (hasLogsContextSupport(ds)) { - newState.logContextSupport[refId] = ds; - } else { - delete newState.logContextSupport[refId]; - } + Promise.all(dsPromises).then((instances) => { + instances.forEach(({ ds, refId }) => { + dsInstances[refId] = ds; }); - - this.setState(newState); + this.setState({ dsInstances }); }); } @@ -167,39 +159,48 @@ class LogsContainer extends PureComponent => { const { logsQueries } = this.props; - if (!origRow.dataFrame.refId || !this.state.logContextSupport[origRow.dataFrame.refId]) { + if (!origRow.dataFrame.refId || !this.state.dsInstances[origRow.dataFrame.refId]) { return Promise.resolve([]); } - const ds = this.state.logContextSupport[origRow.dataFrame.refId]; - const query = this.getQuery(logsQueries, origRow, ds); + const ds = this.state.dsInstances[origRow.dataFrame.refId]; + if (!hasLogsContextSupport(ds)) { + return Promise.resolve([]); + } + const query = this.getQuery(logsQueries, origRow, ds); return query ? ds.getLogRowContext(row, options, query) : Promise.resolve([]); }; getLogRowContextQuery = async (row: LogRowModel, options?: LogRowContextOptions): Promise => { const { logsQueries } = this.props; - if (!row.dataFrame.refId || !this.state.logContextSupport[row.dataFrame.refId]) { + if (!row.dataFrame.refId || !this.state.dsInstances[row.dataFrame.refId]) { return Promise.resolve(null); } - const ds = this.state.logContextSupport[row.dataFrame.refId]; - const query = this.getQuery(logsQueries, row, ds); + const ds = this.state.dsInstances[row.dataFrame.refId]; + if (!hasLogsContextSupport(ds)) { + return Promise.resolve(null); + } + const query = this.getQuery(logsQueries, row, ds); return query && ds.getLogRowContextQuery ? ds.getLogRowContextQuery(row, options, query) : Promise.resolve(null); }; getLogRowContextUi = (row: LogRowModel, runContextQuery?: () => void): React.ReactNode => { const { logsQueries } = this.props; - if (!row.dataFrame.refId || !this.state.logContextSupport[row.dataFrame.refId]) { + if (!row.dataFrame.refId || !this.state.dsInstances[row.dataFrame.refId]) { return <>; } - const ds = this.state.logContextSupport[row.dataFrame.refId]; - const query = this.getQuery(logsQueries, row, ds); + const ds = this.state.dsInstances[row.dataFrame.refId]; + if (!hasLogsContextSupport(ds)) { + return <>; + } + const query = this.getQuery(logsQueries, row, ds); return query && hasLogsContextUiSupport(ds) && ds.getLogRowContextUi ? ( ds.getLogRowContextUi(row, runContextQuery, query) ) : ( @@ -208,11 +209,10 @@ class LogsContainer extends PureComponent { - if (!row?.dataFrame.refId || !this.state.logContextSupport[row.dataFrame.refId]) { + if (!row?.dataFrame.refId || !this.state.dsInstances[row.dataFrame.refId]) { return false; } - - return true; + return hasLogsContextSupport(this.state.dsInstances[row.dataFrame.refId]); }; getFieldLinks = (field: Field, rowIndex: number, dataFrame: DataFrame) => { @@ -220,6 +220,24 @@ class LogsContainer extends PureComponent { + return Object.values(this.state.dsInstances).some( + (ds) => ds?.modifyQuery || hasQueryModificationSupport(ds) || hasToggleableQueryFiltersSupport(ds) + ); + }; + + filterValueAvailable = () => { + return Object.values(this.state.dsInstances).some( + (ds) => hasQueryModificationSupport(ds) && ds?.getSupportedQueryModifications().includes('ADD_STRING_FILTER') + ); + }; + + filterOutValueAvailable = () => { + return Object.values(this.state.dsInstances).some( + (ds) => hasQueryModificationSupport(ds) && ds?.getSupportedQueryModifications().includes('ADD_STRING_FILTER_OUT') + ); + }; + render() { const { loading, @@ -248,7 +266,6 @@ class LogsContainer extends PureComponent loadSupplementaryQueryData(exploreId, SupplementaryQueryType.LogsVolume)} onChangeTime={this.onChangeTime} - onClickFilterLabel={logDetailsFilterAvailable ? onClickFilterLabel : undefined} - onClickFilterOutLabel={logDetailsFilterAvailable ? onClickFilterOutLabel : undefined} + onClickFilterLabel={this.logDetailsFilterAvailable() ? onClickFilterLabel : undefined} + onClickFilterOutLabel={this.logDetailsFilterAvailable() ? onClickFilterOutLabel : undefined} onStartScanning={onStartScanning} onStopScanning={onStopScanning} absoluteRange={absoluteRange} @@ -313,10 +330,10 @@ class LogsContainer extends PureComponent diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 4d951114839..556fec3314b 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -120,17 +120,11 @@ class UnThemedLogRows extends PureComponent { popoverMenuCoordinates: { x: e.clientX - parentBounds.left, y: e.clientY - parentBounds.top }, selectedRow: row, }); + document.addEventListener('click', this.handleDeselection); return true; }; handleDeselection = (e: Event) => { - if ( - targetIsElement(e.target) && - (e.target?.getAttribute('role') === 'menuitem' || e.target?.parentElement?.getAttribute('role') === 'menuitem') - ) { - // Delegate closing the menu to the popover component. - return; - } if (targetIsElement(e.target) && !this.logRowsRef.current?.contains(e.target)) { // The mouseup event comes from outside the log rows, close the menu. this.closePopoverMenu(); @@ -143,6 +137,7 @@ class UnThemedLogRows extends PureComponent { }; closePopoverMenu = () => { + document.removeEventListener('click', this.handleDeselection); this.setState({ selection: '', popoverMenuCoordinates: { x: 0, y: 0 }, @@ -161,11 +156,10 @@ class UnThemedLogRows extends PureComponent { } else { this.renderAllTimer = window.setTimeout(() => this.setState({ renderAll: true }), 2000); } - document.addEventListener('mouseup', this.handleDeselection); } componentWillUnmount() { - document.removeEventListener('mouseup', this.handleDeselection); + document.removeEventListener('click', this.handleDeselection); if (this.renderAllTimer) { clearTimeout(this.renderAllTimer); } diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 22911072163..d40bb41bd3a 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -37,6 +37,7 @@ import { ToggleFilterAction, DataSourceGetTagValuesOptions, AdHocVariableFilter, + DataSourceWithQueryModificationSupport, } from '@grafana/data'; import { DataSourceWithBackend, @@ -107,7 +108,8 @@ export class ElasticDatasource DataSourceWithLogsContextSupport, DataSourceWithQueryImportSupport, DataSourceWithSupplementaryQueriesSupport, - DataSourceWithToggleableQueryFiltersSupport + DataSourceWithToggleableQueryFiltersSupport, + DataSourceWithQueryModificationSupport { basicAuth?: string; withCredentials?: boolean; @@ -962,6 +964,10 @@ export class ElasticDatasource return { ...query, query: expression }; } + getSupportedQueryModifications() { + return ['ADD_FILTER', 'ADD_FILTER_OUT', 'ADD_STRING_FILTER', 'ADD_STRING_FILTER_OUT']; + } + addAdHocFilters(query: string, adhocFilters?: AdHocVariableFilter[]) { if (!adhocFilters) { return query; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index bb658e24ac2..cda7a5cdf9f 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -37,6 +37,7 @@ import { LegacyMetricFindQueryOptions, AdHocVariableFilter, urlUtil, + DataSourceWithQueryModificationSupport, } from '@grafana/data'; import { Duration } from '@grafana/lezer-logql'; import { BackendSrvRequest, config, DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; @@ -135,7 +136,8 @@ export class LokiDatasource DataSourceWithSupplementaryQueriesSupport, DataSourceWithQueryImportSupport, DataSourceWithQueryExportSupport, - DataSourceWithToggleableQueryFiltersSupport + DataSourceWithToggleableQueryFiltersSupport, + DataSourceWithQueryModificationSupport { private streams = new LiveStreams(); private logContextProvider: LogContextProvider; @@ -863,7 +865,7 @@ export class LokiDatasource } /** - * Implemented as part of `DataSourceApi`. Used to modify a query based on the provided action. + * Implemented as part of `DataSourceWithQueryModificationSupport`. Used to modify a query based on the provided action. * It is used, for example, in the Query Builder to apply hints such as parsers, operations, etc. * @returns A new LokiQuery with the specified modification applied. */ @@ -949,6 +951,25 @@ export class LokiDatasource return { ...query, expr: expression }; } + /** + * Implemented as part of `DataSourceWithQueryModificationSupport`. Returns a list of operation + * types that are supported by `modifyQuery()`. + */ + getSupportedQueryModifications() { + return [ + 'ADD_FILTER', + 'ADD_FILTER_OUT', + 'ADD_LOGFMT_PARSER', + 'ADD_JSON_PARSER', + 'ADD_UNPACK_PARSER', + 'ADD_NO_PIPELINE_ERROR', + 'ADD_LEVEL_LABEL_FORMAT', + 'ADD_LABEL_FILTER', + 'ADD_STRING_FILTER', + 'ADD_STRING_FILTER_OUT', + ]; + } + /** * Part of `DataSourceWithLogsContextSupport`, used to retrieve log context for a log row. * @returns A promise that resolves to an object containing the log context data as DataFrames.