Grafana: include a built-in backend datasource (#38571)

This commit is contained in:
Ryan McKinley
2021-09-10 07:44:47 -07:00
committed by GitHub
parent f74421b892
commit 6bda64cb19
18 changed files with 517 additions and 116 deletions

View File

@@ -1,9 +1,16 @@
import React, { PureComponent } from 'react';
import { InlineField, Select, Alert, Input } from '@grafana/ui';
import { QueryEditorProps, SelectableValue, dataFrameFromJSON, rangeUtil } from '@grafana/data';
import { InlineField, Select, Alert, Input, InlineFieldRow } from '@grafana/ui';
import {
QueryEditorProps,
SelectableValue,
dataFrameFromJSON,
rangeUtil,
DataQueryRequest,
DataFrame,
} from '@grafana/data';
import { GrafanaDatasource } from '../datasource';
import { defaultQuery, GrafanaQuery, GrafanaQueryType } from '../types';
import { getBackendSrv } from '@grafana/runtime';
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
type Props = QueryEditorProps<GrafanaDatasource, GrafanaQuery>;
@@ -12,6 +19,7 @@ const labelWidth = 12;
interface State {
channels: Array<SelectableValue<string>>;
channelFields: Record<string, Array<SelectableValue<string>>>;
folders?: Array<SelectableValue<string>>;
}
export class QueryEditor extends PureComponent<Props, State> {
@@ -28,6 +36,11 @@ export class QueryEditor extends PureComponent<Props, State> {
value: GrafanaQueryType.LiveMeasurements,
description: 'Stream real-time measurements from Grafana',
},
{
label: 'List public files',
value: GrafanaQueryType.List,
description: 'Show directory listings for public resources',
},
];
loadChannelInfo() {
@@ -62,6 +75,30 @@ export class QueryEditor extends PureComponent<Props, State> {
});
}
loadFolderInfo() {
const query: DataQueryRequest<GrafanaQuery> = {
targets: [{ queryType: GrafanaQueryType.List, refId: 'A' }],
} as any;
getDataSourceSrv()
.get('-- Grafana --')
.then((ds) => {
const gds = ds as GrafanaDatasource;
gds.query(query).subscribe({
next: (rsp) => {
if (rsp.data.length) {
const names = (rsp.data[0] as DataFrame).fields[0];
const folders = names.values.toArray().map((v) => ({
value: v,
label: v,
}));
this.setState({ folders });
}
},
});
});
}
componentDidMount() {
this.loadChannelInfo();
}
@@ -242,6 +279,49 @@ export class QueryEditor extends PureComponent<Props, State> {
);
}
onFolderChanged = (sel: SelectableValue<string>) => {
const { onChange, query, onRunQuery } = this.props;
onChange({ ...query, path: sel?.value });
onRunQuery();
};
renderListPublicFiles() {
let { path } = this.props.query;
let { folders } = this.state;
if (!folders) {
folders = [];
this.loadFolderInfo();
}
const currentFolder = folders.find((f) => f.value === path);
if (path && !currentFolder) {
folders = [
...folders,
{
value: path,
label: path,
},
];
}
return (
<InlineFieldRow>
<InlineField label="Path" grow={true} labelWidth={labelWidth}>
<Select
menuShouldPortal
options={folders}
value={currentFolder || ''}
onChange={this.onFolderChanged}
allowCustomValue={true}
backspaceRemovesValue={true}
placeholder="Select folder"
isClearable={true}
formatCreateLabel={(input: string) => `Folder: ${input}`}
/>
</InlineField>
</InlineFieldRow>
);
}
render() {
const query = {
...defaultQuery,
@@ -250,7 +330,7 @@ export class QueryEditor extends PureComponent<Props, State> {
return (
<>
<div className="gf-form">
<InlineFieldRow>
<InlineField label="Query type" grow={true} labelWidth={labelWidth}>
<Select
menuShouldPortal
@@ -259,8 +339,9 @@ export class QueryEditor extends PureComponent<Props, State> {
onChange={this.onQueryTypeChange}
/>
</InlineField>
</div>
</InlineFieldRow>
{query.queryType === GrafanaQueryType.LiveMeasurements && this.renderMeasurementsQuery()}
{query.queryType === GrafanaQueryType.List && this.renderListPublicFiles()}
</>
);
}

View File

@@ -1,12 +1,10 @@
import { from, merge, Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, toDataQueryResponse } from '@grafana/runtime';
import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv } from '@grafana/runtime';
import {
AnnotationQuery,
AnnotationQueryRequest,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DatasourceRef,
isValidLiveChannelAddress,
@@ -22,7 +20,7 @@ import { isString } from 'lodash';
let counter = 100;
export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
constructor(instanceSettings: DataSourceInstanceSettings) {
super(instanceSettings);
this.annotations = {
@@ -49,7 +47,8 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
}
query(request: DataQueryRequest<GrafanaQuery>): Observable<DataQueryResponse> {
const queries: Array<Observable<DataQueryResponse>> = [];
const results: Array<Observable<DataQueryResponse>> = [];
const targets: GrafanaQuery[] = [];
const templateSrv = getTemplateSrv();
for (const target of request.targets) {
if (target.queryType === GrafanaQueryType.Annotations) {
@@ -90,7 +89,7 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
buffer.maxDelta = request.range.to.valueOf() - request.range.from.valueOf();
}
queries.push(
results.push(
getGrafanaLiveSrv().getDataStream({
key: `${request.requestId}.${counter++}`,
addr: addr!,
@@ -99,15 +98,28 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
})
);
} else {
queries.push(getRandomWalk(request));
if (!target.queryType) {
target.queryType = GrafanaQueryType.RandomWalk;
}
targets.push(target);
}
}
// With a single query just return the results
if (queries.length === 1) {
return queries[0];
if (targets.length) {
results.push(
super.query({
...request,
targets,
})
);
}
if (queries.length > 1) {
return merge(...queries);
if (results.length) {
// With a single query just return the results
if (results.length === 1) {
return results[0];
}
return merge(...results);
}
return of(); // nothing
}
@@ -171,32 +183,3 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
return Promise.resolve();
}
}
// Note that the query does not actually matter
function getRandomWalk(request: DataQueryRequest): Observable<DataQueryResponse> {
const { intervalMs, maxDataPoints, range, requestId } = request;
// Yes, this implementation ignores multiple targets! But that matches existing behavior
const params: Record<string, any> = {
intervalMs,
maxDataPoints,
from: range.from.valueOf(),
to: range.to.valueOf(),
};
return getBackendSrv()
.fetch({
url: '/api/tsdb/testdata/random-walk',
method: 'GET',
params,
requestId,
})
.pipe(
map((rsp: any) => {
return toDataQueryResponse(rsp);
}),
catchError((err) => {
return of(toDataQueryResponse(err));
})
);
}

View File

@@ -6,9 +6,13 @@ import { LiveDataFilter } from '@grafana/runtime';
//----------------------------------------------
export enum GrafanaQueryType {
RandomWalk = 'randomWalk',
LiveMeasurements = 'measurements',
Annotations = 'annotations',
// backend
RandomWalk = 'randomWalk',
List = 'list',
Read = 'read',
}
export interface GrafanaQuery extends DataQuery {
@@ -16,6 +20,7 @@ export interface GrafanaQuery extends DataQuery {
channel?: string;
filter?: LiveDataFilter;
buffer?: number;
path?: string; // for list and read
}
export const defaultQuery: GrafanaQuery = {