Commit 8f4be08b by Andrej Ocenas Committed by GitHub

Explore: Support wide data frames (#28393)

* Change how isTimeSeries work

* Simplify the decorators and update tests
parent 0bb33839
......@@ -5,6 +5,12 @@ export const initialState: AppNotificationsState = {
appNotifications: [] as AppNotification[],
};
/**
* Reducer and action to show toast notifications of various types (success, warnings, errors etc). Use to show
* transient info to user, like errors that cannot be otherwise handled or success after an action.
*
* Use factory functions in core/copy/appNotifications to create the payload.
*/
const appNotificationsSlice = createSlice({
name: 'appNotifications',
initialState,
......
// Libraries
import { map, throttleTime } from 'rxjs/operators';
import { map, mergeMap, throttleTime } from 'rxjs/operators';
import { identity } from 'rxjs';
import { PayloadAction } from '@reduxjs/toolkit';
import { DataSourceSrv } from '@grafana/runtime';
......@@ -84,7 +84,7 @@ import {
} from './actionTypes';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
import { updateLocation } from '../../../core/actions';
import { notifyApp, updateLocation } from '../../../core/actions';
import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
......@@ -96,6 +96,7 @@ import {
decorateWithLogsResult,
decorateWithTableResult,
} from '../utils/decorators';
import { createErrorNotification } from '../../../core/copy/appNotification';
/**
* Adds a query row after the row with the given index.
......@@ -427,6 +428,8 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
queryResponse,
querySubscription,
history,
refreshInterval,
absoluteRange,
} = exploreItemState;
if (!hasNonEmptyQuery(queries)) {
......@@ -473,12 +476,13 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
// actually can see what is happening.
live ? throttleTime(500) : identity,
map((data: PanelData) => preProcessPanelData(data, queryResponse)),
decorateWithGraphLogsTraceAndTable(getState().explore[exploreId].datasourceInstance),
decorateWithGraphResult(),
decorateWithTableResult(),
decorateWithLogsResult(getState().explore[exploreId])
map(decorateWithGraphLogsTraceAndTable),
map(decorateWithGraphResult),
map(decorateWithLogsResult({ absoluteRange, refreshInterval })),
mergeMap(decorateWithTableResult)
)
.subscribe(data => {
.subscribe(
data => {
if (!data.error && firstResponse) {
// Side-effect: Saving history in localstorage
const nextHistory = updateHistory(history, datasourceId, queries);
......@@ -513,7 +517,13 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
dispatch(scanStopAction({ exploreId }));
}
}
});
},
error => {
dispatch(notifyApp(createErrorNotification('Query processing error', error)));
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Error }));
console.error(error);
}
);
dispatch(queryStoreSubscriptionAction({ exploreId, querySubscription: newQuerySub }));
};
......
import { MonoTypeOperatorFunction, of, OperatorFunction } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AbsoluteTimeRange,
DataFrame,
DataSourceApi,
FieldType,
getDisplayProcessor,
PanelData,
PreferredVisualisationType,
sortLogsResult,
standardTransformers,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { groupBy } from 'lodash';
import { ExploreItemState, ExplorePanelData } from '../../../types';
import { ExplorePanelData } from '../../../types';
import { getGraphSeriesModel } from '../../../plugins/panel/graph2/getGraphSeriesModel';
import { dataFrameToLogsModel } from '../../../core/logs_model';
import { refreshIntervalToSortOrder } from '../../../core/utils/explore';
export const decorateWithGraphLogsTraceAndTable = (
datasourceInstance?: DataSourceApi | null
): OperatorFunction<PanelData, ExplorePanelData> => inputStream =>
inputStream.pipe(
map(data => {
/**
* When processing response first we try to determine what kind of dataframes we got as one query can return multiple
* dataFrames with different type of data. This is later used for type specific processing. As we use this in
* Observable pipeline, it decorates the existing panelData to pass the results to later processing stages.
*/
export const decorateWithGraphLogsTraceAndTable = (data: PanelData): ExplorePanelData => {
if (data.error) {
return {
...data,
......@@ -41,26 +42,29 @@ export const decorateWithGraphLogsTraceAndTable = (
const traceFrames: DataFrame[] = [];
for (const frame of data.series) {
if (shouldShowInVisualisationTypeStrict(frame, 'logs')) {
switch (frame.meta?.preferredVisualisationType) {
case 'logs':
logsFrames.push(frame);
} else if (shouldShowInVisualisationTypeStrict(frame, 'graph')) {
break;
case 'graph':
graphFrames.push(frame);
} else if (shouldShowInVisualisationTypeStrict(frame, 'trace')) {
break;
case 'trace':
traceFrames.push(frame);
} else if (shouldShowInVisualisationTypeStrict(frame, 'table')) {
break;
case 'table':
tableFrames.push(frame);
} else if (isTimeSeries(frame, datasourceInstance?.meta.id)) {
if (shouldShowInVisualisationType(frame, 'graph')) {
break;
default:
if (isTimeSeries(frame)) {
graphFrames.push(frame);
}
if (shouldShowInVisualisationType(frame, 'table')) {
tableFrames.push(frame);
}
} else {
// We fallback to table if we do not have any better meta info about the dataframe.
tableFrames.push(frame);
}
}
}
return {
...data,
......@@ -72,12 +76,9 @@ export const decorateWithGraphLogsTraceAndTable = (
tableResult: null,
logsResult: null,
};
})
);
};
export const decorateWithGraphResult = (): MonoTypeOperatorFunction<ExplorePanelData> => inputStream =>
inputStream.pipe(
map(data => {
export const decorateWithGraphResult = (data: ExplorePanelData): ExplorePanelData => {
if (data.error) {
return { ...data, graphResult: null };
}
......@@ -94,12 +95,14 @@ export const decorateWithGraphResult = (): MonoTypeOperatorFunction<ExplorePanel
);
return { ...data, graphResult };
})
);
export const decorateWithTableResult = (): MonoTypeOperatorFunction<ExplorePanelData> => inputStream =>
inputStream.pipe(
mergeMap(data => {
};
/**
* This processing returns Observable because it uses Transformer internally which result type is also Observable.
* In this case the transformer should return single result but it is possible that in the future it could return
* multiple results and so this should be used with mergeMap or similar to unbox the internal observable.
*/
export const decorateWithTableResult = (data: ExplorePanelData): Observable<ExplorePanelData> => {
if (data.error) {
return of({ ...data, tableResult: null });
}
......@@ -148,67 +151,37 @@ export const decorateWithTableResult = (): MonoTypeOperatorFunction<ExplorePanel
return { ...data, tableResult: frame };
})
);
})
);
};
export const decorateWithLogsResult = (
state: ExploreItemState
): MonoTypeOperatorFunction<ExplorePanelData> => inputStream =>
inputStream.pipe(
map(data => {
options: { absoluteRange?: AbsoluteTimeRange; refreshInterval?: string } = {}
) => (data: ExplorePanelData): ExplorePanelData => {
if (data.error) {
return { ...data, logsResult: null };
}
const { absoluteRange, refreshInterval } = state;
if (data.logsFrames.length === 0) {
return { ...data, logsResult: null };
}
const timeZone = data.request?.timezone ?? 'browser';
const intervalMs = data.request?.intervalMs;
const newResults = dataFrameToLogsModel(data.logsFrames, intervalMs, timeZone, absoluteRange);
const sortOrder = refreshIntervalToSortOrder(refreshInterval);
const newResults = dataFrameToLogsModel(data.logsFrames, intervalMs, timeZone, options.absoluteRange);
const sortOrder = refreshIntervalToSortOrder(options.refreshInterval);
const sortedNewResults = sortLogsResult(newResults, sortOrder);
const rows = sortedNewResults.rows;
const series = sortedNewResults.series;
const logsResult = { ...sortedNewResults, rows, series };
return { ...data, logsResult };
})
);
function isTimeSeries(frame: DataFrame, datasource?: string): boolean {
// TEMP: Temporary hack. Remove when logs/metrics unification is done
if (datasource && datasource === 'cloudwatch') {
return isTimeSeriesCloudWatch(frame);
}
if (frame.fields.length === 2) {
if (frame.fields[0].type === FieldType.time) {
return true;
}
}
return false;
}
function shouldShowInVisualisationType(frame: DataFrame, visualisation: PreferredVisualisationType) {
if (frame.meta?.preferredVisualisationType && frame.meta?.preferredVisualisationType !== visualisation) {
return false;
}
return true;
}
function shouldShowInVisualisationTypeStrict(frame: DataFrame, visualisation: PreferredVisualisationType) {
return frame.meta?.preferredVisualisationType === visualisation;
}
// TEMP: Temporary hack. Remove when logs/metrics unification is done
function isTimeSeriesCloudWatch(frame: DataFrame): boolean {
return (
frame.fields.some(field => field.type === FieldType.time) &&
frame.fields.some(field => field.type === FieldType.number)
};
/**
* Check if frame contains time series, which for our purpose means 1 time column and 1 or more numeric columns.
*/
function isTimeSeries(frame: DataFrame): boolean {
const grouped = groupBy(frame.fields, field => field.type);
return Boolean(
Object.keys(grouped).length === 2 && grouped[FieldType.time]?.length === 1 && grouped[FieldType.number]
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment