Skip to content

Backend port to a node backend #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -10,6 +10,11 @@ Connect and Query from any SQL Datasource (oracle, microsoft sql server, postgre

1. Install Grafana
2. Clone this repo to the grafana plugins folder (`git clone https://github.com/grafana/sqlproxy-grafana-datasource /var/lib/grafana/plugins/sqlproxy-grafana-datasource`)
3. Build the plugin
```
yarn build-all
```
4. (Re)start Grafana

## SQL Proxy

18 changes: 18 additions & 0 deletions backend/DataService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { QueryDataRequest, DataService, DataFrame } from '@grafana/tsbackend';
import { DataQuery } from '@grafana/tsbackend/dist/proto/backend_pb';
import { load } from './sdk';

export class SqlProxyDataService extends DataService {

async QueryData(request: QueryDataRequest): Promise<DataFrame[]> {
const settings = request.getPlugincontext()?.getDatasourceinstancesettings();
const url = settings?.getUrl();
return this.fetchResults(url!, request.getQueriesList());
}

async fetchResults(url: string, queries: DataQuery[]) {
const { fetchResults } = await load('./query');
return fetchResults(url, queries);
}

}
24 changes: 24 additions & 0 deletions backend/DiagnosticsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CheckHealthRequest, CheckHealthResponse, DiagnosticsService, CollectMetricsRequest, CollectMetricsResponse } from '@grafana/tsbackend';
import { doPost } from './sdk';

export class SqlProxyDiagnosticsService extends DiagnosticsService {

CheckHealth = async (request: CheckHealthRequest): Promise<CheckHealthResponse> => {
const settings = request.toObject().plugincontext?.datasourceinstancesettings;
const health: CheckHealthResponse = new CheckHealthResponse();
if (settings) {
const jsonString: string = Buffer.from(settings.jsondata as string, 'base64').toString('ascii');
const status = await doPost(settings.url, jsonString);
health.setStatus(CheckHealthResponse.HealthStatus.OK)
health.setMessage(`Connected Successfully ${JSON.stringify(status)}`);
} else {
health.setStatus(CheckHealthResponse.HealthStatus.ERROR);
health.setMessage("Please configure the datasource first");
}
return health;
}

CollectMetrics = (request: CollectMetricsRequest): Promise<CollectMetricsResponse> => {
throw new Error("Method not implemented.");
}
}
2 changes: 2 additions & 0 deletions backend/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import server from './server';
server.run();
18 changes: 18 additions & 0 deletions backend/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DataFrame } from "@grafana/tsbackend";
import { DataQuery } from "@grafana/tsbackend/dist/proto/backend_pb";
import { SqlQuery } from "../shared/types";
import { doGet, getModel } from "./sdk";
import { ArrayDataFrame } from '@grafana/data';

export async function fetchResults(url: string, queries: DataQuery[]) {
const results = queries.map(q => {
const model = getModel<SqlQuery>(q);
return doGet(`${url}/query?sql=${model.sql}`);
});
const all = await Promise.all(results);
return all.map(r => arrayToDataFrame(r as unknown as Array<any>));
}

function arrayToDataFrame(array: any[]): DataFrame {
return new ArrayDataFrame(array) as unknown as DataFrame;
}
32 changes: 32 additions & 0 deletions backend/sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fetch from 'node-fetch';
import { DataQuery } from "@grafana/tsbackend/dist/proto/backend_pb";
const clear_require = require('clear-require');
const devMode = true;

export function getModel<T>(q: DataQuery): T {
const json = q.getJson();
const jsonString: string = Buffer.from(json as string, 'base64').toString('ascii');
return JSON.parse(jsonString);
}

export function doGet(url: string) {
return fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}).then(r => r.json());
}

export function doPost(url: string, body: string) {
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
}).then(r => r.json());
}

export async function load(path: string) {
if (devMode) {
clear_require(path);
}
return await import(path);
}
9 changes: 9 additions & 0 deletions backend/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BackendServer } from '@grafana/tsbackend';
import { SqlProxyDiagnosticsService } from './DiagnosticsService';
import { SqlProxyDataService } from './DataService';

const server = new BackendServer();
server.addDiagnosticsService(new SqlProxyDiagnosticsService());
server.addDataService(new SqlProxyDataService());

