mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 01:16:31 -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].
|
||||
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) {
|
||||
|
@ -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{
|
||||
|
@ -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,
|
||||
|
@ -24,7 +24,7 @@ export const QueryEditor = ({ query, onChange, onRunQuery, datasource }: Props)
|
||||
</div>
|
||||
);
|
||||
case InfluxVersion.SQL:
|
||||
return <FSQLEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />;
|
||||
return <FSQLEditor datasource={datasource} query={query} onChange={onChange} onRunQuery={onRunQuery} />;
|
||||
case InfluxVersion.InfluxQL:
|
||||
default:
|
||||
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';
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<Input
|
||||
value={props.query.query}
|
||||
onBlur={(e) => onSQLQueryChange(e.currentTarget.value)}
|
||||
onChange={(e) => onSQLQueryChange(e.currentTarget.value)}
|
||||
/>
|
||||
<br />
|
||||
<button onClick={() => onSQLQueryChange()}>run query</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
class UnthemedSQLQueryEditor extends PureComponent<Props> {
|
||||
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 = (
|
||||
<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() {
|
||||
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 (
|
||||
<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)}
|
||||
switch (datasource.version) {
|
||||
case InfluxVersion.Flux:
|
||||
return (
|
||||
<FluxQueryEditor
|
||||
datasource={datasource}
|
||||
query={{
|
||||
refId: 'A',
|
||||
query,
|
||||
}}
|
||||
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