mirror of https://github.com/grafana/grafana
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>pull/72467/head^2
parent
9c6a9a3977
commit
d333c09418
@ -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; |
||||
}; |
||||
|
||||
// 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> |
||||
); |
||||
}; |
||||
datasource: InfluxDatasource; |
||||
} |
||||
|
||||
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(), |
||||
}; |
||||
} |
||||
} |
Loading…
Reference in new issue