From d333c094180e6b0cdaa5d7d9a20adefdb1bf6ed6 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 2 Aug 2023 20:04:16 +0300 Subject: [PATCH] InfluxDB: SQL Query Editor (#72168) * Add influxdbSqlSupport feature toggle * Add SQL option to the config page * Add SQL backend * Add metadata support in config page * Implement unified querying * Fix healthcheck query * fsql tests * secure grpc by default * code cleanup * Query handing for sql mode * Implement a placeholder sql editor * Fix query language dropdown * drop in SQL editor * switch to use rawSql, get sql editor working * fix healthcheck * WIP * memoize component to stop unwanted rerender onQuery * dont reinit datasource on each render of the editor * remove useless memo * clean up * Fix the link * Alpha state warning * Remove console.logs * update model for fsql * remove unused --------- Co-authored-by: Galen --- pkg/tsdb/influxdb/fsql/query_model.go | 4 +- pkg/tsdb/influxdb/healthcheck.go | 2 +- public/app/features/plugins/sql/defaults.ts | 2 +- .../components/editor/query/QueryEditor.tsx | 2 +- .../editor/query/fsql/FSQLEditor.tsx | 145 +++++++++++++++--- .../editor/query/fsql/FlightSQLDatasource.ts | 105 +++++++++++++ .../editor/variable/VariableQueryEditor.tsx | 60 ++++---- 7 files changed, 265 insertions(+), 55 deletions(-) create mode 100644 public/app/plugins/datasource/influxdb/components/editor/query/fsql/FlightSQLDatasource.ts diff --git a/pkg/tsdb/influxdb/fsql/query_model.go b/pkg/tsdb/influxdb/fsql/query_model.go index 13cc4778976..46128708102 100644 --- a/pkg/tsdb/influxdb/fsql/query_model.go +++ b/pkg/tsdb/influxdb/fsql/query_model.go @@ -17,10 +17,10 @@ type queryModel struct { // to [(*FlightSQLDatasource).QueryData]. type queryRequest struct { RefID string `json:"refId"` - RawQuery string `json:"query"` + RawQuery string `json:"rawSql"` IntervalMilliseconds int `json:"intervalMs"` MaxDataPoints int64 `json:"maxDataPoints"` - Format string `json:"resultFormat"` + Format string `json:"format"` } func getQueryModel(dataQuery backend.DataQuery) (*queryModel, error) { diff --git a/pkg/tsdb/influxdb/healthcheck.go b/pkg/tsdb/influxdb/healthcheck.go index b37500a0b27..ecf6d358a3b 100644 --- a/pkg/tsdb/influxdb/healthcheck.go +++ b/pkg/tsdb/influxdb/healthcheck.go @@ -124,7 +124,7 @@ func CheckSQLHealth(ctx context.Context, dsInfo *models.DatasourceInfo, req *bac Queries: []backend.DataQuery{ { RefID: refID, - JSON: []byte(`{ "query": "select 1", "resultFormat": "table" }`), + JSON: []byte(`{ "rawSql": "select 1", "format": "table" }`), Interval: 1 * time.Minute, MaxDataPoints: 423, TimeRange: backend.TimeRange{ diff --git a/public/app/features/plugins/sql/defaults.ts b/public/app/features/plugins/sql/defaults.ts index e67a4128acb..4532bdcc3a6 100644 --- a/public/app/features/plugins/sql/defaults.ts +++ b/public/app/features/plugins/sql/defaults.ts @@ -17,7 +17,7 @@ export function applyQueryDefaults(q?: SQLQuery): SQLQuery { format: q?.format !== undefined ? q.format : QueryFormat.Table, rawSql: q?.rawSql || '', editorMode, - sql: q?.sql || { + sql: q?.sql ?? { columns: [createFunctionField()], groupBy: [setGroupByField()], limit: 50, diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/QueryEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/QueryEditor.tsx index a5a646688a9..8c639067a2c 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/QueryEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/QueryEditor.tsx @@ -24,7 +24,7 @@ export const QueryEditor = ({ query, onChange, onRunQuery, datasource }: Props) ); case InfluxVersion.SQL: - return ; + return ; case InfluxVersion.InfluxQL: default: return ( diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/fsql/FSQLEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/fsql/FSQLEditor.tsx index 362101ec2d6..041e5933bd8 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/fsql/FSQLEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/fsql/FSQLEditor.tsx @@ -1,32 +1,129 @@ -import React from 'react'; +import { css, cx } from '@emotion/css'; +import React, { PureComponent } from 'react'; -import { Input } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data/src'; +import { Alert, InlineFormLabel, LinkButton, Themeable2, withTheme2 } from '@grafana/ui/src'; +import { SQLQuery } from '../../../../../../../features/plugins/sql'; +import { SqlQueryEditor } from '../../../../../../../features/plugins/sql/components/QueryEditor'; +import InfluxDatasource from '../../../../datasource'; import { InfluxQuery } from '../../../../types'; -type Props = { +import { FlightSQLDatasource } from './FlightSQLDatasource'; + +interface Props extends Themeable2 { onChange: (query: InfluxQuery) => void; onRunQuery: () => void; query: InfluxQuery; -}; + datasource: InfluxDatasource; +} -// Flight SQL Editor -export const FSQLEditor = (props: Props) => { - const onSQLQueryChange = (query?: string) => { - if (query) { - props.onChange({ ...props.query, query, resultFormat: 'table' }); - } - props.onRunQuery(); - }; - return ( -
- onSQLQueryChange(e.currentTarget.value)} - onChange={(e) => onSQLQueryChange(e.currentTarget.value)} - /> -
- -
- ); -}; +class UnthemedSQLQueryEditor extends PureComponent { + datasource: FlightSQLDatasource; + + constructor(props: Props) { + super(props); + const { datasource: influxDatasource } = props; + + this.datasource = new FlightSQLDatasource({ + url: influxDatasource.urls[0], + access: influxDatasource.access, + id: influxDatasource.id, + + jsonData: { + // Not applicable to flightSQL? @itsmylife + allowCleartextPasswords: false, + tlsAuth: false, + tlsAuthWithCACert: false, + tlsSkipVerify: false, + maxIdleConns: 1, + maxOpenConns: 1, + maxIdleConnsAuto: true, + connMaxLifetime: 1, + timezone: '', + user: '', + database: '', + url: influxDatasource.urls[0], + timeInterval: '', + }, + meta: influxDatasource.meta, + name: influxDatasource.name, + readOnly: false, + type: influxDatasource.type, + uid: influxDatasource.uid, + }); + } + + transformQuery(query: InfluxQuery & SQLQuery): SQLQuery { + return { + ...query, + }; + } + + render() { + const { query, theme, onRunQuery, onChange } = this.props; + const styles = getStyles(theme); + + const onRunSQLQuery = () => { + return onRunQuery(); + }; + + const onSQLChange = (query: SQLQuery) => { + // query => rawSql for now + onChange({ ...query }); + }; + + const helpTooltip = ( +
+ Type: ctrl+space to show template variable suggestions
+ Many queries can be copied from Chronograf +
+ ); + + return ( + <> + + InfluxDB SQL support is currently in alpha state. It does not have all the features. + + +
+ + SQL language syntax + +
+
+
+ + Help + +
+ + ); + } +} + +const getStyles = (theme: GrafanaTheme2) => ({ + editorContainerStyles: css` + height: 200px; + max-width: 100%; + resize: vertical; + overflow: auto; + background-color: ${theme.isDark ? theme.colors.background.canvas : theme.colors.background.primary}; + padding-bottom: ${theme.spacing(1)}; + `, + editorActions: css` + margin-top: 6px; + `, +}); + +export const FSQLEditor = withTheme2(UnthemedSQLQueryEditor); diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/fsql/FlightSQLDatasource.ts b/public/app/plugins/datasource/influxdb/components/editor/query/fsql/FlightSQLDatasource.ts new file mode 100644 index 00000000000..3a5217879c4 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/components/editor/query/fsql/FlightSQLDatasource.ts @@ -0,0 +1,105 @@ +import { DataSourceInstanceSettings, TimeRange } from '@grafana/data'; +import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental'; +import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource'; +import { DB, SQLQuery } from 'app/features/plugins/sql/types'; +import { formatSQL } from 'app/features/plugins/sql/utils/formatSQL'; + +// @todo These are being imported for PoC, but should probably be reimplemented within the influx datasource? +import { mapFieldsToTypes } from '../../../../../mysql/fields'; +import { buildColumnQuery, buildTableQuery, showDatabases } from '../../../../../mysql/mySqlMetaQuery'; +import { getSqlCompletionProvider } from '../../../../../mysql/sqlCompletionProvider'; +import { quoteIdentifierIfNecessary, quoteLiteral, toRawSql } from '../../../../../mysql/sqlUtil'; +import { MySQLOptions } from '../../../../../mysql/types'; + +export class FlightSQLDatasource extends SqlDatasource { + sqlLanguageDefinition: LanguageDefinition | undefined; + + constructor(private instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + } + + getQueryModel() { + return { quoteLiteral }; + } + + getSqlLanguageDefinition(): LanguageDefinition { + if (this.sqlLanguageDefinition !== undefined) { + return this.sqlLanguageDefinition; + } + + const args = { + getMeta: (identifier?: TableIdentifier) => this.fetchMeta(identifier), + }; + this.sqlLanguageDefinition = { + id: 'mysql', + completionProvider: getSqlCompletionProvider(args), + formatter: formatSQL, + }; + return this.sqlLanguageDefinition; + } + + async fetchDatasets(): Promise { + const datasets = await this.runSql(showDatabases(), { refId: 'datasets' }); + return datasets.map((t) => quoteIdentifierIfNecessary(t[0])); + } + + async fetchTables(dataset?: string): Promise { + const query = buildTableQuery(dataset); + const tables = await this.runSql(query, { refId: 'tables' }); + return tables.map((t) => quoteIdentifierIfNecessary(t[0])); + } + + async fetchFields(query: Partial) { + if (!query.dataset || !query.table) { + return []; + } + const queryString = buildColumnQuery(query.table, query.dataset); + const frame = await this.runSql(queryString, { refId: 'fields' }); + const fields = frame.map((f) => ({ + name: f[0], + text: f[0], + value: quoteIdentifierIfNecessary(f[0]), + type: f[1], + label: f[0], + })); + return mapFieldsToTypes(fields); + } + + async fetchMeta(identifier?: TableIdentifier) { + const defaultDB = this.instanceSettings.jsonData.database; + if (!identifier?.schema && defaultDB) { + const tables = await this.fetchTables(defaultDB); + return tables.map((t) => ({ name: t, completion: `${defaultDB}.${t}`, kind: CompletionItemKind.Class })); + } else if (!identifier?.schema && !defaultDB) { + const datasets = await this.fetchDatasets(); + return datasets.map((d) => ({ name: d, completion: `${d}.`, kind: CompletionItemKind.Module })); + } else { + if (!identifier?.table && (!defaultDB || identifier?.schema)) { + const tables = await this.fetchTables(identifier?.schema); + return tables.map((t) => ({ name: t, completion: t, kind: CompletionItemKind.Class })); + } else if (identifier?.table && identifier.schema) { + const fields = await this.fetchFields({ dataset: identifier.schema, table: identifier.table }); + return fields.map((t) => ({ name: t.name, completion: t.value, kind: CompletionItemKind.Field })); + } else { + return []; + } + } + } + + getDB(): DB { + if (this.db !== undefined) { + return this.db; + } + return { + datasets: () => this.fetchDatasets(), + tables: (dataset?: string) => this.fetchTables(dataset), + fields: (query: SQLQuery) => this.fetchFields(query), + validateQuery: (query: SQLQuery, range?: TimeRange) => + Promise.resolve({ query, error: '', isError: false, isValid: true }), + dsID: () => this.id, + toRawSql, + functions: () => ['VARIANCE', 'STDDEV'], + getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(), + }; + } +} diff --git a/public/app/plugins/datasource/influxdb/components/editor/variable/VariableQueryEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/variable/VariableQueryEditor.tsx index aa3536da533..1df0f2ff78b 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/variable/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/variable/VariableQueryEditor.tsx @@ -19,33 +19,41 @@ export default class VariableQueryEditor extends PureComponent { render() { let { query, datasource, onChange } = this.props; - if (datasource.version === InfluxVersion.Flux) { - return ( - onChange(v.query)} - /> - ); - } - return ( -
- Query -
-