mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: Allow DashboardDS subqueries in MixedDS (#97116)
* Allow dashboardDS to run in mixedDS * Make mixedDS panel wait for dashboardDS panel to load first * cleanup * cleanup * refresh dashboardDS queries within mixedDS when source panel changes * more tests * fix * fixes scenario where source returns an error * do not allow dashboardDS references to mixedDS targets that contain other dashboardDS panels * test * lint * Show invalid panels as invalid and with a message * refactor * avoid bunching shared dashboard queries * skip instead of debouncing to avoid stale data * debouce dashboard ds result stream when coming from mixed ds * restore unnecessarily touched files * fix import * increase debounce interval value to account for slower machines --------- Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com>
This commit is contained in:
parent
e028924fc9
commit
d8d84a000a
@ -13,6 +13,7 @@ import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
|||||||
import { setPluginImportUtils } from '@grafana/runtime';
|
import { setPluginImportUtils } from '@grafana/runtime';
|
||||||
import { SceneDataTransformer, SceneFlexLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
import { SceneDataTransformer, SceneFlexLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||||
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';
|
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';
|
||||||
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
|
|
||||||
import { activateFullSceneTree } from '../utils/test-utils';
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
|
|
||||||
@ -46,6 +47,18 @@ const dashboardDs: DataSourceApi = {
|
|||||||
},
|
},
|
||||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||||
|
|
||||||
|
const mixedDs: DataSourceApi = {
|
||||||
|
meta: {
|
||||||
|
id: 'mixed',
|
||||||
|
},
|
||||||
|
name: MIXED_DATASOURCE_NAME,
|
||||||
|
type: MIXED_DATASOURCE_NAME,
|
||||||
|
uid: MIXED_DATASOURCE_NAME,
|
||||||
|
getRef: () => {
|
||||||
|
return { type: MIXED_DATASOURCE_NAME, uid: MIXED_DATASOURCE_NAME };
|
||||||
|
},
|
||||||
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||||
|
|
||||||
setPluginImportUtils({
|
setPluginImportUtils({
|
||||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||||
getPanelPluginFromCache: (id: string) => undefined,
|
getPanelPluginFromCache: (id: string) => undefined,
|
||||||
@ -85,6 +98,10 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
return dashboardDs;
|
return dashboardDs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ref.uid === MIXED_DATASOURCE_NAME) {
|
||||||
|
return mixedDs;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
|
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
|
||||||
@ -449,6 +466,7 @@ describe('DashboardDatasourceBehaviour', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should wait for library panel to load before running queries', async () => {
|
it('should wait for library panel to load before running queries', async () => {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation();
|
||||||
const libPanelBehavior = new LibraryPanelBehavior({
|
const libPanelBehavior = new LibraryPanelBehavior({
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
title: 'Panel title',
|
title: 'Panel title',
|
||||||
@ -510,6 +528,74 @@ describe('DashboardDatasourceBehaviour', () => {
|
|||||||
expect(spyRunQueries).toHaveBeenCalledTimes(1);
|
expect(spyRunQueries).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DashboardDS within MixedDS', () => {
|
||||||
|
it('Should re-run query of MixedDS panel that contains a dashboardDS when source query re-runs', async () => {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation();
|
||||||
|
const sourcePanel = new VizPanel({
|
||||||
|
title: 'Panel A',
|
||||||
|
pluginId: 'table',
|
||||||
|
key: 'panel-1',
|
||||||
|
$data: new SceneDataTransformer({
|
||||||
|
transformations: [],
|
||||||
|
$data: new SceneQueryRunner({
|
||||||
|
datasource: { uid: 'grafana' },
|
||||||
|
queries: [{ refId: 'A', queryType: 'randomWalk' }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dashboardDSPanel = new VizPanel({
|
||||||
|
title: 'Panel B',
|
||||||
|
pluginId: 'table',
|
||||||
|
key: 'panel-2',
|
||||||
|
$data: new SceneDataTransformer({
|
||||||
|
transformations: [],
|
||||||
|
$data: new SceneQueryRunner({
|
||||||
|
datasource: { uid: MIXED_DATASOURCE_NAME },
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
datasource: { uid: SHARED_DASHBOARD_QUERY },
|
||||||
|
refId: 'B',
|
||||||
|
panelId: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
$behaviors: [new DashboardDatasourceBehaviour({})],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const scene = new DashboardScene({
|
||||||
|
title: 'hello',
|
||||||
|
uid: 'dash-1',
|
||||||
|
meta: {
|
||||||
|
canEdit: true,
|
||||||
|
},
|
||||||
|
body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sceneDeactivate = activateFullSceneTree(scene);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
|
||||||
|
// spy on runQueries that will be called by the behaviour
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries')
|
||||||
|
.mockImplementation();
|
||||||
|
|
||||||
|
// deactivate scene to mimic going into panel edit
|
||||||
|
sceneDeactivate();
|
||||||
|
// run source panel queries and update request ID
|
||||||
|
(sourcePanel.state.$data!.state.$data as SceneQueryRunner).runQueries();
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
|
||||||
|
// activate scene to mimic coming back from panel edit
|
||||||
|
activateFullSceneTree(scene);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function buildTestScene() {
|
async function buildTestScene() {
|
||||||
|
@ -2,6 +2,7 @@ import { Unsubscribable } from 'rxjs';
|
|||||||
|
|
||||||
import { SceneObjectBase, SceneObjectState, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
import { SceneObjectBase, SceneObjectState, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
|
||||||
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findVizPanelByKey,
|
findVizPanelByKey,
|
||||||
@ -25,24 +26,24 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _activationHandler() {
|
private _activationHandler() {
|
||||||
const dashboardDsQueryRunner = this.parent;
|
const queryRunner = this.parent;
|
||||||
let libraryPanelSub: Unsubscribable;
|
let libraryPanelSub: Unsubscribable;
|
||||||
let dashboard: DashboardScene;
|
let dashboard: DashboardScene;
|
||||||
if (!(dashboardDsQueryRunner instanceof SceneQueryRunner)) {
|
if (!(queryRunner instanceof SceneQueryRunner)) {
|
||||||
throw new Error('DashboardDatasourceBehaviour must be attached to a SceneQueryRunner');
|
throw new Error('DashboardDatasourceBehaviour must be attached to a SceneQueryRunner');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dashboardDsQueryRunner.state.datasource?.uid !== SHARED_DASHBOARD_QUERY) {
|
if (!this.containsDashboardDSQueries(queryRunner)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dashboard = getDashboardSceneFor(dashboardDsQueryRunner);
|
dashboard = getDashboardSceneFor(queryRunner);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboardQuery = dashboardDsQueryRunner.state.queries.find((query) => query.panelId !== undefined);
|
const dashboardQuery = queryRunner.state.queries.find((query) => query.panelId !== undefined);
|
||||||
|
|
||||||
if (!dashboardQuery) {
|
if (!dashboardQuery) {
|
||||||
return;
|
return;
|
||||||
@ -61,7 +62,7 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
|
|||||||
const libraryPanelBehaviour = getLibraryPanelBehavior(sourcePanel);
|
const libraryPanelBehaviour = getLibraryPanelBehavior(sourcePanel);
|
||||||
if (libraryPanelBehaviour && !libraryPanelBehaviour.state.isLoaded) {
|
if (libraryPanelBehaviour && !libraryPanelBehaviour.state.isLoaded) {
|
||||||
libraryPanelSub = libraryPanelBehaviour.subscribeToState((newLibPanel) => {
|
libraryPanelSub = libraryPanelBehaviour.subscribeToState((newLibPanel) => {
|
||||||
this.handleLibPanelStateUpdates(newLibPanel, dashboardDsQueryRunner, sourcePanel);
|
this.handleLibPanelStateUpdates(newLibPanel, queryRunner, sourcePanel);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -73,7 +74,7 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.prevRequestId && this.prevRequestId !== sourcePanelQueryRunner.state.data?.request?.requestId) {
|
if (this.prevRequestId && this.prevRequestId !== sourcePanelQueryRunner.state.data?.request?.requestId) {
|
||||||
dashboardDsQueryRunner.runQueries();
|
queryRunner.runQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -84,6 +85,21 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private containsDashboardDSQueries(queryRunner: SceneQueryRunner): boolean {
|
||||||
|
if (queryRunner.state.datasource?.uid === SHARED_DASHBOARD_QUERY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
queryRunner.state.datasource?.uid === MIXED_DATASOURCE_NAME &&
|
||||||
|
queryRunner.state.queries.some((query) => query.datasource?.uid === SHARED_DASHBOARD_QUERY)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private handleLibPanelStateUpdates(
|
private handleLibPanelStateUpdates(
|
||||||
newLibPanel: LibraryPanelBehaviorState,
|
newLibPanel: LibraryPanelBehaviorState,
|
||||||
dashboardDsQueryRunner: SceneQueryRunner,
|
dashboardDsQueryRunner: SceneQueryRunner,
|
||||||
|
@ -139,7 +139,13 @@ const renderDataSource = <TQuery extends DataQuery>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.itemWrapper}>
|
<div className={styles.itemWrapper}>
|
||||||
<DataSourcePicker variables={true} alerting={alerting} current={dataSource.name} onChange={onChangeDataSource} />
|
<DataSourcePicker
|
||||||
|
dashboard={true}
|
||||||
|
variables={true}
|
||||||
|
alerting={alerting}
|
||||||
|
current={dataSource.name}
|
||||||
|
onChange={onChangeDataSource}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,8 +11,9 @@ import {
|
|||||||
createDashboardModelFixture,
|
createDashboardModelFixture,
|
||||||
createPanelSaveModel,
|
createPanelSaveModel,
|
||||||
} from '../../../features/dashboard/state/__fixtures__/dashboardFixtures';
|
} from '../../../features/dashboard/state/__fixtures__/dashboardFixtures';
|
||||||
|
import { MIXED_DATASOURCE_NAME } from '../mixed/MixedDataSource';
|
||||||
|
|
||||||
import { DashboardQueryEditor } from './DashboardQueryEditor';
|
import { DashboardQueryEditor, INVALID_PANEL_DESCRIPTION } from './DashboardQueryEditor';
|
||||||
import { SHARED_DASHBOARD_QUERY } from './constants';
|
import { SHARED_DASHBOARD_QUERY } from './constants';
|
||||||
import { DashboardDatasource } from './datasource';
|
import { DashboardDatasource } from './datasource';
|
||||||
|
|
||||||
@ -61,6 +62,21 @@ describe('DashboardQueryEditor', () => {
|
|||||||
type: 'timeseries',
|
type: 'timeseries',
|
||||||
title: 'Another panel',
|
title: 'Another panel',
|
||||||
}),
|
}),
|
||||||
|
createPanelSaveModel({
|
||||||
|
datasource: {
|
||||||
|
uid: MIXED_DATASOURCE_NAME,
|
||||||
|
},
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
datasource: {
|
||||||
|
uid: SHARED_DASHBOARD_QUERY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 3,
|
||||||
|
type: 'timeseries',
|
||||||
|
title: 'A mixed DS with dashboard DS query panel',
|
||||||
|
}),
|
||||||
createPanelSaveModel({
|
createPanelSaveModel({
|
||||||
datasource: {
|
datasource: {
|
||||||
uid: SHARED_DASHBOARD_QUERY,
|
uid: SHARED_DASHBOARD_QUERY,
|
||||||
@ -95,7 +111,37 @@ describe('DashboardQueryEditor', () => {
|
|||||||
const anotherPanel = await screen.findByText('Another panel');
|
const anotherPanel = await screen.findByText('Another panel');
|
||||||
expect(anotherPanel).toBeInTheDocument();
|
expect(anotherPanel).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByText('A dashboard query panel')).not.toBeInTheDocument();
|
expect(screen.queryByText('A dashboard query panel')?.nextElementSibling).toHaveTextContent(
|
||||||
|
INVALID_PANEL_DESCRIPTION
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show a panel with either SHARED_DASHBOARD_QUERY datasource or MixedDS with SHARED_DASHBOARD_QUERY as an option in the dropdown', async () => {
|
||||||
|
render(
|
||||||
|
<DashboardQueryEditor
|
||||||
|
datasource={{} as DashboardDatasource}
|
||||||
|
query={mockQueries[0]}
|
||||||
|
data={mockPanelData}
|
||||||
|
onChange={mockOnChange}
|
||||||
|
onRunQuery={mockOnRunQueries}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const select = screen.getByText('Choose panel');
|
||||||
|
|
||||||
|
await userEvent.click(select);
|
||||||
|
|
||||||
|
const myFirstPanel = await screen.findByText('My first panel');
|
||||||
|
expect(myFirstPanel).toBeInTheDocument();
|
||||||
|
|
||||||
|
const anotherPanel = await screen.findByText('Another panel');
|
||||||
|
expect(anotherPanel).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByText('A dashboard query panel')?.nextElementSibling).toHaveTextContent(
|
||||||
|
INVALID_PANEL_DESCRIPTION
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('A mixed DS with dashboard DS query panel')?.nextElementSibling).toHaveTextContent(
|
||||||
|
INVALID_PANEL_DESCRIPTION
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show the current panelInEdit as an option in the dropdown', async () => {
|
it('does not show the current panelInEdit as an option in the dropdown', async () => {
|
||||||
@ -118,6 +164,8 @@ describe('DashboardQueryEditor', () => {
|
|||||||
const anotherPanel = await screen.findByText('Another panel');
|
const anotherPanel = await screen.findByText('Another panel');
|
||||||
expect(anotherPanel).toBeInTheDocument();
|
expect(anotherPanel).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByText('A dashboard query panel')).not.toBeInTheDocument();
|
expect(screen.queryByText('A dashboard query panel')?.nextElementSibling).toHaveTextContent(
|
||||||
|
INVALID_PANEL_DESCRIPTION
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,8 @@ import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScen
|
|||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { filterPanelDataToQuery } from 'app/features/query/components/QueryEditorRow';
|
import { filterPanelDataToQuery } from 'app/features/query/components/QueryEditorRow';
|
||||||
|
|
||||||
|
import { MIXED_DATASOURCE_NAME } from '../mixed/MixedDataSource';
|
||||||
|
|
||||||
import { SHARED_DASHBOARD_QUERY } from './constants';
|
import { SHARED_DASHBOARD_QUERY } from './constants';
|
||||||
import { DashboardDatasource } from './datasource';
|
import { DashboardDatasource } from './datasource';
|
||||||
import { DashboardQuery, ResultInfo } from './types';
|
import { DashboardQuery, ResultInfo } from './types';
|
||||||
@ -39,6 +41,8 @@ const topics = [
|
|||||||
{ label: 'Annotations', value: true, description: 'Include annotations as regular data' },
|
{ label: 'Annotations', value: true, description: 'Include annotations as regular data' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const INVALID_PANEL_DESCRIPTION = 'Contains a shared dashboard query';
|
||||||
|
|
||||||
export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Props) {
|
export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Props) {
|
||||||
const { value: defaultDatasource } = useAsync(() => getDatasourceSrv().get());
|
const { value: defaultDatasource } = useAsync(() => getDatasourceSrv().get());
|
||||||
|
|
||||||
@ -104,6 +108,13 @@ export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Prop
|
|||||||
[query, onUpdateQuery]
|
[query, onUpdateQuery]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isMixedDSWithDashboardQueries = (panel: PanelModel) => {
|
||||||
|
return (
|
||||||
|
panel.datasource?.uid === MIXED_DATASOURCE_NAME &&
|
||||||
|
panel.targets.some((t) => t.datasource?.uid === SHARED_DASHBOARD_QUERY)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getPanelDescription = useCallback(
|
const getPanelDescription = useCallback(
|
||||||
(panel: PanelModel): string => {
|
(panel: PanelModel): string => {
|
||||||
const datasource = panel.datasource ?? defaultDatasource;
|
const datasource = panel.datasource ?? defaultDatasource;
|
||||||
@ -120,18 +131,24 @@ export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Prop
|
|||||||
() =>
|
() =>
|
||||||
dashboard?.panels
|
dashboard?.panels
|
||||||
.filter(
|
.filter(
|
||||||
(panel) =>
|
(panel) => config.panels[panel.type] && panel.targets && !isPanelInEdit(panel.id, dashboard.panelInEdit?.id)
|
||||||
config.panels[panel.type] &&
|
|
||||||
panel.targets &&
|
|
||||||
!isPanelInEdit(panel.id, dashboard.panelInEdit?.id) &&
|
|
||||||
panel.datasource?.uid !== SHARED_DASHBOARD_QUERY
|
|
||||||
)
|
)
|
||||||
.map((panel) => ({
|
.map((panel) => {
|
||||||
value: panel.id,
|
let description = getPanelDescription(panel);
|
||||||
label: panel.title ?? 'Panel ' + panel.id,
|
let isDisabled = false;
|
||||||
description: getPanelDescription(panel),
|
if (panel.datasource?.uid === SHARED_DASHBOARD_QUERY || isMixedDSWithDashboardQueries(panel)) {
|
||||||
imgUrl: config.panels[panel.type].info.logos.small,
|
description = INVALID_PANEL_DESCRIPTION;
|
||||||
})) ?? [],
|
isDisabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: panel.id,
|
||||||
|
label: panel.title ?? 'Panel ' + panel.id,
|
||||||
|
imgUrl: config.panels[panel.type].info.logos.small,
|
||||||
|
description,
|
||||||
|
isDisabled,
|
||||||
|
};
|
||||||
|
}) ?? [],
|
||||||
[dashboard, getPanelDescription]
|
[dashboard, getPanelDescription]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { first } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
arrayToDataFrame,
|
arrayToDataFrame,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
@ -19,9 +21,19 @@ import {
|
|||||||
import { getVizPanelKeyForPanelId } from 'app/features/dashboard-scene/utils/utils';
|
import { getVizPanelKeyForPanelId } from 'app/features/dashboard-scene/utils/utils';
|
||||||
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
||||||
|
|
||||||
|
import { MIXED_REQUEST_PREFIX } from '../mixed/MixedDataSource';
|
||||||
|
|
||||||
import { DashboardDatasource } from './datasource';
|
import { DashboardDatasource } from './datasource';
|
||||||
import { DashboardQuery } from './types';
|
import { DashboardQuery } from './types';
|
||||||
|
|
||||||
|
jest.mock('rxjs', () => {
|
||||||
|
const original = jest.requireActual('rxjs');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
first: jest.fn(original.first),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
standardTransformersRegistry.setInit(getStandardTransformers);
|
standardTransformersRegistry.setInit(getStandardTransformers);
|
||||||
setPluginImportUtils({
|
setPluginImportUtils({
|
||||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||||
@ -29,6 +41,10 @@ setPluginImportUtils({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DashboardDatasource', () => {
|
describe('DashboardDatasource', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("should look up the other panel and subscribe to it's data", async () => {
|
it("should look up the other panel and subscribe to it's data", async () => {
|
||||||
const { observable } = setup({ refId: 'A', panelId: 1 });
|
const { observable } = setup({ refId: 'A', panelId: 1 });
|
||||||
|
|
||||||
@ -70,9 +86,25 @@ describe('DashboardDatasource', () => {
|
|||||||
|
|
||||||
expect(sourceData.isActive).toBe(false);
|
expect(sourceData.isActive).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should emit only the first value and complete if used within MixedDS', async () => {
|
||||||
|
const { observable } = setup({ refId: 'A', panelId: 1 }, `${MIXED_REQUEST_PREFIX}1`);
|
||||||
|
|
||||||
|
observable.subscribe({ next: () => {} });
|
||||||
|
|
||||||
|
expect(first).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not get the first emission if requestId does not contain the MixedDS prefix', async () => {
|
||||||
|
const { observable } = setup({ refId: 'A', panelId: 1 });
|
||||||
|
|
||||||
|
observable.subscribe({ next: () => {} });
|
||||||
|
|
||||||
|
expect(first).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setup(query: DashboardQuery) {
|
function setup(query: DashboardQuery, requestId?: string) {
|
||||||
const sourceData = new SceneDataTransformer({
|
const sourceData = new SceneDataTransformer({
|
||||||
$data: new SceneDataNode({
|
$data: new SceneDataNode({
|
||||||
data: {
|
data: {
|
||||||
@ -101,7 +133,7 @@ function setup(query: DashboardQuery) {
|
|||||||
const observable = ds.query({
|
const observable = ds.query({
|
||||||
timezone: 'utc',
|
timezone: 'utc',
|
||||||
targets: [query],
|
targets: [query],
|
||||||
requestId: '',
|
requestId: requestId ?? '',
|
||||||
interval: '',
|
interval: '',
|
||||||
intervalMs: 0,
|
intervalMs: 0,
|
||||||
range: getDefaultTimeRange(),
|
range: getDefaultTimeRange(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Observable, defer, finalize, map, of } from 'rxjs';
|
import { Observable, debounce, defer, finalize, first, interval, map, of } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
DataTopic,
|
DataTopic,
|
||||||
PanelData,
|
PanelData,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
|
LoadingState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { SceneDataProvider, SceneDataTransformer, SceneObject } from '@grafana/scenes';
|
import { SceneDataProvider, SceneDataTransformer, SceneObject } from '@grafana/scenes';
|
||||||
import {
|
import {
|
||||||
@ -18,6 +19,8 @@ import {
|
|||||||
getVizPanelKeyForPanelId,
|
getVizPanelKeyForPanelId,
|
||||||
} from 'app/features/dashboard-scene/utils/utils';
|
} from 'app/features/dashboard-scene/utils/utils';
|
||||||
|
|
||||||
|
import { MIXED_REQUEST_PREFIX } from '../mixed/MixedDataSource';
|
||||||
|
|
||||||
import { DashboardQuery } from './types';
|
import { DashboardQuery } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,10 +39,6 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
|||||||
const sceneScopedVar: ScopedVar | undefined = options.scopedVars?.__sceneObject;
|
const sceneScopedVar: ScopedVar | undefined = options.scopedVars?.__sceneObject;
|
||||||
let scene: SceneObject | undefined = sceneScopedVar ? (sceneScopedVar.value.valueOf() as SceneObject) : undefined;
|
let scene: SceneObject | undefined = sceneScopedVar ? (sceneScopedVar.value.valueOf() as SceneObject) : undefined;
|
||||||
|
|
||||||
if (options.requestId.indexOf('mixed') > -1) {
|
|
||||||
throw new Error('Dashboard data source cannot be used with Mixed data source.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
throw new Error('Can only be called from a scene');
|
throw new Error('Can only be called from a scene');
|
||||||
}
|
}
|
||||||
@ -88,6 +87,7 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
|||||||
key: 'source-ds-provider',
|
key: 'source-ds-provider',
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
this.emitFirstLoadedDataIfMixedDS(options.requestId),
|
||||||
finalize(() => cleanUp?.())
|
finalize(() => cleanUp?.())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -112,6 +112,45 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
|||||||
return findVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId));
|
return findVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitFirstLoadedDataIfMixedDS(
|
||||||
|
requestId: string
|
||||||
|
): (source: Observable<DataQueryResponse>) => Observable<DataQueryResponse> {
|
||||||
|
return (source: Observable<DataQueryResponse>) => {
|
||||||
|
if (requestId.includes(MIXED_REQUEST_PREFIX)) {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
return source.pipe(
|
||||||
|
/*
|
||||||
|
* We can have the following piped values scenarios:
|
||||||
|
* Loading -> Done - initial load
|
||||||
|
* Done -> Loading -> Done - refresh
|
||||||
|
* Done - adding another query in editor
|
||||||
|
*
|
||||||
|
* When we see Done as a first element this is because of ReplaySubject in SceneQueryRunner
|
||||||
|
*
|
||||||
|
* we use first(...) below to emit correct result which is last value with Done/Error states
|
||||||
|
*
|
||||||
|
* to avoid emitting first Done/Error (due to ReplaySubject) we selectively debounce only first value with such states
|
||||||
|
*/
|
||||||
|
debounce((val) => {
|
||||||
|
if ([LoadingState.Done, LoadingState.Error].includes(val.state!) && count === 0) {
|
||||||
|
count++;
|
||||||
|
// in the refresh scenario we need to debounce first Done/Error until Loading arrives
|
||||||
|
// 400ms here is a magic number that was sufficient enough with the 20x cpu throttle
|
||||||
|
// this still might affect slower machines but the issue affects only panel view/edit modes
|
||||||
|
return interval(400);
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
return interval(0);
|
||||||
|
}),
|
||||||
|
first((val) => val.state === LoadingState.Done || val.state === LoadingState.Error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return source;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
testDatasource(): Promise<TestDataSourceResponse> {
|
testDatasource(): Promise<TestDataSourceResponse> {
|
||||||
return Promise.resolve({ message: '', status: '' });
|
return Promise.resolve({ message: '', status: '' });
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,13 @@ import {
|
|||||||
import { getDataSourceSrv, getTemplateSrv, toDataQueryError } from '@grafana/runtime';
|
import { getDataSourceSrv, getTemplateSrv, toDataQueryError } from '@grafana/runtime';
|
||||||
import { CustomFormatterVariable } from '@grafana/scenes';
|
import { CustomFormatterVariable } from '@grafana/scenes';
|
||||||
|
|
||||||
export const MIXED_DATASOURCE_NAME = '-- Mixed --';
|
import { SHARED_DASHBOARD_QUERY } from '../dashboard/constants';
|
||||||
|
|
||||||
export const mixedRequestId = (queryIdx: number, requestId?: string) => `mixed-${queryIdx}-${requestId || ''}`;
|
export const MIXED_DATASOURCE_NAME = '-- Mixed --';
|
||||||
|
export const MIXED_REQUEST_PREFIX = 'mixed-';
|
||||||
|
|
||||||
|
export const mixedRequestId = (queryIdx: number, requestId?: string) =>
|
||||||
|
`${MIXED_REQUEST_PREFIX}${queryIdx}-${requestId || ''}`;
|
||||||
|
|
||||||
export interface BatchedQueries {
|
export interface BatchedQueries {
|
||||||
datasource: Promise<DataSourceApi>;
|
datasource: Promise<DataSourceApi>;
|
||||||
@ -45,7 +49,15 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
|
|||||||
const batches: BatchedQueries[] = [];
|
const batches: BatchedQueries[] = [];
|
||||||
|
|
||||||
for (const key in sets) {
|
for (const key in sets) {
|
||||||
batches.push(...this.getBatchesForQueries(sets[key], request));
|
// dashboard ds expects to have only 1 query with const query = options.targets[0]; therefore
|
||||||
|
// we should not batch them together
|
||||||
|
if (key === SHARED_DASHBOARD_QUERY) {
|
||||||
|
sets[key].forEach((a) => {
|
||||||
|
batches.push(...this.getBatchesForQueries([a], request));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
batches.push(...this.getBatchesForQueries(sets[key], request));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Missing UIDs?
|
// Missing UIDs?
|
||||||
|
Loading…
Reference in New Issue
Block a user