diff --git a/README.md b/README.md index 01415a5..0e01b31 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Grafana Databricks integration allowing direct connection to Databricks to query The plugin is still work in progress and currently only offers limited functionality. #### TODO: +- [x] Full-text query editor - [ ] Add GROUP BY to query form - [ ] Allow multiple metrics query -- [ ] Full-text query editor - [ ] Autocomplete - [ ] ... @@ -28,7 +28,11 @@ allow_loading_unsigned_plugins = mullerpeter-databricks-datasource ![img.png](img/querry_editor.png) -At the moment only simple queries for one value over time are implemented. The Time-range and TimeBucket parameters from Grafana are automatically inserted into the query. +At the moment only simple queries for one value over time are implemented. The Time-range and TimeBucket parameters from Grafana are automatically inserted into the query. + +![img.png](img/full_text_sql_editor.png) + +When using the raw SQL editor, template variables are replaced by the Time-Range and Bucket (see default query in editor for example). ## Plugin Configuration diff --git a/img/full_text_sql_editor.png b/img/full_text_sql_editor.png new file mode 100644 index 0000000..15502ea Binary files /dev/null and b/img/full_text_sql_editor.png differ diff --git a/package.json b/package.json index 1a154db..2a874c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "climeworks-databricks", - "version": "0.0.3", + "version": "0.0.4", "description": "Databricks SQL Connector", "scripts": { "build": "grafana-toolkit plugin:build", @@ -14,8 +14,8 @@ "license": "Apache-2.0", "devDependencies": { "@grafana/data": "latest", - "@grafana/toolkit": "latest", "@grafana/runtime": "latest", + "@grafana/toolkit": "latest", "@grafana/ui": "latest", "@types/lodash": "latest" }, @@ -24,5 +24,8 @@ }, "engines": { "node": ">=14" + }, + "dependencies": { + "@monaco-editor/react": "^4.4.6" } } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index e1e90cf..500aaeb 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -6,13 +6,13 @@ import ( "encoding/json" "fmt" _ "github.com/databricks/databricks-sql-go" - "time" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/live" + "regexp" + "time" ) // Make sure SampleDatasource implements required interfaces. This is important to do @@ -89,6 +89,8 @@ type queryModel struct { ValueColumnName string `json:"valueColumnName"` WhereQuery string `json:"whereQuery"` TableName string `json:"tableName"` + RawSqlQuery string `json:"rawSqlQuery"` + RawSqlSelected bool `json:"rawSqlSelected"` } func (d *SampleDatasource) query(_ context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse { @@ -109,20 +111,76 @@ func (d *SampleDatasource) query(_ context.Context, pCtx backend.PluginContext, if seconds_interval <= 0 { seconds_interval = 1 } + queryString := "" + if qm.RawSqlSelected { + queryString = qm.RawSqlQuery + log.DefaultLogger.Info("Raw SQL Query selected", "query", queryString) + + var rgx = regexp.MustCompile(`\$__timeWindow\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__timeWindow placeholder found") + rs := rgx.FindStringSubmatch(queryString) + timeColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("window(%s, '%d SECONDS')", timeColumnName, seconds_interval)) + + rgx = regexp.MustCompile(`\$__time\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__time placeholder found") + queryString = rgx.ReplaceAllString(queryString, "window.start") + } + + rgx = regexp.MustCompile(`\$__value\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__value placeholder found") + rs = rgx.FindStringSubmatch(queryString) + valueColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("avg(%s) AS value", valueColumnName)) + } + } else { + rgx = regexp.MustCompile(`\$__time\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__time placeholder found") + rs := rgx.FindStringSubmatch(queryString) + timeColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("%s AS time", timeColumnName)) + } + + rgx = regexp.MustCompile(`\$__value\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__value placeholder found") + rs := rgx.FindStringSubmatch(queryString) + valueColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("%s AS value", valueColumnName)) + } + } - whereQuery := "" - if qm.WhereQuery != "" { - whereQuery = fmt.Sprintf(" %s AND", qm.WhereQuery) + rgx = regexp.MustCompile(`\$__timeFilter\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + rs := rgx.FindStringSubmatch(queryString) + timeColumnName := rs[1] + timeRangeFilter := fmt.Sprintf("%s BETWEEN '%s' AND '%s'", + timeColumnName, + query.TimeRange.From.UTC().Format("2006-01-02 15:04:05"), + query.TimeRange.To.UTC().Format("2006-01-02 15:04:05"), + ) + queryString = rgx.ReplaceAllString(queryString, timeRangeFilter) + } + + } else { + whereQuery := "" + if qm.WhereQuery != "" { + whereQuery = fmt.Sprintf(" %s AND", qm.WhereQuery) + } + queryString = fmt.Sprintf("SELECT window.start, avg(%s) AS value FROM %s WHERE%s %s BETWEEN '%s' AND '%s' GROUP BY window(%s, '%d SECONDS')", + qm.ValueColumnName, + qm.TableName, + whereQuery, + qm.TimeColumnName, + query.TimeRange.From.UTC().Format("2006-01-02 15:04:05"), + query.TimeRange.To.UTC().Format("2006-01-02 15:04:05"), + qm.TimeColumnName, + seconds_interval) } - queryString := fmt.Sprintf("SELECT window.start, avg(%s) AS value FROM %s WHERE%s %s BETWEEN '%s' AND '%s' GROUP BY window(%s, '%d SECONDS')", - qm.ValueColumnName, - qm.TableName, - whereQuery, - qm.TimeColumnName, - query.TimeRange.From.UTC().Format("2006-01-02 15:04:05"), - query.TimeRange.To.UTC().Format("2006-01-02 15:04:05"), - qm.TimeColumnName, - seconds_interval) log.DefaultLogger.Info("Query", "query", queryString) rows, err := databricksDB.Query(queryString) diff --git a/src/QueryEditor.tsx b/src/QueryEditor.tsx index 0018a19..7adee86 100644 --- a/src/QueryEditor.tsx +++ b/src/QueryEditor.tsx @@ -1,92 +1,125 @@ import { defaults } from 'lodash'; -import React, { PureComponent, FormEvent } from 'react'; -import { AutoSizeInput, InlineFieldRow, InlineField } from '@grafana/ui'; +import React, {FormEvent, useState} from 'react'; +import { AutoSizeInput, InlineFieldRow, InlineField, useTheme, InlineSwitch } from '@grafana/ui'; import { QueryEditorProps } from '@grafana/data'; import { DataSource } from './datasource'; +import Editor from "@monaco-editor/react"; import { defaultQuery, MyDataSourceOptions, MyQuery } from './types'; +import * as monaco from "monaco-editor"; type Props = QueryEditorProps; -export class QueryEditor extends PureComponent { +export function QueryEditor(props: Props) { - ontableNameChange = (event: FormEvent) => { + const [, setQueryText] = useState("") + const [sqlEditorSelected, setSqlEditorSelected] = useState(false) + const onQuerryChange = (value: string | undefined, ev: monaco.editor.IModelContentChangedEvent) => { + setQueryText(value || "") + const { onChange, query } = props; + onChange({ ...query, rawSqlQuery: value }); + }; + + const onSQLSwitchChange = (event: any) => { + const { onChange, query } = props; + onChange({ ...query, rawSqlSelected: !sqlEditorSelected }); + setSqlEditorSelected(!sqlEditorSelected); + }; + const ontableNameChange = (event: FormEvent) => { console.log(event); - const { onChange, query } = this.props; + const { onChange, query } = props; onChange({ ...query, tableName: event.currentTarget.value }); }; - ontimeColumnNameTextChange = (event: FormEvent) => { + const ontimeColumnNameTextChange = (event: FormEvent) => { console.log(event); - const { onChange, query } = this.props; + const { onChange, query } = props; onChange({ ...query, timeColumnName: event.currentTarget.value }); }; - onvalueColumnNameChange = (event: FormEvent) => { + const onvalueColumnNameChange = (event: FormEvent) => { console.log(event); - const { onChange, query } = this.props; + const { onChange, query } = props; onChange({ ...query, valueColumnName: event.currentTarget.value }); }; - onwhereQueryChange = (event: FormEvent) => { + const onwhereQueryChange = (event: FormEvent) => { console.log(event); - const { onChange, query } = this.props; + const { onChange, query } = props; onChange({ ...query, whereQuery: event.currentTarget.value }); }; - render() { - const query = defaults(this.props.query, defaultQuery); + const query = defaults(props.query, defaultQuery); const { timeColumnName, valueColumnName, whereQuery, tableName } = query; - + const theme = useTheme() return (
+ {sqlEditorSelected ? ( + + ) : ( + <> + + + + + + + + + + + + + + + + + + + + + + )} - - - - - - - - - - - - - - - - - +
); - } } diff --git a/src/types.ts b/src/types.ts index 88f711d..8b5614d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,12 +6,16 @@ export interface MyQuery extends DataQuery { whereQuery?: string; tableName: string; withStreaming: boolean; + rawSqlQuery?: string; + rawSqlSelected: boolean; } export const defaultQuery: Partial = { timeColumnName: "timestamp", valueColumnName: "value", withStreaming: false, + rawSqlSelected: false, + rawSqlQuery: "SELECT $__time(time_column), $__value(value_column) FROM catalog.default.table_name WHERE $__timeFilter(time_column) GROUP BY $__timeWindow(time_column)" }; /** diff --git a/yarn.lock b/yarn.lock index b6b8f52..06a9ddb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1873,7 +1873,7 @@ resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz#15651bd553a67b8581fb398810c98ad86a34524e" integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA== -"@monaco-editor/loader@^1.2.0": +"@monaco-editor/loader@^1.2.0", "@monaco-editor/loader@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.2.tgz#04effbb87052d19cd7d3c9d81c0635490f9bb6d8" integrity sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g== @@ -1888,6 +1888,14 @@ "@monaco-editor/loader" "^1.2.0" prop-types "^15.7.2" +"@monaco-editor/react@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.6.tgz#8ae500b0edf85276d860ed702e7056c316548218" + integrity sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA== + dependencies: + "@monaco-editor/loader" "^1.3.2" + prop-types "^15.7.2" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"