diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go index 12634db..7080357 100644 --- a/pkg/plugin/datasource.go +++ b/pkg/plugin/datasource.go @@ -73,6 +73,7 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques frame.SetMeta(&data.FrameMeta{Channel: channel.String()}) resp.Frames = append(resp.Frames, frame) } else { + count := 0 columnTypes, ch, err := d.engine.RunQuery(ctx, q.SQL) if err != nil { return nil, err @@ -89,7 +90,7 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques return nil, ctx.Err() case row, ok := <-ch: if !ok { - logger.Info("Query finished") + logger.Info("Query finished", "count", count) resp.Frames = append(resp.Frames, frame) break LOOP @@ -99,6 +100,7 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques for i, r := range row { col := columnTypes[i] fData[i] = timeplus.ParseValue(col.Name(), col.DatabaseTypeName(), nil, r, false) + count++ } frame.AppendRow(fData...) @@ -206,7 +208,6 @@ func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRe res.Message = "'Host' cannot be empty" return res, nil } - engine := timeplus.NewEngine(logger, config.Host, config.TCPPort, config.HTTPPort, config.Username, config.Secrets.Password) if err := engine.Ping(ctx); err != nil { diff --git a/pkg/timeplus/engine.go b/pkg/timeplus/engine.go index 6499e14..b755904 100644 --- a/pkg/timeplus/engine.go +++ b/pkg/timeplus/engine.go @@ -37,6 +37,16 @@ type TimeplusEngine struct { } func NewEngine(logger log.Logger, host string, tcpPort, httpPort int, username, password string) *TimeplusEngine { + if tcpPort == 0 { + tcpPort = 8463 + } + if httpPort == 0 { + httpPort = 3218 + } + if len(username) == 0 { + username = "default" + } + connection := protonDriver.OpenDB(&protonDriver.Options{ Addr: []string{fmt.Sprintf("%s:%d", host, tcpPort)}, Auth: protonDriver.Auth{ @@ -150,7 +160,16 @@ func (e *TimeplusEngine) IsStreamingQuery(ctx context.Context, query string) (bo } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 399 { - return false, fmt.Errorf("failed to analyze %d", resp.StatusCode) + var errStr string + + body, err := io.ReadAll(resp.Body) + if err != nil { + errStr = err.Error() + } else { + errStr = string(body) + } + + return false, fmt.Errorf("failed to analyze code: %d, error: %s", resp.StatusCode, errStr) } body, err := io.ReadAll(resp.Body) diff --git a/provisioning/dashboards/carsharing.json b/provisioning/dashboards/carsharing.json index c157b2b..5c84e2b 100644 --- a/provisioning/dashboards/carsharing.json +++ b/provisioning/dashboards/carsharing.json @@ -22,6 +22,70 @@ "links": [], "liveNow": false, "panels": [ + { + "datasource": { + "default": true, + "type": "timeplus-proton-datasource", + "uid": "proton-ds" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.2", + "targets": [ + { + "datasource": { + "type": "timeplus-proton-datasource", + "uid": "proton-ds" + }, + "refId": "A", + "sql": "select count(*) from table(bookings) where _tp_time < to_datetime($__to/1000) and _tp_time > to_datetime($__from/1000) and cid='$cid'" + } + ], + "title": "Total bookings for car $cid between $__from and $__to", + "type": "stat" + }, { "datasource": { "type": "timeplus-proton-datasource", @@ -57,7 +121,7 @@ "h": 6, "w": 3, "x": 0, - "y": 0 + "y": 8 }, "id": 1, "options": { @@ -70,7 +134,7 @@ }, "showHeader": true }, - "pluginVersion": "10.2.3", + "pluginVersion": "11.2.2", "targets": [ { "addNow": false, @@ -78,8 +142,8 @@ "type": "timeplus-proton-datasource", "uid": "proton-ds" }, - "sql": "select now()", - "refId": "A" + "refId": "A", + "sql": "select now()" } ], "title": "select now()", @@ -112,7 +176,7 @@ "h": 6, "w": 7, "x": 3, - "y": 0 + "y": 8 }, "id": 2, "options": { @@ -120,15 +184,17 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.2.3", + "pluginVersion": "11.2.2", "targets": [ { "addNow": false, @@ -136,8 +202,8 @@ "type": "timeplus-proton-datasource", "uid": "proton-ds" }, - "sql": "select count() from car_live_data", - "refId": "A" + "refId": "A", + "sql": "select count() from car_live_data" } ], "title": "select count() from car_live_data", @@ -170,7 +236,7 @@ "h": 6, "w": 14, "x": 10, - "y": 0 + "y": 8 }, "id": 3, "options": { @@ -178,15 +244,17 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.2.3", + "pluginVersion": "11.2.2", "targets": [ { "addNow": false, @@ -194,8 +262,8 @@ "type": "timeplus-proton-datasource", "uid": "proton-ds" }, - "sql": "select count() from car_live_data where _tp_time>earliest_ts()", - "refId": "A" + "refId": "A", + "sql": "select count() from car_live_data where _tp_time>earliest_ts()" } ], "title": "select count() from car_live_data where _tp_time>earliest_ts()", @@ -239,7 +307,7 @@ "h": 9, "w": 24, "x": 0, - "y": 6 + "y": 14 }, "id": 4, "options": { @@ -307,7 +375,7 @@ "zoom": 12 } }, - "pluginVersion": "10.2.3", + "pluginVersion": "11.2.2", "targets": [ { "addNow": false, @@ -315,8 +383,8 @@ "type": "timeplus-proton-datasource", "uid": "proton-ds" }, - "sql": "select longitude, latitude,speed_kmh,cid from car_live_data where cid like '%1'", - "refId": "A" + "refId": "A", + "sql": "select longitude, latitude,speed_kmh,cid from car_live_data where cid like '%1'" } ], "title": "Panel Title", @@ -327,16 +395,43 @@ "schemaVersion": 39, "tags": [], "templating": { - "list": [] + "list": [ + { + "current": { + "selected": true, + "text": "c00000", + "value": "c00000" + }, + "datasource": { + "type": "timeplus-proton-datasource", + "uid": "proton-ds" + }, + "definition": "select cid from table(car_live_data) where _tp_time < to_datetime($__to/1000) and _tp_time > to_datetime($__from/1000)", + "hide": 0, + "includeAll": false, + "label": "Car ID", + "multi": false, + "name": "cid", + "options": [], + "query": { + "query": "select cid from table(car_live_data) where _tp_time < to_datetime($__to/1000) and _tp_time > to_datetime($__from/1000)" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] }, "time": { - "from": "now-6h", + "from": "now-1h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Carsharing Demo Dashboard", "uid": "dd9e271d-7225-47d4-9e57-62133db0df62", - "version": 4, + "version": 1, "weekStart": "" } diff --git a/src/components/ConfigEditor.tsx b/src/components/ConfigEditor.tsx index 83746ff..359006e 100644 --- a/src/components/ConfigEditor.tsx +++ b/src/components/ConfigEditor.tsx @@ -1,8 +1,9 @@ -import React, { ChangeEvent } from 'react'; import { InlineField, Input, SecretInput } from '@grafana/ui'; -import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import React, { ChangeEvent } from 'react'; import { TpDataSourceOptions, TpSecureJsonData } from '../types'; +import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; + interface Props extends DataSourcePluginOptionsEditorProps {} export function ConfigEditor(props: Props) { @@ -75,8 +76,9 @@ export function ConfigEditor(props: Props) { return ( <> - + diff --git a/src/components/QueryEditor.tsx b/src/components/QueryEditor.tsx index 3c8f126..926af26 100644 --- a/src/components/QueryEditor.tsx +++ b/src/components/QueryEditor.tsx @@ -1,9 +1,10 @@ -import React from 'react'; -import { InlineField, Stack, CodeEditor } from '@grafana/ui'; -import { QueryEditorProps } from '@grafana/data'; -import { DataSource } from '../datasource'; +import { CodeEditor, InlineField, Stack } from '@grafana/ui'; import { TpDataSourceOptions, TpQuery } from '../types'; +import { DataSource } from '../datasource'; +import { QueryEditorProps } from '@grafana/data'; +import React from 'react'; + type Props = QueryEditorProps; export function QueryEditor({ query, onChange }: Props) { @@ -11,7 +12,6 @@ export function QueryEditor({ query, onChange }: Props) { onChange({ ...query, sql: sql }); }; - const { sql } = query; return ( diff --git a/src/components/VariableQueryEditor.tsx b/src/components/VariableQueryEditor.tsx new file mode 100644 index 0000000..ffe412b --- /dev/null +++ b/src/components/VariableQueryEditor.tsx @@ -0,0 +1,36 @@ +import { CodeEditor, Field } from '@grafana/ui'; +import React, { useState } from 'react'; + +import { VariableQueryProps } from '../types'; + +export const VariableQueryEditor = ({ onChange, query }: VariableQueryProps) => { + const [state, setState] = useState(query); + + const saveQuery = () => { + onChange(state, `${state.query}`); + }; + + const onSQLChange = (sql: string) => { + setState({ + query: sql, + }); + }; + + return ( + + + + ); +}; diff --git a/src/datasource.ts b/src/datasource.ts index 86284cd..4987cf3 100644 --- a/src/datasource.ts +++ b/src/datasource.ts @@ -1,7 +1,6 @@ -import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; +import { DataQueryRequest, DataSourceInstanceSettings, MetricFindValue, ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; - -import { TpQuery, TpDataSourceOptions } from './types'; +import { TpDataSourceOptions, TpQuery, TpVariableQuery } from './types'; export class DataSource extends DataSourceWithBackend { constructor(instanceSettings: DataSourceInstanceSettings) { @@ -9,10 +8,52 @@ export class DataSource extends DataSourceWithBackend((resolve) => { + const req = { + targets: [{ datasource: options.variable.datasource, + sql: query.query, + refId: String(Math.random()) }], + range: options ? options.range : (getTemplateSrv() as any).timeRange, + } as DataQueryRequest ; + + this.query(req).subscribe((res) => { + const result = res.data[0] || { fields: [] } + + + if (result.fields.length === 2) { + for (let i = 0; i < result.fields[0].values.length; i++) { + metrics.push({ + text: result.fields[1].values[i], + value: result.fields[0].values[i] + }) + } + } else if (result.fields.length === 1) { + metrics = result.fields[0].values.map((v: string) => { + return { + text: v, + value: v + }}); + } + + resolve(metrics); + }); + }) + + return prom + } + applyTemplateVariables(query: TpQuery, scopedVars: ScopedVars) { + const srv = getTemplateSrv() + const sql = srv.replace(query.sql, scopedVars) return { ...query, - sql: getTemplateSrv().replace(query.sql, scopedVars), + sql: sql, }; } diff --git a/src/module.ts b/src/module.ts index d3173f3..a1d9b96 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,9 +1,12 @@ -import { DataSourcePlugin } from '@grafana/data'; -import { DataSource } from './datasource'; +import { TpDataSourceOptions, TpQuery } from './types'; + import { ConfigEditor } from './components/ConfigEditor'; +import { DataSource } from './datasource'; +import { DataSourcePlugin } from '@grafana/data'; import { QueryEditor } from './components/QueryEditor'; -import { TpQuery, TpDataSourceOptions } from './types'; +import { VariableQueryEditor } from './components/VariableQueryEditor'; export const plugin = new DataSourcePlugin(DataSource) .setConfigEditor(ConfigEditor) - .setQueryEditor(QueryEditor); + .setQueryEditor(QueryEditor) + .setVariableQueryEditor(VariableQueryEditor) diff --git a/src/types.ts b/src/types.ts index b9df226..7feed9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ -import { DataSourceJsonData } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; +import { DataSourceJsonData } from '@grafana/data'; export interface TpQuery extends DataQuery { sql: string; @@ -21,3 +21,12 @@ export interface TpDataSourceOptions extends DataSourceJsonData { export interface TpSecureJsonData { password?: string; } + +export interface TpVariableQuery { + query: string; +} + +export interface VariableQueryProps { + query: TpVariableQuery; + onChange: (query: TpVariableQuery, definition: string) => void; +}