Logs: create DataSourceWithQueryModificationSupport + determine popover menu support (#78322)

* DataSourceWithQueryModificationSupportSupport: create interface

* Loki: implement DataSourceWithQueryModificationSupportSupport

* Elasticsearch: implement DataSourceWithQueryModificationSupportSupport

* DataSourceWithQueryModificationSupportSupport: add type guard

* DataSourceWithQueryModificationSupport: rename

* Check for nullish values in guards

* Logs container: replace support map with ds instances map

* Log rows: refactor deselection listener

* Formatting

* Formatting

* DataSourceWithQueryModificationSupport: add missing comment

* Logs container: update method name

* Logs container: check for query modification support

* Create QueryFixType

* QueryFixAction: move back to ds types

* getSupportedQueryModifications: update signature

* getSupportedQueryModifications: update signature
This commit is contained in:
Matias Chomicki 2023-11-23 11:04:23 +01:00 committed by GitHub
parent fd5f66083c
commit 02068662c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 65 deletions

View File

@ -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<string>;

View File

@ -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 = <TQuery extends DataQuery>(
datasource: unknown
): datasource is DataSourceWithToggleableQueryFiltersSupport<TQuery> => {
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<TQuery extends DataQuery> {
/**
* Given a query, applies a query modification `action`, returning the updated query.
* Explore currently supports the following action types:
* - ADD_FILTER: adds a <key, value> filter to the query.
* - ADD_FILTER_OUT: adds a negative <key, value> 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<QueryFixType | string>;
}
/**
* @internal
*/
export const hasQueryModificationSupport = <TQuery extends DataQuery>(
datasource: unknown
): datasource is DataSourceWithQueryModificationSupport<TQuery> => {
return (
datasource != null &&
typeof datasource === 'object' &&
'modifyQuery' in datasource &&
'getSupportedQueryModifications' in datasource
);
};

View File

@ -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<DataQuery>
| (DataSourceApi<DataQuery> & DataSourceWithLogsContextSupport<DataQuery>)
| (DataSourceApi<DataQuery> & DataSourceWithQueryModificationSupport<DataQuery>);
interface LogsContainerState {
logDetailsFilterAvailable: boolean;
logContextSupport: Record<string, DataSourceApi<DataQuery> & DataSourceWithLogsContextSupport<DataQuery>>;
dsInstances: Record<string, DataSourceInstance>;
}
class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState> {
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<string, DataSourceInstance> = {};
// 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<LogsContainerProps, LogsContainerState
if (!query.datasource) {
continue;
}
const mustCheck =
!newState.logContextSupport[query.refId] ||
newState.logContextSupport[query.refId].uid !== query.datasource.uid;
const mustCheck = !dsInstances[query.refId] || dsInstances[query.refId].uid !== query.datasource.uid;
if (mustCheck) {
dsPromises.push(
new Promise((resolve) => {
@ -128,18 +127,11 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
return;
}
Promise.all(dsPromises).then((dsInstances) => {
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<LogsContainerProps, LogsContainerState
): Promise<DataQueryResponse | []> => {
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<DataQuery | null> => {
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<LogsContainerProps, LogsContainerState
};
showContextToggle = (row?: LogRowModel): boolean => {
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<LogsContainerProps, LogsContainerState
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn, range, dataFrame });
};
logDetailsFilterAvailable = () => {
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<LogsContainerProps, LogsContainerState
logsVolume,
scrollElement,
} = this.props;
const { logDetailsFilterAvailable } = this.state;
if (!logRows) {
return null;
@ -293,8 +310,8 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
loadingState={loadingState}
loadLogsVolumeData={() => 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<LogsContainerProps, LogsContainerState
panelState={this.props.panelState}
logsFrames={this.props.logsFrames}
scrollElement={scrollElement}
isFilterLabelActive={logDetailsFilterAvailable ? this.props.isFilterLabelActive : undefined}
isFilterLabelActive={this.logDetailsFilterAvailable() ? this.props.isFilterLabelActive : undefined}
range={range}
onClickFilterValue={this.props.onClickFilterValue}
onClickFilterOutValue={this.props.onClickFilterOutValue}
onClickFilterValue={this.filterValueAvailable() ? this.props.onClickFilterValue : undefined}
onClickFilterOutValue={this.filterOutValueAvailable() ? this.props.onClickFilterOutValue : undefined}
/>
</LogsCrossFadeTransition>
</>

View File

@ -120,17 +120,11 @@ class UnThemedLogRows extends PureComponent<Props, State> {
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<Props, State> {
};
closePopoverMenu = () => {
document.removeEventListener('click', this.handleDeselection);
this.setState({
selection: '',
popoverMenuCoordinates: { x: 0, y: 0 },
@ -161,11 +156,10 @@ class UnThemedLogRows extends PureComponent<Props, State> {
} 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);
}

View File

@ -37,6 +37,7 @@ import {
ToggleFilterAction,
DataSourceGetTagValuesOptions,
AdHocVariableFilter,
DataSourceWithQueryModificationSupport,
} from '@grafana/data';
import {
DataSourceWithBackend,
@ -107,7 +108,8 @@ export class ElasticDatasource
DataSourceWithLogsContextSupport,
DataSourceWithQueryImportSupport<ElasticsearchQuery>,
DataSourceWithSupplementaryQueriesSupport<ElasticsearchQuery>,
DataSourceWithToggleableQueryFiltersSupport<ElasticsearchQuery>
DataSourceWithToggleableQueryFiltersSupport<ElasticsearchQuery>,
DataSourceWithQueryModificationSupport<ElasticsearchQuery>
{
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;

View File

@ -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<LokiQuery>,
DataSourceWithQueryImportSupport<LokiQuery>,
DataSourceWithQueryExportSupport<LokiQuery>,
DataSourceWithToggleableQueryFiltersSupport<LokiQuery>
DataSourceWithToggleableQueryFiltersSupport<LokiQuery>,
DataSourceWithQueryModificationSupport<LokiQuery>
{
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.