Skip to content

Commit

Permalink
init fork
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxime Le Conte des Floris committed Mar 24, 2022
1 parent 6cc1ed1 commit 5d8a6cc
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 260 deletions.
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# JSON API data source for Grafana
# JSON local data source for Grafana

[![Build](https://github.com/marcusolsson/grafana-json-datasource/workflows/CI/badge.svg)](https://github.com/marcusolsson/grafana-json-datasource/actions?query=workflow%3A%22CI%22)
[![Release](https://github.com/marcusolsson/grafana-json-datasource/workflows/Release/badge.svg)](https://github.com/marcusolsson/grafana-json-datasource/actions?query=workflow%3ARelease)
Expand All @@ -7,16 +7,10 @@
[![License](https://img.shields.io/github/license/marcusolsson/grafana-json-datasource)](LICENSE)
[![Twitter](https://img.shields.io/twitter/follow/marcusolsson?color=%231DA1F2&label=twitter&style=plastic)](https://twitter.com/marcusolsson)

A data source plugin for loading JSON APIs into [Grafana](https://grafana.com) using [JSONPath](https://goessner.net/articles/JsonPath/).
A data source plugin for loading local JSON into [Grafana](https://grafana.com) using [JSONPath](https://goessner.net/articles/JsonPath/).

![Screenshot](https://github.com/marcusolsson/grafana-json-datasource/raw/main/src/img/dark.png)

## Documentation

Full documentation for the plugin is available on the [website](https://marcusolsson.github.io/grafana-json-datasource).

## Maintenance

I maintain [several plugins](https://marcus.se.net/projects/) for Grafana. While my employer allows me to spend some time on developing plugins, most of the work happens on evenings and weekends. At the moment, I'm prioritizing fixing bugs and reviewing PRs over introducing new features.

If you'd still like to propose a new feature, [create a new Discussion](https://github.com/marcusolsson/grafana-json-datasource/discussions/new?category=ideas). While I likely won't be able to work on features myself, I'd be happy to accept pull requests. If you'd like to contribute a feature, please let me know before you start working on it.
31 changes: 5 additions & 26 deletions src/components/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {} from '@emotion/core';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { DataSourceHttpSettings, InlineField, InlineFieldRow, Input } from '@grafana/ui';
import { TextArea } from '@grafana/ui';
import React, { ChangeEvent } from 'react';
import { JsonApiDataSourceOptions } from '../types';

Expand All @@ -11,41 +11,20 @@ type Props = DataSourcePluginOptionsEditorProps<JsonApiDataSourceOptions>;
* authentication.
*/
export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
const onParamsChange = (e: ChangeEvent<HTMLInputElement>) => {
const onParamsChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
queryParams: e.currentTarget.value,
data: e.currentTarget.value,
},
});
};

return (
<>
{/* DataSourceHttpSettings handles most the settings for connecting over
HTTP. */}
<DataSourceHttpSettings
defaultUrl="http://localhost:8080"
dataSourceConfig={options}
onChange={onOptionsChange}
/>

{/* The Grafana proxy strips query parameters from the URL set in
DataSourceHttpSettings. To support custom query parameters, the user need
to set them explicitly. */}
<h3 className="page-heading">Misc</h3>
<InlineFieldRow>
<InlineField label="Query string" tooltip="Add a custom query string to your queries.">
<Input
width={50}
value={options.jsonData.queryParams}
onChange={onParamsChange}
spellCheck={false}
placeholder="page=1&limit=100"
/>
</InlineField>
</InlineFieldRow>
<h3 className="page-heading">RAW Json</h3>
<TextArea onChange={onParamsChange} value={options.jsonData.data} spellCheck={false} rows={20}></TextArea>
</>
);
};
154 changes: 3 additions & 151 deletions src/components/TabbedQueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import { TimeRange } from '@grafana/data';
import { CodeEditor, InfoBox, InlineField, InlineFieldRow, RadioButtonGroup, Segment, useTheme } from '@grafana/ui';
import { InlineField, InlineFieldRow, RadioButtonGroup } from '@grafana/ui';
import { JsonDataSource } from 'datasource';
import { css } from 'emotion';
import defaults from 'lodash/defaults';
import React, { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { defaultQuery, JsonApiQuery, Pair } from '../types';
import { KeyValueEditor } from './KeyValueEditor';
import { PathEditor } from './PathEditor';

// Display a warning message when user adds any of the following headers.
const sensitiveHeaders = ['authorization', 'proxy-authorization', 'x-api-key'];
import { JsonApiQuery } from '../types';

interface Props {
onChange: (query: JsonApiQuery) => void;
Expand All @@ -25,118 +17,14 @@ interface Props {
experimentalTab: React.ReactNode;
}

export const TabbedQueryEditor = ({ query, onChange, onRunQuery, fieldsTab, experimentalTab }: Props) => {
const [bodyType, setBodyType] = useState('plaintext');
export const TabbedQueryEditor = ({ fieldsTab }: Props) => {
const [tabIndex, setTabIndex] = useState(0);
const theme = useTheme();

const q = defaults(query, defaultQuery);

const onBodyChange = (body: string) => {
onChange({ ...q, body });
onRunQuery();
};

const onParamsChange = (params: Array<Pair<string, string>>) => {
onChange({ ...q, params });
onRunQuery();
};

const onHeadersChange = (headers: Array<Pair<string, string>>) => {
onChange({ ...q, headers });
onRunQuery();
};

const tabs = [
{
title: 'Fields',
content: fieldsTab,
},
{
title: 'Path',
content: (
<PathEditor
method={q.method ?? 'GET'}
onMethodChange={(method) => {
onChange({ ...q, method });
onRunQuery();
}}
path={q.urlPath ?? ''}
onPathChange={(path) => {
onChange({ ...q, urlPath: path });
onRunQuery();
}}
/>
),
},
{
title: 'Params',
content: (
<KeyValueEditor
addRowLabel={'Add param'}
columns={['Key', 'Value']}
values={q.params ?? []}
onChange={onParamsChange}
onBlur={() => onRunQuery()}
/>
),
},
{
title: 'Headers',
content: (
<KeyValueEditor
addRowLabel={'Add header'}
columns={['Key', 'Value']}
values={q.headers ?? []}
onChange={onHeadersChange}
onBlur={() => onRunQuery()}
/>
),
},
{
title: 'Body',
content: (
<>
<InlineFieldRow>
<InlineField label="Syntax highlighting">
<RadioButtonGroup
value={bodyType}
onChange={(v) => setBodyType(v ?? 'plaintext')}
options={[
{ label: 'Text', value: 'plaintext' },
{ label: 'JSON', value: 'json' },
{ label: 'XML', value: 'xml' },
]}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<AutoSizer
disableHeight
className={css`
margin-bottom: ${theme.spacing.sm};
`}
>
{({ width }) => (
<CodeEditor
value={q.body || ''}
language={bodyType}
width={width}
height="200px"
showMiniMap={false}
showLineNumbers={true}
onBlur={onBodyChange}
/>
)}
</AutoSizer>
</InlineFieldRow>
</>
),
},
{
title: 'Experimental',
content: experimentalTab,
},
];

return (
Expand All @@ -149,44 +37,8 @@ export const TabbedQueryEditor = ({ query, onChange, onRunQuery, fieldsTab, expe
options={tabs.map((tab, idx) => ({ label: tab.title, value: idx }))}
/>
</InlineField>
<InlineField
label="Cache Time"
tooltip="Time in seconds that the response will be cached in Grafana after receiving it."
>
<Segment
value={{ label: formatCacheTimeLabel(q.cacheDurationSeconds), value: q.cacheDurationSeconds }}
options={[0, 5, 10, 30, 60, 60 * 2, 60 * 5, 60 * 10, 60 * 30, 3600, 3600 * 2, 3600 * 5].map((value) => ({
label: formatCacheTimeLabel(value),
value,
description: value ? '' : 'Response is not cached at all',
}))}
onChange={({ value }) => onChange({ ...q, cacheDurationSeconds: value! })}
/>
</InlineField>
</InlineFieldRow>
{q.method === 'GET' && q.body && (
<InfoBox severity="warning" style={{ maxWidth: '700px', whiteSpace: 'normal' }}>
{"GET requests can't have a body. The body you've defined will be ignored."}
</InfoBox>
)}
{(q.headers ?? []).map(([key, _]) => key.toLowerCase()).find((_) => sensitiveHeaders.includes(_)) && (
<InfoBox severity="warning" style={{ maxWidth: '700px', whiteSpace: 'normal' }}>
{
"It looks like you're adding credentials in the header. Since queries are stored unencrypted, it's strongly recommended that you add any secrets to the data source config instead."
}
</InfoBox>
)}
{tabs[tabIndex].content}
</>
);
};

export const formatCacheTimeLabel = (s: number) => {
if (s < 60) {
return s + 's';
} else if (s < 3600) {
return s / 60 + 'm';
}

return s / 3600 + 'h';
};
79 changes: 26 additions & 53 deletions src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,25 @@ import { getTemplateSrv } from '@grafana/runtime';
import jsonata from 'jsonata';
import { JSONPath } from 'jsonpath-plus';
import _ from 'lodash';
import API from './api';

import { detectFieldType } from './detectFieldType';
import { parseValues } from './parseValues';
import { JsonApiDataSourceOptions, JsonApiQuery, Pair } from './types';
import { JsonApiDataSourceOptions, JsonApiQuery } from './types';

export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOptions> {
api: API;
data: Object;
isValid: boolean;

constructor(instanceSettings: DataSourceInstanceSettings<JsonApiDataSourceOptions>) {
super(instanceSettings);

this.api = new API(instanceSettings.url!, instanceSettings.jsonData.queryParams || '');
try {
this.data = JSON.parse(instanceSettings.jsonData.data || '{}');
this.isValid = true;
} catch (err: any) {
this.isValid = false;
this.data = err;
}
}

/**
Expand All @@ -37,7 +44,7 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
* name it as you like.
*/
async metadataRequest(query: JsonApiQuery, range?: TimeRange) {
return this.requestJson(query, replace({}, range));
return this.requestJson();
}

async query(request: DataQueryRequest<JsonApiQuery>): Promise<DataQueryResponse> {
Expand Down Expand Up @@ -83,47 +90,24 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
* Checks whether we can connect to the API.
*/
async testDatasource() {
const defaultErrorMessage = 'Cannot connect to API';

try {
const response = await this.api.test();

if (response.status === 200) {
return {
status: 'success',
message: 'Success',
};
} else {
return {
status: 'error',
message: response.statusText ? response.statusText : defaultErrorMessage,
};
}
} catch (err: any) {
if (_.isString(err)) {
return {
status: 'error',
message: err,
};
} else {
let message = 'JSON API: ';
message += err.statusText ? err.statusText : defaultErrorMessage;
if (err.data && err.data.error && err.data.error.code) {
message += ': ' + err.data.error.code + '. ' + err.data.error.message;
}

return {
status: 'error',
message,
};
}
if (this.isValid) {
return {
status: 'success',
message: 'Success',
};
}

return {
status: 'error',
message: `Invalid JSON: ${this.data}`,
};
}

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

const json = await this.requestJson(query, replaceWithVars);
// const json = await this.requestJson(query, replaceWithVars);
const json = Object.assign({}, this.data);

if (!json) {
throw new Error('Query returned empty data');
Expand Down Expand Up @@ -228,19 +212,8 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
return res;
}

async requestJson(query: JsonApiQuery, interpolate: (text: string) => string) {
const interpolateKeyValue = ([key, value]: Pair<string, string>): Pair<string, string> => {
return [interpolate(key), interpolate(value)];
};

return await this.api.cachedGet(
query.cacheDurationSeconds,
query.method,
interpolate(query.urlPath),
(query.params ?? []).map(interpolateKeyValue),
(query.headers ?? []).map(interpolateKeyValue),
interpolate(query.body)
);
requestJson() {
return Promise.resolve(this.data);
}
}

Expand Down
Loading

0 comments on commit 5d8a6cc

Please sign in to comment.