export default server;
15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -3,24 +3,33 @@
"version": "1.0.0",
"description": "Sql Proxy Datasource",
"scripts": {
"prebuild": "cd ./node_modules/@grafana/tsbackend/ && yarn install",
"build": "grafana-toolkit plugin:build",
"test": "grafana-toolkit plugin:test",
"dev": "grafana-toolkit plugin:dev",
"watch": "grafana-toolkit plugin:dev --watch"
"dev": "grafana-toolkit plugin:dev && yarn backend",
"backend": "./node_modules/@grafana/tsbackend/bin/grafana-tsbackend gpx_sqlproxy_darwin_amd64",
"watch": "grafana-toolkit plugin:dev --watch",
"build-all": "yarn build && yarn backend",
"build-backend": "tsc --project tsconfig.backend.json",
"watch-backend": "tsc --project tsconfig.backend.json --watch"
},
"private": true,
"license": "SEE LICENSE IN LICENSE",
"devDependencies": {
"@grafana/data": "7.1.3",
"@grafana/runtime": "7.1.3",
"@grafana/toolkit": "7.1.3",
"@grafana/tsbackend": "srclosson/grafana-tsbackend#master",
"@grafana/ui": "7.1.3",
"@types/lodash": "4.14.123",
"@types/node-fetch": "2.5.7",
"@types/react": "16.8.16 ",
"@types/request-promise-native": "^1.0.17"
"@types/request-promise-native": "^1.0.17",
"node-fetch": "2.6.1"
},
"screenshots": [],
"dependencies": {
"clear-require": "^3.0.0",
"codemirror": "^5.51.0",
"date-fns": "^2.16.1",
"react-codemirror2": "^6.0.0"
15 changes: 15 additions & 0 deletions shared/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DataQuery, DataSourceJsonData, DataSourceSettings } from '@grafana/data';

export interface SqlQuery extends DataQuery {
sql: string;
}

export interface ProxySettings extends DataSourceSettings {
host: string;
backend?: boolean;
}

export interface Settings extends DataSourceJsonData {
host: string;
backend?: boolean;
}
10 changes: 10 additions & 0 deletions src/Datasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceWithBackend } from '@grafana/runtime';
import { Settings, SqlQuery } from '../shared/types';

// TODO - migrate template variables and macros from SqlProxyDatasource.ts
export class DataSource extends DataSourceWithBackend<SqlQuery, Settings> {
constructor(instanceSettings: DataSourceInstanceSettings<Settings>) {
super(instanceSettings);
}
}
21 changes: 12 additions & 9 deletions src/SqlProxyDatasource.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
// TODO - merge with new Datasource.ts
import _ from 'lodash';
import {
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
MutableDataFrame,
DataFrame,
guessFieldTypeFromValue,
guessFieldTypeFromValue,
FieldType,
} from '@grafana/data';

import { SqlQuery } from './types';
import { Settings, SqlQuery } from '../shared/types';
import { getBackendSrv } from '@grafana/runtime';
import { format, roundToNearestMinutes } from 'date-fns';
import { format } from 'date-fns';

