mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13758 from grafana/mysql_query_builder
mysql graphical query builder
This commit is contained in:
commit
f348acccf3
@ -73,6 +73,58 @@ Example:
|
||||
|
||||
You can use wildcards (`*`) in place of database or table if you want to grant access to more databases and tables.
|
||||
|
||||
## Query Editor
|
||||
|
||||
> Only available in Grafana v5.4+.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v54/mysql_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v54/mysql_query.gif" >}}
|
||||
|
||||
You find the MySQL query editor in the metrics tab in a panel's edit mode. You enter edit mode by clicking the
|
||||
panel title, then edit.
|
||||
|
||||
The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed.
|
||||
|
||||
### Select table, time column and metric column (FROM)
|
||||
|
||||
When you enter edit mode for the first time or add a new query Grafana will try to prefill the query builder with the first table that has a timestamp column and a numeric column.
|
||||
|
||||
In the FROM field, Grafana will suggest tables that are in the configured database. To select a table or view in another database that your database user has access to you can manually enter a fully qualified name (database.table) like `otherDb.metrics`.
|
||||
|
||||
The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name.
|
||||
|
||||
The metric column suggestions will only contain columns with a text datatype (text, tinytext, mediumtext, longtext, varchar, char).
|
||||
If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `CAST(numericColumn as CHAR)`.
|
||||
You may also enter arbitrary SQL expressions in the metric column field that evaluate to a text datatype like
|
||||
`CONCAT(column1, " ", CAST(numericColumn as CHAR))`.
|
||||
|
||||
### Columns and Aggregation functions (SELECT)
|
||||
|
||||
In the `SELECT` row you can specify what columns and functions you want to use.
|
||||
In the column field you may write arbitrary expressions instead of a column name like `column1 * column2 / column3`.
|
||||
|
||||
If you use aggregate functions you need to group your resultset. The editor will automatically add a `GROUP BY time` if you add an aggregate function.
|
||||
|
||||
You may add further value columns by clicking the plus button and selecting `Column` from the menu. Multiple value columns will be plotted as separate series in the graph panel.
|
||||
|
||||
### Filter data (WHERE)
|
||||
To add a filter click the plus icon to the right of the `WHERE` condition. You can remove filters by clicking on
|
||||
the filter and selecting `Remove`. A filter for the current selected timerange is automatically added to new queries.
|
||||
|
||||
### Group By
|
||||
To group by time or any other columns click the plus icon at the end of the GROUP BY row. The suggestion dropdown will only show text columns of your currently selected table but you may manually enter any column.
|
||||
You can remove the group by clicking on the item and then selecting `Remove`.
|
||||
|
||||
If you add any grouping, all selected columns need to have an aggregate function applied. The query builder will automatically add aggregate functions to all columns without aggregate functions when you add groupings.
|
||||
|
||||
#### Gap Filling
|
||||
|
||||
Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with.
|
||||
|
||||
### Text Editor Mode (RAW)
|
||||
You can switch to the raw query editor mode by clicking the hamburger icon and selecting `Switch editor mode` or by clicking `Edit SQL` below the query.
|
||||
|
||||
> If you use the raw query editor, be sure your query at minimum has `ORDER BY time` and a filter on the returned time range.
|
||||
|
||||
## Macros
|
||||
|
||||
To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros.
|
||||
|
@ -1,24 +1,27 @@
|
||||
import _ from 'lodash';
|
||||
import ResponseParser from './response_parser';
|
||||
import MysqlQuery from 'app/plugins/datasource/mysql/mysql_query';
|
||||
|
||||
export class MysqlDatasource {
|
||||
id: any;
|
||||
name: any;
|
||||
responseParser: ResponseParser;
|
||||
queryModel: MysqlQuery;
|
||||
interval: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings, private backendSrv, private $q, private templateSrv) {
|
||||
constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) {
|
||||
this.name = instanceSettings.name;
|
||||
this.id = instanceSettings.id;
|
||||
this.responseParser = new ResponseParser(this.$q);
|
||||
this.queryModel = new MysqlQuery({});
|
||||
this.interval = (instanceSettings.jsonData || {}).timeInterval;
|
||||
}
|
||||
|
||||
interpolateVariable(value, variable) {
|
||||
interpolateVariable = (value, variable) => {
|
||||
if (typeof value === 'string') {
|
||||
if (variable.multi || variable.includeAll) {
|
||||
return "'" + value.replace(/'/g, `''`) + "'";
|
||||
return this.queryModel.quoteLiteral(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
@ -28,27 +31,25 @@ export class MysqlDatasource {
|
||||
return value;
|
||||
}
|
||||
|
||||
const quotedValues = _.map(value, val => {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return "'" + val.replace(/'/g, `''`) + "'";
|
||||
const quotedValues = _.map(value, v => {
|
||||
return this.queryModel.quoteLiteral(v);
|
||||
});
|
||||
return quotedValues.join(',');
|
||||
}
|
||||
};
|
||||
|
||||
query(options) {
|
||||
const queries = _.filter(options.targets, item => {
|
||||
return item.hide !== true;
|
||||
}).map(item => {
|
||||
const queries = _.filter(options.targets, target => {
|
||||
return target.hide !== true;
|
||||
}).map(target => {
|
||||
const queryModel = new MysqlQuery(target, this.templateSrv, options.scopedVars);
|
||||
|
||||
return {
|
||||
refId: item.refId,
|
||||
refId: target.refId,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
datasourceId: this.id,
|
||||
rawSql: this.templateSrv.replace(item.rawSql, options.scopedVars, this.interpolateVariable),
|
||||
format: item.format,
|
||||
rawSql: queryModel.render(this.interpolateVariable),
|
||||
format: target.format,
|
||||
};
|
||||
});
|
||||
|
||||
@ -109,8 +110,11 @@ export class MysqlDatasource {
|
||||
format: 'table',
|
||||
};
|
||||
|
||||
const range = this.timeSrv.timeRange();
|
||||
const data = {
|
||||
queries: [interpolatedQuery],
|
||||
from: range.from.valueOf().toString(),
|
||||
to: range.to.valueOf().toString(),
|
||||
};
|
||||
|
||||
if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
|
||||
|
142
public/app/plugins/datasource/mysql/meta_query.ts
Normal file
142
public/app/plugins/datasource/mysql/meta_query.ts
Normal file
@ -0,0 +1,142 @@
|
||||
export class MysqlMetaQuery {
|
||||
constructor(private target, private queryModel) {}
|
||||
|
||||
getOperators(datatype: string) {
|
||||
switch (datatype) {
|
||||
case 'double':
|
||||
case 'float': {
|
||||
return ['=', '!=', '<', '<=', '>', '>='];
|
||||
}
|
||||
case 'text':
|
||||
case 'tinytext':
|
||||
case 'mediumtext':
|
||||
case 'longtext':
|
||||
case 'varchar':
|
||||
case 'char': {
|
||||
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE'];
|
||||
}
|
||||
default: {
|
||||
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// quote identifier as literal to use in metadata queries
|
||||
quoteIdentAsLiteral(value) {
|
||||
return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value));
|
||||
}
|
||||
|
||||
findMetricTable() {
|
||||
// query that returns first table found that has a timestamp(tz) column and a float column
|
||||
const query = `
|
||||
SELECT
|
||||
table_name as table_name,
|
||||
( SELECT
|
||||
column_name as column_name
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
c.data_type IN ('timestamp', 'datetime')
|
||||
ORDER BY ordinal_position LIMIT 1
|
||||
) AS time_column,
|
||||
( SELECT
|
||||
column_name AS column_name
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
c.data_type IN('float', 'int', 'bigint')
|
||||
ORDER BY ordinal_position LIMIT 1
|
||||
) AS value_column
|
||||
FROM information_schema.tables t
|
||||
WHERE
|
||||
t.table_schema = database() AND
|
||||
EXISTS
|
||||
( SELECT 1
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
c.data_type IN ('timestamp', 'datetime')
|
||||
) AND
|
||||
EXISTS
|
||||
( SELECT 1
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
c.data_type IN('float', 'int', 'bigint')
|
||||
)
|
||||
LIMIT 1
|
||||
;`;
|
||||
return query;
|
||||
}
|
||||
|
||||
buildTableConstraint(table: string) {
|
||||
let query = '';
|
||||
|
||||
// check for schema qualified table
|
||||
if (table.includes('.')) {
|
||||
const parts = table.split('.');
|
||||
query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]);
|
||||
query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]);
|
||||
return query;
|
||||
} else {
|
||||
query = 'table_schema = database() AND table_name = ' + this.quoteIdentAsLiteral(table);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
buildTableQuery() {
|
||||
return 'SELECT table_name FROM information_schema.tables WHERE table_schema = database() ORDER BY table_name';
|
||||
}
|
||||
|
||||
buildColumnQuery(type?: string) {
|
||||
let query = 'SELECT column_name FROM information_schema.columns WHERE ';
|
||||
query += this.buildTableConstraint(this.target.table);
|
||||
|
||||
switch (type) {
|
||||
case 'time': {
|
||||
query += " AND data_type IN ('timestamp','datetime','bigint','int','double','float')";
|
||||
break;
|
||||
}
|
||||
case 'metric': {
|
||||
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
query += " AND data_type IN ('bigint','int','smallint','mediumint','tinyint','double','decimal','float')";
|
||||
query += ' AND column_name <> ' + this.quoteIdentAsLiteral(this.target.timeColumn);
|
||||
break;
|
||||
}
|
||||
case 'group': {
|
||||
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
query += ' ORDER BY column_name';
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
buildValueQuery(column: string) {
|
||||
let query = 'SELECT DISTINCT QUOTE(' + column + ')';
|
||||
query += ' FROM ' + this.target.table;
|
||||
query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')';
|
||||
query += ' ORDER BY 1 LIMIT 100';
|
||||
return query;
|
||||
}
|
||||
|
||||
buildDatatypeQuery(column: string) {
|
||||
let query = `
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE `;
|
||||
query += ' table_name = ' + this.quoteIdentAsLiteral(this.target.table);
|
||||
query += ' AND column_name = ' + this.quoteIdentAsLiteral(column);
|
||||
return query;
|
||||
}
|
||||
}
|
233
public/app/plugins/datasource/mysql/mysql_query.ts
Normal file
233
public/app/plugins/datasource/mysql/mysql_query.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class MysqlQuery {
|
||||
target: any;
|
||||
templateSrv: any;
|
||||
scopedVars: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(target, templateSrv?, scopedVars?) {
|
||||
this.target = target;
|
||||
this.templateSrv = templateSrv;
|
||||
this.scopedVars = scopedVars;
|
||||
|
||||
target.format = target.format || 'time_series';
|
||||
target.timeColumn = target.timeColumn || 'time';
|
||||
target.metricColumn = target.metricColumn || 'none';
|
||||
|
||||
target.group = target.group || [];
|
||||
target.where = target.where || [{ type: 'macro', name: '$__timeFilter', params: [] }];
|
||||
target.select = target.select || [[{ type: 'column', params: ['value'] }]];
|
||||
|
||||
// handle pre query gui panels gracefully
|
||||
if (!('rawQuery' in this.target)) {
|
||||
if ('rawSql' in target) {
|
||||
// pre query gui panel
|
||||
target.rawQuery = true;
|
||||
} else {
|
||||
// new panel
|
||||
target.rawQuery = false;
|
||||
}
|
||||
}
|
||||
|
||||
// give interpolateQueryStr access to this
|
||||
this.interpolateQueryStr = this.interpolateQueryStr.bind(this);
|
||||
}
|
||||
|
||||
// remove identifier quoting from identifier to use in metadata queries
|
||||
unquoteIdentifier(value) {
|
||||
if (value[0] === '"' && value[value.length - 1] === '"') {
|
||||
return value.substring(1, value.length - 1).replace(/""/g, '"');
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
quoteIdentifier(value) {
|
||||
return '"' + value.replace(/"/g, '""') + '"';
|
||||
}
|
||||
|
||||
quoteLiteral(value) {
|
||||
return "'" + value.replace(/'/g, "''") + "'";
|
||||
}
|
||||
|
||||
escapeLiteral(value) {
|
||||
return value.replace(/'/g, "''");
|
||||
}
|
||||
|
||||
hasTimeGroup() {
|
||||
return _.find(this.target.group, (g: any) => g.type === 'time');
|
||||
}
|
||||
|
||||
hasMetricColumn() {
|
||||
return this.target.metricColumn !== 'none';
|
||||
}
|
||||
|
||||
interpolateQueryStr(value, variable, defaultFormatFn) {
|
||||
// if no multi or include all do not regexEscape
|
||||
if (!variable.multi && !variable.includeAll) {
|
||||
return this.escapeLiteral(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return this.quoteLiteral(value);
|
||||
}
|
||||
|
||||
const escapedValues = _.map(value, this.quoteLiteral);
|
||||
return escapedValues.join(',');
|
||||
}
|
||||
|
||||
render(interpolate?) {
|
||||
const target = this.target;
|
||||
|
||||
// new query with no table set yet
|
||||
if (!this.target.rawQuery && !('table' in this.target)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!target.rawQuery) {
|
||||
target.rawSql = this.buildQuery();
|
||||
}
|
||||
|
||||
if (interpolate) {
|
||||
return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr);
|
||||
} else {
|
||||
return target.rawSql;
|
||||
}
|
||||
}
|
||||
|
||||
hasUnixEpochTimecolumn() {
|
||||
return ['int', 'bigint', 'double'].indexOf(this.target.timeColumnType) > -1;
|
||||
}
|
||||
|
||||
buildTimeColumn(alias = true) {
|
||||
const timeGroup = this.hasTimeGroup();
|
||||
let query;
|
||||
let macro = '$__timeGroup';
|
||||
|
||||
if (timeGroup) {
|
||||
let args;
|
||||
if (timeGroup.params.length > 1 && timeGroup.params[1] !== 'none') {
|
||||
args = timeGroup.params.join(',');
|
||||
} else {
|
||||
args = timeGroup.params[0];
|
||||
}
|
||||
if (this.hasUnixEpochTimecolumn()) {
|
||||
macro = '$__unixEpochGroup';
|
||||
}
|
||||
if (alias) {
|
||||
macro += 'Alias';
|
||||
}
|
||||
query = macro + '(' + this.target.timeColumn + ',' + args + ')';
|
||||
} else {
|
||||
query = this.target.timeColumn;
|
||||
if (alias) {
|
||||
query += ' AS "time"';
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
buildMetricColumn() {
|
||||
if (this.hasMetricColumn()) {
|
||||
return this.target.metricColumn + ' AS metric';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
buildValueColumns() {
|
||||
let query = '';
|
||||
for (const column of this.target.select) {
|
||||
query += ',\n ' + this.buildValueColumn(column);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
buildValueColumn(column) {
|
||||
let query = '';
|
||||
|
||||
const columnName = _.find(column, (g: any) => g.type === 'column');
|
||||
query = columnName.params[0];
|
||||
|
||||
const aggregate = _.find(column, (g: any) => g.type === 'aggregate');
|
||||
|
||||
if (aggregate) {
|
||||
const func = aggregate.params[0];
|
||||
query = func + '(' + query + ')';
|
||||
}
|
||||
|
||||
const alias = _.find(column, (g: any) => g.type === 'alias');
|
||||
if (alias) {
|
||||
query += ' AS ' + this.quoteIdentifier(alias.params[0]);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
buildWhereClause() {
|
||||
let query = '';
|
||||
const conditions = _.map(this.target.where, (tag, index) => {
|
||||
switch (tag.type) {
|
||||
case 'macro':
|
||||
return tag.name + '(' + this.target.timeColumn + ')';
|
||||
break;
|
||||
case 'expression':
|
||||
return tag.params.join(' ');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = '\nWHERE\n ' + conditions.join(' AND\n ');
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
buildGroupClause() {
|
||||
let query = '';
|
||||
let groupSection = '';
|
||||
|
||||
for (let i = 0; i < this.target.group.length; i++) {
|
||||
const part = this.target.group[i];
|
||||
if (i > 0) {
|
||||
groupSection += ', ';
|
||||
}
|
||||
if (part.type === 'time') {
|
||||
groupSection += '1';
|
||||
} else {
|
||||
groupSection += part.params[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (groupSection.length) {
|
||||
query = '\nGROUP BY ' + groupSection;
|
||||
if (this.hasMetricColumn()) {
|
||||
query += ',2';
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
buildQuery() {
|
||||
let query = 'SELECT';
|
||||
|
||||
query += '\n ' + this.buildTimeColumn();
|
||||
if (this.hasMetricColumn()) {
|
||||
query += ',\n ' + this.buildMetricColumn();
|
||||
}
|
||||
query += this.buildValueColumns();
|
||||
|
||||
query += '\nFROM ' + this.target.table;
|
||||
|
||||
query += this.buildWhereClause();
|
||||
query += this.buildGroupClause();
|
||||
|
||||
query += '\nORDER BY ' + this.buildTimeColumn(false);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
@ -1,43 +1,141 @@
|
||||
<query-editor-row query-ctrl="ctrl" can-collapse="false">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<code-editor content="ctrl.target.rawSql" datasource="ctrl.datasource" on-change="ctrl.panelCtrl.refresh()" data-mode="sql">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||
|
||||
<div ng-if="ctrl.target.rawQuery">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<code-editor content="ctrl.target.rawSql" datasource="ctrl.datasource" on-change="ctrl.panelCtrl.refresh()" data-mode="sql">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.target.rawQuery">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">FROM</label>
|
||||
<metric-segment segment="ctrl.tableSegment" get-options="ctrl.getTableSegments()" on-change="ctrl.tableChanged()"></metric-segment>
|
||||
|
||||
<label class="gf-form-label query-keyword width-7">Time column</label>
|
||||
<metric-segment segment="ctrl.timeColumnSegment" get-options="ctrl.getTimeColumnSegments()" on-change="ctrl.timeColumnChanged()"></metric-segment>
|
||||
|
||||
<label class="gf-form-label query-keyword width-9">
|
||||
Metric column
|
||||
<info-popover mode="right-normal">Column to be used as metric name for the value column.</info-popover>
|
||||
</label>
|
||||
<metric-segment segment="ctrl.metricColumnSegment" get-options="ctrl.getMetricColumnSegments()" on-change="ctrl.metricColumnChanged()"></metric-segment>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-repeat="selectParts in ctrl.selectParts">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">
|
||||
<span ng-show="$index === 0">SELECT</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-repeat="part in selectParts">
|
||||
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleSelectPartEvent(selectParts, part, $event)">
|
||||
</sql-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="dropdown"
|
||||
dropdown-typeahead="ctrl.selectMenu"
|
||||
dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">WHERE</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-repeat="part in ctrl.whereParts">
|
||||
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleWherePartEvent(ctrl.whereParts, part, $event, $index)">
|
||||
</sql-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<metric-segment segment="ctrl.whereAdd" get-options="ctrl.getWhereOptions()" on-change="ctrl.addWhereAction(part, $index)"></metric-segment>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">
|
||||
<span>GROUP BY</span>
|
||||
</label>
|
||||
|
||||
<sql-part-editor ng-repeat="part in ctrl.groupParts"
|
||||
part="part" class="gf-form-label sql-part"
|
||||
handle-event="ctrl.handleGroupPartEvent(part, $index, $event)">
|
||||
</sql-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<metric-segment segment="ctrl.groupAdd" get-options="ctrl.getGroupOptions()" on-change="ctrl.addGroupAction(part, $index)"></metric-segment>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword">Format as</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
<label class="gf-form-label query-keyword">Format as</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleEditorMode()" ng-show="ctrl.panelCtrl.panel.type !== 'table'">
|
||||
<span ng-show="ctrl.target.rawQuery">Query Builder</span>
|
||||
<span ng-hide="ctrl.target.rawQuery">Edit SQL</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
Show Help
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showHelp"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
|
||||
Generated SQL
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showLastQuerySQL"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showLastQuerySQL"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
|
||||
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.sql}}</pre>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
|
||||
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.sql}}</pre>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showHelp">
|
||||
<pre class="gf-form-pre alert alert-info">Time series:
|
||||
<div class="gf-form" ng-show="ctrl.showHelp">
|
||||
<pre class="gf-form-pre alert alert-info">Time series:
|
||||
- return column named time or time_sec (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
|
||||
- return column(s) with numeric datatype as values
|
||||
Optional:
|
||||
@ -64,7 +162,7 @@ Macros:
|
||||
|
||||
Example of group by and order by with $__timeGroup:
|
||||
SELECT
|
||||
$__timeGroup(timestamp_col, '1h') AS time,
|
||||
$__timeGroupAlias(timestamp_col, '1h'),
|
||||
sum(value_double) as value
|
||||
FROM yourtable
|
||||
GROUP BY 1
|
||||
@ -75,13 +173,13 @@ Or build your own conditionals using these macros which just return the values:
|
||||
- $__timeTo() -> '2017-04-21T05:01:17Z'
|
||||
- $__unixEpochFrom() -> 1492750877
|
||||
- $__unixEpochTo() -> 1492750877
|
||||
</pre>
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryError">
|
||||
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryError">
|
||||
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
|
||||
</div>
|
||||
|
||||
</query-editor-row>
|
||||
|
@ -1,12 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { MysqlMetaQuery } from './meta_query';
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
|
||||
export interface MysqlQuery {
|
||||
refId: string;
|
||||
format: string;
|
||||
alias: string;
|
||||
rawSql: string;
|
||||
}
|
||||
import { SqlPart } from 'app/core/components/sql_part/sql_part';
|
||||
import MysqlQuery from './mysql_query';
|
||||
import sqlPart from './sql_part';
|
||||
|
||||
export interface QueryMeta {
|
||||
sql: string;
|
||||
@ -26,17 +24,31 @@ export class MysqlQueryCtrl extends QueryCtrl {
|
||||
|
||||
showLastQuerySQL: boolean;
|
||||
formats: any[];
|
||||
target: MysqlQuery;
|
||||
lastQueryMeta: QueryMeta;
|
||||
lastQueryError: string;
|
||||
showHelp: boolean;
|
||||
|
||||
queryModel: MysqlQuery;
|
||||
metaBuilder: MysqlMetaQuery;
|
||||
tableSegment: any;
|
||||
whereAdd: any;
|
||||
timeColumnSegment: any;
|
||||
metricColumnSegment: any;
|
||||
selectMenu: any[];
|
||||
selectParts: SqlPart[][];
|
||||
groupParts: SqlPart[];
|
||||
whereParts: SqlPart[];
|
||||
groupAdd: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector) {
|
||||
constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
|
||||
super($scope, $injector);
|
||||
|
||||
this.target.format = this.target.format || 'time_series';
|
||||
this.target.alias = '';
|
||||
this.target = this.target;
|
||||
this.queryModel = new MysqlQuery(this.target, templateSrv, this.panel.scopedVars);
|
||||
this.metaBuilder = new MysqlMetaQuery(this.target, this.queryModel);
|
||||
this.updateProjection();
|
||||
|
||||
this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
|
||||
|
||||
if (!this.target.rawSql) {
|
||||
@ -44,15 +56,199 @@ export class MysqlQueryCtrl extends QueryCtrl {
|
||||
if (this.panelCtrl.panel.type === 'table') {
|
||||
this.target.format = 'table';
|
||||
this.target.rawSql = 'SELECT 1';
|
||||
this.target.rawQuery = true;
|
||||
} else {
|
||||
this.target.rawSql = defaultQuery;
|
||||
this.datasource.metricFindQuery(this.metaBuilder.findMetricTable()).then(result => {
|
||||
if (result.length > 0) {
|
||||
this.target.table = result[0].text;
|
||||
let segment = this.uiSegmentSrv.newSegment(this.target.table);
|
||||
this.tableSegment.html = segment.html;
|
||||
this.tableSegment.value = segment.value;
|
||||
|
||||
this.target.timeColumn = result[1].text;
|
||||
segment = this.uiSegmentSrv.newSegment(this.target.timeColumn);
|
||||
this.timeColumnSegment.html = segment.html;
|
||||
this.timeColumnSegment.value = segment.value;
|
||||
|
||||
this.target.timeColumnType = 'timestamp';
|
||||
this.target.select = [[{ type: 'column', params: [result[2].text] }]];
|
||||
this.updateProjection();
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.target.table) {
|
||||
this.tableSegment = uiSegmentSrv.newSegment({ value: 'select table', fake: true });
|
||||
} else {
|
||||
this.tableSegment = uiSegmentSrv.newSegment(this.target.table);
|
||||
}
|
||||
|
||||
this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn);
|
||||
this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn);
|
||||
|
||||
this.buildSelectMenu();
|
||||
this.whereAdd = this.uiSegmentSrv.newPlusButton();
|
||||
this.groupAdd = this.uiSegmentSrv.newPlusButton();
|
||||
|
||||
this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
|
||||
this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
|
||||
}
|
||||
|
||||
updateProjection() {
|
||||
this.selectParts = _.map(this.target.select, (parts: any) => {
|
||||
return _.map(parts, sqlPart.create).filter(n => n);
|
||||
});
|
||||
this.whereParts = _.map(this.target.where, sqlPart.create).filter(n => n);
|
||||
this.groupParts = _.map(this.target.group, sqlPart.create).filter(n => n);
|
||||
}
|
||||
|
||||
updatePersistedParts() {
|
||||
this.target.select = _.map(this.selectParts, selectParts => {
|
||||
return _.map(selectParts, (part: any) => {
|
||||
return { type: part.def.type, datatype: part.datatype, params: part.params };
|
||||
});
|
||||
});
|
||||
this.target.where = _.map(this.whereParts, (part: any) => {
|
||||
return { type: part.def.type, datatype: part.datatype, name: part.name, params: part.params };
|
||||
});
|
||||
this.target.group = _.map(this.groupParts, (part: any) => {
|
||||
return { type: part.def.type, datatype: part.datatype, params: part.params };
|
||||
});
|
||||
}
|
||||
|
||||
buildSelectMenu() {
|
||||
this.selectMenu = [];
|
||||
const aggregates = {
|
||||
text: 'Aggregate Functions',
|
||||
value: 'aggregate',
|
||||
submenu: [
|
||||
{ text: 'Average', value: 'avg' },
|
||||
{ text: 'Count', value: 'count' },
|
||||
{ text: 'Maximum', value: 'max' },
|
||||
{ text: 'Minimum', value: 'min' },
|
||||
{ text: 'Sum', value: 'sum' },
|
||||
{ text: 'Standard deviation', value: 'stddev' },
|
||||
{ text: 'Variance', value: 'variance' },
|
||||
],
|
||||
};
|
||||
|
||||
this.selectMenu.push(aggregates);
|
||||
this.selectMenu.push({ text: 'Alias', value: 'alias' });
|
||||
this.selectMenu.push({ text: 'Column', value: 'column' });
|
||||
}
|
||||
|
||||
toggleEditorMode() {
|
||||
if (this.target.rawQuery) {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Warning',
|
||||
text2: 'Switching to query builder may overwrite your raw SQL.',
|
||||
icon: 'fa-exclamation',
|
||||
yesText: 'Switch',
|
||||
onConfirm: () => {
|
||||
this.target.rawQuery = !this.target.rawQuery;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.target.rawQuery = !this.target.rawQuery;
|
||||
}
|
||||
}
|
||||
|
||||
resetPlusButton(button) {
|
||||
const plusButton = this.uiSegmentSrv.newPlusButton();
|
||||
button.html = plusButton.html;
|
||||
button.value = plusButton.value;
|
||||
}
|
||||
|
||||
getTableSegments() {
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildTableQuery())
|
||||
.then(this.transformToSegments({}))
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
}
|
||||
|
||||
tableChanged() {
|
||||
this.target.table = this.tableSegment.value;
|
||||
this.target.where = [];
|
||||
this.target.group = [];
|
||||
this.updateProjection();
|
||||
|
||||
const segment = this.uiSegmentSrv.newSegment('none');
|
||||
this.metricColumnSegment.html = segment.html;
|
||||
this.metricColumnSegment.value = segment.value;
|
||||
this.target.metricColumn = 'none';
|
||||
|
||||
const task1 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('time')).then(result => {
|
||||
// check if time column is still valid
|
||||
if (result.length > 0 && !_.find(result, (r: any) => r.text === this.target.timeColumn)) {
|
||||
const segment = this.uiSegmentSrv.newSegment(result[0].text);
|
||||
this.timeColumnSegment.html = segment.html;
|
||||
this.timeColumnSegment.value = segment.value;
|
||||
}
|
||||
return this.timeColumnChanged(false);
|
||||
});
|
||||
const task2 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('value')).then(result => {
|
||||
if (result.length > 0) {
|
||||
this.target.select = [[{ type: 'column', params: [result[0].text] }]];
|
||||
this.updateProjection();
|
||||
}
|
||||
});
|
||||
|
||||
this.$q.all([task1, task2]).then(() => {
|
||||
this.panelCtrl.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
getTimeColumnSegments() {
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildColumnQuery('time'))
|
||||
.then(this.transformToSegments({}))
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
}
|
||||
|
||||
timeColumnChanged(refresh?: boolean) {
|
||||
this.target.timeColumn = this.timeColumnSegment.value;
|
||||
return this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(this.target.timeColumn)).then(result => {
|
||||
if (result.length === 1) {
|
||||
if (this.target.timeColumnType !== result[0].text) {
|
||||
this.target.timeColumnType = result[0].text;
|
||||
}
|
||||
let partModel;
|
||||
if (this.queryModel.hasUnixEpochTimecolumn()) {
|
||||
partModel = sqlPart.create({ type: 'macro', name: '$__unixEpochFilter', params: [] });
|
||||
} else {
|
||||
partModel = sqlPart.create({ type: 'macro', name: '$__timeFilter', params: [] });
|
||||
}
|
||||
|
||||
if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') {
|
||||
// replace current macro
|
||||
this.whereParts[0] = partModel;
|
||||
} else {
|
||||
this.whereParts.splice(0, 0, partModel);
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePersistedParts();
|
||||
if (refresh !== false) {
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getMetricColumnSegments() {
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildColumnQuery('metric'))
|
||||
.then(this.transformToSegments({ addNone: true }))
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
}
|
||||
|
||||
metricColumnChanged() {
|
||||
this.target.metricColumn = this.metricColumnSegment.value;
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
|
||||
onDataReceived(dataList) {
|
||||
this.lastQueryMeta = null;
|
||||
this.lastQueryError = null;
|
||||
@ -72,4 +268,356 @@ export class MysqlQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformToSegments(config) {
|
||||
return results => {
|
||||
const segments = _.map(results, segment => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: segment.text,
|
||||
expandable: segment.expandable,
|
||||
});
|
||||
});
|
||||
|
||||
if (config.addTemplateVars) {
|
||||
for (const variable of this.templateSrv.variables) {
|
||||
let value;
|
||||
value = '$' + variable.name;
|
||||
if (config.templateQuoter && variable.multi === false) {
|
||||
value = config.templateQuoter(value);
|
||||
}
|
||||
|
||||
segments.unshift(
|
||||
this.uiSegmentSrv.newSegment({
|
||||
type: 'template',
|
||||
value: value,
|
||||
expandable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.addNone) {
|
||||
segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: 'none', expandable: true }));
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
}
|
||||
|
||||
findAggregateIndex(selectParts) {
|
||||
return _.findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile');
|
||||
}
|
||||
|
||||
findWindowIndex(selectParts) {
|
||||
return _.findIndex(selectParts, (p: any) => p.def.type === 'window' || p.def.type === 'moving_window');
|
||||
}
|
||||
|
||||
addSelectPart(selectParts, item, subItem) {
|
||||
let partType = item.value;
|
||||
if (subItem && subItem.type) {
|
||||
partType = subItem.type;
|
||||
}
|
||||
let partModel = sqlPart.create({ type: partType });
|
||||
if (subItem) {
|
||||
partModel.params[0] = subItem.value;
|
||||
}
|
||||
let addAlias = false;
|
||||
|
||||
switch (partType) {
|
||||
case 'column':
|
||||
const parts = _.map(selectParts, (part: any) => {
|
||||
return sqlPart.create({ type: part.def.type, params: _.clone(part.params) });
|
||||
});
|
||||
this.selectParts.push(parts);
|
||||
break;
|
||||
case 'percentile':
|
||||
case 'aggregate':
|
||||
// add group by if no group by yet
|
||||
if (this.target.group.length === 0) {
|
||||
this.addGroup('time', '$__interval');
|
||||
}
|
||||
const aggIndex = this.findAggregateIndex(selectParts);
|
||||
if (aggIndex !== -1) {
|
||||
// replace current aggregation
|
||||
selectParts[aggIndex] = partModel;
|
||||
} else {
|
||||
selectParts.splice(1, 0, partModel);
|
||||
}
|
||||
if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) {
|
||||
addAlias = true;
|
||||
}
|
||||
break;
|
||||
case 'moving_window':
|
||||
case 'window':
|
||||
const windowIndex = this.findWindowIndex(selectParts);
|
||||
if (windowIndex !== -1) {
|
||||
// replace current window function
|
||||
selectParts[windowIndex] = partModel;
|
||||
} else {
|
||||
const aggIndex = this.findAggregateIndex(selectParts);
|
||||
if (aggIndex !== -1) {
|
||||
selectParts.splice(aggIndex + 1, 0, partModel);
|
||||
} else {
|
||||
selectParts.splice(1, 0, partModel);
|
||||
}
|
||||
}
|
||||
if (!_.find(selectParts, (p: any) => p.def.type === 'alias')) {
|
||||
addAlias = true;
|
||||
}
|
||||
break;
|
||||
case 'alias':
|
||||
addAlias = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (addAlias) {
|
||||
// set initial alias name to column name
|
||||
partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace(/"/g, '')] });
|
||||
if (selectParts[selectParts.length - 1].def.type === 'alias') {
|
||||
selectParts[selectParts.length - 1] = partModel;
|
||||
} else {
|
||||
selectParts.push(partModel);
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePersistedParts();
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
|
||||
removeSelectPart(selectParts, part) {
|
||||
if (part.def.type === 'column') {
|
||||
// remove all parts of column unless its last column
|
||||
if (this.selectParts.length > 1) {
|
||||
const modelsIndex = _.indexOf(this.selectParts, selectParts);
|
||||
this.selectParts.splice(modelsIndex, 1);
|
||||
}
|
||||
} else {
|
||||
const partIndex = _.indexOf(selectParts, part);
|
||||
selectParts.splice(partIndex, 1);
|
||||
}
|
||||
|
||||
this.updatePersistedParts();
|
||||
}
|
||||
|
||||
handleSelectPartEvent(selectParts, part, evt) {
|
||||
switch (evt.name) {
|
||||
case 'get-param-options': {
|
||||
switch (part.def.type) {
|
||||
// case 'aggregate':
|
||||
// return this.datasource
|
||||
// .metricFindQuery(this.metaBuilder.buildAggregateQuery())
|
||||
// .then(this.transformToSegments({}))
|
||||
// .catch(this.handleQueryError.bind(this));
|
||||
case 'column':
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildColumnQuery('value'))
|
||||
.then(this.transformToSegments({}))
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
}
|
||||
}
|
||||
case 'part-param-changed': {
|
||||
this.updatePersistedParts();
|
||||
this.panelCtrl.refresh();
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
this.removeSelectPart(selectParts, part);
|
||||
this.panelCtrl.refresh();
|
||||
break;
|
||||
}
|
||||
case 'get-part-actions': {
|
||||
return this.$q.when([{ text: 'Remove', value: 'remove-part' }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleGroupPartEvent(part, index, evt) {
|
||||
switch (evt.name) {
|
||||
case 'get-param-options': {
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildColumnQuery())
|
||||
.then(this.transformToSegments({}))
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
}
|
||||
case 'part-param-changed': {
|
||||
this.updatePersistedParts();
|
||||
this.panelCtrl.refresh();
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
this.removeGroup(part, index);
|
||||
this.panelCtrl.refresh();
|
||||
break;
|
||||
}
|
||||
case 'get-part-actions': {
|
||||
return this.$q.when([{ text: 'Remove', value: 'remove-part' }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addGroup(partType, value) {
|
||||
let params = [value];
|
||||
if (partType === 'time') {
|
||||
params = ['$__interval', 'none'];
|
||||
}
|
||||
const partModel = sqlPart.create({ type: partType, params: params });
|
||||
|
||||
if (partType === 'time') {
|
||||
// put timeGroup at start
|
||||
this.groupParts.splice(0, 0, partModel);
|
||||
} else {
|
||||
this.groupParts.push(partModel);
|
||||
}
|
||||
|
||||
// add aggregates when adding group by
|
||||
for (const selectParts of this.selectParts) {
|
||||
if (!selectParts.some(part => part.def.type === 'aggregate')) {
|
||||
const aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] });
|
||||
selectParts.splice(1, 0, aggregate);
|
||||
if (!selectParts.some(part => part.def.type === 'alias')) {
|
||||
const alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] });
|
||||
selectParts.push(alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePersistedParts();
|
||||
}
|
||||
|
||||
removeGroup(part, index) {
|
||||
if (part.def.type === 'time') {
|
||||
// remove aggregations
|
||||
this.selectParts = _.map(this.selectParts, (s: any) => {
|
||||
return _.filter(s, (part: any) => {
|
||||
if (part.def.type === 'aggregate' || part.def.type === 'percentile') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.groupParts.splice(index, 1);
|
||||
this.updatePersistedParts();
|
||||
}
|
||||
|
||||
handleWherePartEvent(whereParts, part, evt, index) {
|
||||
switch (evt.name) {
|
||||
case 'get-param-options': {
|
||||
switch (evt.param.name) {
|
||||
case 'left':
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildColumnQuery())
|
||||
.then(this.transformToSegments({}))
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
case 'right':
|
||||
if (['int', 'bigint', 'double', 'datetime'].indexOf(part.datatype) > -1) {
|
||||
// don't do value lookups for numerical fields
|
||||
return this.$q.when([]);
|
||||
} else {
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildValueQuery(part.params[0]))
|
||||
.then(
|
||||
this.transformToSegments({
|
||||
addTemplateVars: true,
|
||||
templateQuoter: (v: string) => {
|
||||
return this.queryModel.quoteLiteral(v);
|
||||
},
|
||||
})
|
||||
)
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
}
|
||||
case 'op':
|
||||
return this.$q.when(this.uiSegmentSrv.newOperators(this.metaBuilder.getOperators(part.datatype)));
|
||||
default:
|
||||
return this.$q.when([]);
|
||||
}
|
||||
}
|
||||
case 'part-param-changed': {
|
||||
this.updatePersistedParts();
|
||||
this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(part.params[0])).then((d: any) => {
|
||||
if (d.length === 1) {
|
||||
part.datatype = d[0].text;
|
||||
}
|
||||
});
|
||||
this.panelCtrl.refresh();
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
// remove element
|
||||
whereParts.splice(index, 1);
|
||||
this.updatePersistedParts();
|
||||
this.panelCtrl.refresh();
|
||||
break;
|
||||
}
|
||||
case 'get-part-actions': {
|
||||
return this.$q.when([{ text: 'Remove', value: 'remove-part' }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getWhereOptions() {
|
||||
const options = [];
|
||||
if (this.queryModel.hasUnixEpochTimecolumn()) {
|
||||
options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__unixEpochFilter' }));
|
||||
} else {
|
||||
options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__timeFilter' }));
|
||||
}
|
||||
options.push(this.uiSegmentSrv.newSegment({ type: 'expression', value: 'Expression' }));
|
||||
return this.$q.when(options);
|
||||
}
|
||||
|
||||
addWhereAction(part, index) {
|
||||
switch (this.whereAdd.type) {
|
||||
case 'macro': {
|
||||
const partModel = sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] });
|
||||
if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') {
|
||||
// replace current macro
|
||||
this.whereParts[0] = partModel;
|
||||
} else {
|
||||
this.whereParts.splice(0, 0, partModel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] }));
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePersistedParts();
|
||||
this.resetPlusButton(this.whereAdd);
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
|
||||
getGroupOptions() {
|
||||
return this.datasource
|
||||
.metricFindQuery(this.metaBuilder.buildColumnQuery('group'))
|
||||
.then(tags => {
|
||||
const options = [];
|
||||
if (!this.queryModel.hasTimeGroup()) {
|
||||
options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time($__interval,none)' }));
|
||||
}
|
||||
for (const tag of tags) {
|
||||
options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text }));
|
||||
}
|
||||
return options;
|
||||
})
|
||||
.catch(this.handleQueryError.bind(this));
|
||||
}
|
||||
|
||||
addGroupAction() {
|
||||
switch (this.groupAdd.value) {
|
||||
default: {
|
||||
this.addGroup(this.groupAdd.type, this.groupAdd.value);
|
||||
}
|
||||
}
|
||||
|
||||
this.resetPlusButton(this.groupAdd);
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
|
||||
handleQueryError(err) {
|
||||
this.error = err.message || 'Failed to issue metric query';
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,23 @@ describe('MySQLDatasource', () => {
|
||||
replace: jest.fn(text => text),
|
||||
};
|
||||
|
||||
const raw = {
|
||||
from: moment.utc('2018-04-25 10:00'),
|
||||
to: moment.utc('2018-04-25 11:00'),
|
||||
};
|
||||
const ctx = {
|
||||
backendSrv,
|
||||
timeSrvMock: {
|
||||
timeRange: () => ({
|
||||
from: raw.from,
|
||||
to: raw.to,
|
||||
raw: raw,
|
||||
}),
|
||||
},
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv);
|
||||
ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv, ctx.timeSrvMock);
|
||||
});
|
||||
|
||||
describe('When performing annotationQuery', () => {
|
||||
|
86
public/app/plugins/datasource/mysql/sql_part.ts
Normal file
86
public/app/plugins/datasource/mysql/sql_part.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { SqlPartDef, SqlPart } from 'app/core/components/sql_part/sql_part';
|
||||
|
||||
const index = [];
|
||||
|
||||
function createPart(part): any {
|
||||
const def = index[part.type];
|
||||
if (!def) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SqlPart(part, def);
|
||||
}
|
||||
|
||||
function register(options: any) {
|
||||
index[options.type] = new SqlPartDef(options);
|
||||
}
|
||||
|
||||
register({
|
||||
type: 'column',
|
||||
style: 'label',
|
||||
params: [{ type: 'column', dynamicLookup: true }],
|
||||
defaultParams: ['value'],
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'expression',
|
||||
style: 'expression',
|
||||
label: 'Expr:',
|
||||
params: [
|
||||
{ name: 'left', type: 'string', dynamicLookup: true },
|
||||
{ name: 'op', type: 'string', dynamicLookup: true },
|
||||
{ name: 'right', type: 'string', dynamicLookup: true },
|
||||
],
|
||||
defaultParams: ['value', '=', 'value'],
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'macro',
|
||||
style: 'label',
|
||||
label: 'Macro:',
|
||||
params: [],
|
||||
defaultParams: [],
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'aggregate',
|
||||
style: 'label',
|
||||
params: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
options: ['avg', 'count', 'min', 'max', 'sum', 'stddev', 'variance'],
|
||||
},
|
||||
],
|
||||
defaultParams: ['avg'],
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'alias',
|
||||
style: 'label',
|
||||
params: [{ name: 'name', type: 'string', quote: 'double' }],
|
||||
defaultParams: ['alias'],
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'time',
|
||||
style: 'function',
|
||||
label: 'time',
|
||||
params: [
|
||||
{
|
||||
name: 'interval',
|
||||
type: 'interval',
|
||||
options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'],
|
||||
},
|
||||
{
|
||||
name: 'fill',
|
||||
type: 'string',
|
||||
options: ['none', 'NULL', 'previous', '0'],
|
||||
},
|
||||
],
|
||||
defaultParams: ['$__interval', 'none'],
|
||||
});
|
||||
|
||||
export default {
|
||||
create: createPart,
|
||||
};
|
Loading…
Reference in New Issue
Block a user