DashboardQuery: Expand query options (#53998)

This commit is contained in:
Ryan McKinley 2022-09-16 10:28:47 -07:00 committed by GitHub
parent 4dc0d49025
commit 17b2fb04e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 755 additions and 106 deletions

View File

@ -6062,9 +6062,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/dashboard/runSharedRequest.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

View File

@ -0,0 +1,536 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 1394,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 3,
"w": 24,
"x": 0,
"y": 0
},
"id": 9,
"options": {
"content": "Dashboard queries allow re-using the same results from one panel in another panel context.\n\nThis dashboard shows a single panel that makes a real query and applies transformations. The other panels, all use the same results rather than make their own query requests.",
"mode": "markdown"
},
"pluginVersion": "9.2.0-pre",
"type": "text"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 18,
"w": 7,
"x": 0,
"y": 3
},
"id": 2,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_file"
},
{
"csvFileName": "population_by_state.csv",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "B",
"scenarioId": "csv_file"
}
],
"title": "Raw data -- with outer join",
"transformations": [
{
"id": "seriesToColumns",
"options": {
"byField": "State",
"mode": "outer"
}
},
{
"id": "organize",
"options": {
"excludeByName": {
"1980 population_by_state.csv": true,
"2000 population_by_state.csv": true,
"DestLocation flight_info_by_state.csv": true,
"Lat flight_info_by_state.csv": true,
"Lng flight_info_by_state.csv": true,
"Price flight_info_by_state.csv": true
},
"indexByName": {},
"renameByName": {
"2020 population_by_state.csv": "2020 population",
"Count flight_info_by_state.csv": "Flight count",
"Price flight_info_by_state.csv": ""
}
}
}
],
"type": "table"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 11,
"x": 7,
"y": 3
},
"id": 4,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"title": "Reused data (without transform)",
"type": "table"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 6,
"x": 18,
"y": 3
},
"id": 5,
"options": {
"basemap": {
"config": {},
"name": "Layer 0",
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"showLegend": true,
"style": {
"color": {
"fixed": "dark-green"
},
"opacity": 0.4,
"rotation": {
"fixed": 0,
"max": 360,
"min": -360,
"mode": "mod"
},
"size": {
"field": "Flight count",
"fixed": 5,
"max": 15,
"min": 2
},
"symbol": {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed"
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
"offsetY": 0,
"textAlign": "center",
"textBaseline": "middle"
}
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "lookup"
},
"name": "Flight count",
"tooltip": true,
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 35.70008,
"lon": -93.558296,
"zoom": 3.09
}
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A",
"withTransforms": true
}
],
"title": "Reused data (without transform)",
"type": "geomap"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 11,
"x": 7,
"y": 12
},
"id": 6,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A",
"withTransforms": true
}
],
"title": "Reused data (with transform)",
"type": "table"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 6,
"x": 18,
"y": 12
},
"id": 7,
"options": {
"basemap": {
"config": {},
"name": "Layer 0",
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"showLegend": true,
"style": {
"color": {
"fixed": "dark-green"
},
"opacity": 0.4,
"rotation": {
"fixed": 0,
"max": 360,
"min": -360,
"mode": "mod"
},
"size": {
"field": "2020 population",
"fixed": 5,
"max": 15,
"min": 2
},
"symbol": {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed"
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
"offsetY": 0,
"textAlign": "center",
"textBaseline": "middle"
}
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "lookup"
},
"name": "2022 Population",
"tooltip": true,
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 35.70008,
"lon": -93.558296,
"zoom": 3.09
}
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A",
"withTransforms": true
}
],
"title": "Reused data (with transform)",
"type": "geomap"
}
],
"schemaVersion": 37,
"style": "dark",
"tags": ["devenv"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Reuse dashboard queries",
"uid": "fYGWTVW4k"
}

View File