export class DataSource extends DataSourceApi<SqlQuery, DataSourceJsonData> {
export class DataSource extends DataSourceApi<SqlQuery, Settings> {
/** @ngInject */
constructor(private instanceSettings: DataSourceInstanceSettings<DataSourceJsonData>, public templateSrv: any) {
constructor(private instanceSettings: DataSourceInstanceSettings<Settings>, public templateSrv: any) {
super(instanceSettings);
}

@@ -30,7 +30,10 @@ export class DataSource extends DataSourceApi<SqlQuery, DataSourceJsonData> {
options.startTime = range.from.valueOf();
options.endTime = range.to.valueOf();

const baseUrl = this.instanceSettings.url!;
let baseUrl = this.instanceSettings.url!;
if (this.instanceSettings.jsonData.backend) {
baseUrl = 'api/ds/';
}
const route = baseUrl.endsWith('/') ? 'query?' : '/query?';

const opts = this.interpolate(options);
@@ -100,7 +103,7 @@ export class DataSource extends DataSourceApi<SqlQuery, DataSourceJsonData> {
return sql;
}

applyMacroFunction(macro: string, sql: string, options: DataQueryRequest<SqlQuery>) {
applyMacroFunction(macro: string, sql: string, options: DataQueryRequest<SqlQuery>): string {
if (sql.includes(macro)) {
let time;
if (macro === '$__timeFrom(') {
@@ -127,7 +130,7 @@ export class DataSource extends DataSourceApi<SqlQuery, DataSourceJsonData> {
return getBackendSrv()
.datasourceRequest({ url })
.then(res => {
return res.data.map(v => ({ text: Object.values(v) }));
return res.data.map((v: any) => ({ text: Object.values(v) }));
});
}

43 changes: 30 additions & 13 deletions src/components/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -2,17 +2,13 @@ import React, { PureComponent } from 'react';
import { Input, LegacyForms } from '@grafana/ui';
const { FormField } = LegacyForms;

import { DataSourcePluginOptionsEditorProps, DataSourceJsonData, DataSourceSettings } from '@grafana/data';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { css, cx } from 'emotion';
import { ProxySettings, Settings } from '../../shared/types';

interface Props extends DataSourcePluginOptionsEditorProps<DataSourceJsonData> {}
interface ProxySettings extends DataSourceSettings {
host: string;
}

interface State extends DataSourceSettings {}
export type Props = DataSourcePluginOptionsEditorProps<ProxySettings | Settings>;

export class ConfigEditor extends PureComponent<Props, State> {
export class ConfigEditor extends PureComponent<Props, ProxySettings | Settings> {
isValidUrl = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/.test(
this.props.options.url
);
@@ -35,31 +31,36 @@ export class ConfigEditor extends PureComponent<Props, State> {
];

componentWillMount() {
this.setState(this.props.options);
this.setState(this.props.options.jsonData);
}

onChange(option: ProxySettings) {
const state = this.state;
const jsonData = { ...state.jsonData, ...option };
const settings = { ...state, ...option };

const { onOptionsChange, options } = this.props;
const opt = { ...options, url: option.url || options.url };
onOptionsChange({
...opt,
jsonData,
jsonData: settings,
});

this.setState({ jsonData });
this.setState(settings);
}

onToggleChange = opt => {
const on = opt.target.value === 'on';
this.onChange({ backend: on } as ProxySettings);
};

getElement(input) {
return (
<Input
type={input.type}
css={input.css}
className={input.style}
placeholder={input.placeholder}
value={this.state.jsonData[input.key]}
value={this.state[input.key]}
autoComplete={'new-password'}
onChange={event => this.onChange(({ [input.key]: event.currentTarget.value } as unknown) as ProxySettings)}
/>
@@ -121,6 +122,22 @@ export class ConfigEditor extends PureComponent<Props, State> {
/>
</div>
</div>

<h3 className="page-heading">Alerting</h3>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<LegacyForms.Switch
label="Enable"
tooltip="Enable alerting"
checked={this.state.backend || false}
onChange={this.onToggleChange}
labelClass="width-10"
switchClass="max-width-6"
/>
</div>
</div>
</div>
</div>
);
}
10 changes: 5 additions & 5 deletions src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import React, { Component } from 'react';
import { DataSource } from '../SqlProxyDatasource';
import { QueryEditorProps, DataSourceJsonData } from '@grafana/data';
import { SqlQuery } from 'types';
import { DataSource } from '../Datasource';
import { QueryEditorProps } from '@grafana/data';
import { Settings, SqlQuery } from '../../shared/types';
import { Controlled as CodeMirror } from 'react-codemirror2';

import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/darcula.css';
require('codemirror/mode/sql/sql');
import './QueryEditor.scss';

type Props = QueryEditorProps<DataSource, SqlQuery, DataSourceJsonData>;
type Props = QueryEditorProps<DataSource, SqlQuery, Settings>;

interface State {
sql: string;
}

export class QueryEditor extends Component<Props, State> {
constructor(props: QueryEditorProps<DataSource, SqlQuery, DataSourceJsonData>, context: any) {
constructor(props: QueryEditorProps<DataSource, SqlQuery, Settings>, context: any) {
super(props, context);
this.state = { sql: props.query.sql };
}
8 changes: 4 additions & 4 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { DataSourcePlugin, DataSourceJsonData } from '@grafana/data';
import { DataSourcePlugin } from '@grafana/data';
import { ConfigEditor } from './components/ConfigEditor';
import { QueryEditor } from './components/QueryEditor';
import { SqlQuery } from './types';
import { DataSource } from './SqlProxyDatasource';
import { SqlQuery, Settings } from '../shared/types';
import { DataSource } from './Datasource';

export const plugin = new DataSourcePlugin<DataSource, SqlQuery, DataSourceJsonData>(DataSource)
export const plugin = new DataSourcePlugin<DataSource, SqlQuery, Settings>(DataSource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor);
2 changes: 2 additions & 0 deletions src/plugin.json
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@
"type": "datasource",
"metrics": true,
"annotations": false,
"backend": true,
"executable": "gpx_sqlproxy",
"includes": [
],
"routes": [
5 changes: 0 additions & 5 deletions src/types.ts

This file was deleted.

19 changes: 19 additions & 0 deletions tsconfig.backend.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"sourceMap": true,
"rootDirs": [
"./shared",
"./backend",
],
"outDir": "dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"typeRoots": ["node_modules/@types", "types"]
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
"include": ["backend/**/*.ts*"]
}
7 changes: 6 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -2,8 +2,13 @@
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json",
"include": ["src", "types"],
"compilerOptions": {
"target": "ES6",
"module": "es2015",
"jsx": "react",
"rootDir": "./src",
"rootDirs": [
"./shared",
"./src",
],
"baseUrl": "./src",
"typeRoots": ["./node_modules/@types"],
"noImplicitAny": false
595 changes: 585 additions & 10 deletions yarn.lock

Large diffs are not rendered by default.