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