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 <galen.kistler@grafana.com>
This commit is contained in:
ismail simsek 2023-08-02 20:04:16 +03:00 committed by GitHub
parent 9c6a9a3977
commit d333c09418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 265 additions and 55 deletions

View File

@ -17,10 +17,10 @@ type queryModel struct {
// to [(*FlightSQLDatasource).QueryData]. // to [(*FlightSQLDatasource).QueryData].
type queryRequest struct { type queryRequest struct {
RefID string `json:"refId"` RefID string `json:"refId"`
RawQuery string `json:"query"` RawQuery string `json:"rawSql"`
IntervalMilliseconds int `json:"intervalMs"` IntervalMilliseconds int `json:"intervalMs"`
MaxDataPoints int64 `json:"maxDataPoints"` MaxDataPoints int64 `json:"maxDataPoints"`
Format string `json:"resultFormat"` Format string `json:"format"`
} }
func getQueryModel(dataQuery backend.DataQuery) (*queryModel, error) { func getQueryModel(dataQuery backend.DataQuery) (*queryModel, error) {

View File

@ -124,7 +124,7 @@ func CheckSQLHealth(ctx context.Context, dsInfo *models.DatasourceInfo, req *bac
Queries: []backend.DataQuery{ Queries: []backend.DataQuery{
{ {
RefID: refID, RefID: refID,
JSON: []byte(`{ "query": "select 1", "resultFormat": "table" }`), JSON: []byte(`{ "rawSql": "select 1", "format": "table" }`),
Interval: 1 * time.Minute, Interval: 1 * time.Minute,
MaxDataPoints: 423, MaxDataPoints: 423,
TimeRange: backend.TimeRange{ TimeRange: backend.TimeRange{

View File

@ -17,7 +17,7 @@ export function applyQueryDefaults(q?: SQLQuery): SQLQuery {
format: q?.format !== undefined ? q.format : QueryFormat.Table, format: q?.format !== undefined ? q.format : QueryFormat.Table,
rawSql: q?.rawSql || '', rawSql: q?.rawSql || '',
editorMode, editorMode,
sql: q?.sql || { sql: q?.sql ?? {
columns: [createFunctionField()], columns: [createFunctionField()],
groupBy: [setGroupByField()], groupBy: [setGroupByField()],
limit: 50, limit: 50,

View File

@ -24,7 +24,7 @@ export const QueryEditor = ({ query, onChange, onRunQuery, datasource }: Props)
</div> </div>
); );
case InfluxVersion.SQL: case InfluxVersion.SQL:
return <FSQLEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />; return <FSQLEditor datasource={datasource} query={query} onChange={onChange} onRunQuery={onRunQuery} />;
case InfluxVersion.InfluxQL: case InfluxVersion.InfluxQL:
default: default:
return ( return (

View File

@ -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'; import { InfluxQuery } from '../../../../types';
type Props = { import { FlightSQLDatasource } from './FlightSQLDatasource';
interface Props extends Themeable2 {
onChange: (query: InfluxQuery) => void; onChange: (query: InfluxQuery) => void;
onRunQuery: () => void; onRunQuery: () => void;
query: InfluxQuery; query: InfluxQuery;
}; datasource: InfluxDatasource;
}
// Flight SQL Editor class UnthemedSQLQueryEditor extends PureComponent<Props> {
export const FSQLEditor = (props: Props) => { datasource: FlightSQLDatasource;
const onSQLQueryChange = (query?: string) => {
if (query) { constructor(props: Props) {
props.onChange({ ...props.query, query, resultFormat: 'table' }); 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,
});
} }
props.onRunQuery();
transformQuery(query: InfluxQuery & SQLQuery): SQLQuery {
return {
...query,
}; };
return ( }
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 = (
<div> <div>
<Input Type: <i>ctrl+space</i> to show template variable suggestions <br />
value={props.query.query} Many queries can be copied from Chronograf
onBlur={(e) => onSQLQueryChange(e.currentTarget.value)}
onChange={(e) => onSQLQueryChange(e.currentTarget.value)}
/>
<br />
<button onClick={() => onSQLQueryChange()}>run query</button>
</div> </div>
); );
};
return (
<>
<Alert title="Warning" severity="warning">
InfluxDB SQL support is currently in alpha state. It does not have all the features.
</Alert>
<SqlQueryEditor
datasource={this.datasource}
query={this.transformQuery(query)}
onRunQuery={onRunSQLQuery}
onChange={onSQLChange}
/>
<div className={cx('gf-form-inline', styles.editorActions)}>
<LinkButton
icon="external-link-alt"
variant="secondary"
target="blank"
href="https://docs.influxdata.com/influxdb/cloud-serverless/query-data/sql/"
>
SQL language syntax
</LinkButton>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow"></div>
</div>
<InlineFormLabel width={5} tooltip={helpTooltip}>
Help
</InlineFormLabel>
</div>
</>
);
}
}
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);

View File

@ -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<MySQLOptions>) {
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<string[]> {
const datasets = await this.runSql<string[]>(showDatabases(), { refId: 'datasets' });
return datasets.map((t) => quoteIdentifierIfNecessary(t[0]));
}
async fetchTables(dataset?: string): Promise<string[]> {
const query = buildTableQuery(dataset);
const tables = await this.runSql<string[]>(query, { refId: 'tables' });
return tables.map((t) => quoteIdentifierIfNecessary(t[0]));
}
async fetchFields(query: Partial<SQLQuery>) {
if (!query.dataset || !query.table) {
return [];
}
const queryString = buildColumnQuery(query.table, query.dataset);
const frame = await this.runSql<string[]>(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(),
};
}
}

View File

@ -19,7 +19,9 @@ export default class VariableQueryEditor extends PureComponent<Props> {
render() { render() {
let { query, datasource, onChange } = this.props; let { query, datasource, onChange } = this.props;
if (datasource.version === InfluxVersion.Flux) {
switch (datasource.version) {
case InfluxVersion.Flux:
return ( return (
<FluxQueryEditor <FluxQueryEditor
datasource={datasource} datasource={datasource}
@ -31,8 +33,13 @@ export default class VariableQueryEditor extends PureComponent<Props> {
onChange={(v) => onChange(v.query)} onChange={(v) => onChange(v.query)}
/> />
); );
} //@todo add support for SQL
case InfluxVersion.SQL:
return <div className="gf-form-inline">TODO</div>;
// Influx/default case
case InfluxVersion.InfluxQL:
default:
return ( return (
<div className="gf-form-inline"> <div className="gf-form-inline">
<InlineFormLabel width={10}>Query</InlineFormLabel> <InlineFormLabel width={10}>Query</InlineFormLabel>
@ -48,4 +55,5 @@ export default class VariableQueryEditor extends PureComponent<Props> {
</div> </div>
); );
} }
}
} }