mirror of https://github.com/grafana/grafana
Explore: Support mixed data sources for supplementary query (#63036)
* Consolidate logs volume logic (full range and limited) * Fix showing limited histogram message * Test passing meta data to logs volume provider * Improve readability * Clean up types * Add basic support for multiple log volumes * Move the comment back to the right place * Improve readability * Clean up the logic to support Logs Samples * Update docs * Sort log volumes * Provide title to logs volume panel * Move logs volume cache to the provider factory * Add helper functions * Reuse only if queries are the same * Fix alphabetical sorting * Move caching out of the provider * Support errors and loading state * Remove unused code * Consolidate supplementary query utils * Add tests for supplementaryQueries * Update tests * Simplify logs volume extra info * Update tests * Remove comment * Update tests * Fix hiding the histogram for hidden queries * Simplify loading message * Update tests * Wait for full fallback histogram to load before showing it * Fix a typo * Add feedback comments * Move feedback comments to github * Do not filter out hidden queries as they may be used as references in other queries * Group log volume by refId * Support showing fallback histograms per query to avoid duplicates * Improve type-checking * Fix supplementaryQueries.test.ts * Fix logsModel.test.ts * Fix loading fallback results * Fix unit tests * WIP * Update deprecated styles * Simplify test * Simplify rendering zoom info * Update deprecated styles * Simplify getLogsVolumeDataSourceInfo * Simplify isLogsVolumeLimited() * Simplify rendering zoom infopull/63734/head^2
parent
b2d7bea78b
commit
a7238ba933
@ -0,0 +1,54 @@ |
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react'; |
||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import { DataQueryResponse, LoadingState, EventBusSrv } from '@grafana/data'; |
||||||
|
|
||||||
|
import { LogsVolumePanelList } from './LogsVolumePanelList'; |
||||||
|
|
||||||
|
jest.mock('./Graph/ExploreGraph', () => { |
||||||
|
const ExploreGraph = () => <span>ExploreGraph</span>; |
||||||
|
return { |
||||||
|
ExploreGraph, |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
function renderPanel(logsVolumeData?: DataQueryResponse) { |
||||||
|
render( |
||||||
|
<LogsVolumePanelList |
||||||
|
absoluteRange={{ from: 0, to: 1 }} |
||||||
|
timeZone="timeZone" |
||||||
|
splitOpen={() => {}} |
||||||
|
width={100} |
||||||
|
onUpdateTimeRange={() => {}} |
||||||
|
logsVolumeData={logsVolumeData} |
||||||
|
onLoadLogsVolume={() => {}} |
||||||
|
onHiddenSeriesChanged={() => null} |
||||||
|
eventBus={new EventBusSrv()} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
describe('LogsVolumePanelList', () => { |
||||||
|
it('shows loading message', () => { |
||||||
|
renderPanel({ state: LoadingState.Loading, error: undefined, data: [] }); |
||||||
|
expect(screen.getByText('Loading...')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows short warning message', () => { |
||||||
|
renderPanel({ state: LoadingState.Error, error: { data: { message: 'Test error message' } }, data: [] }); |
||||||
|
expect(screen.getByText('Failed to load log volume for this query')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('Test error message')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows long warning message', () => { |
||||||
|
// we make a long message
|
||||||
|
const messagePart = 'One two three four five six seven eight nine ten.'; |
||||||
|
const message = messagePart + ' ' + messagePart + ' ' + messagePart; |
||||||
|
|
||||||
|
renderPanel({ state: LoadingState.Error, error: { data: { message } }, data: [] }); |
||||||
|
expect(screen.getByText('Failed to load log volume for this query')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText(message)).not.toBeInTheDocument(); |
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show details' })); |
||||||
|
expect(screen.getByText(message)).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,120 @@ |
|||||||
|
import { css } from '@emotion/css'; |
||||||
|
import { groupBy } from 'lodash'; |
||||||
|
import React, { useMemo } from 'react'; |
||||||
|
|
||||||
|
import { |
||||||
|
AbsoluteTimeRange, |
||||||
|
DataFrame, |
||||||
|
DataQueryResponse, |
||||||
|
EventBus, |
||||||
|
GrafanaTheme2, |
||||||
|
isLogsVolumeLimited, |
||||||
|
LoadingState, |
||||||
|
SplitOpen, |
||||||
|
TimeZone, |
||||||
|
} from '@grafana/data'; |
||||||
|
import { Button, InlineField, useStyles2 } from '@grafana/ui'; |
||||||
|
|
||||||
|
import { LogsVolumePanel } from './LogsVolumePanel'; |
||||||
|
import { SupplementaryResultError } from './SupplementaryResultError'; |
||||||
|
|
||||||
|
type Props = { |
||||||
|
logsVolumeData: DataQueryResponse | undefined; |
||||||
|
absoluteRange: AbsoluteTimeRange; |
||||||
|
timeZone: TimeZone; |
||||||
|
splitOpen: SplitOpen; |
||||||
|
width: number; |
||||||
|
onUpdateTimeRange: (timeRange: AbsoluteTimeRange) => void; |
||||||
|
onLoadLogsVolume: () => void; |
||||||
|
onHiddenSeriesChanged: (hiddenSeries: string[]) => void; |
||||||
|
eventBus: EventBus; |
||||||
|
}; |
||||||
|
|
||||||
|
export const LogsVolumePanelList = ({ |
||||||
|
logsVolumeData, |
||||||
|
absoluteRange, |
||||||
|
onUpdateTimeRange, |
||||||
|
width, |
||||||
|
onLoadLogsVolume, |
||||||
|
onHiddenSeriesChanged, |
||||||
|
eventBus, |
||||||
|
splitOpen, |
||||||
|
timeZone, |
||||||
|
}: Props) => { |
||||||
|
const logVolumes = useMemo( |
||||||
|
() => groupBy(logsVolumeData?.data || [], 'meta.custom.sourceQuery.refId'), |
||||||
|
[logsVolumeData] |
||||||
|
); |
||||||
|
|
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
const numberOfLogVolumes = Object.keys(logVolumes).length; |
||||||
|
|
||||||
|
const containsZoomed = Object.values(logVolumes).some((data: DataFrame[]) => { |
||||||
|
const zoomRatio = logsLevelZoomRatio(data, absoluteRange); |
||||||
|
return !isLogsVolumeLimited(data) && zoomRatio && zoomRatio < 1; |
||||||
|
}); |
||||||
|
|
||||||
|
if (logsVolumeData?.state === LoadingState.Loading) { |
||||||
|
return <span>Loading...</span>; |
||||||
|
} |
||||||
|
if (logsVolumeData?.error !== undefined) { |
||||||
|
return <SupplementaryResultError error={logsVolumeData.error} title="Failed to load log volume for this query" />; |
||||||
|
} |
||||||
|
return ( |
||||||
|
<div className={styles.listContainer}> |
||||||
|
{Object.keys(logVolumes).map((name, index) => { |
||||||
|
const logsVolumeData = { data: logVolumes[name] }; |
||||||
|
return ( |
||||||
|
<LogsVolumePanel |
||||||
|
key={index} |
||||||
|
absoluteRange={absoluteRange} |
||||||
|
width={width} |
||||||
|
logsVolumeData={logsVolumeData} |
||||||
|
onUpdateTimeRange={onUpdateTimeRange} |
||||||
|
timeZone={timeZone} |
||||||
|
splitOpen={splitOpen} |
||||||
|
onLoadLogsVolume={onLoadLogsVolume} |
||||||
|
// TODO: Support filtering level from multiple log levels
|
||||||
|
onHiddenSeriesChanged={numberOfLogVolumes > 1 ? () => {} : onHiddenSeriesChanged} |
||||||
|
eventBus={eventBus} |
||||||
|
/> |
||||||
|
); |
||||||
|
})} |
||||||
|
{containsZoomed && ( |
||||||
|
<div className={styles.extraInfoContainer}> |
||||||
|
<InlineField label="Reload log volume" transparent> |
||||||
|
<Button size="xs" icon="sync" variant="secondary" onClick={onLoadLogsVolume} id="reload-volume" /> |
||||||
|
</InlineField> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => { |
||||||
|
return { |
||||||
|
listContainer: css` |
||||||
|
padding-top: 10px; |
||||||
|
`,
|
||||||
|
extraInfoContainer: css` |
||||||
|
display: flex; |
||||||
|
justify-content: end; |
||||||
|
position: absolute; |
||||||
|
right: 5px; |
||||||
|
top: 5px; |
||||||
|
`,
|
||||||
|
oldInfoText: css` |
||||||
|
font-size: ${theme.typography.bodySmall.fontSize}; |
||||||
|
color: ${theme.colors.text.secondary}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
function logsLevelZoomRatio( |
||||||
|
logsVolumeData: DataFrame[] | undefined, |
||||||
|
selectedTimeRange: AbsoluteTimeRange |
||||||
|
): number | undefined { |
||||||
|
const dataRange = logsVolumeData && logsVolumeData[0] && logsVolumeData[0].meta?.custom?.absoluteRange; |
||||||
|
return dataRange ? (selectedTimeRange.from - selectedTimeRange.to) / (dataRange.from - dataRange.to) : undefined; |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import { Observable, of } from 'rxjs'; |
||||||
|
|
||||||
|
import { getDefaultTimeRange, LoadingState, LogsModel } from '@grafana/data'; |
||||||
|
|
||||||
|
import { ExplorePanelData } from '../../../types'; |
||||||
|
|
||||||
|
type MockProps = { |
||||||
|
logsResult?: Partial<LogsModel>; |
||||||
|
}; |
||||||
|
|
||||||
|
export const mockExplorePanelData = (props?: MockProps): Observable<ExplorePanelData> => { |
||||||
|
const data: ExplorePanelData = { |
||||||
|
flameGraphFrames: [], |
||||||
|
graphFrames: [], |
||||||
|
graphResult: [], |
||||||
|
logsFrames: [], |
||||||
|
logsResult: { |
||||||
|
hasUniqueLabels: false, |
||||||
|
rows: [], |
||||||
|
meta: [], |
||||||
|
series: [], |
||||||
|
queries: [], |
||||||
|
...(props?.logsResult || {}), |
||||||
|
}, |
||||||
|
nodeGraphFrames: [], |
||||||
|
rawPrometheusFrames: [], |
||||||
|
rawPrometheusResult: null, |
||||||
|
series: [], |
||||||
|
state: LoadingState.Done, |
||||||
|
tableFrames: [], |
||||||
|
tableResult: [], |
||||||
|
timeRange: getDefaultTimeRange(), |
||||||
|
traceFrames: [], |
||||||
|
}; |
||||||
|
|
||||||
|
return of(data); |
||||||
|
}; |
@ -1,49 +0,0 @@ |
|||||||
import { Observable } from 'rxjs'; |
|
||||||
|
|
||||||
import { |
|
||||||
DataSourceApi, |
|
||||||
SupplementaryQueryType, |
|
||||||
DataQueryResponse, |
|
||||||
hasSupplementaryQuerySupport, |
|
||||||
DataQueryRequest, |
|
||||||
LoadingState, |
|
||||||
LogsVolumeType, |
|
||||||
} from '@grafana/data'; |
|
||||||
|
|
||||||
import { ExplorePanelData } from '../../../types'; |
|
||||||
|
|
||||||
export const getSupplementaryQueryProvider = ( |
|
||||||
datasourceInstance: DataSourceApi, |
|
||||||
type: SupplementaryQueryType, |
|
||||||
request: DataQueryRequest, |
|
||||||
explorePanelData: Observable<ExplorePanelData> |
|
||||||
): Observable<DataQueryResponse> | undefined => { |
|
||||||
if (hasSupplementaryQuerySupport(datasourceInstance, type)) { |
|
||||||
return datasourceInstance.getDataProvider(type, request); |
|
||||||
} else if (type === SupplementaryQueryType.LogsVolume) { |
|
||||||
// Create a fallback to results based logs volume
|
|
||||||
return new Observable<DataQueryResponse>((observer) => { |
|
||||||
explorePanelData.subscribe((exploreData) => { |
|
||||||
if (exploreData.logsResult?.series && exploreData.logsResult?.visibleRange) { |
|
||||||
observer.next({ |
|
||||||
data: exploreData.logsResult.series.map((d) => { |
|
||||||
const custom = d.meta?.custom || {}; |
|
||||||
return { |
|
||||||
...d, |
|
||||||
meta: { |
|
||||||
custom: { |
|
||||||
...custom, |
|
||||||
logsVolumeType: LogsVolumeType.Limited, |
|
||||||
absoluteRange: exploreData.logsResult?.visibleRange, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}; |
|
||||||
}), |
|
||||||
state: LoadingState.Done, |
|
||||||
}); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
return undefined; |
|
||||||
}; |
|
@ -0,0 +1,357 @@ |
|||||||
|
import { flatten } from 'lodash'; |
||||||
|
import { from, Observable } from 'rxjs'; |
||||||
|
|
||||||
|
import { |
||||||
|
DataFrame, |
||||||
|
DataQueryRequest, |
||||||
|
DataQueryResponse, |
||||||
|
DataSourceApi, |
||||||
|
DataSourceWithSupplementaryQueriesSupport, |
||||||
|
FieldType, |
||||||
|
LoadingState, |
||||||
|
LogLevel, |
||||||
|
LogsVolumeType, |
||||||
|
MutableDataFrame, |
||||||
|
SupplementaryQueryType, |
||||||
|
toDataFrame, |
||||||
|
} from '@grafana/data'; |
||||||
|
import { getDataSourceSrv } from '@grafana/runtime'; |
||||||
|
import { DataQuery } from '@grafana/schema'; |
||||||
|
|
||||||
|
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; |
||||||
|
import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query'; |
||||||
|
import { ExplorePanelData } from '../../../types'; |
||||||
|
import { mockExplorePanelData } from '../__mocks__/data'; |
||||||
|
|
||||||
|
import { getSupplementaryQueryProvider } from './supplementaryQueries'; |
||||||
|
|
||||||
|
class MockDataSourceWithSupplementaryQuerySupport |
||||||
|
extends MockDataSourceApi |
||||||
|
implements DataSourceWithSupplementaryQueriesSupport<DataQuery> |
||||||
|
{ |
||||||
|
private supplementaryQueriesResults: Record<SupplementaryQueryType, DataFrame[] | undefined> = { |
||||||
|
[SupplementaryQueryType.LogsVolume]: undefined, |
||||||
|
[SupplementaryQueryType.LogsSample]: undefined, |
||||||
|
}; |
||||||
|
|
||||||
|
withSupplementaryQuerySupport(type: SupplementaryQueryType, data: DataFrame[]) { |
||||||
|
this.supplementaryQueriesResults[type] = data; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
getDataProvider( |
||||||
|
type: SupplementaryQueryType, |
||||||
|
request: DataQueryRequest<DataQuery> |
||||||
|
): Observable<DataQueryResponse> | undefined { |
||||||
|
const data = this.supplementaryQueriesResults[type]; |
||||||
|
if (data) { |
||||||
|
return from([ |
||||||
|
{ state: LoadingState.Loading, data: [] }, |
||||||
|
{ state: LoadingState.Done, data }, |
||||||
|
]); |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
getSupplementaryQuery(type: SupplementaryQueryType, query: DataQuery): DataQuery | undefined { |
||||||
|
return query; |
||||||
|
} |
||||||
|
|
||||||
|
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] { |
||||||
|
return Object.values(SupplementaryQueryType).filter((type) => this.supplementaryQueriesResults[type]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const createSupplementaryQueryResponse = (type: SupplementaryQueryType, id: string) => { |
||||||
|
return [ |
||||||
|
toDataFrame({ |
||||||
|
refId: `1-${type}-${id}`, |
||||||
|
fields: [{ name: 'value', type: FieldType.string, values: [1] }], |
||||||
|
meta: { |
||||||
|
custom: { |
||||||
|
logsVolumeType: LogsVolumeType.FullRange, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}), |
||||||
|
toDataFrame({ |
||||||
|
refId: `2-${type}-${id}`, |
||||||
|
fields: [{ name: 'value', type: FieldType.string, values: [2] }], |
||||||
|
meta: { |
||||||
|
custom: { |
||||||
|
logsVolumeType: LogsVolumeType.FullRange, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}), |
||||||
|
]; |
||||||
|
}; |
||||||
|
|
||||||
|
const mockRow = (refId: string) => { |
||||||
|
return { |
||||||
|
rowIndex: 0, |
||||||
|
entryFieldIndex: 0, |
||||||
|
dataFrame: new MutableDataFrame({ refId, fields: [{ name: 'A', values: [] }] }), |
||||||
|
entry: '', |
||||||
|
hasAnsi: false, |
||||||
|
hasUnescapedContent: false, |
||||||
|
labels: {}, |
||||||
|
logLevel: LogLevel.info, |
||||||
|
raw: '', |
||||||
|
timeEpochMs: 0, |
||||||
|
timeEpochNs: '0', |
||||||
|
timeFromNow: '', |
||||||
|
timeLocal: '', |
||||||
|
timeUtc: '', |
||||||
|
uid: '1', |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
const mockExploreDataWithLogs = () => |
||||||
|
mockExplorePanelData({ |
||||||
|
logsResult: { |
||||||
|
rows: [mockRow('0'), mockRow('1')], |
||||||
|
visibleRange: { from: 0, to: 1 }, |
||||||
|
bucketSize: 1000, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
const datasources: DataSourceApi[] = [ |
||||||
|
new MockDataSourceWithSupplementaryQuerySupport('logs-volume-a').withSupplementaryQuerySupport( |
||||||
|
SupplementaryQueryType.LogsVolume, |
||||||
|
createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-a') |
||||||
|
), |
||||||
|
new MockDataSourceWithSupplementaryQuerySupport('logs-volume-b').withSupplementaryQuerySupport( |
||||||
|
SupplementaryQueryType.LogsVolume, |
||||||
|
createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-b') |
||||||
|
), |
||||||
|
new MockDataSourceWithSupplementaryQuerySupport('logs-sample-a').withSupplementaryQuerySupport( |
||||||
|
SupplementaryQueryType.LogsSample, |
||||||
|
createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-a') |
||||||
|
), |
||||||
|
new MockDataSourceWithSupplementaryQuerySupport('logs-sample-b').withSupplementaryQuerySupport( |
||||||
|
SupplementaryQueryType.LogsSample, |
||||||
|
createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-b') |
||||||
|
), |
||||||
|
new MockDataSourceApi('no-data-providers'), |
||||||
|
new MockDataSourceApi('no-data-providers-2'), |
||||||
|
new MockDataSourceApi('mixed').setupMixed(true), |
||||||
|
]; |
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({ |
||||||
|
...jest.requireActual('@grafana/runtime'), |
||||||
|
getDataSourceSrv: () => { |
||||||
|
return { |
||||||
|
get: async ({ uid }: { uid: string }) => datasources.find((ds) => ds.name === uid) || undefined, |
||||||
|
}; |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const setup = async (rootDataSource: string, type: SupplementaryQueryType, targetSources?: string[]) => { |
||||||
|
const rootDataSourceApiMock = await getDataSourceSrv().get({ uid: rootDataSource }); |
||||||
|
|
||||||
|
targetSources = targetSources || [rootDataSource]; |
||||||
|
|
||||||
|
const requestMock = new MockDataQueryRequest({ |
||||||
|
targets: targetSources.map((source, i) => new MockQuery(`${i}`, 'a', { uid: source })), |
||||||
|
}); |
||||||
|
const explorePanelDataMock: Observable<ExplorePanelData> = mockExploreDataWithLogs(); |
||||||
|
|
||||||
|
return getSupplementaryQueryProvider(rootDataSourceApiMock, type, requestMock, explorePanelDataMock); |
||||||
|
}; |
||||||
|
|
||||||
|
const assertDataFrom = (type: SupplementaryQueryType, ...datasources: string[]) => { |
||||||
|
return flatten( |
||||||
|
datasources.map((name: string) => { |
||||||
|
return [{ refId: `1-${type}-${name}` }, { refId: `2-${type}-${name}` }]; |
||||||
|
}) |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const assertDataFromLogsResults = () => { |
||||||
|
return [{ meta: { custom: { logsVolumeType: LogsVolumeType.Limited } } }]; |
||||||
|
}; |
||||||
|
|
||||||
|
describe('SupplementaryQueries utils', function () { |
||||||
|
describe('Non-mixed data source', function () { |
||||||
|
it('Returns result from the provider', async () => { |
||||||
|
const testProvider = await setup('logs-volume-a', SupplementaryQueryType.LogsVolume); |
||||||
|
|
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ data: [], state: LoadingState.Loading }, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
it('Uses fallback for logs volume', async () => { |
||||||
|
const testProvider = await setup('no-data-providers', SupplementaryQueryType.LogsVolume); |
||||||
|
|
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ |
||||||
|
data: assertDataFromLogsResults(), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
it('Does not use a fallback for logs sample', async () => { |
||||||
|
const testProvider = await setup('no-data-providers', SupplementaryQueryType.LogsSample); |
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ |
||||||
|
state: LoadingState.NotStarted, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Mixed data source', function () { |
||||||
|
describe('Logs volume', function () { |
||||||
|
describe('All data sources support full range logs volume', function () { |
||||||
|
it('Merges all data frames into a single response', async () => { |
||||||
|
const testProvider = await setup('mixed', SupplementaryQueryType.LogsVolume, [ |
||||||
|
'logs-volume-a', |
||||||
|
'logs-volume-b', |
||||||
|
]); |
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ data: [], state: LoadingState.Loading }, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a', 'logs-volume-b'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('All data sources do not support full range logs volume', function () { |
||||||
|
it('Creates single fallback result', async () => { |
||||||
|
const testProvider = await setup('mixed', SupplementaryQueryType.LogsVolume, [ |
||||||
|
'no-data-providers', |
||||||
|
'no-data-providers-2', |
||||||
|
]); |
||||||
|
|
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ |
||||||
|
data: assertDataFromLogsResults(), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: [...assertDataFromLogsResults(), ...assertDataFromLogsResults()], |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Some data sources support full range logs volume, while others do not', function () { |
||||||
|
it('Creates merged result containing full range and limited logs volume', async () => { |
||||||
|
const testProvider = await setup('mixed', SupplementaryQueryType.LogsVolume, [ |
||||||
|
'logs-volume-a', |
||||||
|
'no-data-providers', |
||||||
|
'logs-volume-b', |
||||||
|
'no-data-providers-2', |
||||||
|
]); |
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ |
||||||
|
data: [], |
||||||
|
state: LoadingState.Loading, |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: [ |
||||||
|
...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), |
||||||
|
...assertDataFromLogsResults(), |
||||||
|
], |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: [ |
||||||
|
...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), |
||||||
|
...assertDataFromLogsResults(), |
||||||
|
...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-b'), |
||||||
|
], |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Logs sample', function () { |
||||||
|
describe('All data sources support logs sample', function () { |
||||||
|
it('Merges all responses into single result', async () => { |
||||||
|
const testProvider = await setup('mixed', SupplementaryQueryType.LogsSample, [ |
||||||
|
'logs-sample-a', |
||||||
|
'logs-sample-b', |
||||||
|
]); |
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ data: [], state: LoadingState.Loading }, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('All data sources do not support full range logs volume', function () { |
||||||
|
it('Does not provide fallback result', async () => { |
||||||
|
const testProvider = await setup('mixed', SupplementaryQueryType.LogsSample, [ |
||||||
|
'no-data-providers', |
||||||
|
'no-data-providers-2', |
||||||
|
]); |
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([{ state: LoadingState.NotStarted, data: [] }]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Some data sources support full range logs volume, while others do not', function () { |
||||||
|
it('Returns results only for data sources supporting logs sample', async () => { |
||||||
|
const testProvider = await setup('mixed', SupplementaryQueryType.LogsSample, [ |
||||||
|
'logs-sample-a', |
||||||
|
'no-data-providers', |
||||||
|
'logs-sample-b', |
||||||
|
'no-data-providers-2', |
||||||
|
]); |
||||||
|
await expect(testProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ data: [], state: LoadingState.Loading }, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'), |
||||||
|
state: LoadingState.Done, |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,30 @@ |
|||||||
|
import { CoreApp, DataQueryRequest, getDefaultTimeRange } from '@grafana/data'; |
||||||
|
import { DataQuery, DataSourceRef } from '@grafana/schema'; |
||||||
|
|
||||||
|
export class MockQuery implements DataQuery { |
||||||
|
refId: string; |
||||||
|
testQuery: string; |
||||||
|
datasource?: DataSourceRef; |
||||||
|
|
||||||
|
constructor(refId = 'A', testQuery = '', datasourceRef?: DataSourceRef) { |
||||||
|
this.refId = refId; |
||||||
|
this.testQuery = testQuery; |
||||||
|
this.datasource = datasourceRef; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class MockDataQueryRequest implements DataQueryRequest<MockQuery> { |
||||||
|
app = CoreApp.Unknown; |
||||||
|
interval = ''; |
||||||
|
intervalMs = 0; |
||||||
|
range = getDefaultTimeRange(); |
||||||
|
requestId = '1'; |
||||||
|
scopedVars = {}; |
||||||
|
startTime = 0; |
||||||
|
targets: MockQuery[]; |
||||||
|
timezone = 'utc'; |
||||||
|
|
||||||
|
constructor({ targets }: { targets: MockQuery[] }) { |
||||||
|
this.targets = targets || []; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue