mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Support wide data frames (#28393)
* Change how isTimeSeries work * Simplify the decorators and update tests
This commit is contained in:
parent
0bb33839f5
commit
8f4be08b00
@ -5,6 +5,12 @@ export const initialState: AppNotificationsState = {
|
|||||||
appNotifications: [] as AppNotification[],
|
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({
|
const appNotificationsSlice = createSlice({
|
||||||
name: 'appNotifications',
|
name: 'appNotifications',
|
||||||
initialState,
|
initialState,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import { map, throttleTime } from 'rxjs/operators';
|
import { map, mergeMap, throttleTime } from 'rxjs/operators';
|
||||||
import { identity } from 'rxjs';
|
import { identity } from 'rxjs';
|
||||||
import { PayloadAction } from '@reduxjs/toolkit';
|
import { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { DataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv } from '@grafana/runtime';
|
||||||
@ -84,7 +84,7 @@ import {
|
|||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
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 { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||||
import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
|
import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
|
||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
@ -96,6 +96,7 @@ import {
|
|||||||
decorateWithLogsResult,
|
decorateWithLogsResult,
|
||||||
decorateWithTableResult,
|
decorateWithTableResult,
|
||||||
} from '../utils/decorators';
|
} from '../utils/decorators';
|
||||||
|
import { createErrorNotification } from '../../../core/copy/appNotification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a query row after the row with the given index.
|
* Adds a query row after the row with the given index.
|
||||||
@ -427,6 +428,8 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
|||||||
queryResponse,
|
queryResponse,
|
||||||
querySubscription,
|
querySubscription,
|
||||||
history,
|
history,
|
||||||
|
refreshInterval,
|
||||||
|
absoluteRange,
|
||||||
} = exploreItemState;
|
} = exploreItemState;
|
||||||
|
|
||||||
if (!hasNonEmptyQuery(queries)) {
|
if (!hasNonEmptyQuery(queries)) {
|
||||||
@ -473,47 +476,54 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
|||||||
// actually can see what is happening.
|
// actually can see what is happening.
|
||||||
live ? throttleTime(500) : identity,
|
live ? throttleTime(500) : identity,
|
||||||
map((data: PanelData) => preProcessPanelData(data, queryResponse)),
|
map((data: PanelData) => preProcessPanelData(data, queryResponse)),
|
||||||
decorateWithGraphLogsTraceAndTable(getState().explore[exploreId].datasourceInstance),
|
map(decorateWithGraphLogsTraceAndTable),
|
||||||
decorateWithGraphResult(),
|
map(decorateWithGraphResult),
|
||||||
decorateWithTableResult(),
|
map(decorateWithLogsResult({ absoluteRange, refreshInterval })),
|
||||||
decorateWithLogsResult(getState().explore[exploreId])
|
mergeMap(decorateWithTableResult)
|
||||||
)
|
)
|
||||||
.subscribe(data => {
|
.subscribe(
|
||||||
if (!data.error && firstResponse) {
|
data => {
|
||||||
// Side-effect: Saving history in localstorage
|
if (!data.error && firstResponse) {
|
||||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
// Side-effect: Saving history in localstorage
|
||||||
const nextRichHistory = addToRichHistory(
|
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||||
richHistory || [],
|
const nextRichHistory = addToRichHistory(
|
||||||
datasourceId,
|
richHistory || [],
|
||||||
datasourceName,
|
datasourceId,
|
||||||
queries,
|
datasourceName,
|
||||||
false,
|
queries,
|
||||||
'',
|
false,
|
||||||
''
|
'',
|
||||||
);
|
''
|
||||||
dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
|
);
|
||||||
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
|
dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
|
||||||
|
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
|
||||||
|
|
||||||
// We save queries to the URL here so that only successfully run queries change the URL.
|
// We save queries to the URL here so that only successfully run queries change the URL.
|
||||||
dispatch(stateSave());
|
dispatch(stateSave());
|
||||||
}
|
|
||||||
|
|
||||||
firstResponse = false;
|
|
||||||
|
|
||||||
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
|
|
||||||
|
|
||||||
// Keep scanning for results if this was the last scanning transaction
|
|
||||||
if (getState().explore[exploreId].scanning) {
|
|
||||||
if (data.state === LoadingState.Done && data.series.length === 0) {
|
|
||||||
const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
|
|
||||||
dispatch(updateTime({ exploreId, absoluteRange: range }));
|
|
||||||
dispatch(runQueries(exploreId));
|
|
||||||
} else {
|
|
||||||
// We can stop scanning if we have a result
|
|
||||||
dispatch(scanStopAction({ exploreId }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
firstResponse = false;
|
||||||
|
|
||||||
|
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
|
||||||
|
|
||||||
|
// Keep scanning for results if this was the last scanning transaction
|
||||||
|
if (getState().explore[exploreId].scanning) {
|
||||||
|
if (data.state === LoadingState.Done && data.series.length === 0) {
|
||||||
|
const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
|
||||||
|
dispatch(updateTime({ exploreId, absoluteRange: range }));
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
|
} else {
|
||||||
|
// We can stop scanning if we have a result
|
||||||
|
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 }));
|
dispatch(queryStoreSubscriptionAction({ exploreId, querySubscription: newQuerySub }));
|
||||||
};
|
};
|
||||||
|
@ -3,15 +3,12 @@ jest.mock('@grafana/data/src/datetime/formatter', () => ({
|
|||||||
dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked',
|
dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
import {
|
import {
|
||||||
ArrayVector,
|
ArrayVector,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataSourceApi,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
observableTester,
|
|
||||||
PanelData,
|
PanelData,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
toDataFrame,
|
toDataFrame,
|
||||||
@ -24,7 +21,7 @@ import {
|
|||||||
decorateWithTableResult,
|
decorateWithTableResult,
|
||||||
} from './decorators';
|
} from './decorators';
|
||||||
import { describe } from '../../../../test/lib/common';
|
import { describe } from '../../../../test/lib/common';
|
||||||
import { ExploreItemState, ExplorePanelData } from 'app/types';
|
import { ExplorePanelData } from 'app/types';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
|
|
||||||
const getTestContext = () => {
|
const getTestContext = () => {
|
||||||
@ -37,6 +34,7 @@ const getTestContext = () => {
|
|||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
{ name: 'A-series', type: FieldType.number, values: [4, 5, 6] },
|
{ name: 'A-series', type: FieldType.number, values: [4, 5, 6] },
|
||||||
|
{ name: 'B-series', type: FieldType.number, values: [7, 8, 9] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,450 +84,337 @@ const createExplorePanelData = (args: Partial<ExplorePanelData>): ExplorePanelDa
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('decorateWithGraphLogsTraceAndTable', () => {
|
describe('decorateWithGraphLogsTraceAndTable', () => {
|
||||||
describe('when used without error', () => {
|
it('should correctly classify the dataFrames', () => {
|
||||||
it('then the result should be correct', done => {
|
const { table, logs, timeSeries, emptyTable } = getTestContext();
|
||||||
const { table, logs, timeSeries, emptyTable } = getTestContext();
|
const series = [table, logs, timeSeries, emptyTable];
|
||||||
const datasourceInstance = ({ meta: { id: 'prometheus' } } as unknown) as DataSourceApi;
|
const panelData: PanelData = {
|
||||||
const series = [table, logs, timeSeries, emptyTable];
|
series,
|
||||||
const panelData: PanelData = {
|
state: LoadingState.Done,
|
||||||
series,
|
timeRange: ({} as unknown) as TimeRange,
|
||||||
state: LoadingState.Done,
|
};
|
||||||
timeRange: ({} as unknown) as TimeRange,
|
|
||||||
};
|
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
expect(decorateWithGraphLogsTraceAndTable(panelData)).toEqual({
|
||||||
observable: of(panelData).pipe(decorateWithGraphLogsTraceAndTable(datasourceInstance)),
|
series,
|
||||||
expect: value => {
|
state: LoadingState.Done,
|
||||||
expect(value).toEqual({
|
timeRange: {},
|
||||||
series,
|
graphFrames: [timeSeries],
|
||||||
state: LoadingState.Done,
|
tableFrames: [table, emptyTable],
|
||||||
timeRange: {},
|
logsFrames: [logs],
|
||||||
graphFrames: [timeSeries],
|
traceFrames: [],
|
||||||
tableFrames: [table, emptyTable],
|
graphResult: null,
|
||||||
logsFrames: [logs],
|
tableResult: null,
|
||||||
traceFrames: [],
|
logsResult: null,
|
||||||
graphResult: null,
|
|
||||||
tableResult: null,
|
|
||||||
logsResult: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used without frames', () => {
|
it('should handle empty array', () => {
|
||||||
it('then the result should be correct', done => {
|
const series: DataFrame[] = [];
|
||||||
const datasourceInstance = ({ meta: { id: 'prometheus' } } as unknown) as DataSourceApi;
|
const panelData: PanelData = {
|
||||||
const series: DataFrame[] = [];
|
series,
|
||||||
const panelData: PanelData = {
|
state: LoadingState.Done,
|
||||||
series,
|
timeRange: ({} as unknown) as TimeRange,
|
||||||
state: LoadingState.Done,
|
};
|
||||||
timeRange: ({} as unknown) as TimeRange,
|
|
||||||
};
|
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
expect(decorateWithGraphLogsTraceAndTable(panelData)).toEqual({
|
||||||
observable: of(panelData).pipe(decorateWithGraphLogsTraceAndTable(datasourceInstance)),
|
series: [],
|
||||||
expect: value => {
|
state: LoadingState.Done,
|
||||||
expect(value).toEqual({
|
timeRange: {},
|
||||||
series: [],
|
graphFrames: [],
|
||||||
state: LoadingState.Done,
|
tableFrames: [],
|
||||||
timeRange: {},
|
logsFrames: [],
|
||||||
graphFrames: [],
|
traceFrames: [],
|
||||||
tableFrames: [],
|
graphResult: null,
|
||||||
logsFrames: [],
|
tableResult: null,
|
||||||
traceFrames: [],
|
logsResult: null,
|
||||||
graphResult: null,
|
|
||||||
tableResult: null,
|
|
||||||
logsResult: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used with an error', () => {
|
it('should handle query error', () => {
|
||||||
it('then the result should be correct', done => {
|
const { timeSeries, logs, table } = getTestContext();
|
||||||
const { timeSeries, logs, table } = getTestContext();
|
const series: DataFrame[] = [timeSeries, logs, table];
|
||||||
const datasourceInstance = ({ meta: { id: 'prometheus' } } as unknown) as DataSourceApi;
|
const panelData: PanelData = {
|
||||||
const series: DataFrame[] = [timeSeries, logs, table];
|
series,
|
||||||
const panelData: PanelData = {
|
error: {},
|
||||||
series,
|
state: LoadingState.Error,
|
||||||
error: {},
|
timeRange: ({} as unknown) as TimeRange,
|
||||||
state: LoadingState.Error,
|
};
|
||||||
timeRange: ({} as unknown) as TimeRange,
|
|
||||||
};
|
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
expect(decorateWithGraphLogsTraceAndTable(panelData)).toEqual({
|
||||||
observable: of(panelData).pipe(decorateWithGraphLogsTraceAndTable(datasourceInstance)),
|
series: [timeSeries, logs, table],
|
||||||
expect: value => {
|
error: {},
|
||||||
expect(value).toEqual({
|
state: LoadingState.Error,
|
||||||
series: [timeSeries, logs, table],
|
timeRange: {},
|
||||||
error: {},
|
graphFrames: [],
|
||||||
state: LoadingState.Error,
|
tableFrames: [],
|
||||||
timeRange: {},
|
logsFrames: [],
|
||||||
graphFrames: [],
|
traceFrames: [],
|
||||||
tableFrames: [],
|
graphResult: null,
|
||||||
logsFrames: [],
|
tableResult: null,
|
||||||
traceFrames: [],
|
logsResult: null,
|
||||||
graphResult: null,
|
|
||||||
tableResult: null,
|
|
||||||
logsResult: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('decorateWithGraphResult', () => {
|
describe('decorateWithGraphResult', () => {
|
||||||
describe('when used without error', () => {
|
it('should process the graph dataFrames', () => {
|
||||||
it('then the graphResult should be correct', done => {
|
const { timeSeries } = getTestContext();
|
||||||
const { timeSeries } = getTestContext();
|
const panelData = createExplorePanelData({ graphFrames: [timeSeries] });
|
||||||
const timeField = timeSeries.fields[0];
|
console.log(decorateWithGraphResult(panelData).graphResult);
|
||||||
const valueField = timeSeries.fields[1];
|
expect(decorateWithGraphResult(panelData).graphResult).toMatchObject([
|
||||||
const panelData = createExplorePanelData({ graphFrames: [timeSeries] });
|
{
|
||||||
|
label: 'A-series',
|
||||||
observableTester().subscribeAndExpectOnNext({
|
data: [
|
||||||
observable: of(panelData).pipe(decorateWithGraphResult()),
|
[100, 4],
|
||||||
expect: panelData => {
|
[200, 5],
|
||||||
expect(panelData.graphResult![0]).toEqual({
|
[300, 6],
|
||||||
label: 'A-series',
|
],
|
||||||
color: '#7EB26D',
|
isVisible: true,
|
||||||
data: [
|
yAxis: {
|
||||||
[100, 4],
|
index: 1,
|
||||||
[200, 5],
|
|
||||||
[300, 6],
|
|
||||||
],
|
|
||||||
info: [],
|
|
||||||
isVisible: true,
|
|
||||||
yAxis: {
|
|
||||||
index: 1,
|
|
||||||
},
|
|
||||||
seriesIndex: 0,
|
|
||||||
timeField,
|
|
||||||
valueField,
|
|
||||||
timeStep: 100,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
done,
|
seriesIndex: 0,
|
||||||
});
|
timeStep: 100,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
label: 'B-series',
|
||||||
|
data: [
|
||||||
|
[100, 7],
|
||||||
|
[200, 8],
|
||||||
|
[300, 9],
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
yAxis: {
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
seriesIndex: 1,
|
||||||
|
timeStep: 100,
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used without error but graph frames are empty', () => {
|
it('returns null if it gets empty array', () => {
|
||||||
it('then the graphResult should be null', done => {
|
const panelData = createExplorePanelData({ graphFrames: [] });
|
||||||
const panelData = createExplorePanelData({ graphFrames: [] });
|
expect(decorateWithGraphResult(panelData).graphResult).toBeNull();
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
|
||||||
observable: of(panelData).pipe(decorateWithGraphResult()),
|
|
||||||
expect: panelData => {
|
|
||||||
expect(panelData.graphResult).toBeNull();
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used with error', () => {
|
it('returns null if panelData has error', () => {
|
||||||
it('then the graphResult should be null', done => {
|
const { timeSeries } = getTestContext();
|
||||||
const { timeSeries } = getTestContext();
|
const panelData = createExplorePanelData({ error: {}, graphFrames: [timeSeries] });
|
||||||
const panelData = createExplorePanelData({ error: {}, graphFrames: [timeSeries] });
|
expect(decorateWithGraphResult(panelData).graphResult).toBeNull();
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
|
||||||
observable: of(panelData).pipe(decorateWithGraphResult()),
|
|
||||||
expect: panelData => {
|
|
||||||
expect(panelData.graphResult).toBeNull();
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('decorateWithTableResult', () => {
|
describe('decorateWithTableResult', () => {
|
||||||
describe('when used without error', () => {
|
it('should process table type dataFrame', async () => {
|
||||||
it('then the tableResult should be correct', done => {
|
const { table, emptyTable } = getTestContext();
|
||||||
const { table, emptyTable } = getTestContext();
|
const panelData = createExplorePanelData({ tableFrames: [table, emptyTable] });
|
||||||
const panelData = createExplorePanelData({ tableFrames: [table, emptyTable] });
|
const panelResult = await decorateWithTableResult(panelData).toPromise();
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
let theResult = panelResult.tableResult;
|
||||||
observable: of(panelData).pipe(decorateWithTableResult()),
|
|
||||||
expect: panelData => {
|
|
||||||
let theResult = panelData.tableResult;
|
|
||||||
|
|
||||||
expect(theResult?.fields[0].name).toEqual('value');
|
expect(theResult?.fields[0].name).toEqual('value');
|
||||||
expect(theResult?.fields[1].name).toEqual('time');
|
expect(theResult?.fields[1].name).toEqual('time');
|
||||||
expect(theResult?.fields[2].name).toEqual('tsNs');
|
expect(theResult?.fields[2].name).toEqual('tsNs');
|
||||||
expect(theResult?.fields[3].name).toEqual('message');
|
expect(theResult?.fields[3].name).toEqual('message');
|
||||||
expect(theResult?.fields[1].display).not.toBeNull();
|
expect(theResult?.fields[1].display).not.toBeNull();
|
||||||
expect(theResult?.length).toBe(3);
|
expect(theResult?.length).toBe(3);
|
||||||
|
|
||||||
// I don't understand the purpose of the code below, feels like this belongs in toDataFrame tests?
|
// I don't understand the purpose of the code below, feels like this belongs in toDataFrame tests?
|
||||||
// Same data though a DataFrame
|
// Same data though a DataFrame
|
||||||
theResult = toDataFrame(
|
theResult = toDataFrame(
|
||||||
new TableModel({
|
new TableModel({
|
||||||
columns: [
|
columns: [
|
||||||
{ text: 'value', type: 'number' },
|
{ text: 'value', type: 'number' },
|
||||||
{ text: 'time', type: 'time' },
|
{ text: 'time', type: 'time' },
|
||||||
{ text: 'tsNs', type: 'time' },
|
{ text: 'tsNs', type: 'time' },
|
||||||
{ text: 'message', type: 'string' },
|
{ text: 'message', type: 'string' },
|
||||||
],
|
],
|
||||||
rows: [
|
rows: [
|
||||||
[4, 100, '100000000', 'this is a message'],
|
[4, 100, '100000000', 'this is a message'],
|
||||||
[5, 200, '100000000', 'second message'],
|
[5, 200, '100000000', 'second message'],
|
||||||
[6, 300, '100000000', 'third'],
|
[6, 300, '100000000', 'third'],
|
||||||
],
|
],
|
||||||
type: 'table',
|
type: 'table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(theResult.fields[0].name).toEqual('value');
|
expect(theResult.fields[0].name).toEqual('value');
|
||||||
expect(theResult.fields[1].name).toEqual('time');
|
expect(theResult.fields[1].name).toEqual('time');
|
||||||
expect(theResult.fields[2].name).toEqual('tsNs');
|
expect(theResult.fields[2].name).toEqual('tsNs');
|
||||||
expect(theResult.fields[3].name).toEqual('message');
|
expect(theResult.fields[3].name).toEqual('message');
|
||||||
expect(theResult.fields[1].display).not.toBeNull();
|
expect(theResult.fields[1].display).not.toBeNull();
|
||||||
expect(theResult.length).toBe(3);
|
expect(theResult.length).toBe(3);
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do join transform if all series are timeseries', done => {
|
|
||||||
const tableFrames = [
|
|
||||||
toDataFrame({
|
|
||||||
name: 'A-series',
|
|
||||||
refId: 'A',
|
|
||||||
fields: [
|
|
||||||
{ name: 'Time', type: FieldType.time, values: [100, 200, 300] },
|
|
||||||
{ name: 'A-series', type: FieldType.number, values: [4, 5, 6] },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
toDataFrame({
|
|
||||||
name: 'B-series',
|
|
||||||
refId: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'Time', type: FieldType.time, values: [100, 200, 300] },
|
|
||||||
{ name: 'B-series', type: FieldType.number, values: [4, 5, 6] },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
const panelData = createExplorePanelData({ tableFrames });
|
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
|
||||||
observable: of(panelData).pipe(decorateWithTableResult()),
|
|
||||||
expect: panelData => {
|
|
||||||
const result = panelData.tableResult;
|
|
||||||
|
|
||||||
expect(result?.fields[0].name).toBe('Time');
|
|
||||||
expect(result?.fields[1].name).toBe('A-series');
|
|
||||||
expect(result?.fields[2].name).toBe('B-series');
|
|
||||||
expect(result?.fields[0].values.toArray()).toEqual([100, 200, 300]);
|
|
||||||
expect(result?.fields[1].values.toArray()).toEqual([4, 5, 6]);
|
|
||||||
expect(result?.fields[2].values.toArray()).toEqual([4, 5, 6]);
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not override fields display property when filled', done => {
|
|
||||||
const tableFrames = [
|
|
||||||
toDataFrame({
|
|
||||||
name: 'A-series',
|
|
||||||
refId: 'A',
|
|
||||||
fields: [{ name: 'Text', type: FieldType.string, values: ['someText'] }],
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
const displayFunctionMock = jest.fn();
|
|
||||||
tableFrames[0].fields[0].display = displayFunctionMock;
|
|
||||||
|
|
||||||
const panelData = createExplorePanelData({ tableFrames });
|
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
|
||||||
observable: of(panelData).pipe(decorateWithTableResult()),
|
|
||||||
expect: panelData => {
|
|
||||||
const data = panelData.tableResult;
|
|
||||||
expect(data?.fields[0].display).toBe(displayFunctionMock);
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used without error but table frames are empty', () => {
|
it('should do join transform if all series are timeseries', async () => {
|
||||||
it('then the tableResult should be null', done => {
|
const tableFrames = [
|
||||||
const panelData = createExplorePanelData({ tableFrames: [] });
|
toDataFrame({
|
||||||
|
name: 'A-series',
|
||||||
|
refId: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'A-series', type: FieldType.number, values: [4, 5, 6] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
toDataFrame({
|
||||||
|
name: 'B-series',
|
||||||
|
refId: 'B',
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'B-series', type: FieldType.number, values: [4, 5, 6] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const panelData = createExplorePanelData({ tableFrames });
|
||||||
|
const panelResult = await decorateWithTableResult(panelData).toPromise();
|
||||||
|
const result = panelResult.tableResult;
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
expect(result?.fields[0].name).toBe('Time');
|
||||||
observable: of(panelData).pipe(decorateWithTableResult()),
|
expect(result?.fields[1].name).toBe('A-series');
|
||||||
expect: panelData => {
|
expect(result?.fields[2].name).toBe('B-series');
|
||||||
expect(panelData.tableResult).toBeNull();
|
expect(result?.fields[0].values.toArray()).toEqual([100, 200, 300]);
|
||||||
},
|
expect(result?.fields[1].values.toArray()).toEqual([4, 5, 6]);
|
||||||
done,
|
expect(result?.fields[2].values.toArray()).toEqual([4, 5, 6]);
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used with error', () => {
|
it('should not override fields display property when filled', async () => {
|
||||||
it('then the tableResult should be null', done => {
|
const tableFrames = [
|
||||||
const { table, emptyTable } = getTestContext();
|
toDataFrame({
|
||||||
const panelData = createExplorePanelData({ error: {}, tableFrames: [table, emptyTable] });
|
name: 'A-series',
|
||||||
|
refId: 'A',
|
||||||
|
fields: [{ name: 'Text', type: FieldType.string, values: ['someText'] }],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const displayFunctionMock = jest.fn();
|
||||||
|
tableFrames[0].fields[0].display = displayFunctionMock;
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
const panelData = createExplorePanelData({ tableFrames });
|
||||||
observable: of(panelData).pipe(decorateWithTableResult()),
|
const panelResult = await decorateWithTableResult(panelData).toPromise();
|
||||||
expect: panelData => {
|
expect(panelResult.tableResult?.fields[0].display).toBe(displayFunctionMock);
|
||||||
expect(panelData.tableResult).toBeNull();
|
});
|
||||||
},
|
|
||||||
done,
|
it('should return null when passed empty array', async () => {
|
||||||
});
|
const panelData = createExplorePanelData({ tableFrames: [] });
|
||||||
});
|
const panelResult = await decorateWithTableResult(panelData).toPromise();
|
||||||
|
expect(panelResult.tableResult).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if panelData has error', async () => {
|
||||||
|
const { table, emptyTable } = getTestContext();
|
||||||
|
const panelData = createExplorePanelData({ error: {}, tableFrames: [table, emptyTable] });
|
||||||
|
const panelResult = await decorateWithTableResult(panelData).toPromise();
|
||||||
|
expect(panelResult.tableResult).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('decorateWithLogsResult', () => {
|
describe('decorateWithLogsResult', () => {
|
||||||
describe('when used without error', () => {
|
it('should correctly transform logs dataFrames', () => {
|
||||||
it('then the logsResult should be correct', done => {
|
const { logs } = getTestContext();
|
||||||
const { logs } = getTestContext();
|
const request = ({ timezone: 'utc', intervalMs: 60000 } as unknown) as DataQueryRequest;
|
||||||
const state = ({
|
const panelData = createExplorePanelData({ logsFrames: [logs], request });
|
||||||
queryIntervals: { intervalMs: 10 },
|
expect(decorateWithLogsResult()(panelData).logsResult).toEqual({
|
||||||
} as unknown) as ExploreItemState;
|
hasUniqueLabels: false,
|
||||||
const request = ({ timezone: 'utc', intervalMs: 60000 } as unknown) as DataQueryRequest;
|
meta: [],
|
||||||
const panelData = createExplorePanelData({ logsFrames: [logs], request });
|
rows: [
|
||||||
|
{
|
||||||
observableTester().subscribeAndExpectOnNext({
|
rowIndex: 0,
|
||||||
observable: of(panelData).pipe(decorateWithLogsResult(state)),
|
dataFrame: logs,
|
||||||
expect: panelData => {
|
entry: 'this is a message',
|
||||||
const theResult = panelData.logsResult;
|
entryFieldIndex: 3,
|
||||||
|
hasAnsi: false,
|
||||||
expect(theResult).toEqual({
|
labels: {},
|
||||||
hasUniqueLabels: false,
|
logLevel: 'unknown',
|
||||||
meta: [],
|
raw: 'this is a message',
|
||||||
rows: [
|
searchWords: [] as string[],
|
||||||
{
|
timeEpochMs: 100,
|
||||||
rowIndex: 0,
|
timeEpochNs: '100000002',
|
||||||
dataFrame: logs,
|
timeFromNow: 'fromNow() jest mocked',
|
||||||
entry: 'this is a message',
|
timeLocal: 'format() jest mocked',
|
||||||
entryFieldIndex: 3,
|
timeUtc: 'format() jest mocked',
|
||||||
hasAnsi: false,
|
uid: '0',
|
||||||
labels: {},
|
uniqueLabels: {},
|
||||||
logLevel: 'unknown',
|
|
||||||
raw: 'this is a message',
|
|
||||||
searchWords: [] as string[],
|
|
||||||
timeEpochMs: 100,
|
|
||||||
timeEpochNs: '100000002',
|
|
||||||
timeFromNow: 'fromNow() jest mocked',
|
|
||||||
timeLocal: 'format() jest mocked',
|
|
||||||
timeUtc: 'format() jest mocked',
|
|
||||||
uid: '0',
|
|
||||||
uniqueLabels: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rowIndex: 2,
|
|
||||||
dataFrame: logs,
|
|
||||||
entry: 'third',
|
|
||||||
entryFieldIndex: 3,
|
|
||||||
hasAnsi: false,
|
|
||||||
labels: {},
|
|
||||||
logLevel: 'unknown',
|
|
||||||
raw: 'third',
|
|
||||||
searchWords: [] as string[],
|
|
||||||
timeEpochMs: 100,
|
|
||||||
timeEpochNs: '100000001',
|
|
||||||
timeFromNow: 'fromNow() jest mocked',
|
|
||||||
timeLocal: 'format() jest mocked',
|
|
||||||
timeUtc: 'format() jest mocked',
|
|
||||||
uid: '2',
|
|
||||||
uniqueLabels: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rowIndex: 1,
|
|
||||||
dataFrame: logs,
|
|
||||||
entry: 'second message',
|
|
||||||
entryFieldIndex: 3,
|
|
||||||
hasAnsi: false,
|
|
||||||
labels: {},
|
|
||||||
logLevel: 'unknown',
|
|
||||||
raw: 'second message',
|
|
||||||
searchWords: [] as string[],
|
|
||||||
timeEpochMs: 100,
|
|
||||||
timeEpochNs: '100000000',
|
|
||||||
timeFromNow: 'fromNow() jest mocked',
|
|
||||||
timeLocal: 'format() jest mocked',
|
|
||||||
timeUtc: 'format() jest mocked',
|
|
||||||
uid: '1',
|
|
||||||
uniqueLabels: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
label: 'unknown',
|
|
||||||
color: '#8e8e8e',
|
|
||||||
data: [[0, 3]],
|
|
||||||
isVisible: true,
|
|
||||||
yAxis: {
|
|
||||||
index: 1,
|
|
||||||
min: 0,
|
|
||||||
tickDecimals: 0,
|
|
||||||
},
|
|
||||||
seriesIndex: 0,
|
|
||||||
timeField: {
|
|
||||||
name: 'Time',
|
|
||||||
type: 'time',
|
|
||||||
config: {},
|
|
||||||
values: new ArrayVector([0]),
|
|
||||||
index: 0,
|
|
||||||
display: expect.anything(),
|
|
||||||
},
|
|
||||||
valueField: {
|
|
||||||
name: 'unknown',
|
|
||||||
type: 'number',
|
|
||||||
config: { unit: undefined, color: '#8e8e8e' },
|
|
||||||
values: new ArrayVector([3]),
|
|
||||||
labels: undefined,
|
|
||||||
index: 1,
|
|
||||||
display: expect.anything(),
|
|
||||||
state: expect.anything(),
|
|
||||||
},
|
|
||||||
timeStep: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
visibleRange: undefined,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
done,
|
{
|
||||||
});
|
rowIndex: 2,
|
||||||
|
dataFrame: logs,
|
||||||
|
entry: 'third',
|
||||||
|
entryFieldIndex: 3,
|
||||||
|
hasAnsi: false,
|
||||||
|
labels: {},
|
||||||
|
logLevel: 'unknown',
|
||||||
|
raw: 'third',
|
||||||
|
searchWords: [] as string[],
|
||||||
|
timeEpochMs: 100,
|
||||||
|
timeEpochNs: '100000001',
|
||||||
|
timeFromNow: 'fromNow() jest mocked',
|
||||||
|
timeLocal: 'format() jest mocked',
|
||||||
|
timeUtc: 'format() jest mocked',
|
||||||
|
uid: '2',
|
||||||
|
uniqueLabels: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rowIndex: 1,
|
||||||
|
dataFrame: logs,
|
||||||
|
entry: 'second message',
|
||||||
|
entryFieldIndex: 3,
|
||||||
|
hasAnsi: false,
|
||||||
|
labels: {},
|
||||||
|
logLevel: 'unknown',
|
||||||
|
raw: 'second message',
|
||||||
|
searchWords: [] as string[],
|
||||||
|
timeEpochMs: 100,
|
||||||
|
timeEpochNs: '100000000',
|
||||||
|
timeFromNow: 'fromNow() jest mocked',
|
||||||
|
timeLocal: 'format() jest mocked',
|
||||||
|
timeUtc: 'format() jest mocked',
|
||||||
|
uid: '1',
|
||||||
|
uniqueLabels: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
label: 'unknown',
|
||||||
|
color: '#8e8e8e',
|
||||||
|
data: [[0, 3]],
|
||||||
|
isVisible: true,
|
||||||
|
yAxis: {
|
||||||
|
index: 1,
|
||||||
|
min: 0,
|
||||||
|
tickDecimals: 0,
|
||||||
|
},
|
||||||
|
seriesIndex: 0,
|
||||||
|
timeField: {
|
||||||
|
name: 'Time',
|
||||||
|
type: 'time',
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([0]),
|
||||||
|
index: 0,
|
||||||
|
display: expect.anything(),
|
||||||
|
},
|
||||||
|
valueField: {
|
||||||
|
name: 'unknown',
|
||||||
|
type: 'number',
|
||||||
|
config: { unit: undefined, color: '#8e8e8e' },
|
||||||
|
values: new ArrayVector([3]),
|
||||||
|
labels: undefined,
|
||||||
|
index: 1,
|
||||||
|
display: expect.anything(),
|
||||||
|
state: expect.anything(),
|
||||||
|
},
|
||||||
|
timeStep: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
visibleRange: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used without error but logs frames are empty', () => {
|
it('returns null if passed empty array', () => {
|
||||||
it('then the graphResult should be null', done => {
|
const panelData = createExplorePanelData({ logsFrames: [] });
|
||||||
const panelData = createExplorePanelData({ logsFrames: [] });
|
expect(decorateWithLogsResult()(panelData).logsResult).toBeNull();
|
||||||
const state = ({} as unknown) as ExploreItemState;
|
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
|
||||||
observable: of(panelData).pipe(decorateWithLogsResult(state)),
|
|
||||||
expect: panelData => {
|
|
||||||
expect(panelData.logsResult).toBeNull();
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when used with error', () => {
|
it('returns null if panelData has error', () => {
|
||||||
it('then the graphResult should be null', done => {
|
const { logs } = getTestContext();
|
||||||
const { logs } = getTestContext();
|
const panelData = createExplorePanelData({ error: {}, logsFrames: [logs] });
|
||||||
const panelData = createExplorePanelData({ error: {}, logsFrames: [logs] });
|
expect(decorateWithLogsResult()(panelData).logsResult).toBeNull();
|
||||||
const state = ({} as unknown) as ExploreItemState;
|
|
||||||
|
|
||||||
observableTester().subscribeAndExpectOnNext({
|
|
||||||
observable: of(panelData).pipe(decorateWithLogsResult(state)),
|
|
||||||
expect: panelData => {
|
|
||||||
expect(panelData.logsResult).toBeNull();
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,214 +1,187 @@
|
|||||||
import { MonoTypeOperatorFunction, of, OperatorFunction } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map, mergeMap } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
AbsoluteTimeRange,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataSourceApi,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
getDisplayProcessor,
|
getDisplayProcessor,
|
||||||
PanelData,
|
PanelData,
|
||||||
PreferredVisualisationType,
|
|
||||||
sortLogsResult,
|
sortLogsResult,
|
||||||
standardTransformers,
|
standardTransformers,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
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 { getGraphSeriesModel } from '../../../plugins/panel/graph2/getGraphSeriesModel';
|
||||||
import { dataFrameToLogsModel } from '../../../core/logs_model';
|
import { dataFrameToLogsModel } from '../../../core/logs_model';
|
||||||
import { refreshIntervalToSortOrder } from '../../../core/utils/explore';
|
import { refreshIntervalToSortOrder } from '../../../core/utils/explore';
|
||||||
|
|
||||||
export const decorateWithGraphLogsTraceAndTable = (
|
/**
|
||||||
datasourceInstance?: DataSourceApi | null
|
* When processing response first we try to determine what kind of dataframes we got as one query can return multiple
|
||||||
): OperatorFunction<PanelData, ExplorePanelData> => inputStream =>
|
* dataFrames with different type of data. This is later used for type specific processing. As we use this in
|
||||||
inputStream.pipe(
|
* Observable pipeline, it decorates the existing panelData to pass the results to later processing stages.
|
||||||
map(data => {
|
*/
|
||||||
if (data.error) {
|
export const decorateWithGraphLogsTraceAndTable = (data: PanelData): ExplorePanelData => {
|
||||||
return {
|
if (data.error) {
|
||||||
...data,
|
return {
|
||||||
graphFrames: [],
|
...data,
|
||||||
tableFrames: [],
|
graphFrames: [],
|
||||||
logsFrames: [],
|
tableFrames: [],
|
||||||
traceFrames: [],
|
logsFrames: [],
|
||||||
graphResult: null,
|
traceFrames: [],
|
||||||
tableResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
tableResult: null,
|
||||||
};
|
logsResult: null,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const graphFrames: DataFrame[] = [];
|
const graphFrames: DataFrame[] = [];
|
||||||
const tableFrames: DataFrame[] = [];
|
const tableFrames: DataFrame[] = [];
|
||||||
const logsFrames: DataFrame[] = [];
|
const logsFrames: DataFrame[] = [];
|
||||||
const traceFrames: DataFrame[] = [];
|
const traceFrames: DataFrame[] = [];
|
||||||
|
|
||||||
for (const frame of data.series) {
|
for (const frame of data.series) {
|
||||||
if (shouldShowInVisualisationTypeStrict(frame, 'logs')) {
|
switch (frame.meta?.preferredVisualisationType) {
|
||||||
logsFrames.push(frame);
|
case 'logs':
|
||||||
} else if (shouldShowInVisualisationTypeStrict(frame, 'graph')) {
|
logsFrames.push(frame);
|
||||||
|
break;
|
||||||
|
case 'graph':
|
||||||
|
graphFrames.push(frame);
|
||||||
|
break;
|
||||||
|
case 'trace':
|
||||||
|
traceFrames.push(frame);
|
||||||
|
break;
|
||||||
|
case 'table':
|
||||||
|
tableFrames.push(frame);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (isTimeSeries(frame)) {
|
||||||
graphFrames.push(frame);
|
graphFrames.push(frame);
|
||||||
} else if (shouldShowInVisualisationTypeStrict(frame, 'trace')) {
|
|
||||||
traceFrames.push(frame);
|
|
||||||
} else if (shouldShowInVisualisationTypeStrict(frame, 'table')) {
|
|
||||||
tableFrames.push(frame);
|
tableFrames.push(frame);
|
||||||
} else if (isTimeSeries(frame, datasourceInstance?.meta.id)) {
|
|
||||||
if (shouldShowInVisualisationType(frame, 'graph')) {
|
|
||||||
graphFrames.push(frame);
|
|
||||||
}
|
|
||||||
if (shouldShowInVisualisationType(frame, 'table')) {
|
|
||||||
tableFrames.push(frame);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// We fallback to table if we do not have any better meta info about the dataframe.
|
// We fallback to table if we do not have any better meta info about the dataframe.
|
||||||
tableFrames.push(frame);
|
tableFrames.push(frame);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
graphFrames,
|
|
||||||
tableFrames,
|
|
||||||
logsFrames,
|
|
||||||
traceFrames,
|
|
||||||
graphResult: null,
|
|
||||||
tableResult: null,
|
|
||||||
logsResult: null,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const decorateWithGraphResult = (): MonoTypeOperatorFunction<ExplorePanelData> => inputStream =>
|
|
||||||
inputStream.pipe(
|
|
||||||
map(data => {
|
|
||||||
if (data.error) {
|
|
||||||
return { ...data, graphResult: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const graphResult =
|
|
||||||
data.graphFrames.length === 0
|
|
||||||
? null
|
|
||||||
: getGraphSeriesModel(
|
|
||||||
data.graphFrames,
|
|
||||||
data.request?.timezone ?? 'browser',
|
|
||||||
{},
|
|
||||||
{ showBars: false, showLines: true, showPoints: false },
|
|
||||||
{ asTable: false, isVisible: true, placement: 'under' }
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ...data, graphResult };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const decorateWithTableResult = (): MonoTypeOperatorFunction<ExplorePanelData> => inputStream =>
|
|
||||||
inputStream.pipe(
|
|
||||||
mergeMap(data => {
|
|
||||||
if (data.error) {
|
|
||||||
return of({ ...data, tableResult: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.tableFrames.length === 0) {
|
|
||||||
return of({ ...data, tableResult: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
data.tableFrames.sort((frameA: DataFrame, frameB: DataFrame) => {
|
|
||||||
const frameARefId = frameA.refId!;
|
|
||||||
const frameBRefId = frameB.refId!;
|
|
||||||
|
|
||||||
if (frameARefId > frameBRefId) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (frameARefId < frameBRefId) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasOnlyTimeseries = data.tableFrames.every(df => isTimeSeries(df));
|
|
||||||
|
|
||||||
// If we have only timeseries we do join on default time column which makes more sense. If we are showing
|
|
||||||
// non timeseries or some mix of data we are not trying to join on anything and just try to merge them in
|
|
||||||
// single table, which may not make sense in most cases, but it's up to the user to query something sensible.
|
|
||||||
const transformer = hasOnlyTimeseries
|
|
||||||
? of(data.tableFrames).pipe(standardTransformers.seriesToColumnsTransformer.operator({}))
|
|
||||||
: of(data.tableFrames).pipe(standardTransformers.mergeTransformer.operator({}));
|
|
||||||
|
|
||||||
return transformer.pipe(
|
|
||||||
map(frames => {
|
|
||||||
const frame = frames[0];
|
|
||||||
|
|
||||||
// set display processor
|
|
||||||
for (const field of frame.fields) {
|
|
||||||
field.display =
|
|
||||||
field.display ??
|
|
||||||
getDisplayProcessor({
|
|
||||||
field,
|
|
||||||
theme: config.theme,
|
|
||||||
timeZone: data.request?.timezone ?? 'browser',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...data, tableResult: frame };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const decorateWithLogsResult = (
|
|
||||||
state: ExploreItemState
|
|
||||||
): MonoTypeOperatorFunction<ExplorePanelData> => inputStream =>
|
|
||||||
inputStream.pipe(
|
|
||||||
map(data => {
|
|
||||||
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 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;
|
return {
|
||||||
}
|
...data,
|
||||||
|
graphFrames,
|
||||||
|
tableFrames,
|
||||||
|
logsFrames,
|
||||||
|
traceFrames,
|
||||||
|
graphResult: null,
|
||||||
|
tableResult: null,
|
||||||
|
logsResult: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function shouldShowInVisualisationType(frame: DataFrame, visualisation: PreferredVisualisationType) {
|
export const decorateWithGraphResult = (data: ExplorePanelData): ExplorePanelData => {
|
||||||
if (frame.meta?.preferredVisualisationType && frame.meta?.preferredVisualisationType !== visualisation) {
|
if (data.error) {
|
||||||
return false;
|
return { ...data, graphResult: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
const graphResult =
|
||||||
}
|
data.graphFrames.length === 0
|
||||||
|
? null
|
||||||
|
: getGraphSeriesModel(
|
||||||
|
data.graphFrames,
|
||||||
|
data.request?.timezone ?? 'browser',
|
||||||
|
{},
|
||||||
|
{ showBars: false, showLines: true, showPoints: false },
|
||||||
|
{ asTable: false, isVisible: true, placement: 'under' }
|
||||||
|
);
|
||||||
|
|
||||||
function shouldShowInVisualisationTypeStrict(frame: DataFrame, visualisation: PreferredVisualisationType) {
|
return { ...data, graphResult };
|
||||||
return frame.meta?.preferredVisualisationType === visualisation;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// TEMP: Temporary hack. Remove when logs/metrics unification is done
|
/**
|
||||||
function isTimeSeriesCloudWatch(frame: DataFrame): boolean {
|
* This processing returns Observable because it uses Transformer internally which result type is also Observable.
|
||||||
return (
|
* In this case the transformer should return single result but it is possible that in the future it could return
|
||||||
frame.fields.some(field => field.type === FieldType.time) &&
|
* multiple results and so this should be used with mergeMap or similar to unbox the internal observable.
|
||||||
frame.fields.some(field => field.type === FieldType.number)
|
*/
|
||||||
|
export const decorateWithTableResult = (data: ExplorePanelData): Observable<ExplorePanelData> => {
|
||||||
|
if (data.error) {
|
||||||
|
return of({ ...data, tableResult: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.tableFrames.length === 0) {
|
||||||
|
return of({ ...data, tableResult: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
data.tableFrames.sort((frameA: DataFrame, frameB: DataFrame) => {
|
||||||
|
const frameARefId = frameA.refId!;
|
||||||
|
const frameBRefId = frameB.refId!;
|
||||||
|
|
||||||
|
if (frameARefId > frameBRefId) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (frameARefId < frameBRefId) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasOnlyTimeseries = data.tableFrames.every(df => isTimeSeries(df));
|
||||||
|
|
||||||
|
// If we have only timeseries we do join on default time column which makes more sense. If we are showing
|
||||||
|
// non timeseries or some mix of data we are not trying to join on anything and just try to merge them in
|
||||||
|
// single table, which may not make sense in most cases, but it's up to the user to query something sensible.
|
||||||
|
const transformer = hasOnlyTimeseries
|
||||||
|
? of(data.tableFrames).pipe(standardTransformers.seriesToColumnsTransformer.operator({}))
|
||||||
|
: of(data.tableFrames).pipe(standardTransformers.mergeTransformer.operator({}));
|
||||||
|
|
||||||
|
return transformer.pipe(
|
||||||
|
map(frames => {
|
||||||
|
const frame = frames[0];
|
||||||
|
|
||||||
|
// set display processor
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
field.display =
|
||||||
|
field.display ??
|
||||||
|
getDisplayProcessor({
|
||||||
|
field,
|
||||||
|
theme: config.theme,
|
||||||
|
timeZone: data.request?.timezone ?? 'browser',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...data, tableResult: frame };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decorateWithLogsResult = (
|
||||||
|
options: { absoluteRange?: AbsoluteTimeRange; refreshInterval?: string } = {}
|
||||||
|
) => (data: ExplorePanelData): ExplorePanelData => {
|
||||||
|
if (data.error) {
|
||||||
|
return { ...data, logsResult: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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 };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user