Skip to content

Commit

Permalink
feat: add full text sql query editor
Browse files Browse the repository at this point in the history
  • Loading branch information
mullerpeter committed Nov 11, 2022
1 parent c25f683 commit 4e02aaa
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 73 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- [ ] ...

Expand All @@ -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

Expand Down
Binary file added img/full_text_sql_editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"
},
Expand All @@ -24,5 +24,8 @@
},
"engines": {
"node": ">=14"
},
"dependencies": {
"@monaco-editor/react": "^4.4.6"
}
}
86 changes: 72 additions & 14 deletions pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
141 changes: 87 additions & 54 deletions src/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -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<DataSource, MyQuery, MyDataSourceOptions>;

export class QueryEditor extends PureComponent<Props> {
export function QueryEditor(props: Props) {

ontableNameChange = (event: FormEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
console.log(event);
const { onChange, query } = this.props;
const { onChange, query } = props;
onChange({ ...query, tableName: event.currentTarget.value });
};


ontimeColumnNameTextChange = (event: FormEvent<HTMLInputElement>) => {
const ontimeColumnNameTextChange = (event: FormEvent<HTMLInputElement>) => {
console.log(event);
const { onChange, query } = this.props;
const { onChange, query } = props;
onChange({ ...query, timeColumnName: event.currentTarget.value });
};


onvalueColumnNameChange = (event: FormEvent<HTMLInputElement>) => {
const onvalueColumnNameChange = (event: FormEvent<HTMLInputElement>) => {
console.log(event);
const { onChange, query } = this.props;
const { onChange, query } = props;
onChange({ ...query, valueColumnName: event.currentTarget.value });
};


onwhereQueryChange = (event: FormEvent<HTMLInputElement>) => {
const onwhereQueryChange = (event: FormEvent<HTMLInputElement>) => {
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 (
<div className="gf-form" style={{ flexDirection: "column", rowGap: "8px"}}>
{sqlEditorSelected ? (
<Editor
height="200px"
theme={theme.isDark ? "vs-dark" : "vs-light"}
defaultLanguage="sql"
defaultValue="SELECT $__time(time_column), $__value(value_column) FROM catalog.default.table_name WHERE $__timeFilter(time_column) GROUP BY $__timeWindow(time_column)"
onChange={onQuerryChange}
/>
) : (
<>
<InlineFieldRow>
<InlineField label="Time Column" labelWidth={16} tooltip="The column name of the time column">
<AutoSizeInput
value={timeColumnName || ''}
onCommitChange={ontimeColumnNameTextChange}
minWidth={32}
defaultValue="timestamp"
required
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Value Column" labelWidth={16} tooltip="The column name of the value to query">
<AutoSizeInput
value={valueColumnName || ''}
onCommitChange={onvalueColumnNameChange}
minWidth={32}
defaultValue="value"
required
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Table Name" labelWidth={16} tooltip="(Catalog), Schema and Table Name in the format (<catalog>).<schema>.<table_name>">
<AutoSizeInput
value={tableName || ''}
onCommitChange={ontableNameChange}
minWidth={32}
placeholder="catalog.default.table_name"
required
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Where SQL" labelWidth={16} tooltip="Additional WHERE conditions to filter the queried data (Databricks SQL)">
<AutoSizeInput
value={whereQuery || ''}
onCommitChange={onwhereQueryChange}
minWidth={32}
placeholder="id = 1 AND name = 'Peter'"
/>
</InlineField>
</InlineFieldRow>
</>
)}
<InlineFieldRow>
<InlineField label="Time Column" labelWidth={16} tooltip="The column name of the time column">
<AutoSizeInput
value={timeColumnName || ''}
onCommitChange={this.ontimeColumnNameTextChange}
minWidth={32}
defaultValue="timestamp"
required
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Value Column" labelWidth={16} tooltip="The column name of the value to query">
<AutoSizeInput
value={valueColumnName || ''}
onCommitChange={this.onvalueColumnNameChange}
minWidth={32}
defaultValue="value"
required
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Table Name" labelWidth={16} tooltip="Schema and Table Name in the format <schema>.<table_name>">
<AutoSizeInput
value={tableName || ''}
onCommitChange={this.ontableNameChange}
minWidth={32}
placeholder="default.table_name"
required
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Where SQL" labelWidth={16} tooltip="Additional WHERE conditions to filter the queried data (Databricks SQL)">
<AutoSizeInput
value={whereQuery || ''}
onCommitChange={this.onwhereQueryChange}
minWidth={32}
placeholder="id = 1 AND name = 'Peter'"
<InlineField label="SQL Editor" labelWidth={16}>
<InlineSwitch
value={sqlEditorSelected}
onChange={onSQLSwitchChange}
/>
</InlineField>
</InlineFieldRow>
</div>
);
}
}
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ export interface MyQuery extends DataQuery {
whereQuery?: string;
tableName: string;
withStreaming: boolean;
rawSqlQuery?: string;
rawSqlSelected: boolean;
}

export const defaultQuery: Partial<MyQuery> = {
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)"
};

/**
Expand Down
10 changes: 9 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand All @@ -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/[email protected]":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
Expand Down

0 comments on commit 4e02aaa

Please sign in to comment.