Skip to content

Commit 549711c

Browse files
committed
Add experimental group by feature
1 parent 093337f commit 549711c

File tree

5 files changed

+132
-16
lines changed

5 files changed

+132
-16
lines changed

src/components/ExperimentalEditor.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { InfoBox } from '@grafana/ui';
2+
import React from 'react';
3+
import { InlineFieldRow, InlineField, Select } from '@grafana/ui';
4+
import { JsonApiQuery } from 'types';
5+
import { JSONPath } from 'jsonpath-plus';
6+
7+
interface Props {
8+
query: JsonApiQuery;
9+
onChange: (query: JsonApiQuery) => void;
10+
onRunQuery: () => void;
11+
}
12+
13+
export const ExperimentalEditor = ({ query, onChange, onRunQuery }: Props) => {
14+
const { groupByField } = query;
15+
16+
const onGroupByChange = (field?: string) => {
17+
onChange({ ...query, groupByField: field });
18+
onRunQuery();
19+
};
20+
21+
const fieldNames = query.fields
22+
.map((field) => {
23+
const pathArray = (JSONPath as any).toPathArray(field.jsonPath);
24+
return pathArray[pathArray.length - 1];
25+
})
26+
.map((path) => ({ label: path, value: path }));
27+
28+
return (
29+
<>
30+
<InfoBox severity="warning">
31+
{`The features listed here are experimental. They might change or be removed without notice. In the tooltip for each feature, there's a link to a pull request where you can submit feedback for that feature.`}
32+
</InfoBox>
33+
<InlineFieldRow>
34+
<InlineField
35+
label="Group by"
36+
tooltip={
37+
<>
38+
<p>
39+
{
40+
'Groups the query result into multiple results. This can be useful when you want to graph multiple time series in the same panel.'
41+
}
42+
</p>
43+
<a href="https://github.com/marcusolsson/grafana-json-datasource">Submit feedback</a>
44+
</>
45+
}
46+
>
47+
<Select
48+
placeholder={'Field'}
49+
width={12}
50+
isClearable={true}
51+
value={fieldNames.find((v) => v.value === groupByField)}
52+
options={fieldNames}
53+
onChange={(value) => onGroupByChange(value?.value)}
54+
/>
55+
</InlineField>
56+
</InlineFieldRow>
57+
</>
58+
);
59+
};

src/components/FieldEditor.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,12 @@ export const FieldEditor = ({ value, onChange, limit, onComplete }: Props) => {
2727

2828
const addField = (i: number) => () => {
2929
if (!limit || value.length < limit) {
30-
onChange({
31-
fields: [...value.slice(0, i + 1), { name: '', jsonPath: '' }, ...value.slice(i + 1)],
32-
});
30+
onChange([...value.slice(0, i + 1), { name: '', jsonPath: '' }, ...value.slice(i + 1)]);
3331
}
3432
};
3533

3634
const removeField = (i: number) => () => {
37-
onChange({
38-
fields: [...value.slice(0, i), ...value.slice(i + 1)],
39-
});
35+
onChange([...value.slice(0, i), ...value.slice(i + 1)]);
4036
};
4137

4238
return (

src/components/QueryEditor.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Pair } from '../types';
1010
import { JsonDataSource } from 'datasource';
1111
import { FieldEditor } from './FieldEditor';
1212
import { PathEditor } from './PathEditor';
13+
import { ExperimentalEditor } from './ExperimentalEditor';
1314

1415
// Display a warning message when user adds any of the following headers.
1516
const sensitiveHeaders = ['authorization', 'proxy-authorization', 'x-api-key'];
@@ -141,6 +142,10 @@ export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, limitFields
141142
</>
142143
),
143144
},
145+
{
146+
title: 'Experimental',
147+
content: <ExperimentalEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />,
148+
},
144149
];
145150

146151
return (

src/datasource.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
MetricFindValue,
1111
ScopedVars,
1212
TimeRange,
13+
DataFrame,
14+
Field,
15+
ArrayVector,
1316
} from '@grafana/data';
1417
import { getTemplateSrv } from '@grafana/runtime';
1518

