mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
fd5f66083c
commit
02068662c1
@ -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>;
|
||||
|
@ -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
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user