@ -31,6 +31,6 @@ function setup() {
describe('SupportSnapshot', () => {
it('Can render', async () => {
setup();
expect(await screen.findByRole('button', { name: 'Dashboard (2.94 KiB)' })).toBeInTheDocument();
expect(await screen.findByRole('button', { name: 'Dashboard (2.97 KiB)' })).toBeInTheDocument();
});
});

View File

@ -118,15 +118,19 @@ export async function getDebugDashboard(panel: PanelModel, rand: Randomize, time
],
};
if (data.annotations?.length) {
const anno: DataFrameJSON[] = [];
for (const f of frames) {
if (f.schema?.meta?.dataTopic) {
delete f.schema.meta.dataTopic;
anno.push(f);
}
}
if (saveModel.transformations?.length) {
const last = dashboard.panels[dashboard.panels.length - 1];
last.title = last.title + ' (after transformations)';
const before = cloneDeep(last);
before.id = 100;
before.title = 'Data (before transformations)';
before.gridPos.w = 24; // full width
before.targets[0].withTransforms = false;
dashboard.panels.push(before);
}
if (data.annotations?.length) {
dashboard.panels.push({
id: 7,
gridPos: {
@ -138,17 +142,22 @@ export async function getDebugDashboard(panel: PanelModel, rand: Randomize, time
type: 'table',
title: 'Annotations',
datasource: {
type: 'grafana',
uid: 'grafana',
type: 'datasource',
uid: '-- Dashboard --',
},
options: {
showTypeIcons: true,
},
targets: [
{
datasource: {
type: 'datasource',
uid: '-- Dashboard --',
},
panelId: 2,
withTransforms: true,
topic: DataTopic.Annotations,
refId: 'A',
rawFrameContent: JSON.stringify(anno),
scenarioId: 'raw_frame',
},
],
});
@ -287,6 +296,7 @@ const embeddedDataTemplate: any = {
uid: '-- Dashboard --',
},
panelId: 2,
withTransforms: true,
refId: 'A',
},
],

View File

@ -216,7 +216,7 @@ export class PanelQueryRunner {
} = options;
if (isSharedDashboardQuery(datasource)) {
this.pipeToSubject(runSharedRequest(options), panelId);
this.pipeToSubject(runSharedRequest(options, queries[0]), panelId);
return;
}

View File

@ -4,15 +4,14 @@ import pluralize from 'pluralize';
import React, { useCallback, useMemo } from 'react';
import { useAsync } from 'react-use';
import { DataQuery, GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
import { InlineField, Select, useStyles2, VerticalGroup } from '@grafana/ui';
import { DataQuery, GrafanaTheme2, PanelData, SelectableValue, DataTopic } from '@grafana/data';
import { Field, Select, useStyles2, VerticalGroup, Spinner, Switch, RadioButtonGroup, Icon } from '@grafana/ui';
import config from 'app/core/config';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { PanelModel } from 'app/features/dashboard/state';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { filterPanelDataToQuery } from 'app/features/query/components/QueryEditorRow';
import { DashboardQueryRow } from './DashboardQueryRow';
import { DashboardQuery, ResultInfo, SHARED_DASHBOARD_QUERY } from './types';
function getQueryDisplayText(query: DataQuery): string {
@ -26,25 +25,30 @@ interface Props {
onRunQueries: () => void;
}
const topics = [
{ label: 'All data', value: false },
{ label: 'Annotations', value: true, description: 'Include annotations as regular data' },
];
export function DashboardQueryEditor({ panelData, queries, onChange, onRunQueries }: Props) {
const { value: defaultDatasource } = useAsync(() => getDatasourceSrv().get());
const { value: results, loading: loadingResults } = useAsync(async (): Promise<ResultInfo[]> => {
const query = queries[0] as DashboardQuery;
const dashboard = getDashboardSrv().getCurrent();
const panel = dashboard?.getPanelById(query.panelId ?? -124134);
const query = queries[0] as DashboardQuery;
const panel = useMemo(() => {
const dashboard = getDashboardSrv().getCurrent();
return dashboard?.getPanelById(query.panelId ?? -124134);
}, [query.panelId]);
const { value: results, loading: loadingResults } = useAsync(async (): Promise<ResultInfo[]> => {
if (!panel) {
return [];
}
const mainDS = await getDatasourceSrv().get(panel.datasource);
return Promise.all(
panel.targets.map(async (query) => {
const ds = query.datasource ? await getDatasourceSrv().get(query.datasource) : mainDS;
const fmt = ds.getQueryDisplayText || getQueryDisplayText;
const queryData = filterPanelDataToQuery(panelData, query.refId) ?? panelData;
return {
refId: query.refId,
query: fmt(query),
@ -54,21 +58,41 @@ export function DashboardQueryEditor({ panelData, queries, onChange, onRunQuerie
};
})
);
}, [panelData, queries]);
}, [panelData, panel]);
const query = queries[0] as DashboardQuery;
const onUpdateQuery = useCallback(
(query: DashboardQuery) => {
onChange([query]);
onRunQueries();
},
[onChange, onRunQueries]
);
const onPanelChanged = useCallback(
(id: number) => {
onChange([
{
...query,
panelId: id,
} as DashboardQuery,
]);
onRunQueries();
onUpdateQuery({
...query,
panelId: id,
});
},
[query, onChange, onRunQueries]
[query, onUpdateQuery]
);
const onTransformToggle = useCallback(() => {
onUpdateQuery({
...query,
withTransforms: !query.withTransforms,
});
}, [query, onUpdateQuery]);
const onTopicChanged = useCallback(
(t: boolean) => {
onUpdateQuery({
...query,
topic: t ? DataTopic.Annotations : undefined,
});
},
[query, onUpdateQuery]
);
const getPanelDescription = useCallback(
@ -119,10 +143,11 @@ export function DashboardQueryEditor({ panelData, queries, onChange, onRunQuerie
const selected = panels.find((panel) => panel.value === query.panelId);
// Same as current URL, but different panelId
const editURL = `d/${dashboard.uid}/${dashboard.title}?&editPanel=${query.panelId}`;
const showTransforms = Boolean(query.withTransforms || panel?.transformations?.length);
return (
<>
<InlineField label="Use results from panel" grow>
<Field label="Source" description="Use the same results as panel">
<Select
inputId={selectId}
placeholder="Choose panel"
@ -131,19 +156,45 @@ export function DashboardQueryEditor({ panelData, queries, onChange, onRunQuerie
value={selected}
onChange={(item) => onPanelChanged(item.value!)}
/>
</InlineField>
</Field>
{results && !loadingResults && (
<div className={styles.results}>
{query.panelId && (
<VerticalGroup spacing="sm">
{results.map((target, i) => (
<DashboardQueryRow editURL={editURL} target={target} key={`DashboardQueryRow-${i}`} />
))}
</VerticalGroup>
{loadingResults ? (
<Spinner />
) : (
<>
{results && Boolean(results.length) && (
<Field label="Queries">
<VerticalGroup spacing="sm">
{results.map((target, i) => (
<div className={styles.queryEditorRowHeader} key={`DashboardQueryRow-${i}`}>
<div>
<img src={target.img} width={16} />
<span className={styles.refId}>{target.refId}:</span>
</div>
<div>
<a href={editURL}>
{target.query}
&nbsp;
<Icon name="external-link-alt" />
</a>
</div>
</div>
))}
</VerticalGroup>
</Field>
)}
</div>
</>
)}
{showTransforms && (
<Field label="Transform" description="Apply panel transformations from the source panel">
<Switch value={Boolean(query.withTransforms)} onChange={onTransformToggle} />
</Field>
)}
<Field label="Data">
<RadioButtonGroup options={topics} value={query.topic === DataTopic.Annotations} onChange={onTopicChanged} />
</Field>
</>
);
}
@ -156,5 +207,16 @@ function getStyles(theme: GrafanaTheme2) {
noQueriesText: css({
padding: theme.spacing(1.25),
}),
refId: css({
padding: theme.spacing(1.25),
}),
queryEditorRowHeader: css`
label: queryEditorRowHeader;
display: flex;
padding: 4px 8px;
flex-flow: row wrap;
background: ${theme.colors.background.secondary};
align-items: center;
`,
};
}

View File

@ -1,49 +0,0 @@
import { css } from '@emotion/css';
import React, { ReactElement } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { Icon, useStyles } from '@grafana/ui';
import { ResultInfo } from './types';
interface Props {
editURL: string;
target: ResultInfo;
}
export function DashboardQueryRow({ editURL, target }: Props): ReactElement {
const style = useStyles(getStyles);
return (
<div className={style.queryEditorRowHeader}>
<div>
<img src={target.img} width={16} className={style.logo} />
<span>{`${target.refId}:`}</span>
</div>
<div>
<a href={editURL}>
{target.query}
&nbsp;
<Icon name="external-link-alt" />
</a>
</div>
</div>
);
}
function getStyles(theme: GrafanaTheme) {
return {
logo: css`
label: logo;
margin-right: ${theme.spacing.sm};
`,
queryEditorRowHeader: css`
label: queryEditorRowHeader;
display: flex;
padding: 4px 8px;
flex-flow: row wrap;
background: ${theme.colors.bg2};
align-items: center;
`,
};
}

View File

@ -1,8 +1,32 @@
import { DataSourceApi } from '@grafana/data';
import { of } from 'rxjs';
import { isSharedDashboardQuery } from './runSharedRequest';
import { DataSourceApi, DataTopic, LoadingState, PanelData } from '@grafana/data';
import { getDashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { isSharedDashboardQuery, runSharedRequest } from './runSharedRequest';
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('SharedQueryRunner', () => {
let panelData: PanelData = {} as any;
const origDashbaordSrv = getDashboardSrv();
beforeEach(() => {
setDashboardSrv({
getCurrent: () => ({
getPanelById: () => ({
getQueryRunner: () => ({
getData: () => of(panelData),
}),
}),
}),
} as any);
});
afterEach(() => {
setDashboardSrv(origDashbaordSrv);
});
it('should identify shared queries', () => {
expect(isSharedDashboardQuery('-- Dashboard --')).toBe(true);
@ -20,4 +44,56 @@ describe('SharedQueryRunner', () => {
ds.meta!.name = 'something else';
expect(isSharedDashboardQuery(ds)).toBe(false);
});
it('can filter annotation data', (done) => {
// Get the data
panelData = {
state: LoadingState.Done,
series: [{ refId: 'A', fields: [], length: 0 }],
annotations: [{ refId: 'X', fields: [], length: 0 }],
timeRange: panelData.timeRange,
};
runSharedRequest({ queries: [{ panelId: 1 }] } as any, {
refId: 'Q',
}).subscribe((v) => {
expect(v).toBe(panelData);
done();
});
});
it('can move annotations to the series topic', (done) => {
// Get the data
panelData = {
state: LoadingState.Done,
series: [{ refId: 'A', fields: [], length: 0 }],
annotations: [{ refId: 'X', fields: [], length: 0 }],
timeRange: panelData.timeRange,
};
runSharedRequest({ queries: [{ panelId: 1 }] } as any, {
refId: 'Q',
topic: DataTopic.Annotations,
}).subscribe((v) => {
try {
expect(v).toMatchInlineSnapshot(`
Object {
"annotations": undefined,
"series": Array [
Object {
"fields": Array [],
"length": 0,
"refId": "X",
},
],
"state": "Done",
"timeRange": undefined,
}
`);
done();
} catch (err) {
done(err);
}
});
});
});

View File

@ -8,6 +8,7 @@ import {
getDefaultTimeRange,
LoadingState,
PanelData,
DataTopic,
} from '@grafana/data';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { QueryRunnerOptions } from 'app/features/query/state/PanelQueryRunner';
@ -31,7 +32,7 @@ export function isSharedDashboardQuery(datasource: string | DataSourceRef | Data
return datasource.uid === SHARED_DASHBOARD_QUERY;
}
export function runSharedRequest(options: QueryRunnerOptions): Observable<PanelData> {
export function runSharedRequest(options: QueryRunnerOptions, query: DashboardQuery): Observable<PanelData> {
return new Observable<PanelData>((subscriber) => {
const dashboard = getDashboardSrv().getCurrent();
const listenToPanelId = getPanelIdFromQuery(options.queries);
@ -49,11 +50,24 @@ export function runSharedRequest(options: QueryRunnerOptions): Observable<PanelD
}
const listenToRunner = listenToPanel.getQueryRunner();
const subscription = listenToRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({
next: (data: PanelData) => {
subscriber.next(data);
},
});
const subscription = listenToRunner
.getData({
withTransforms: Boolean(query?.withTransforms),
withFieldConfig: false,
})
.subscribe({
next: (data: PanelData) => {
// Use annotation data for series
if (query?.topic === DataTopic.Annotations) {
data = {
...data,
series: data.annotations ?? [],
annotations: undefined, // remove annotations
};
}
subscriber.next(data);
},
});
// If we are in fullscreen the other panel will not execute any queries
// So we have to trigger it from here

View File

@ -1,9 +1,11 @@
import { DataFrame, DataQuery, DataQueryError } from '@grafana/data';
import { DataFrame, DataQuery, DataQueryError, DataTopic } from '@grafana/data';
export const SHARED_DASHBOARD_QUERY = '-- Dashboard --';
export interface DashboardQuery extends DataQuery {
panelId?: number;
withTransforms?: boolean;
topic?: DataTopic;
}
export type ResultInfo = {