@@ -39,12 +42,14 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
3942
}
4043

4144
async query(request: DataQueryRequest<JsonApiQuery>): Promise<DataQueryResponse> {
42-
const promises = request.targets
45+
const promises = await request.targets
4346
.filter((query) => !query.hide)
44-
.map((query) => this.doRequest(query, request.range, request.scopedVars));
47+
.flatMap((query) => this.doRequest(query, request.range, request.scopedVars));
48+
49+
const res: DataFrame[][] = await Promise.all(promises);
4550

4651
// Wait for all queries to finish before returning the result.
47-
return Promise.all(promises).then((data) => ({ data }));
52+
return { data: res.flatMap((frames) => frames) };
4853
}
4954

5055
/**
@@ -53,7 +58,8 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
5358
* @param query
5459
*/
5560
async metricFindQuery?(query: JsonApiQuery): Promise<MetricFindValue[]> {
56-
const frame = await this.doRequest(query);
61+
const frames = await this.doRequest(query);
62+
const frame = frames[0];
5763
return frame.fields.length > 0 ? frame.fields[0].values.toArray().map((_) => ({ text: _ })) : [];
5864
}
5965

@@ -103,7 +109,7 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
103109
}
104110
}
105111

106-
async doRequest(query: JsonApiQuery, range?: TimeRange, scopedVars?: ScopedVars) {
112+
async doRequest(query: JsonApiQuery, range?: TimeRange, scopedVars?: ScopedVars): Promise<DataFrame[]> {
107113
const replaceWithVars = replace(scopedVars, range);
108114

109115
const json = await this.requestJson(query, replaceWithVars);
@@ -141,11 +147,24 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
141147
throw new Error('Fields have different lengths');
142148
}
143149

144-
return toDataFrame({
145-
name: query.refId,
146-
refId: query.refId,
147-
fields: fields,
148-
});
150+
if (query.groupByField) {
151+
return groupBy(
152+
toDataFrame({
153+
name: query.refId,
154+
refId: query.refId,
155+
fields: fields,
156+
}),
157+
query.groupByField
158+
);
159+
}
160+
161+
return [
162+
toDataFrame({
163+
name: query.refId,
164+
refId: query.refId,
165+
fields: fields,
166+
}),
167+
];
149168
}
150169

151170
async requestJson(query: JsonApiQuery, interpolate: (text: string) => string) {
@@ -176,3 +195,37 @@ const replaceMacros = (str: string, range?: TimeRange) => {
176195
.replace(/\$__unixEpochTo\(\)/g, range.to.unix().toString())
177196
: str;
178197
};
198+
199+
export const groupBy = (frame: DataFrame, fieldName: string): DataFrame[] => {
200+
const groupByField = frame.fields.find((field) => field.name === fieldName);
201+
if (!groupByField) {
202+
return [frame];
203+
}
204+
205+
const uniqueValues = new Set<string>(groupByField.values.toArray());
206+
207+
const frames = [...uniqueValues].map((groupByValue) => {
208+
const fields: Field[] = frame.fields
209+
// Skip the field we're grouping on.
210+
.filter((field) => field.name !== groupByField.name)
211+
.map((field) => ({
212+
...field,
213+
config: {
214+
// displayNameFromDS: groupByValue,
215+
},
216+
values: new ArrayVector(
217+
field.values.toArray().filter((_, idx) => {
218+
return groupByField.values.get(idx) === groupByValue;
219+
})
220+
),
221+
}));
222+
223+
return toDataFrame({
224+
name: groupByValue,
225+
refId: frame.refId,
226+
fields,
227+
});
228+
});
229+
230+
return frames;
231+
};

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export interface JsonApiQuery extends DataQuery {
2020

2121
// Keep for backwards compatibility with older version of variables query editor.
2222
jsonPath?: string;
23+
24+
// Experimental
25+
groupByField?: string;
2326
}
2427

2528
export const defaultQuery: Partial<JsonApiQuery> = {

0 commit comments

Comments
 (0)