Skip to content

Commit 8f918cd

Browse files
committed
New query editor
1 parent 1255d4f commit 8f918cd

File tree

9 files changed

+496
-73
lines changed

9 files changed

+496
-73
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Changelog
22

3+
## 0.9.0 (2021-02-01)
4+
5+
[Full changelog](https://github.com/marcusolsson/grafana-json-datasource/compare/v0.8.0...v0.9.0)
6+
7+
**BREAKING CHANGE:** Query parameters set by the query editor no longer overrides the data source config, to match how headers are handled in the Grafana proxy. This establishes the convention that any configuration made by an administrator should have higher priority.
8+
9+
**IMPORTANT:** This release contains many new changes that touches several aspects of the plugin. **Make sure that you back up your dashboards before updating your plugin.**
10+
11+
This release introduces a new query editor that gives more control of the request.
12+
13+
- Support for both GET and POST methods
14+
- Support for request bodies (when using POST)
15+
- Support for headers
16+
17+
It introduces a new key value editor for query parameters and headers, as well as a Monaco-based editor for editing the request body with syntax highlighting.
18+
19+
This release deprecates the `queryString` property in the query model, in favor of the new `params`. The query string config _should_ be backwards-compatible (and forward-compatible) with previous versions, but make sure to back up your dashboard before upgrading.
20+
321
## 0.8.0 (2021-01-08)
422

523
[Full changelog](https://github.com/marcusolsson/grafana-json-datasource/compare/v0.7.1...v0.8.0)

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "marcusolsson-json-datasource",
3-
"version": "0.8.0",
3+
"version": "0.9.0",
44
"description": "A data source plugin for loading JSON APIs into Grafana.",
55
"scripts": {
66
"build": "grafana-toolkit plugin:build",
@@ -17,9 +17,11 @@
1717
"@grafana/ui": "^7.3.0",
1818
"@testing-library/jest-dom": "5.4.0",
1919
"@testing-library/react": "^10.0.2",
20+
"@types/jsonpath": "^0.2.0",
2021
"@types/lodash": "latest",
2122
"@types/memory-cache": "^0.2.1",
22-
"@types/jsonpath": "^0.2.0"
23+
"@types/react-virtualized-auto-sizer": "^1.0.0",
24+
"react-virtualized-auto-sizer": "^1.0.4"
2325
},
2426
"engines": {
2527
"node": ">=12 <13"

src/api.ts

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { getBackendSrv } from '@grafana/runtime';
1+
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
22
import cache from 'memory-cache';
3+
import { Observable } from 'rxjs';
4+
import { Pair } from 'types';
35

46
export default class Api {
57
cache: any;
@@ -16,20 +18,28 @@ export default class Api {
1618
/**
1719
* Queries the API and returns the response data.
1820
*/
19-
async get(path: string, params?: string) {
20-
const data: Record<string, string> = {};
21-
22-
this.params.forEach((value, key) => {
23-
data[key] = value;
21+
async get(
22+
method: string,
23+
path: string,
24+
params?: Array<Pair<string, string>>,
25+
headers?: Array<Pair<string, string>>,
26+
body?: string
27+
) {
28+
const paramsData: Record<string, string> = {};
29+
30+
(params ?? []).forEach(([key, value]) => {
31+
if (key) {
32+
paramsData[key] = value;
33+
}
2434
});
2535

26-
new URLSearchParams('?' + params).forEach((value, key) => {
27-
data[key] = value;
36+
this.params.forEach((value, key) => {
37+
paramsData[key] = value;
2838
});
2939

30-
const response = await this._request(path, data);
40+
const response = await this._request(method, path, paramsData, headers, body);
3141

32-
return response.data;
42+
return (await response.toPromise()).data;
3343
}
3444

3545
/**
@@ -42,15 +52,22 @@ export default class Api {
4252
data[key] = value;
4353
});
4454

45-
return this._request('', data);
55+
return this._request('GET', '', data).toPromise();
4656
}
4757

4858
/**
4959
* Returns a cached API response if it exists, otherwise queries the API.
5060
*/
51-
async cachedGet(cacheDurationSeconds: number, path: string, params: string) {
61+
async cachedGet(
62+
cacheDurationSeconds: number,
63+
method: string,
64+
path: string,
65+
params: Array<Pair<string, string>>,
66+
headers?: Array<Pair<string, string>>,
67+
body?: string
68+
) {
5269
if (cacheDurationSeconds === 0) {
53-
return await this.get(path, params);
70+
return await this.get(method, path, params, headers, body);
5471
}
5572

5673
const rawUrl = this.baseUrl + path;
@@ -67,7 +84,7 @@ export default class Api {
6784
}
6885
this.lastCacheDuration = cacheDurationSeconds;
6986

70-
const result = await this.get(path, params);
87+
const result = await this.get(method, path, params, headers, body);
7188

7289
this.cache.put(rawUrl, result, Math.max(cacheDurationSeconds * 1000, 1));
7390

@@ -79,30 +96,49 @@ export default class Api {
7996
* Allows the user to append a path, or override query parameters.
8097
*
8198
* @param path to append to URL
82-
* @param data to set as query parameters
99+
* @param params to set as query parameters
83100
*/
84-
async _request(path: string, data?: Record<string, string>) {
85-
const req = {
101+
_request(
102+
method: string,
103+
path: string,
104+
params?: Record<string, string>,
105+
headers?: Array<Pair<string, string>>,
106+
data?: string
107+
): Observable<any> {
108+
const recordHeaders: Record<string, any> = {};
109+
110+
(headers ?? [])
111+
.filter(([key, _]) => key)
112+
.forEach(([key, value]) => {
113+
recordHeaders[key] = value;
114+
});
115+
116+
const req: BackendSrvRequest = {
86117
url: `${this.baseUrl}${path}`,
87-
method: 'GET',
118+
method,
119+
headers: recordHeaders,
88120
};
89121

122+
if (method !== 'GET' && data) {
123+
req.data = data;
124+
}
125+
90126
// Deduplicate forward slashes, i.e. /// becomes /. This enables sensible
91127
// defaults for empty variables.
92128
//
93129
// For example, `/orgs/${orgId}/list` becomes `/orgs/list` instead of
94130
// `/orgs//list`.
95131
req.url = req.url.replace(/[\/]+/g, '/');
96132

97-
if (data && Object.keys(data).length > 0) {
133+
if (params && Object.keys(params).length > 0) {
98134
req.url =
99135
req.url +
100136
(req.url.search(/\?/) >= 0 ? '&' : '?') +
101-
Object.entries(data)
137+
Object.entries(params)
102138
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
103139
.join('&');
104140
}
105141

106-
return getBackendSrv().datasourceRequest(req);
142+
return getBackendSrv().fetch(req);
107143
}
108144
}

src/components/KeyValueEditor.tsx

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React from 'react';
2+
3+
import { css } from 'emotion';
4+
import { Button, Icon, useTheme } from '@grafana/ui';
5+
import { Pair } from '../types';
6+
7+
interface Props {
8+
columns: string[];
9+
values: Array<Pair<string, string>>;
10+
addRowLabel: string;
11+
12+
onChange: (rows: Array<Pair<string, string>>) => void;
13+
onBlur: () => void;
14+
}
15+
16+
export const KeyValueEditor = ({ columns, values, onChange, addRowLabel, onBlur }: Props) => {
17+
const theme = useTheme();
18+
19+
const updateCell = (colIdx: number, rowIdx: number, value: string) => {
20+
onChange(
21+
values.map(([key, val], idx) => {
22+
if (rowIdx === idx) {
23+
if (colIdx === 0) {
24+
return [value, val];
25+
} else if (colIdx === 1) {
26+
return [key, value];
27+
} else {
28+
return [key, val];
29+
}
30+
}
31+
return [key, val];
32+
})
33+
);
34+
};
35+
36+
const addRow = (i: number) => {
37+
onChange([...values.slice(0, i + 1), ['', ''], ...values.slice(i + 1)]);
38+
};
39+
40+
const removeRow = (i: number) => {
41+
onChange([...values.slice(0, i), ...values.slice(i + 1)]);
42+
};
43+
44+
const styles = {
45+
root: css`
46+
table-layout: auto;
47+
border: 1px solid ${theme.colors.formInputBorder};
48+
border-collapse: separate;
49+
border-radius: ${theme.border.radius.sm};
50+
border-spacing: 0;
51+
border-left: 0;
52+
width: 100%;
53+
`,
54+
thead: css`
55+
display: table-header-group;
56+
vertical-align: middle;
57+
border-color: inherit;
58+
border-collapse: separate;
59+
60+
&:first-child tr:first-child th:first-child {
61+
border-radius: ${theme.border.radius.sm} 0 0 0;
62+
}
63+
&:last-child tr:last-child th:first-child {
64+
border-radius: 0 0 0 ${theme.border.radius.sm};
65+
}
66+
`,
67+
tbody: css`
68+
&:first-child tr:first-child td:first-child {
69+
border-radius: ${theme.border.radius.sm} 0 0 0;
70+
}
71+
72+
&:last-child tr:last-child td:first-child {
73+
border-radius: 0 0 0 ${theme.border.radius.sm};
74+
}
75+
`,
76+
input: css`
77+
outline: none;
78+
border: 0;
79+
background: transparent;
80+
width: 100%;
81+
`,
82+
row: css`
83+
display: table-row;
84+
vertical-align: inherit;
85+
border-color: inherit;
86+
`,
87+
th: css`
88+
padding: ${theme.spacing.xs} ${theme.spacing.sm};
89+
border-left: solid ${theme.colors.formInputBorder} 1px;
90+
font-size: ${theme.typography.size.sm};
91+
color: ${theme.colors.textSemiWeak};
92+
font-weight: ${theme.typography.weight.regular};
93+
94+
&:last-child {
95+
border-left: 0;
96+
}
97+
`,
98+
td: css`
99+
padding: ${theme.spacing.xs} ${theme.spacing.sm};
100+
border: 1px solid transparent;
101+
border-left: solid ${theme.colors.formInputBorder} 1px;
102+
border-top: solid ${theme.colors.formInputBorder} 1px;
103+
background-color: ${theme.colors.formInputBg};
104+
&:last-child {
105+
border-left: 0;
106+
width: 32px;
107+
padding-left: 0;
108+
padding-right: ${theme.spacing.xs};
109+
}
110+
`,
111+
};
112+
113+
return values.length === 0 ? (
114+
<Button
115+
variant="secondary"
116+
onClick={() => {
117+
addRow(0);
118+
}}
119+
>
120+
{addRowLabel}
121+
</Button>
122+
) : (
123+
<table className={styles.root}>
124+
<thead className={styles.thead}>
125+
<tr className={styles.row}>
126+
{columns.map(_ => (
127+
<th className={styles.th}>{_}</th>
128+
))}
129+
<th className={styles.th}></th>
130+
</tr>
131+
</thead>
132+
<tbody className={styles.tbody}>
133+
{values.map((row, rowIdx) => (
134+
<tr key={rowIdx} className={styles.row}>
135+
{row.map((cell, colIdx) => (
136+
<td key={colIdx} className={styles.td}>
137+
<input
138+
value={cell}
139+
onChange={e => updateCell(colIdx, rowIdx, e.currentTarget.value)}
140+
onBlur={onBlur}
141+
className={styles.input}
142+
/>
143+
</td>
144+
))}
145+
<td className={styles.td}>
146+
<div
147+
className={css`
148+
display: flex;
149+
& > * {
150+
margin-right: ${theme.spacing.xs};
151+
}
152+
& > *:last-child {
153+
margin-right: 0;
154+
}
155+
`}
156+
>
157+
<a
158+
className={css`
159+
display: flex;
160+
background: ${theme.colors.bg2};
161+
padding: ${theme.spacing.xs} ${theme.spacing.sm};
162+
align-items: center;
163+
border-radius: ${theme.border.radius.sm};
164+
`}
165+
onClick={() => addRow(rowIdx)}
166+
>
167+
<Icon name="plus" />
168+
</a>
169+
<a
170+
className={css`
171+
display: flex;
172+
background: ${theme.colors.bg2};
173+
padding: ${theme.spacing.xs} ${theme.spacing.sm};
174+
align-items: center;
175+
border-radius: ${theme.border.radius.sm};
176+
`}
177+
onClick={() => removeRow(rowIdx)}
178+
>
179+
<Icon name="minus" />
180+
</a>
181+
</div>
182+
</td>
183+
</tr>
184+
))}
185+
</tbody>
186+
</table>
187+
);
188+
};

0 commit comments

Comments
 (0)