mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 09:26:43 -06:00
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:
parent
9c6a9a3977
commit
d333c09418
@ -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) {
|
||||||
|
@ -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{
|
||||||
|
@ -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,
|
||||||
|
@ -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 (
|
||||||
|
@ -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;
|
||||||
props.onRunQuery();
|
|
||||||
};
|
this.datasource = new FlightSQLDatasource({
|
||||||
return (
|
url: influxDatasource.urls[0],
|
||||||
<div>
|
access: influxDatasource.access,
|
||||||
<Input
|
id: influxDatasource.id,
|
||||||
value={props.query.query}
|
|
||||||
onBlur={(e) => onSQLQueryChange(e.currentTarget.value)}
|
jsonData: {
|
||||||
onChange={(e) => onSQLQueryChange(e.currentTarget.value)}
|
// Not applicable to flightSQL? @itsmylife
|
||||||
/>
|
allowCleartextPasswords: false,
|
||||||
<br />
|
tlsAuth: false,
|
||||||
<button onClick={() => onSQLQueryChange()}>run query</button>
|
tlsAuthWithCACert: false,
|
||||||
</div>
|
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 = (
|
||||||
|
<div>
|
||||||
|
Type: <i>ctrl+space</i> to show template variable suggestions <br />
|
||||||
|
Many queries can be copied from Chronograf
|
||||||
|
</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);
|
||||||
|
@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -19,33 +19,41 @@ 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) {
|
|
||||||
return (
|
|
||||||
<FluxQueryEditor
|
|
||||||
datasource={datasource}
|
|
||||||
query={{
|
|
||||||
refId: 'A',
|
|
||||||
query,
|
|
||||||
}}
|
|
||||||
onRunQuery={this.onRefresh}
|
|
||||||
onChange={(v) => onChange(v.query)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
switch (datasource.version) {
|
||||||
<div className="gf-form-inline">
|
case InfluxVersion.Flux:
|
||||||
<InlineFormLabel width={10}>Query</InlineFormLabel>
|
return (
|
||||||
<div className="gf-form-inline gf-form--grow">
|
<FluxQueryEditor
|
||||||
<TextArea
|
datasource={datasource}
|
||||||
defaultValue={query || ''}
|
query={{
|
||||||
placeholder="metric name or tags query"
|
refId: 'A',
|
||||||
rows={1}
|
query,
|
||||||
className="gf-form-input"
|
}}
|
||||||
onBlur={(e) => onChange(e.currentTarget.value)}
|
onRunQuery={this.onRefresh}
|
||||||
|
onChange={(v) => onChange(v.query)}
|
||||||
/>
|
/>
|
||||||
</div>
|
);
|
||||||
</div>
|
//@todo add support for SQL
|
||||||
);
|
case InfluxVersion.SQL:
|
||||||
|
return <div className="gf-form-inline">TODO</div>;
|
||||||
|
|
||||||
|
// Influx/default case
|
||||||
|
case InfluxVersion.InfluxQL:
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<InlineFormLabel width={10}>Query</InlineFormLabel>
|
||||||
|
<div className="gf-form-inline gf-form--grow">
|
||||||
|
<TextArea
|
||||||
|
defaultValue={query || ''}
|
||||||
|
placeholder="metric name or tags query"
|
||||||
|
rows={1}
|
||||||
|
className="gf-form-input"
|
||||||
|
onBlur={(e) => onChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user