From 41b5dae606b834b29f218be5ab727e7985897c9d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 30 Aug 2018 16:52:12 +0200 Subject: [PATCH 001/156] start implementing mysql query editor as a copy of postgres query editor --- .../plugins/datasource/mysql/meta_query.ts | 139 +++++ .../plugins/datasource/mysql/mysql_query.ts | 285 +++++++++ .../mysql/partials/query.editor.html | 106 +++- .../plugins/datasource/mysql/query_ctrl.ts | 570 +++++++++++++++++- .../app/plugins/datasource/mysql/sql_part.ts | 86 +++ 5 files changed, 1168 insertions(+), 18 deletions(-) create mode 100644 public/app/plugins/datasource/mysql/meta_query.ts create mode 100644 public/app/plugins/datasource/mysql/mysql_query.ts create mode 100644 public/app/plugins/datasource/mysql/sql_part.ts diff --git a/public/app/plugins/datasource/mysql/meta_query.ts b/public/app/plugins/datasource/mysql/meta_query.ts new file mode 100644 index 00000000000..94e3e8fc3d6 --- /dev/null +++ b/public/app/plugins/datasource/mysql/meta_query.ts @@ -0,0 +1,139 @@ +export class MysqlMetaQuery { + constructor(private target, private queryModel) {} + + getOperators(datatype: string) { + switch (datatype) { + case 'float4': + case 'float8': { + return ['=', '!=', '<', '<=', '>', '>=']; + } + case 'text': + 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 + let 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 + 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('.')) { + let parts = table.split('.'); + query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]); + query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]); + return query; + } else { + query = ' table_name = ' + this.quoteIdentAsLiteral(table); + + return query; + } + } + + buildTableQuery() { + return 'SELECT table_name FROM information_schema.tables 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','float')"; + break; + } + case 'metric': { + query += " AND data_type IN ('text' 'tinytext','mediumtext', 'longtext', 'varchar')"; + break; + } + case 'value': { + query += + " AND data_type IN ('bigint','int','float','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')"; + 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; + } +} diff --git a/public/app/plugins/datasource/mysql/mysql_query.ts b/public/app/plugins/datasource/mysql/mysql_query.ts new file mode 100644 index 00000000000..1c4b927ceea --- /dev/null +++ b/public/app/plugins/datasource/mysql/mysql_query.ts @@ -0,0 +1,285 @@ +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); + } + + let escapedValues = _.map(value, this.quoteLiteral); + return escapedValues.join(','); + } + + render(interpolate?) { + let 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 ['int4', 'int8', 'float4', 'float8', 'numeric'].indexOf(this.target.timeColumnType) > -1; + } + + buildTimeColumn(alias = true) { + let 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 (let column of this.target.select) { + query += ',\n ' + this.buildValueColumn(column); + } + + return query; + } + + buildValueColumn(column) { + let query = ''; + + let columnName = _.find(column, (g: any) => g.type === 'column'); + query = columnName.params[0]; + + let aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); + let windows = _.find(column, (g: any) => g.type === 'window' || g.type === 'moving_window'); + + if (aggregate) { + let func = aggregate.params[0]; + switch (aggregate.type) { + case 'aggregate': + if (func === 'first' || func === 'last') { + query = func + '(' + query + ',' + this.target.timeColumn + ')'; + } else { + query = func + '(' + query + ')'; + } + break; + case 'percentile': + query = func + '(' + aggregate.params[1] + ') WITHIN GROUP (ORDER BY ' + query + ')'; + break; + } + } + + if (windows) { + let overParts = []; + if (this.hasMetricColumn()) { + overParts.push('PARTITION BY ' + this.target.metricColumn); + } + overParts.push('ORDER BY ' + this.buildTimeColumn(false)); + + let over = overParts.join(' '); + let curr: string; + let prev: string; + switch (windows.type) { + case 'window': + switch (windows.params[0]) { + case 'increase': + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + break; + case 'rate': + let timeColumn = this.target.timeColumn; + if (aggregate) { + timeColumn = 'min(' + timeColumn + ')'; + } + + curr = query; + prev = 'lag(' + curr + ') OVER (' + over + ')'; + query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev + ' ELSE ' + curr + ' END)'; + query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; + break; + default: + query = windows.params[0] + '(' + query + ') OVER (' + over + ')'; + break; + } + break; + case 'moving_window': + query = windows.params[0] + '(' + query + ') OVER (' + over + ' ROWS ' + windows.params[1] + ' PRECEDING)'; + break; + } + } + + let alias = _.find(column, (g: any) => g.type === 'alias'); + if (alias) { + query += ' AS ' + this.quoteIdentifier(alias.params[0]); + } + + return query; + } + + buildWhereClause() { + let query = ''; + let 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++) { + let 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 1'; + + return query; + } +} diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 1e829a1175d..0c630947657 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -1,10 +1,102 @@ - -
-
- - -
-
+ + +
+
+
+ + +
+
+
+ +
+
+
+ + + + + + + + +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+ +
+
+ + + + +
+ +
+ +
+ +
+
+
+
+ +
diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts index 1de1fb768ad..1c911368ed8 100644 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ b/public/app/plugins/datasource/mysql/query_ctrl.ts @@ -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, function(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, function(selectParts) { + return _.map(selectParts, function(part: any) { + return { type: part.def.type, datatype: part.datatype, params: part.params }; + }); + }); + this.target.where = _.map(this.whereParts, function(part: any) { + return { type: part.def.type, datatype: part.datatype, name: part.name, params: part.params }; + }); + this.target.group = _.map(this.groupParts, function(part: any) { + return { type: part.def.type, datatype: part.datatype, params: part.params }; + }); + } + + buildSelectMenu() { + this.selectMenu = []; + let 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) { + let 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(); + + let segment = this.uiSegmentSrv.newSegment('none'); + this.metricColumnSegment.html = segment.html; + this.metricColumnSegment.value = segment.value; + this.target.metricColumn = 'none'; + + let 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)) { + let segment = this.uiSegmentSrv.newSegment(result[0].text); + this.timeColumnSegment.html = segment.html; + this.timeColumnSegment.value = segment.value; + } + return this.timeColumnChanged(false); + }); + let 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 => { + let segments = _.map(results, segment => { + return this.uiSegmentSrv.newSegment({ + value: segment.text, + expandable: segment.expandable, + }); + }); + + if (config.addTemplateVars) { + for (let 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': + let parts = _.map(selectParts, function(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'); + } + let 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': + let windowIndex = this.findWindowIndex(selectParts); + if (windowIndex !== -1) { + // replace current window function + selectParts[windowIndex] = partModel; + } else { + let 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) { + let modelsIndex = _.indexOf(this.selectParts, selectParts); + this.selectParts.splice(modelsIndex, 1); + } + } else { + let 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']; + } + let 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 (let selectParts of this.selectParts) { + if (!selectParts.some(part => part.def.type === 'aggregate')) { + let aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); + selectParts.splice(1, 0, aggregate); + if (!selectParts.some(part => part.def.type === 'alias')) { + let 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 (['int4', 'int8', 'float4', 'float8', 'timestamp', 'timestamptz'].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() { + var 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': { + let 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 => { + var options = []; + if (!this.queryModel.hasTimeGroup()) { + options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time($__interval,none)' })); + } + for (let 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 []; + } } diff --git a/public/app/plugins/datasource/mysql/sql_part.ts b/public/app/plugins/datasource/mysql/sql_part.ts new file mode 100644 index 00000000000..25cdd09baa6 --- /dev/null +++ b/public/app/plugins/datasource/mysql/sql_part.ts @@ -0,0 +1,86 @@ +import { SqlPartDef, SqlPart } from 'app/core/components/sql_part/sql_part'; + +let index = []; + +function createPart(part): any { + let 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, +}; From 0e10fdb4150fbaf8f845c6273791b62af4defb45 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 13:24:49 +0300 Subject: [PATCH 002/156] graph: legend as React component --- public/app/core/angular_wrappers.ts | 2 + public/app/plugins/panel/graph/Legend.tsx | 189 ++++++++++++++++++++++ public/app/plugins/panel/graph/graph.ts | 19 ++- 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/graph/Legend.tsx diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index a4439509f8e..b1105268543 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -6,6 +6,7 @@ import LoginBackground from './components/Login/LoginBackground'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import DashboardPermissions from './components/Permissions/DashboardPermissions'; +import { GraphLegend } from 'app/plugins/panel/graph/Legend'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -19,4 +20,5 @@ export function registerAngularDirectives() { ['tagOptions', { watchDepth: 'reference' }], ]); react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); + react2AngularDirective('graphLegendReact', GraphLegend, ['seriesList', 'className']); } diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx new file mode 100644 index 00000000000..a4bfbefd541 --- /dev/null +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -0,0 +1,189 @@ +import _ from 'lodash'; +import React from 'react'; + +const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; + +export interface GraphLegendProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; + alignAsTable?: boolean; + rightSide?: boolean; + sideWidth?: number; + sort?: 'min' | 'max' | 'avg' | 'current' | 'total'; + sortDesc?: boolean; + className?: string; +} + +export interface GraphLegendState {} + +export class GraphLegend extends React.PureComponent { + sortLegend() { + let seriesList = this.props.seriesList || []; + if (this.props.sort) { + seriesList = _.sortBy(seriesList, function(series) { + let sort = series.stats[this.props.sort]; + if (sort === null) { + sort = -Infinity; + } + return sort; + }); + if (this.props.sortDesc) { + seriesList = seriesList.reverse(); + } + } + return seriesList; + } + + render() { + const { className = '', hiddenSeries } = this.props; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + const seriesList = this.sortLegend(); + return ( +
+
+
+ {this.props.alignAsTable ? ( + + ) : ( + seriesList.map((series, i) => ( + + )) + )} +
+
+
+ ); + } +} + +interface LegendTableProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendTable extends React.PureComponent { + render() { + const seriesList = this.props.seriesList; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + const headerStyle: React.CSSProperties = { + textAlign: 'left', + }; + + return ( + + + + {LEGEND_STATS.map( + statName => seriesValuesProps[statName] && + )} + + {seriesList.map((series, i) => ( + + ))} + + ); + } +} + +interface LegendTableHeaderProps { + statName: string; + sortDesc?: boolean; +} + +function LegendTableHeader(props: LegendTableHeaderProps) { + return ( + + {props.statName} + + + ); +} + +interface LegendSeriesItemProps { + series: any; + index: number; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendSeriesItem extends React.Component { + constructor(props) { + super(props); + } + + render() { + const { series, index, hiddenSeries } = this.props; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; + return ( +
+
+ +
+ + {series.aliasEscaped} + + {valueItems} +
+ ); + } +} + +function LegendValue(props) { + const value = props.value; + const valueName = props.valueName; + return
{value}
; +} + +function renderLegendValues(props: LegendSeriesItemProps, series) { + const legendValueItems = []; + for (const valueName of LEGEND_STATS) { + if (props[valueName]) { + const valueFormatted = series.formatValue(series.stats[valueName]); + legendValueItems.push(); + } + } + return legendValueItems; +} + +function getOptionSeriesCSSClasses(series, hiddenSeries) { + const classes = []; + if (series.yaxis === 2) { + classes.push('graph-legend-series--right-y'); + } + if (hiddenSeries[series.alias]) { + classes.push('graph-legend-series-hidden'); + } + return classes.join(' '); +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 7f9fa0e1693..37841313c82 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -20,6 +20,9 @@ import { EventManager } from 'app/features/annotations/all'; import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { GraphLegend, GraphLegendProps } from './Legend'; import { GraphCtrl } from './module'; @@ -82,7 +85,21 @@ class GraphElement { const graphHeight = this.elem.height(); updateLegendValues(this.data, this.panel, graphHeight); - this.ctrl.events.emit('render-legend'); + // this.ctrl.events.emit('render-legend'); + const { values, min, max, avg, current, total } = this.panel.legend; + const { alignAsTable, rightSide, sideWidth } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth }; + const valueOptions = { values, min, max, avg, current, total }; + const legendProps: GraphLegendProps = { + seriesList: this.data, + hiddenSeries: this.ctrl.hiddenSeries, + ...legendOptions, + ...valueOptions, + }; + const legendReactElem = React.createElement(GraphLegend, legendProps); + const legendElem = this.elem.parent().find('.graph-legend'); + ReactDOM.render(legendReactElem, legendElem[0]); + this.onLegendRenderingComplete(); } onGraphHover(evt) { From 329f39e4d796a255057ee9e768474fcdd1bbea18 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 16:34:22 +0300 Subject: [PATCH 003/156] graph: make table markup corresponding to standards --- public/app/plugins/panel/graph/Legend.tsx | 186 +++++++++++++--------- 1 file changed, 110 insertions(+), 76 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index a4bfbefd541..ba11ba988f2 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -41,90 +41,39 @@ export class GraphLegend extends React.PureComponent -
-
- {this.props.alignAsTable ? ( - - ) : ( - seriesList.map((series, i) => ( - - )) - )} -
+
+
+ {this.props.alignAsTable ? ( + + ) : ( + seriesList.map((series, i) => ( + + )) + )}
); } } -interface LegendTableProps { - seriesList: any[]; - hiddenSeries: any; - values?: boolean; - min?: boolean; - max?: boolean; - avg?: boolean; - current?: boolean; - total?: boolean; -} - -class LegendTable extends React.PureComponent { - render() { - const seriesList = this.props.seriesList; - const { values, min, max, avg, current, total } = this.props; - const seriesValuesProps = { values, min, max, avg, current, total }; - const headerStyle: React.CSSProperties = { - textAlign: 'left', - }; - - return ( - - - - {LEGEND_STATS.map( - statName => seriesValuesProps[statName] && - )} - - {seriesList.map((series, i) => ( - - ))} - - ); - } -} - -interface LegendTableHeaderProps { - statName: string; - sortDesc?: boolean; -} - -function LegendTableHeader(props: LegendTableHeaderProps) { - return ( - - {props.statName} - - - ); -} - interface LegendSeriesItemProps { series: any; index: number; @@ -163,20 +112,105 @@ class LegendSeriesItem extends React.Component { function LegendValue(props) { const value = props.value; const valueName = props.valueName; + if (props.asTable) { + return {value}; + } return
{value}
; } -function renderLegendValues(props: LegendSeriesItemProps, series) { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { const valueFormatted = series.formatValue(series.stats[valueName]); - legendValueItems.push(); + legendValueItems.push( + + ); } } return legendValueItems; } +interface LegendTableProps { + seriesList: any[]; + hiddenSeries: any; + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +class LegendTable extends React.PureComponent { + render() { + const seriesList = this.props.seriesList; + const { values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + + return ( + + + + + {seriesList.map((series, i) => ( + + ))} + +
+ {LEGEND_STATS.map( + statName => seriesValuesProps[statName] && + )} +
+ ); + } +} + +class LegendSeriesItemAsTable extends React.Component { + constructor(props) { + super(props); + } + + render() { + const { series, index, hiddenSeries } = this.props; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series, true) : []; + return ( + + +
+ +
+ + {series.aliasEscaped} + + + {valueItems} + + ); + } +} + +interface LegendTableHeaderProps { + statName: string; + sortDesc?: boolean; +} + +function LegendTableHeader(props: LegendTableHeaderProps) { + return ( + + {props.statName} + + + ); +} + function getOptionSeriesCSSClasses(series, hiddenSeries) { const classes = []; if (series.yaxis === 2) { From 390472aa99ce631dc7f9cf37cb2c8dfea643a827 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 15:40:58 +0200 Subject: [PATCH 004/156] render query from query builder --- .../app/plugins/datasource/mysql/datasource.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index e41417e155c..612ab9adb6c 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -1,16 +1,19 @@ 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; /** @ngInject **/ constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(this.$q); + this.queryModel = new MysqlQuery({}); } interpolateVariable(value, variable) { @@ -37,16 +40,18 @@ export class MysqlDatasource { } 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 => { + let 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, }; }); From 8d73f53e973fbf0cd014c8d0965bd31f9f086140 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 16:27:48 +0200 Subject: [PATCH 005/156] use quoting functions from MysqlQuery in datasource --- public/app/plugins/datasource/mysql/datasource.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 612ab9adb6c..d5bd2594f24 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -19,7 +19,7 @@ export class MysqlDatasource { interpolateVariable(value, variable) { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { - return "'" + value.replace(/'/g, `''`) + "'"; + return this.queryModel.quoteLiteral(value); } else { return value; } @@ -29,12 +29,8 @@ export class MysqlDatasource { return value; } - const quotedValues = _.map(value, function(val) { - if (typeof value === 'number') { - return value; - } - - return "'" + val.replace(/'/g, `''`) + "'"; + const quotedValues = _.map(value, v => { + return this.queryModel.quoteLiteral(v); }); return quotedValues.join(','); } From 60146109ab09d101879c9a9b7832b7e1ae14c2ba Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Aug 2018 17:27:57 +0300 Subject: [PATCH 006/156] graph legend: minor refactor --- public/app/plugins/panel/graph/Legend.tsx | 51 ++++++++++++++--------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index ba11ba988f2..e1748ea3655 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -41,19 +41,23 @@ export class GraphLegend extends React.PureComponent +
{this.props.alignAsTable ? ( @@ -97,18 +101,32 @@ class LegendSeriesItem extends React.Component { const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; return (
-
- -
- - {series.aliasEscaped} - + {valueItems}
); } } +interface LegendSeriesLabelProps { + label: string; + color: string; +} + +function LegendSeriesLabel(props: LegendSeriesLabelProps) { + const { label, color } = props; + return ( +
+
+ +
+ + {label} + +
+ ); +} + function LegendValue(props) { const value = props.value; const valueName = props.valueName; @@ -118,7 +136,7 @@ function LegendValue(props) { return
{value}
; } -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false): React.ReactElement[] { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { @@ -184,12 +202,7 @@ class LegendSeriesItemAsTable extends React.Component { return ( -
- -
- - {series.aliasEscaped} - + {valueItems} From cd708d6cb2100f8c76c967e9432fda36f5a9f289 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 16:52:26 +0200 Subject: [PATCH 007/156] ignore information_schema tables --- public/app/plugins/datasource/mysql/meta_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/mysql/meta_query.ts b/public/app/plugins/datasource/mysql/meta_query.ts index 94e3e8fc3d6..d5383e85ff7 100644 --- a/public/app/plugins/datasource/mysql/meta_query.ts +++ b/public/app/plugins/datasource/mysql/meta_query.ts @@ -86,7 +86,7 @@ export class MysqlMetaQuery { } buildTableQuery() { - return 'SELECT table_name FROM information_schema.tables ORDER BY table_name'; + return "SELECT table_name FROM information_schema.tables WHERE table_schema <> 'information_schema' ORDER BY table_name"; } buildColumnQuery(type?: string) { From bcfb841cb48178b7eb9fc9908e304d47a313a59a Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 31 Aug 2018 18:24:09 +0200 Subject: [PATCH 008/156] pass timerange in meta data queries --- public/app/plugins/datasource/mysql/datasource.ts | 5 ++++- .../datasource/mysql/specs/datasource.test.ts | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index d5bd2594f24..7b112ef4336 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -9,7 +9,7 @@ export class MysqlDatasource { queryModel: MysqlQuery; /** @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); @@ -108,8 +108,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) { diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index e75ba5e32ee..163f3afe671 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -9,12 +9,23 @@ describe('MySQLDatasource', function() { 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, + }), + }, }; beforeEach(() => { - ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv); + ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv, ctx.timeSrvMock); }); describe('When performing annotationQuery', function() { From e8a52117a5f55e05579d29c773ca1b2e83cd2d76 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 3 Sep 2018 16:54:52 +0300 Subject: [PATCH 009/156] graph legend: react component refactor --- public/app/plugins/panel/graph/Legend.tsx | 171 ++++++++++++---------- public/app/plugins/panel/graph/graph.ts | 5 +- 2 files changed, 97 insertions(+), 79 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index e1748ea3655..becb52d1aeb 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,32 +1,62 @@ import _ from 'lodash'; import React from 'react'; +import { TimeSeries } from 'app/core/core'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; -export interface GraphLegendProps { - seriesList: any[]; +interface LegendProps { + seriesList: TimeSeries[]; + optionalClass?: string; +} + +interface LegendDisplayProps { hiddenSeries: any; + hideEmpty?: boolean; + hideZero?: boolean; + alignAsTable?: boolean; + rightSide?: boolean; + sideWidth?: number; +} + +interface LegendValuesProps { values?: boolean; min?: boolean; max?: boolean; avg?: boolean; current?: boolean; total?: boolean; - alignAsTable?: boolean; - rightSide?: boolean; - sideWidth?: number; +} + +interface LegendSortProps { sort?: 'min' | 'max' | 'avg' | 'current' | 'total'; sortDesc?: boolean; - className?: string; } +export type GraphLegendProps = LegendProps & LegendDisplayProps & LegendValuesProps & LegendSortProps; + +const defaultGraphLegendProps: Partial = { + values: false, + min: false, + max: false, + avg: false, + current: false, + total: false, + alignAsTable: false, + rightSide: false, + sort: undefined, + sortDesc: false, + optionalClass: '', +}; + export interface GraphLegendState {} export class GraphLegend extends React.PureComponent { + static defaultProps = defaultGraphLegendProps; + sortLegend() { let seriesList = this.props.seriesList || []; if (this.props.sort) { - seriesList = _.sortBy(seriesList, function(series) { + seriesList = _.sortBy(seriesList, series => { let sort = series.stats[this.props.sort]; if (sort === null) { sort = -Infinity; @@ -41,11 +71,12 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); + const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; // Set min-width if side style and there is a value, otherwise remove the CSS property // Set width so it works with IE11 @@ -62,15 +93,7 @@ export class GraphLegend extends React.PureComponent ) : ( - seriesList.map((series, i) => ( - - )) + )}
@@ -78,23 +101,24 @@ export class GraphLegend extends React.PureComponent { + render() { + const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; + const seriesValuesProps = { values, min, max, avg, current, total }; + return seriesList.map((series, i) => ( + + )); + } } -class LegendSeriesItem extends React.Component { - constructor(props) { - super(props); - } +interface LegendSeriesProps { + series: TimeSeries; + index: number; +} +type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; + +class LegendSeriesItem extends React.PureComponent { render() { const { series, index, hiddenSeries } = this.props; const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); @@ -113,21 +137,27 @@ interface LegendSeriesLabelProps { color: string; } -function LegendSeriesLabel(props: LegendSeriesLabelProps) { - const { label, color } = props; - return ( -
-
+class LegendSeriesLabel extends React.PureComponent { + render() { + const { label, color } = this.props; + return [ +
-
- +
, + {label} - -
- ); + , + ]; + } } -function LegendValue(props) { +interface LegendValueProps { + value: string; + valueName: string; + asTable?: boolean; +} + +function LegendValue(props: LegendValueProps) { const value = props.value; const valueName = props.valueName; if (props.asTable) { @@ -149,30 +179,21 @@ function renderLegendValues(props: LegendSeriesItemProps, series, asTable = fals return legendValueItems; } -interface LegendTableProps { - seriesList: any[]; - hiddenSeries: any; - values?: boolean; - min?: boolean; - max?: boolean; - avg?: boolean; - current?: boolean; - total?: boolean; -} - -class LegendTable extends React.PureComponent { +class LegendTable extends React.PureComponent> { render() { const seriesList = this.props.seriesList; - const { values, min, max, avg, current, total } = this.props; + const { values, min, max, avg, current, total, sort, sortDesc } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; - return ( {seriesList.map((series, i) => ( @@ -190,11 +211,21 @@ class LegendTable extends React.PureComponent { } } -class LegendSeriesItemAsTable extends React.Component { - constructor(props) { - super(props); - } +interface LegendTableHeaderProps { + statName: string; +} +function LegendTableHeader(props: LegendTableHeaderProps & LegendSortProps) { + const { statName, sort, sortDesc } = props; + return ( + + ); +} + +class LegendSeriesItemAsTable extends React.PureComponent { render() { const { series, index, hiddenSeries } = this.props; const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); @@ -210,20 +241,6 @@ class LegendSeriesItemAsTable extends React.Component { } } -interface LegendTableHeaderProps { - statName: string; - sortDesc?: boolean; -} - -function LegendTableHeader(props: LegendTableHeaderProps) { - return ( - - ); -} - function getOptionSeriesCSSClasses(series, hiddenSeries) { const classes = []; if (series.yaxis === 2) { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 37841313c82..a1066295048 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -86,9 +86,10 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); + console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; - const { alignAsTable, rightSide, sideWidth } = this.panel.legend; - const legendOptions = { alignAsTable, rightSide, sideWidth }; + const { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero }; const valueOptions = { values, min, max, avg, current, total }; const legendProps: GraphLegendProps = { seriesList: this.data, From b891a858ca0934fbec5fd54b64d55d2763bf6f80 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 4 Sep 2018 12:49:13 +0300 Subject: [PATCH 010/156] graph legend: implement series toggling and sorting --- public/app/plugins/panel/graph/Legend.tsx | 88 +++++++++++++++++++---- public/app/plugins/panel/graph/graph.ts | 8 ++- public/app/plugins/panel/graph/module.ts | 6 ++ 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index becb52d1aeb..362ca238e80 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -7,6 +7,8 @@ const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; interface LegendProps { seriesList: TimeSeries[]; optionalClass?: string; + onToggleSeries?: (series: TimeSeries, event: Event) => void; + onToggleSort?: (sortBy, sortDesc) => void; } interface LegendDisplayProps { @@ -70,11 +72,18 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; @@ -87,14 +96,19 @@ export class GraphLegend extends React.PureComponent this.onToggleSeries(s, e), + onToggleSort: (sortBy, sortDesc) => this.props.onToggleSort(sortBy, sortDesc), + ...seriesValuesProps, + ...sortProps, + }; + return (
- {this.props.alignAsTable ? ( - - ) : ( - - )} + {this.props.alignAsTable ? : }
); @@ -106,7 +120,14 @@ class LegendSeriesList extends React.PureComponent { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; return seriesList.map((series, i) => ( - + this.props.onToggleSeries(series, e)} + /> )); } } @@ -114,6 +135,7 @@ class LegendSeriesList extends React.PureComponent { interface LegendSeriesProps { series: TimeSeries; index: number; + onLabelClick?: (event) => void; } type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; @@ -125,7 +147,11 @@ class LegendSeriesItem extends React.PureComponent { const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; return (
- + this.props.onLabelClick(e)} + /> {valueItems}
); @@ -135,16 +161,18 @@ class LegendSeriesItem extends React.PureComponent { interface LegendSeriesLabelProps { label: string; color: string; + onLabelClick?: (event) => void; + onIconClick?: (event) => void; } class LegendSeriesLabel extends React.PureComponent { render() { const { label, color } = this.props; return [ -
+
this.props.onIconClick(e)}>
, - + this.props.onLabelClick(e)}> {label} , ]; @@ -180,6 +208,24 @@ function renderLegendValues(props: LegendSeriesItemProps, series, asTable = fals } class LegendTable extends React.PureComponent> { + onToggleSort(stat) { + let sortDesc = this.props.sortDesc; + let sortBy = this.props.sort; + if (stat !== sortBy) { + sortDesc = null; + } + + // if already sort ascending, disable sorting + if (sortDesc === false) { + sortBy = null; + sortDesc = null; + } else { + sortDesc = !sortDesc; + sortBy = stat; + } + this.props.onToggleSort(sortBy, sortDesc); + } + render() { const seriesList = this.props.seriesList; const { values, min, max, avg, current, total, sort, sortDesc } = this.props; @@ -192,7 +238,13 @@ class LegendTable extends React.PureComponent> { {LEGEND_STATS.map( statName => seriesValuesProps[statName] && ( - + this.onToggleSort(statName)} + /> ) )}
@@ -203,6 +255,7 @@ class LegendTable extends React.PureComponent> { index={i} hiddenSeries={this.props.hiddenSeries} {...seriesValuesProps} + onLabelClick={e => this.props.onToggleSeries(series, e)} /> ))} @@ -213,12 +266,13 @@ class LegendTable extends React.PureComponent> { interface LegendTableHeaderProps { statName: string; + onClick?: (event) => void; } -function LegendTableHeader(props: LegendTableHeaderProps & LegendSortProps) { +function LegendTableHeaderItem(props: LegendTableHeaderProps & LegendSortProps) { const { statName, sort, sortDesc } = props; return ( - @@ -233,7 +287,11 @@ class LegendSeriesItemAsTable extends React.PureComponent return ( {valueItems} @@ -246,7 +304,7 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { if (series.yaxis === 2) { classes.push('graph-legend-series--right-y'); } - if (hiddenSeries[series.alias]) { + if (hiddenSeries[series.alias] && hiddenSeries[series.alias] === true) { classes.push('graph-legend-series-hidden'); } return classes.join(' '); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index a1066295048..2a6962d78e7 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -86,16 +86,18 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); - console.log(this.ctrl); + // console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; - const { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero } = this.panel.legend; - const legendOptions = { alignAsTable, rightSide, sideWidth, hideEmpty, hideZero }; + const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; + const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; const valueOptions = { values, min, max, avg, current, total }; const legendProps: GraphLegendProps = { seriesList: this.data, hiddenSeries: this.ctrl.hiddenSeries, ...legendOptions, ...valueOptions, + onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), + onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), }; const legendReactElem = React.createElement(GraphLegend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 6467f4e816a..a83417f6e2a 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -287,6 +287,12 @@ class GraphCtrl extends MetricsPanelCtrl { } } + toggleSort(sortBy, sortDesc) { + this.panel.legend.sort = sortBy; + this.panel.legend.sortDesc = sortDesc; + this.render(); + } + toggleAxis(info) { var override = _.find(this.panel.seriesOverrides, { alias: info.alias }); if (!override) { From b2ba9c516626dbf3b87a3d0b5895b3aa84ffcc5a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 14:23:28 +0300 Subject: [PATCH 011/156] wrapper for react-custom-scrollbars component --- package.json | 2 + .../components/ScrollBar/withScrollBar.tsx | 53 +++++++++++++++++++ public/sass/components/_scrollbar.scss | 43 +++++++++++++++ yarn.lock | 42 ++++++++++++++- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/package.json b/package.json index 9cc47ff71b8..d7f136cb1b2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/jest": "^21.1.4", "@types/node": "^8.0.31", "@types/react": "^16.0.25", + "@types/react-custom-scrollbars": "^4.0.5", "@types/react-dom": "^16.0.3", "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", @@ -154,6 +155,7 @@ "prop-types": "^15.6.0", "rc-cascader": "^0.14.0", "react": "^16.2.0", + "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.2.0", "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx new file mode 100644 index 00000000000..9f8ad942167 --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface WithScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const withScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +export default function withScrollBar

(WrappedComponent: React.ComponentType

) { + return class extends React.Component

{ + static defaultProps = withScrollBarDefaultProps; + + render() { + // Use type casting here in order to get rest of the props working. See more + // https://github.com/Microsoft/TypeScript/issues/14409 + // https://github.com/Microsoft/TypeScript/pull/13288 + const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this + .props as WithScrollBarProps; + const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; + + return ( +

} + renderTrackVertical={props =>
} + renderThumbHorizontal={props =>
} + renderThumbVertical={props =>
} + renderView={props =>
} + {...scrollProps} + > + + + ); + } + }; +} diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 78173b73f47..adb9e0c54c0 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -294,3 +294,46 @@ padding-top: 1px; } } + +// Custom styles for 'react-custom-scrollbars' + +.custom-scrollbars { + // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to + // make scroll working it should fit outer container size (scroll appears only when inner container size is + // greater than outer one). + display: flex; + flex-grow: 1; + + .view { + display: flex; + flex-grow: 1; + } + + .track-vertical { + border-radius: 3px; + width: 6px !important; + + right: 2px; + bottom: 2px; + top: 2px; + } + + .track-horizontal { + border-radius: 3px; + height: 6px !important; + + right: 2px; + bottom: 2px; + left: 2px; + } + + .thumb-vertical { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } + + .thumb-horizontal { + @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } +} diff --git a/yarn.lock b/yarn.lock index c15c77cc45f..54f7572d5d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -473,6 +473,10 @@ add-dom-event-listener@1.x: dependencies: object-assign "4.x" +add-px-to-style@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a" + agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" @@ -3406,6 +3410,14 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-css@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202" + dependencies: + add-px-to-style "1.0.0" + prefix-style "2.0.1" + to-camel-case "1.0.0" + dom-helpers@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" @@ -9137,6 +9149,10 @@ prebuild-install@^2.3.0: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" +prefix-style@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -9388,7 +9404,7 @@ qw@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" -raf@^3.4.0: +raf@^3.1.0, raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" dependencies: @@ -9496,6 +9512,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-custom-scrollbars@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db" + dependencies: + dom-css "^2.0.0" + prop-types "^15.5.10" + raf "^3.1.0" + react-dom@^16.2.0: version "16.4.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e" @@ -11335,10 +11359,20 @@ to-buffer@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" +to-camel-case@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" + dependencies: + to-space-case "^1.0.0" + to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +to-no-case@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -11361,6 +11395,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-space-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" + dependencies: + to-no-case "^1.0.0" + toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" From 8db2960d0da06e2178c6d52e18cdad13803d6e89 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:06:54 +0300 Subject: [PATCH 012/156] graph legend: use 'react-custom-scrollbars' for legend scroll --- public/app/plugins/panel/graph/Legend.tsx | 12 +++++++----- public/app/plugins/panel/graph/graph.ts | 5 ++--- public/sass/components/_panel_graph.scss | 7 +------ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 362ca238e80..15f8c7c982a 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,6 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; +import withScrollBar from 'app/core/components/ScrollBar/withScrollBar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -85,7 +86,7 @@ export class GraphLegend extends React.PureComponent !series.hideFromLegend(seriesHideProps)); - const legendCustomClasses = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; + const legendClass = `${this.props.alignAsTable ? 'graph-legend-table' : ''} ${optionalClass}`; // Set min-width if side style and there is a value, otherwise remove the CSS property // Set width so it works with IE11 @@ -106,10 +107,8 @@ export class GraphLegend extends React.PureComponent -
- {this.props.alignAsTable ? : } -
+
+ {this.props.alignAsTable ? : }
); } @@ -309,3 +308,6 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { } return classes.join(' '); } + +export const Legend = withScrollBar(GraphLegend); +export default Legend; diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 2a6962d78e7..8d510242dfa 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -22,7 +22,7 @@ import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; import React from 'react'; import ReactDOM from 'react-dom'; -import { GraphLegend, GraphLegendProps } from './Legend'; +import { Legend, GraphLegendProps } from './Legend'; import { GraphCtrl } from './module'; @@ -86,7 +86,6 @@ class GraphElement { updateLegendValues(this.data, this.panel, graphHeight); // this.ctrl.events.emit('render-legend'); - // console.log(this.ctrl); const { values, min, max, avg, current, total } = this.panel.legend; const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; @@ -99,7 +98,7 @@ class GraphElement { onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), }; - const legendReactElem = React.createElement(GraphLegend, legendProps); + const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); ReactDOM.render(legendReactElem, legendElem[0]); this.onLegendRenderingComplete(); diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 72f3ca3dbbe..0d7d4ff05ed 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -57,9 +57,6 @@ padding-top: 6px; position: relative; - // fix for Firefox (white stripe on the right of scrollbar) - width: calc(100% - 1px); - .popover-content { padding: 0; } @@ -67,11 +64,9 @@ .graph-legend-content { position: relative; - - // fix for Firefox (white stripe on the right of scrollbar) - width: calc(100% - 1px); } +// @TODO: delete unused class .graph-legend-scroll { position: relative; overflow: auto !important; From 28cc605e320bf7ea1e0539df220b744e3baf6dda Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 15:36:22 +0300 Subject: [PATCH 013/156] tests for withScrollBar() wrapper --- .../__snapshots__/withScrollBar.test.tsx.snap | 86 +++++++++++++++++++ .../ScrollBar/withScrollBar.test.tsx | 23 +++++ 2 files changed, 109 insertions(+) create mode 100644 public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap create mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap new file mode 100644 index 00000000000..c6b9b5bb37d --- /dev/null +++ b/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`withScrollBar renders correctly 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/public/app/core/components/ScrollBar/withScrollBar.test.tsx b/public/app/core/components/ScrollBar/withScrollBar.test.tsx new file mode 100644 index 00000000000..89a24a7db8e --- /dev/null +++ b/public/app/core/components/ScrollBar/withScrollBar.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import withScrollBar from './withScrollBar'; + +class TestComponent extends React.Component { + render() { + return
; + } +} + +describe('withScrollBar', () => { + it('renders correctly', () => { + const TestComponentWithScroll = withScrollBar(TestComponent); + const tree = renderer + .create( + +

Scrollable content

+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); From e67b8a3e1ad449a2d94c1578cd508438e0715222 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:14 +0300 Subject: [PATCH 014/156] scrollbar refactor: replace HOC by component with children --- .../ScrollBar/GrafanaScrollbar.test.tsx | 16 ++++++ .../components/ScrollBar/GrafanaScrollbar.tsx | 48 +++++++++++++++++ ...sx.snap => GrafanaScrollbar.test.tsx.snap} | 8 +-- .../ScrollBar/withScrollBar.test.tsx | 23 -------- .../components/ScrollBar/withScrollBar.tsx | 53 ------------------- 5 files changed, 68 insertions(+), 80 deletions(-) create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx create mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.tsx rename public/app/core/components/ScrollBar/__snapshots__/{withScrollBar.test.tsx.snap => GrafanaScrollbar.test.tsx.snap} (94%) delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.test.tsx delete mode 100644 public/app/core/components/ScrollBar/withScrollBar.tsx diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx new file mode 100644 index 00000000000..7e519acd29d --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import GrafanaScrollbar from './GrafanaScrollbar'; + +describe('GrafanaScrollbar', () => { + it('renders correctly', () => { + const tree = renderer + .create( + +

Scrollable content

+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx new file mode 100644 index 00000000000..24e5b0d8828 --- /dev/null +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import Scrollbars from 'react-custom-scrollbars'; + +interface GrafanaScrollBarProps { + customClassName?: string; + autoHide?: boolean; + autoHideTimeout?: number; + autoHideDuration?: number; + hideTracksWhenNotNeeded?: boolean; +} + +const grafanaScrollBarDefaultProps: Partial = { + customClassName: 'custom-scrollbars', + autoHide: true, + autoHideTimeout: 200, + autoHideDuration: 200, + hideTracksWhenNotNeeded: false, +}; + +/** + * Wraps component into component from `react-custom-scrollbars` + */ +class GrafanaScrollbar extends React.Component { + static defaultProps = grafanaScrollBarDefaultProps; + + render() { + const { customClassName, children, ...scrollProps } = this.props; + + return ( +
} + renderTrackVertical={props =>
} + renderThumbHorizontal={props =>
} + renderThumbVertical={props =>
} + renderView={props =>
} + {...scrollProps} + > + {children} + + ); + } +} + +export default GrafanaScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap similarity index 94% rename from public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap rename to public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap index c6b9b5bb37d..8e4f51e3587 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/withScrollBar.test.tsx.snap +++ b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`withScrollBar renders correctly 1`] = ` +exports[`GrafanaScrollbar renders correctly 1`] = `
-
+

+ Scrollable content +

; - } -} - -describe('withScrollBar', () => { - it('renders correctly', () => { - const TestComponentWithScroll = withScrollBar(TestComponent); - const tree = renderer - .create( - -

Scrollable content

-
- ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/ScrollBar/withScrollBar.tsx b/public/app/core/components/ScrollBar/withScrollBar.tsx deleted file mode 100644 index 9f8ad942167..00000000000 --- a/public/app/core/components/ScrollBar/withScrollBar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Scrollbars from 'react-custom-scrollbars'; - -interface WithScrollBarProps { - customClassName?: string; - autoHide?: boolean; - autoHideTimeout?: number; - autoHideDuration?: number; - hideTracksWhenNotNeeded?: boolean; -} - -const withScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - -/** - * Wraps component into component from `react-custom-scrollbars` - */ -export default function withScrollBar

(WrappedComponent: React.ComponentType

) { - return class extends React.Component

{ - static defaultProps = withScrollBarDefaultProps; - - render() { - // Use type casting here in order to get rest of the props working. See more - // https://github.com/Microsoft/TypeScript/issues/14409 - // https://github.com/Microsoft/TypeScript/pull/13288 - const { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded, customClassName, ...props } = this - .props as WithScrollBarProps; - const scrollProps = { autoHide, autoHideTimeout, autoHideDuration, hideTracksWhenNotNeeded }; - - return ( -

} - renderTrackVertical={props =>
} - renderThumbHorizontal={props =>
} - renderThumbVertical={props =>
} - renderView={props =>
} - {...scrollProps} - > - - - ); - } - }; -} From 729cc94dafd884f01806413ccbde55f1a55a02c3 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 6 Sep 2018 22:52:56 +0300 Subject: [PATCH 015/156] graph legend: scroll component refactor --- public/app/plugins/panel/graph/Legend.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 15f8c7c982a..2a9b60f392f 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; -import withScrollBar from 'app/core/components/ScrollBar/withScrollBar'; +import GrafanaScrollbar from 'app/core/components/ScrollBar/GrafanaScrollbar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -309,5 +309,14 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { return classes.join(' '); } -export const Legend = withScrollBar(GraphLegend); +export class Legend extends React.Component { + render() { + return ( + + + + ); + } +} + export default Legend; From 349b2787cbb0ff664d784cb41ae2849a82141e5c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 14:31:56 +0300 Subject: [PATCH 016/156] scrollbar: use enzyme for tests instead of react-test-renderer --- .../ScrollBar/GrafanaScrollbar.test.tsx | 17 +- .../GrafanaScrollbar.test.tsx.snap | 176 ++++++++++-------- 2 files changed, 111 insertions(+), 82 deletions(-) diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx index 7e519acd29d..d4d3de6aea7 100644 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx +++ b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx @@ -1,16 +1,15 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; import GrafanaScrollbar from './GrafanaScrollbar'; describe('GrafanaScrollbar', () => { it('renders correctly', () => { - const tree = renderer - .create( - -

Scrollable content

-
- ) - .toJSON(); - expect(tree).toMatchSnapshot(); + const tree = mount( + +

Scrollable content

+
+ ); + expect(toJson(tree)).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap index 8e4f51e3587..7d0af38a6dc 100644 --- a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap +++ b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap @@ -1,86 +1,116 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`GrafanaScrollbar renders correctly 1`] = ` -
-
-

- Scrollable content -

-
-
-
-
-
-
-
+ > +
+

+ Scrollable content +

+
+
+
+
+
+
+
+
+ + `; From e4a488baf1279d9d41afd6a4bbe84e9cb6c9a1b5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Sep 2018 16:12:28 +0300 Subject: [PATCH 017/156] graph legend: use refactored version of scrollbar, #13175 --- .../ScrollBar/GrafanaScrollbar.test.tsx | 15 --- .../components/ScrollBar/GrafanaScrollbar.tsx | 48 -------- .../GrafanaScrollbar.test.tsx.snap | 116 ------------------ public/app/plugins/panel/graph/Legend.tsx | 8 +- 4 files changed, 4 insertions(+), 183 deletions(-) delete mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx delete mode 100644 public/app/core/components/ScrollBar/GrafanaScrollbar.tsx delete mode 100644 public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx deleted file mode 100644 index d4d3de6aea7..00000000000 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import GrafanaScrollbar from './GrafanaScrollbar'; - -describe('GrafanaScrollbar', () => { - it('renders correctly', () => { - const tree = mount( - -

Scrollable content

-
- ); - expect(toJson(tree)).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx b/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx deleted file mode 100644 index 24e5b0d8828..00000000000 --- a/public/app/core/components/ScrollBar/GrafanaScrollbar.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import Scrollbars from 'react-custom-scrollbars'; - -interface GrafanaScrollBarProps { - customClassName?: string; - autoHide?: boolean; - autoHideTimeout?: number; - autoHideDuration?: number; - hideTracksWhenNotNeeded?: boolean; -} - -const grafanaScrollBarDefaultProps: Partial = { - customClassName: 'custom-scrollbars', - autoHide: true, - autoHideTimeout: 200, - autoHideDuration: 200, - hideTracksWhenNotNeeded: false, -}; - -/** - * Wraps component into component from `react-custom-scrollbars` - */ -class GrafanaScrollbar extends React.Component { - static defaultProps = grafanaScrollBarDefaultProps; - - render() { - const { customClassName, children, ...scrollProps } = this.props; - - return ( -
} - renderTrackVertical={props =>
} - renderThumbHorizontal={props =>
} - renderThumbVertical={props =>
} - renderView={props =>
} - {...scrollProps} - > - {children} - - ); - } -} - -export default GrafanaScrollbar; diff --git a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap b/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap deleted file mode 100644 index 7d0af38a6dc..00000000000 --- a/public/app/core/components/ScrollBar/__snapshots__/GrafanaScrollbar.test.tsx.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GrafanaScrollbar renders correctly 1`] = ` - - -
-
-

- Scrollable content -

-
-
-
-
-
-
-
-
- - -`; diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index 2a9b60f392f..e493d7d5020 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React from 'react'; import { TimeSeries } from 'app/core/core'; -import GrafanaScrollbar from 'app/core/components/ScrollBar/GrafanaScrollbar'; +import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -193,7 +193,7 @@ function LegendValue(props: LegendValueProps) { return
{value}
; } -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false): React.ReactElement[] { +function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { const legendValueItems = []; for (const valueName of LEGEND_STATS) { if (props[valueName]) { @@ -312,9 +312,9 @@ function getOptionSeriesCSSClasses(series, hiddenSeries) { export class Legend extends React.Component { render() { return ( - + - + ); } } From 0bf2d5ebcd297392bad68b1a9a7d2a0f52da838c Mon Sep 17 00:00:00 2001 From: Carlos Mondragon Date: Wed, 10 Oct 2018 16:05:50 -0700 Subject: [PATCH 018/156] Extract ApiKeyCount from state. --- public/app/features/api-keys/state/selectors.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/api-keys/state/selectors.ts b/public/app/features/api-keys/state/selectors.ts index 8065c252e85..789237ae9a3 100644 --- a/public/app/features/api-keys/state/selectors.ts +++ b/public/app/features/api-keys/state/selectors.ts @@ -1,5 +1,7 @@ import { ApiKeysState } from 'app/types'; +export const getApiKeysCount = (state: ApiKeysState) => state.keys.length; + export const getApiKeys = (state: ApiKeysState) => { const regex = RegExp(state.searchQuery, 'i'); From d2573a6bc88ea306710d529ebf65a41eceb7b948 Mon Sep 17 00:00:00 2001 From: Carlos Mondragon Date: Wed, 10 Oct 2018 16:07:54 -0700 Subject: [PATCH 019/156] Show CTA if there are no ApiKeys, otherwise show table. --- public/app/features/api-keys/ApiKeysPage.tsx | 231 ++++++++++--------- 1 file changed, 127 insertions(+), 104 deletions(-) diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 6052b0f4fc8..ab5f8f2c8bc 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -1,10 +1,10 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import ReactDOMServer from 'react-dom/server'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; -import { getApiKeys } from './state/selectors'; +import { getApiKeys, getApiKeysCount } from './state/selectors'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import SlideDown from 'app/core/components/Animations/SlideDown'; @@ -12,6 +12,7 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader'; import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; export interface Props { navModel: NavModel; @@ -22,6 +23,7 @@ export interface Props { deleteApiKey: typeof deleteApiKey; setSearchQuery: typeof setSearchQuery; addApiKey: typeof addApiKey; + apiKeysCount: number; } export interface State { @@ -101,115 +103,135 @@ export class ApiKeysPage extends PureComponent { }); }; - renderTable() { - const { apiKeys } = this.props; + renderEmptyList() { + return ( +
+ +
+ ); + } - return [ -

- Existing Keys -

, -
{LEGEND_STATS.map( - statName => seriesValuesProps[statName] && + statName => + seriesValuesProps[statName] && ( + + ) )}
+ {statName} + {sort === statName && } + - {props.statName} - -
+ props.onClick(e)}> {statName} {sort === statName && }
- + this.props.onLabelClick(e)} + />
- - - - - - - {apiKeys.length > 0 && ( - - {apiKeys.map(key => { - return ( - - - - - - ); - })} - - )} -
NameRole -
{key.name}{key.role} - this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> - - -
, - ]; + renderApiKeyList() { + const { newApiKey, isAdding } = this.state; + const { apiKeys, searchQuery } = this.props; + + return ( +
+
+
+ +
+ +
+ +
+ + +
+ +
Add API Key
+
+
+
+ Key name + this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} + /> +
+
+ Role + + + +
+
+ +
+
+
+
+
+ +

Existing Keys

+ + + + + + + + {apiKeys.length > 0 ? ( + + {apiKeys.map(key => { + return ( + + + + + + ); + })} + + ) : null} +
NameRole +
{key.name}{key.role} + this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> + + +
+
+ ); } render() { - const { newApiKey, isAdding } = this.state; - const { hasFetched, navModel, searchQuery } = this.props; + const { hasFetched, navModel, apiKeysCount } = this.props; return (
-
-
-
- -
- -
- -
- - -
- -
Add API Key
-
-
-
- Key name - this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} - /> -
-
- Role - - - -
-
- -
-
-
-
-
- {hasFetched ? this.renderTable() : } -
+ {hasFetched ? + (apiKeysCount > 0 ? this.renderApiKeyList() : this.renderEmptyList()) + : }
); } @@ -220,7 +242,8 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, 'apikeys'), apiKeys: getApiKeys(state.apiKeys), searchQuery: state.apiKeys.searchQuery, - hasFetched: state.apiKeys.hasFetched, + apiKeysCount: getApiKeysCount(state.apiKeys), + hasFetched: state.apiKeys.hasFetched }; } From f03fa364dfb8321396a69882184aa01998907762 Mon Sep 17 00:00:00 2001 From: Carlos Mondragon Date: Wed, 10 Oct 2018 16:17:00 -0700 Subject: [PATCH 020/156] Update tests for ApiKeys CTA screen. --- .../features/api-keys/ApiKeysPage.test.tsx | 18 ++- .../__snapshots__/ApiKeysPage.test.tsx.snap | 147 ++---------------- 2 files changed, 27 insertions(+), 138 deletions(-) diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index 8bc6e9338fc..a6d0335b064 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from 'react'; import { shallow } from 'enzyme'; import { Props, ApiKeysPage } from './ApiKeysPage'; import { NavModel, ApiKey } from 'app/types'; @@ -14,6 +14,7 @@ const setup = (propOverrides?: object) => { deleteApiKey: jest.fn(), setSearchQuery: jest.fn(), addApiKey: jest.fn(), + apiKeysCount: 0, }; Object.assign(props, propOverrides); @@ -28,15 +29,20 @@ const setup = (propOverrides?: object) => { }; describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); + it('should render API keys table if there are any keys', () => { + const { wrapper } = setup({ + apiKeys: getMultipleMockKeys(5), + apiKeysCount: 5, + }); + expect(wrapper).toMatchSnapshot(); }); - it('should render API keys table', () => { + it('should render CTA if theres are no API keys', () => { const { wrapper } = setup({ - apiKeys: getMultipleMockKeys(5), - hasFetched: true, + apiKeys: getMultipleMockKeys(0), + apiKeysCount: 0, + hasFetched: true, }); expect(wrapper).toMatchSnapshot(); diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index b1cac8469be..923d0fec0e6 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Render should render API keys table 1`] = ` +exports[`Render should render API keys table if there are any keys 1`] = `
`; -exports[`Render should render component 1`] = ` +exports[`Render should render CTA if theres are no API keys 1`] = `
-
-
- -
-
- -
- -
- -
- Add API Key -
-
-
-
- - Key name - - -
-
- - Role - - - - -
-
- -
-
-
-
-
-
From 081cb7a6957747af8588acd373de133fb8270eba Mon Sep 17 00:00:00 2001 From: Carlos Mondragon Date: Wed, 10 Oct 2018 16:21:47 -0700 Subject: [PATCH 021/156] Updated protip, not sure what to write there. --- public/app/features/api-keys/ApiKeysPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index ab5f8f2c8bc..1430da03e50 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -112,7 +112,7 @@ export class ApiKeysPage extends PureComponent { buttonIcon: 'fa fa-plus', buttonLink: 'org/apikeys/new', buttonTitle: ' New API Key', - proTip: 'Assign folder and dashboard permissions to teams instead of users to ease administration.', + proTip: 'Remember you can provide view-only API access to other applications.', proTipLink: '', proTipLinkTitle: '', proTipTarget: '_blank', From af985743d23a7d09bd5eeff5e1e82b525ce20714 Mon Sep 17 00:00:00 2001 From: Carlos Mondragon Date: Wed, 10 Oct 2018 16:22:32 -0700 Subject: [PATCH 022/156] Updated tests for new protip. --- .../features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index 923d0fec0e6..2d597244a02 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -284,7 +284,7 @@ exports[`Render should render CTA if theres are no API keys 1`] = ` "buttonIcon": "fa fa-plus", "buttonLink": "org/apikeys/new", "buttonTitle": " New API Key", - "proTip": "Assign folder and dashboard permissions to teams instead of users to ease administration.", + "proTip": "Remember you can provide view-only API access to other applications.", "proTipLink": "", "proTipLinkTitle": "", "proTipTarget": "_blank", From 0937335f1446ca367b9d1d9f7efbd5ca46a6be95 Mon Sep 17 00:00:00 2001 From: Carlos Mondragon Date: Wed, 10 Oct 2018 17:07:05 -0700 Subject: [PATCH 023/156] Add onClick handler to CTA. --- public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx | 1 + public/app/core/components/EmptyListCTA/EmptyListCTA.tsx | 3 ++- .../EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx index 4af60f3c839..ff92dc0d5c7 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx @@ -7,6 +7,7 @@ const model = { buttonIcon: 'ga css class', buttonLink: 'http://url/to/destination', buttonTitle: 'Click me', + onClick: 'handler', proTip: 'This is a tip', proTipLink: 'http://url/to/tip/destination', proTipLinkTitle: 'Learn more', diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index 5ece360e36a..ae0e39cc26d 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -11,6 +11,7 @@ class EmptyListCTA extends Component { buttonIcon, buttonLink, buttonTitle, + onClick, proTip, proTipLink, proTipLinkTitle, @@ -19,7 +20,7 @@ class EmptyListCTA extends Component { return (
{title}
- + {buttonTitle} diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap index 6d47c984d5e..fc76544b112 100644 --- a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap @@ -12,6 +12,7 @@ exports[`EmptyListCTA renders correctly 1`] = ` Date: Wed, 10 Oct 2018 17:18:43 -0700 Subject: [PATCH 024/156] Add form to both the CTA page and the regular list. --- public/app/features/api-keys/ApiKeysPage.tsx | 100 ++++++++++-------- .../__snapshots__/ApiKeysPage.test.tsx.snap | 96 ++++++++++++++++- 2 files changed, 150 insertions(+), 46 deletions(-) diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 1430da03e50..d6c83c2a566 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -110,7 +110,8 @@ export class ApiKeysPage extends PureComponent { model={{ title: "You haven't added any API Keys yet.", buttonIcon: 'fa fa-plus', - buttonLink: 'org/apikeys/new', + buttonLink: '#', + onClick: this.onToggleAdding, buttonTitle: ' New API Key', proTip: 'Remember you can provide view-only API access to other applications.', proTipLink: '', @@ -118,12 +119,63 @@ export class ApiKeysPage extends PureComponent { proTipTarget: '_blank', }} /> + {this.renderAddApiKeyForm()}
); } - renderApiKeyList() { + renderAddApiKeyForm() { const { newApiKey, isAdding } = this.state; + + return ( + +
+ +
Add API Key
+
+
+
+ Key name + this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} + /> +
+
+ Role + + + +
+
+ +
+
+
+
+
+ ); + } + + renderApiKeyList() { + const { isAdding } = this.state; const { apiKeys, searchQuery } = this.props; return ( @@ -148,49 +200,7 @@ export class ApiKeysPage extends PureComponent {
- -
- -
Add API Key
-
-
-
- Key name - this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} - /> -
-
- Role - - - -
-
- -
-
-
-
-
+ {this.renderAddApiKeyForm()}

Existing Keys

diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index 2d597244a02..fcd22a14479 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -282,8 +282,9 @@ exports[`Render should render CTA if theres are no API keys 1`] = ` model={ Object { "buttonIcon": "fa fa-plus", - "buttonLink": "org/apikeys/new", + "buttonLink": "#", "buttonTitle": " New API Key", + "onClick": [Function], "proTip": "Remember you can provide view-only API access to other applications.", "proTipLink": "", "proTipLinkTitle": "", @@ -292,6 +293,99 @@ exports[`Render should render CTA if theres are no API keys 1`] = ` } } /> + +
+ +
+ Add API Key +
+
+
+
+ + Key name + + +
+
+ + Role + + + + +
+
+ +
+
+ +
+
`; From b12170010371a9c3e642ba7dae04357f3894a870 Mon Sep 17 00:00:00 2001 From: Carlos Mondragon Date: Wed, 10 Oct 2018 17:22:48 -0700 Subject: [PATCH 025/156] Add fancy delete button for ApiKeys. --- public/app/features/api-keys/ApiKeysPage.tsx | 5 +- .../__snapshots__/ApiKeysPage.test.tsx.snap | 55 +++++-------------- 2 files changed, 17 insertions(+), 43 deletions(-) diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index d6c83c2a566..bd22d850f6d 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -13,6 +13,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; export interface Props { navModel: NavModel; @@ -219,9 +220,7 @@ export class ApiKeysPage extends PureComponent { ); diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index fcd22a14479..6ea7fe57124 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -174,14 +174,9 @@ exports[`Render should render API keys table if there are any keys 1`] = ` Viewer From dc9e822cc7b95d7e6c4a109cd44fe5e3bd645ff4 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 11 Oct 2018 11:56:32 +0200 Subject: [PATCH 026/156] Remove CTA when CTA-action is clicked instead of a /new route #13471 --- .../core/components/Animations/SlideDown.tsx | 11 ++++-- .../features/api-keys/ApiKeysPage.test.tsx | 2 +- public/app/features/api-keys/ApiKeysPage.tsx | 34 +++++++++++-------- .../__snapshots__/ApiKeysPage.test.tsx.snap | 14 +++++++- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx index 4d515f98f16..497af02ade9 100644 --- a/public/app/core/components/Animations/SlideDown.tsx +++ b/public/app/core/components/Animations/SlideDown.tsx @@ -1,15 +1,20 @@ import React from 'react'; import Transition from 'react-transition-group/Transition'; +interface Style { + transition?: string; + overflow?: string; +} + const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value. // If this is not enough, pass in - + {!isAdding && ( + + )} {this.renderAddApiKeyForm()} ); @@ -127,9 +130,10 @@ export class ApiKeysPage extends PureComponent { renderAddApiKeyForm() { const { newApiKey, isAdding } = this.state; + const slideDownStyle = isAdding ? slideDownDefaultStyle : { ...slideDownDefaultStyle, transition: 'unset' }; return ( - +
`; -exports[`Render should render CTA if theres are no API keys 1`] = ` +exports[`Render should render CTA if there are no API keys 1`] = `
Date: Thu, 11 Oct 2018 14:01:13 +0200 Subject: [PATCH 027/156] Update snapshots after merge --- .../features/api-keys/ApiKeysPage.test.tsx | 2 +- .../__snapshots__/ApiKeysPage.test.tsx.snap | 246 +----------------- 2 files changed, 4 insertions(+), 244 deletions(-) diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index 42912fc9d96..54200234ddc 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -42,7 +42,7 @@ describe('Render', () => { const { wrapper } = setup({ apiKeys: getMultipleMockKeys(0), apiKeysCount: 0, - hasFetched: true, + hasFetched: true, }); expect(wrapper).toMatchSnapshot(); diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index 375ed788e27..63b92a16ee0 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -5,249 +5,9 @@ exports[`Render should render API keys table if there are any keys 1`] = ` -
-
-
- -
-
- -
- -
- -
- Add API Key -
-
-
-
- - Key name - - -
-
- - Role - - - - -
-
- -
-
- -
-
-

- Existing Keys -

-
{key.name} {key.role} - this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> - - + this.onDeleteApiKey(key)} />
- - - +
- - - +
- - - +
- - - +
- - - +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Name - - Role - -
- test-1 - - Viewer - - -
- test-2 - - Viewer - - -
- test-3 - - Viewer - - -
- test-4 - - Viewer - - -
- test-5 - - Viewer - - -
-
+
`; From 46ec15a11ed6d6819f2fd88efe73622d29cecdc6 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 16 Oct 2018 16:50:43 +0300 Subject: [PATCH 028/156] graph legend: add color picker (react) --- public/app/plugins/panel/graph/Legend.tsx | 63 ++++++++++++++++++++++- public/app/plugins/panel/graph/graph.ts | 1 + 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend.tsx index e493d7d5020..5e8ea441623 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend.tsx @@ -1,7 +1,10 @@ import _ from 'lodash'; import React from 'react'; +import ReactDOM from 'react-dom'; import { TimeSeries } from 'app/core/core'; import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; +import Drop from 'tether-drop'; +import { ColorPickerPopover } from 'app/core/components/colorpicker/ColorPickerPopover'; const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -10,6 +13,7 @@ interface LegendProps { optionalClass?: string; onToggleSeries?: (series: TimeSeries, event: Event) => void; onToggleSort?: (sortBy, sortDesc) => void; + onColorChange?: (series: TimeSeries, color: string) => void; } interface LegendDisplayProps { @@ -102,6 +106,7 @@ export class GraphLegend extends React.PureComponent this.onToggleSeries(s, e), onToggleSort: (sortBy, sortDesc) => this.props.onToggleSort(sortBy, sortDesc), + onColorChange: (series, color) => this.props.onColorChange(series, color), ...seriesValuesProps, ...sortProps, }; @@ -126,6 +131,7 @@ class LegendSeriesList extends React.PureComponent { hiddenSeries={hiddenSeries} {...seriesValuesProps} onLabelClick={e => this.props.onToggleSeries(series, e)} + onColorChange={color => this.props.onColorChange(series, color)} /> )); } @@ -135,6 +141,7 @@ interface LegendSeriesProps { series: TimeSeries; index: number; onLabelClick?: (event) => void; + onColorChange?: (color: string) => void; } type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; @@ -150,6 +157,7 @@ class LegendSeriesItem extends React.PureComponent { label={series.aliasEscaped} color={series.color} onLabelClick={e => this.props.onLabelClick(e)} + onColorChange={e => this.props.onColorChange(e)} /> {valueItems}
@@ -161,14 +169,63 @@ interface LegendSeriesLabelProps { label: string; color: string; onLabelClick?: (event) => void; - onIconClick?: (event) => void; + onColorChange?: (color: string) => void; } class LegendSeriesLabel extends React.PureComponent { + pickerElem: any; + colorPickerDrop: any; + + openColorPicker() { + if (this.colorPickerDrop) { + this.destroyDrop(); + } + + const dropContent = ; + const dropContentElem = document.createElement('div'); + ReactDOM.render(dropContent, dropContentElem); + + const drop = new Drop({ + target: this.pickerElem, + content: dropContentElem, + position: 'top center', + classes: 'drop-popover', + openOn: 'hover', + hoverCloseDelay: 200, + remove: true, + tetherOptions: { + constraints: [{ to: 'scrollParent', attachment: 'none both' }], + }, + }); + + drop.on('close', this.closeColorPicker.bind(this)); + + this.colorPickerDrop = drop; + this.colorPickerDrop.open(); + } + + closeColorPicker() { + setTimeout(() => { + this.destroyDrop(); + }, 100); + } + + destroyDrop() { + if (this.colorPickerDrop && this.colorPickerDrop.tether) { + this.colorPickerDrop.destroy(); + this.colorPickerDrop = null; + } + } + render() { const { label, color } = this.props; return [ -
this.props.onIconClick(e)}> +
(this.pickerElem = e)} + onClick={() => this.openColorPicker()} + >
,
this.props.onLabelClick(e)}> @@ -255,6 +312,7 @@ class LegendTable extends React.PureComponent> { hiddenSeries={this.props.hiddenSeries} {...seriesValuesProps} onLabelClick={e => this.props.onToggleSeries(series, e)} + onColorChange={color => this.props.onColorChange(series, color)} /> ))} @@ -290,6 +348,7 @@ class LegendSeriesItemAsTable extends React.PureComponent label={series.aliasEscaped} color={series.color} onLabelClick={e => this.props.onLabelClick(e)} + onColorChange={e => this.props.onColorChange(e)} /> {valueItems} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index d41898d397d..640e189859c 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -97,6 +97,7 @@ class GraphElement { ...valueOptions, onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), + onColorChange: this.ctrl.changeSeriesColor.bind(this.ctrl), }; const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); From fe0c5c73ddcaad1657559f64a6940e0b46bfd86e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 17 Oct 2018 15:07:31 +0300 Subject: [PATCH 029/156] graph legend: refactor --- public/app/core/angular_wrappers.ts | 2 - .../colorpicker/SeriesColorPicker.tsx | 54 +++-- .../colorpicker/withColorPicker.tsx | 83 +++++++ .../panel/graph/{ => Legend}/Legend.tsx | 215 +++--------------- .../panel/graph/Legend/LegendSeriesItem.tsx | 173 ++++++++++++++ public/app/plugins/panel/graph/graph.ts | 3 +- 6 files changed, 321 insertions(+), 209 deletions(-) create mode 100644 public/app/core/components/colorpicker/withColorPicker.tsx rename public/app/plugins/panel/graph/{ => Legend}/Legend.tsx (52%) create mode 100644 public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 7e72f53204e..6974d40aac8 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; -import { GraphLegend } from 'app/plugins/panel/graph/Legend'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -18,5 +17,4 @@ export function registerAngularDirectives() { ['onSelect', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); - react2AngularDirective('graphLegendReact', GraphLegend, ['seriesList', 'className']); } diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index b514899e2e2..9abd3574ae1 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -2,30 +2,53 @@ import React from 'react'; import { ColorPickerPopover } from './ColorPickerPopover'; import { react2AngularDirective } from 'app/core/utils/react2angular'; -export interface Props { - series: any; +export interface SeriesColorPickerProps { + // series: any; + color: string; + yaxis?: number; onColorChange: (color: string) => void; + onToggleAxis?: () => void; +} + +export class SeriesColorPicker extends React.PureComponent { + render() { + return ( +
+ {this.props.yaxis && } + +
+ ); + } +} + +interface AxisSelectorProps { + yaxis: number; onToggleAxis: () => void; } -export class SeriesColorPicker extends React.Component { +interface AxisSelectorState { + yaxis: number; +} + +export class AxisSelector extends React.PureComponent { constructor(props) { super(props); - this.onColorChange = this.onColorChange.bind(this); + this.state = { + yaxis: this.props.yaxis, + }; this.onToggleAxis = this.onToggleAxis.bind(this); } - onColorChange(color) { - this.props.onColorChange(color); - } - onToggleAxis() { + this.setState({ + yaxis: this.state.yaxis === 2 ? 1 : 2, + }); this.props.onToggleAxis(); } - renderAxisSelection() { - const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse'; - const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse'; + render() { + const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse'; + const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse'; return (
@@ -39,15 +62,6 @@ export class SeriesColorPicker extends React.Component {
); } - - render() { - return ( -
- {this.props.series.yaxis && this.renderAxisSelection()} - -
- ); - } } react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); diff --git a/public/app/core/components/colorpicker/withColorPicker.tsx b/public/app/core/components/colorpicker/withColorPicker.tsx new file mode 100644 index 00000000000..d0567fe4e18 --- /dev/null +++ b/public/app/core/components/colorpicker/withColorPicker.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Drop from 'tether-drop'; +import { SeriesColorPicker } from './SeriesColorPicker'; + +export interface WithSeriesColorPickerProps { + color: string; + yaxis?: number; + optionalClass?: string; + onColorChange: (newColor: string) => void; + onToggleAxis?: () => void; +} + +export default function withSeriesColorPicker(WrappedComponent) { + return class extends React.Component { + pickerElem: any; + colorPickerDrop: any; + + static defaultProps = { + optionalClass: '', + yaxis: undefined, + onToggleAxis: () => {}, + }; + + constructor(props) { + super(props); + this.openColorPicker = this.openColorPicker.bind(this); + } + + openColorPicker() { + if (this.colorPickerDrop) { + this.destroyDrop(); + } + + const { color, yaxis, onColorChange, onToggleAxis } = this.props; + const dropContent = ( + + ); + const dropContentElem = document.createElement('div'); + ReactDOM.render(dropContent, dropContentElem); + + const drop = new Drop({ + target: this.pickerElem, + content: dropContentElem, + position: 'top center', + classes: 'drop-popover', + openOn: 'hover', + hoverCloseDelay: 200, + remove: true, + tetherOptions: { + constraints: [{ to: 'scrollParent', attachment: 'none both' }], + }, + }); + + drop.on('close', this.closeColorPicker.bind(this)); + + this.colorPickerDrop = drop; + this.colorPickerDrop.open(); + } + + closeColorPicker() { + setTimeout(() => { + this.destroyDrop(); + }, 100); + } + + destroyDrop() { + if (this.colorPickerDrop && this.colorPickerDrop.tether) { + this.colorPickerDrop.destroy(); + this.colorPickerDrop = null; + } + } + + render() { + const { optionalClass, onColorChange, ...wrappedComponentProps } = this.props; + return ( +
(this.pickerElem = e)} onClick={this.openColorPicker}> + +
+ ); + } + }; +} diff --git a/public/app/plugins/panel/graph/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx similarity index 52% rename from public/app/plugins/panel/graph/Legend.tsx rename to public/app/plugins/panel/graph/Legend/Legend.tsx index 5e8ea441623..f6daf778848 100644 --- a/public/app/plugins/panel/graph/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -1,18 +1,15 @@ import _ from 'lodash'; import React from 'react'; -import ReactDOM from 'react-dom'; import { TimeSeries } from 'app/core/core'; import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; -import Drop from 'tether-drop'; -import { ColorPickerPopover } from 'app/core/components/colorpicker/ColorPickerPopover'; - -const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; +import { LegendItem, LEGEND_STATS } from './LegendSeriesItem'; interface LegendProps { seriesList: TimeSeries[]; optionalClass?: string; onToggleSeries?: (series: TimeSeries, event: Event) => void; onToggleSort?: (sortBy, sortDesc) => void; + onToggleAxis?: (series: TimeSeries) => void; onColorChange?: (series: TimeSeries, color: string) => void; } @@ -41,24 +38,24 @@ interface LegendSortProps { export type GraphLegendProps = LegendProps & LegendDisplayProps & LegendValuesProps & LegendSortProps; -const defaultGraphLegendProps: Partial = { - values: false, - min: false, - max: false, - avg: false, - current: false, - total: false, - alignAsTable: false, - rightSide: false, - sort: undefined, - sortDesc: false, - optionalClass: '', -}; - -export interface GraphLegendState {} - -export class GraphLegend extends React.PureComponent { - static defaultProps = defaultGraphLegendProps; +export class GraphLegend extends React.PureComponent { + static defaultProps: Partial = { + values: false, + min: false, + max: false, + avg: false, + current: false, + total: false, + alignAsTable: false, + rightSide: false, + sort: undefined, + sortDesc: false, + optionalClass: '', + onToggleSeries: () => {}, + onToggleSort: () => {}, + onToggleAxis: () => {}, + onColorChange: () => {}, + }; sortLegend() { let seriesList = this.props.seriesList || []; @@ -107,6 +104,7 @@ export class GraphLegend extends React.PureComponent this.onToggleSeries(s, e), onToggleSort: (sortBy, sortDesc) => this.props.onToggleSort(sortBy, sortDesc), onColorChange: (series, color) => this.props.onColorChange(series, color), + onToggleAxis: series => this.props.onToggleAxis(series), ...seriesValuesProps, ...sortProps, }; @@ -124,7 +122,7 @@ class LegendSeriesList extends React.PureComponent { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; return seriesList.map((series, i) => ( - { {...seriesValuesProps} onLabelClick={e => this.props.onToggleSeries(series, e)} onColorChange={color => this.props.onColorChange(series, color)} + onToggleAxis={() => this.props.onToggleAxis(series)} /> )); } } -interface LegendSeriesProps { - series: TimeSeries; - index: number; - onLabelClick?: (event) => void; - onColorChange?: (color: string) => void; -} - -type LegendSeriesItemProps = LegendSeriesProps & LegendDisplayProps & LegendValuesProps; - -class LegendSeriesItem extends React.PureComponent { - render() { - const { series, index, hiddenSeries } = this.props; - const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); - const valueItems = this.props.values ? renderLegendValues(this.props, series) : []; - return ( -
- this.props.onLabelClick(e)} - onColorChange={e => this.props.onColorChange(e)} - /> - {valueItems} -
- ); - } -} - -interface LegendSeriesLabelProps { - label: string; - color: string; - onLabelClick?: (event) => void; - onColorChange?: (color: string) => void; -} - -class LegendSeriesLabel extends React.PureComponent { - pickerElem: any; - colorPickerDrop: any; - - openColorPicker() { - if (this.colorPickerDrop) { - this.destroyDrop(); - } - - const dropContent = ; - const dropContentElem = document.createElement('div'); - ReactDOM.render(dropContent, dropContentElem); - - const drop = new Drop({ - target: this.pickerElem, - content: dropContentElem, - position: 'top center', - classes: 'drop-popover', - openOn: 'hover', - hoverCloseDelay: 200, - remove: true, - tetherOptions: { - constraints: [{ to: 'scrollParent', attachment: 'none both' }], - }, - }); - - drop.on('close', this.closeColorPicker.bind(this)); - - this.colorPickerDrop = drop; - this.colorPickerDrop.open(); - } - - closeColorPicker() { - setTimeout(() => { - this.destroyDrop(); - }, 100); - } - - destroyDrop() { - if (this.colorPickerDrop && this.colorPickerDrop.tether) { - this.colorPickerDrop.destroy(); - this.colorPickerDrop = null; - } - } - - render() { - const { label, color } = this.props; - return [ -
(this.pickerElem = e)} - onClick={() => this.openColorPicker()} - > - -
, -
this.props.onLabelClick(e)}> - {label} - , - ]; - } -} - -interface LegendValueProps { - value: string; - valueName: string; - asTable?: boolean; -} - -function LegendValue(props: LegendValueProps) { - const value = props.value; - const valueName = props.valueName; - if (props.asTable) { - return {value}; - } - return
{value}
; -} - -function renderLegendValues(props: LegendSeriesItemProps, series, asTable = false) { - const legendValueItems = []; - for (const valueName of LEGEND_STATS) { - if (props[valueName]) { - const valueFormatted = series.formatValue(series.stats[valueName]); - legendValueItems.push( - - ); - } - } - return legendValueItems; -} - class LegendTable extends React.PureComponent> { onToggleSort(stat) { let sortDesc = this.props.sortDesc; @@ -305,14 +178,16 @@ class LegendTable extends React.PureComponent> { )} {seriesList.map((series, i) => ( - this.props.onToggleSeries(series, e)} onColorChange={color => this.props.onColorChange(series, color)} + onToggleAxis={() => this.props.onToggleAxis(series)} + {...seriesValuesProps} /> ))} @@ -329,46 +204,14 @@ interface LegendTableHeaderProps { function LegendTableHeaderItem(props: LegendTableHeaderProps & LegendSortProps) { const { statName, sort, sortDesc } = props; return ( - props.onClick(e)}> + props.onClick(e)}> {statName} {sort === statName && } ); } -class LegendSeriesItemAsTable extends React.PureComponent { - render() { - const { series, index, hiddenSeries } = this.props; - const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); - const valueItems = this.props.values ? renderLegendValues(this.props, series, true) : []; - return ( - - - this.props.onLabelClick(e)} - onColorChange={e => this.props.onColorChange(e)} - /> - - {valueItems} - - ); - } -} - -function getOptionSeriesCSSClasses(series, hiddenSeries) { - const classes = []; - if (series.yaxis === 2) { - classes.push('graph-legend-series--right-y'); - } - if (hiddenSeries[series.alias] && hiddenSeries[series.alias] === true) { - classes.push('graph-legend-series-hidden'); - } - return classes.join(' '); -} - -export class Legend extends React.Component { +export class Legend extends React.Component { render() { return ( diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx new file mode 100644 index 00000000000..ce0ed048336 --- /dev/null +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { TimeSeries } from 'app/core/core'; +import withColorPicker from 'app/core/components/colorpicker/withColorPicker'; + +export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; + +export interface LegendLabelProps { + index: number; + series: TimeSeries; + asTable?: boolean; + hiddenSeries?: any; + onLabelClick?: (event) => void; + onColorChange?: (color: string) => void; + onToggleAxis?: () => void; +} + +export interface LegendValuesProps { + values?: boolean; + min?: boolean; + max?: boolean; + avg?: boolean; + current?: boolean; + total?: boolean; +} + +type LegendItemProps = LegendLabelProps & LegendValuesProps; + +export class LegendItem extends React.PureComponent { + static defaultProps = { + asTable: false, + hiddenSeries: undefined, + onLabelClick: () => {}, + onColorChange: () => {}, + onToggleAxis: () => {}, + }; + + render() { + const { series, hiddenSeries, asTable } = this.props; + const { aliasEscaped, color, yaxis } = this.props.series; + const seriesOptionClasses = getOptionSeriesCSSClasses(series, hiddenSeries); + const valueItems = this.props.values ? renderLegendValues(this.props, series, asTable) : []; + const seriesLabel = ( + + ); + + if (asTable) { + return ( + + {seriesLabel} + {valueItems} + + ); + } else { + return ( +
+ {seriesLabel} + {valueItems} +
+ ); + } + } +} + +interface LegendSeriesLabelProps { + label: string; + color: string; + yaxis?: number; + onLabelClick?: (event) => void; +} + +class LegendSeriesLabel extends React.PureComponent { + static defaultProps = { + yaxis: undefined, + onLabelClick: () => {}, + }; + + render() { + const { label, color, yaxis } = this.props; + const { onColorChange, onToggleAxis } = this.props; + return [ + , + this.props.onLabelClick(e)}> + {label} + , + ]; + } +} + +interface LegendSeriesIconProps { + color: string; + yaxis?: number; + onColorChange?: (color: string) => void; + onToggleAxis?: () => void; +} + +function SeriesIcon(props) { + return ; +} + +class LegendSeriesIcon extends React.PureComponent { + static defaultProps = { + yaxis: undefined, + onColorChange: () => {}, + onToggleAxis: () => {}, + }; + + render() { + const { color, yaxis } = this.props; + const IconWithColorPicker = withColorPicker(SeriesIcon); + + return ( + + ); + } +} + +interface LegendValueProps { + value: string; + valueName: string; + asTable?: boolean; +} + +function LegendValue(props: LegendValueProps) { + const value = props.value; + const valueName = props.valueName; + if (props.asTable) { + return {value}; + } + return
{value}
; +} + +function renderLegendValues(props: LegendItemProps, series, asTable = false) { + const legendValueItems = []; + for (const valueName of LEGEND_STATS) { + if (props[valueName]) { + const valueFormatted = series.formatValue(series.stats[valueName]); + legendValueItems.push( + + ); + } + } + return legendValueItems; +} + +function getOptionSeriesCSSClasses(series, hiddenSeries) { + const classes = []; + if (series.yaxis === 2) { + classes.push('graph-legend-series--right-y'); + } + if (hiddenSeries[series.alias] && hiddenSeries[series.alias] === true) { + classes.push('graph-legend-series-hidden'); + } + return classes.join(' '); +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 640e189859c..cc9e9660d3e 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -22,7 +22,7 @@ import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Legend, GraphLegendProps } from './Legend'; +import { Legend, GraphLegendProps } from './Legend/Legend'; import { GraphCtrl } from './module'; @@ -98,6 +98,7 @@ class GraphElement { onToggleSeries: this.ctrl.toggleSeries.bind(this.ctrl), onToggleSort: this.ctrl.toggleSort.bind(this.ctrl), onColorChange: this.ctrl.changeSeriesColor.bind(this.ctrl), + onToggleAxis: this.ctrl.toggleAxis.bind(this.ctrl), }; const legendReactElem = React.createElement(Legend, legendProps); const legendElem = this.elem.parent().find('.graph-legend'); From 5f712ab529e609b0861e2cb612b8764d637ca410 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 18 Oct 2018 12:31:06 +0300 Subject: [PATCH 030/156] graph legend: remove unused code --- public/app/plugins/panel/graph/graph.ts | 9 +- public/app/plugins/panel/graph/legend.ts | 305 ----------------------- public/app/plugins/panel/graph/module.ts | 1 - 3 files changed, 4 insertions(+), 311 deletions(-) delete mode 100644 public/app/plugins/panel/graph/legend.ts diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index cc9e9660d3e..63cfbf68f4e 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -85,7 +85,6 @@ class GraphElement { const graphHeight = this.elem.height(); updateLegendValues(this.data, this.panel, graphHeight); - // this.ctrl.events.emit('render-legend'); const { values, min, max, avg, current, total } = this.panel.legend; const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; @@ -106,6 +105,10 @@ class GraphElement { this.onLegendRenderingComplete(); } + onLegendRenderingComplete() { + this.render_panel(); + } + onGraphHover(evt) { // ignore other graph hover events if shared tooltip is disabled if (!this.dashboard.sharedTooltipModeEnabled()) { @@ -129,10 +132,6 @@ class GraphElement { } } - onLegendRenderingComplete() { - this.render_panel(); - } - onGraphHoverClear(event, info) { if (this.plot) { this.tooltip.clear(this.plot); diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts deleted file mode 100644 index cf317389941..00000000000 --- a/public/app/plugins/panel/graph/legend.ts +++ /dev/null @@ -1,305 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; -import $ from 'jquery'; -import baron from 'baron'; - -const module = angular.module('grafana.directives'); - -module.directive('graphLegend', (popoverSrv, $timeout) => { - return { - link: (scope, elem) => { - let firstRender = true; - const ctrl = scope.ctrl; - const panel = ctrl.panel; - let data; - let seriesList; - let i; - let legendScrollbar; - const legendRightDefaultWidth = 10; - const legendElem = elem.parent(); - - scope.$on('$destroy', () => { - destroyScrollbar(); - }); - - ctrl.events.on('render-legend', () => { - data = ctrl.seriesList; - if (data) { - render(); - } - ctrl.events.emit('legend-rendering-complete'); - }); - - function getSeriesIndexForElement(el) { - return el.parents('[data-series-index]').data('series-index'); - } - - function openColorSelector(e) { - // if we clicked inside poup container ignore click - if ($(e.target).parents('.popover').length) { - return; - } - - const el = $(e.currentTarget).find('.fa-minus'); - const index = getSeriesIndexForElement(el); - const series = seriesList[index]; - - $timeout(() => { - popoverSrv.show({ - element: el[0], - position: 'bottom left', - targetAttachment: 'top left', - template: - '' + - '', - openOn: 'hover', - model: { - series: series, - toggleAxis: () => { - ctrl.toggleAxis(series); - }, - colorSelected: color => { - ctrl.changeSeriesColor(series, color); - }, - }, - }); - }); - } - - function toggleSeries(e) { - const el = $(e.currentTarget); - const index = getSeriesIndexForElement(el); - const seriesInfo = seriesList[index]; - const scrollPosition = legendScrollbar.scroller.scrollTop; - ctrl.toggleSeries(seriesInfo, e); - legendScrollbar.scroller.scrollTop = scrollPosition; - } - - function sortLegend(e) { - const el = $(e.currentTarget); - const stat = el.data('stat'); - - if (stat !== panel.legend.sort) { - panel.legend.sortDesc = null; - } - - // if already sort ascending, disable sorting - if (panel.legend.sortDesc === false) { - panel.legend.sort = null; - panel.legend.sortDesc = null; - ctrl.render(); - return; - } - - panel.legend.sortDesc = !panel.legend.sortDesc; - panel.legend.sort = stat; - ctrl.render(); - } - - function getTableHeaderHtml(statName) { - if (!panel.legend[statName]) { - return ''; - } - let html = '' + statName; - - if (panel.legend.sort === statName) { - const cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'; - html += ' '; - } - - return html + ''; - } - - function render() { - const legendWidth = legendElem.width(); - if (!ctrl.panel.legend.show) { - elem.empty(); - firstRender = true; - return; - } - - if (firstRender) { - elem.on('click', '.graph-legend-icon', openColorSelector); - elem.on('click', '.graph-legend-alias', toggleSeries); - elem.on('click', 'th', sortLegend); - firstRender = false; - } - - seriesList = data; - - elem.empty(); - - // Set min-width if side style and there is a value, otherwise remove the CSS property - // Set width so it works with IE11 - const width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : ''; - const ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : ''; - legendElem.css('min-width', width); - legendElem.css('width', ieWidth); - - elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true); - - let tableHeaderElem; - if (panel.legend.alignAsTable) { - let header = ''; - header += ''; - if (panel.legend.values) { - header += getTableHeaderHtml('min'); - header += getTableHeaderHtml('max'); - header += getTableHeaderHtml('avg'); - header += getTableHeaderHtml('current'); - header += getTableHeaderHtml('total'); - } - header += ''; - tableHeaderElem = $(header); - } - - if (panel.legend.sort) { - seriesList = _.sortBy(seriesList, series => { - let sort = series.stats[panel.legend.sort]; - if (sort === null) { - sort = -Infinity; - } - return sort; - }); - if (panel.legend.sortDesc) { - seriesList = seriesList.reverse(); - } - } - - // render first time for getting proper legend height - if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) { - renderLegendElement(tableHeaderElem); - elem.empty(); - } - - renderLegendElement(tableHeaderElem); - } - - function renderSeriesLegendElements() { - const seriesElements = []; - for (i = 0; i < seriesList.length; i++) { - const series = seriesList[i]; - - if (series.hideFromLegend(panel.legend)) { - continue; - } - - let html = '
'; - html += '
'; - html += ''; - html += '
'; - - html += - '' + series.aliasEscaped + ''; - - if (panel.legend.values) { - const avg = series.formatValue(series.stats.avg); - const current = series.formatValue(series.stats.current); - const min = series.formatValue(series.stats.min); - const max = series.formatValue(series.stats.max); - const total = series.formatValue(series.stats.total); - - if (panel.legend.min) { - html += '
' + min + '
'; - } - if (panel.legend.max) { - html += '
' + max + '
'; - } - if (panel.legend.avg) { - html += '
' + avg + '
'; - } - if (panel.legend.current) { - html += '
' + current + '
'; - } - if (panel.legend.total) { - html += '
' + total + '
'; - } - } - - html += '
'; - seriesElements.push($(html)); - } - return seriesElements; - } - - function renderLegendElement(tableHeaderElem) { - const legendWidth = elem.width(); - - const seriesElements = renderSeriesLegendElements(); - - if (panel.legend.alignAsTable) { - const tbodyElem = $(''); - tbodyElem.append(tableHeaderElem); - tbodyElem.append(seriesElements); - elem.append(tbodyElem); - tbodyElem.wrap('
'); - } else { - elem.append('
'); - elem.find('.graph-legend-scroll').append(seriesElements); - } - - if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) { - addScrollbar(); - } else { - destroyScrollbar(); - } - } - - function addScrollbar() { - const scrollRootClass = 'baron baron__root'; - const scrollerClass = 'baron__scroller'; - const scrollBarHTML = ` -
-
-
- `; - - const scrollRoot = elem; - const scroller = elem.find('.graph-legend-scroll'); - - // clear existing scroll bar track to prevent duplication - scrollRoot.find('.baron__track').remove(); - - scrollRoot.addClass(scrollRootClass); - $(scrollBarHTML).appendTo(scrollRoot); - scroller.addClass(scrollerClass); - - const scrollbarParams = { - root: scrollRoot[0], - scroller: scroller[0], - bar: '.baron__bar', - track: '.baron__track', - barOnCls: '_scrollbar', - scrollingCls: '_scrolling', - }; - - if (!legendScrollbar) { - legendScrollbar = baron(scrollbarParams); - } else { - destroyScrollbar(); - legendScrollbar = baron(scrollbarParams); - } - - // #11830 - compensates for Firefox scrollbar calculation error in the baron framework - scroller[0].style.marginRight = '-' + (scroller[0].offsetWidth - scroller[0].clientWidth) + 'px'; - - legendScrollbar.scroll(); - } - - function destroyScrollbar() { - if (legendScrollbar) { - legendScrollbar.dispose(); - legendScrollbar = undefined; - } - } - }, - }; -}); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index e89cc6ef172..e897ccaaf98 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -1,5 +1,4 @@ import './graph'; -import './legend'; import './series_overrides_ctrl'; import './thresholds_form'; From c4452ba335ce5366f799a6e771411510eb3b5150 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 18 Oct 2018 20:01:40 +0200 Subject: [PATCH 031/156] Fix tslint errors --- .../plugins/datasource/mysql/datasource.ts | 4 +- .../plugins/datasource/mysql/meta_query.ts | 4 +- .../plugins/datasource/mysql/mysql_query.ts | 26 ++++----- .../plugins/datasource/mysql/query_ctrl.ts | 56 +++++++++---------- .../datasource/mysql/specs/datasource.test.ts | 4 +- .../app/plugins/datasource/mysql/sql_part.ts | 4 +- 6 files changed, 49 insertions(+), 49 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 23bee7dbb6e..4b4c3c3a526 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -9,7 +9,7 @@ export class MysqlDatasource { queryModel: MysqlQuery; interval: string; - /** @ngInject **/ + /** @ngInject */ constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; @@ -41,7 +41,7 @@ export class MysqlDatasource { const queries = _.filter(options.targets, target => { return target.hide !== true; }).map(target => { - let queryModel = new MysqlQuery(target, this.templateSrv, options.scopedVars); + const queryModel = new MysqlQuery(target, this.templateSrv, options.scopedVars); return { refId: target.refId, diff --git a/public/app/plugins/datasource/mysql/meta_query.ts b/public/app/plugins/datasource/mysql/meta_query.ts index d5383e85ff7..21fe490df43 100644 --- a/public/app/plugins/datasource/mysql/meta_query.ts +++ b/public/app/plugins/datasource/mysql/meta_query.ts @@ -25,7 +25,7 @@ export class MysqlMetaQuery { findMetricTable() { // query that returns first table found that has a timestamp(tz) column and a float column - let query = ` + const query = ` SELECT table_name as table_name, ( SELECT @@ -74,7 +74,7 @@ export class MysqlMetaQuery { // check for schema qualified table if (table.includes('.')) { - let parts = table.split('.'); + const parts = table.split('.'); query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]); query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]); return query; diff --git a/public/app/plugins/datasource/mysql/mysql_query.ts b/public/app/plugins/datasource/mysql/mysql_query.ts index 1c4b927ceea..e617433dbd3 100644 --- a/public/app/plugins/datasource/mysql/mysql_query.ts +++ b/public/app/plugins/datasource/mysql/mysql_query.ts @@ -73,12 +73,12 @@ export default class MysqlQuery { return this.quoteLiteral(value); } - let escapedValues = _.map(value, this.quoteLiteral); + const escapedValues = _.map(value, this.quoteLiteral); return escapedValues.join(','); } render(interpolate?) { - let target = this.target; + const target = this.target; // new query with no table set yet if (!this.target.rawQuery && !('table' in this.target)) { @@ -101,7 +101,7 @@ export default class MysqlQuery { } buildTimeColumn(alias = true) { - let timeGroup = this.hasTimeGroup(); + const timeGroup = this.hasTimeGroup(); let query; let macro = '$__timeGroup'; @@ -139,7 +139,7 @@ export default class MysqlQuery { buildValueColumns() { let query = ''; - for (let column of this.target.select) { + for (const column of this.target.select) { query += ',\n ' + this.buildValueColumn(column); } @@ -149,14 +149,14 @@ export default class MysqlQuery { buildValueColumn(column) { let query = ''; - let columnName = _.find(column, (g: any) => g.type === 'column'); + const columnName = _.find(column, (g: any) => g.type === 'column'); query = columnName.params[0]; - let aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); - let windows = _.find(column, (g: any) => g.type === 'window' || g.type === 'moving_window'); + const aggregate = _.find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); + const windows = _.find(column, (g: any) => g.type === 'window' || g.type === 'moving_window'); if (aggregate) { - let func = aggregate.params[0]; + const func = aggregate.params[0]; switch (aggregate.type) { case 'aggregate': if (func === 'first' || func === 'last') { @@ -172,13 +172,13 @@ export default class MysqlQuery { } if (windows) { - let overParts = []; + const overParts = []; if (this.hasMetricColumn()) { overParts.push('PARTITION BY ' + this.target.metricColumn); } overParts.push('ORDER BY ' + this.buildTimeColumn(false)); - let over = overParts.join(' '); + const over = overParts.join(' '); let curr: string; let prev: string; switch (windows.type) { @@ -211,7 +211,7 @@ export default class MysqlQuery { } } - let alias = _.find(column, (g: any) => g.type === 'alias'); + const alias = _.find(column, (g: any) => g.type === 'alias'); if (alias) { query += ' AS ' + this.quoteIdentifier(alias.params[0]); } @@ -221,7 +221,7 @@ export default class MysqlQuery { buildWhereClause() { let query = ''; - let conditions = _.map(this.target.where, (tag, index) => { + const conditions = _.map(this.target.where, (tag, index) => { switch (tag.type) { case 'macro': return tag.name + '(' + this.target.timeColumn + ')'; @@ -244,7 +244,7 @@ export default class MysqlQuery { let groupSection = ''; for (let i = 0; i < this.target.group.length; i++) { - let part = this.target.group[i]; + const part = this.target.group[i]; if (i > 0) { groupSection += ', '; } diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts index 1c911368ed8..b7520d8a645 100644 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ b/public/app/plugins/datasource/mysql/query_ctrl.ts @@ -40,7 +40,7 @@ export class MysqlQueryCtrl extends QueryCtrl { whereParts: SqlPart[]; groupAdd: any; - /** @ngInject **/ + /** @ngInject */ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); @@ -98,7 +98,7 @@ export class MysqlQueryCtrl extends QueryCtrl { } updateProjection() { - this.selectParts = _.map(this.target.select, function(parts: any) { + 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); @@ -106,22 +106,22 @@ export class MysqlQueryCtrl extends QueryCtrl { } updatePersistedParts() { - this.target.select = _.map(this.selectParts, function(selectParts) { - return _.map(selectParts, function(part: any) { + 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, function(part: any) { + 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, function(part: any) { + this.target.group = _.map(this.groupParts, (part: any) => { return { type: part.def.type, datatype: part.datatype, params: part.params }; }); } buildSelectMenu() { this.selectMenu = []; - let aggregates = { + const aggregates = { text: 'Aggregate Functions', value: 'aggregate', submenu: [ @@ -157,7 +157,7 @@ export class MysqlQueryCtrl extends QueryCtrl { } resetPlusButton(button) { - let plusButton = this.uiSegmentSrv.newPlusButton(); + const plusButton = this.uiSegmentSrv.newPlusButton(); button.html = plusButton.html; button.value = plusButton.value; } @@ -175,21 +175,21 @@ export class MysqlQueryCtrl extends QueryCtrl { this.target.group = []; this.updateProjection(); - let segment = this.uiSegmentSrv.newSegment('none'); + const segment = this.uiSegmentSrv.newSegment('none'); this.metricColumnSegment.html = segment.html; this.metricColumnSegment.value = segment.value; this.target.metricColumn = 'none'; - let task1 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('time')).then(result => { + 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)) { - let segment = this.uiSegmentSrv.newSegment(result[0].text); + const segment = this.uiSegmentSrv.newSegment(result[0].text); this.timeColumnSegment.html = segment.html; this.timeColumnSegment.value = segment.value; } return this.timeColumnChanged(false); }); - let task2 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('value')).then(result => { + 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(); @@ -271,7 +271,7 @@ export class MysqlQueryCtrl extends QueryCtrl { transformToSegments(config) { return results => { - let segments = _.map(results, segment => { + const segments = _.map(results, segment => { return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable, @@ -279,7 +279,7 @@ export class MysqlQueryCtrl extends QueryCtrl { }); if (config.addTemplateVars) { - for (let variable of this.templateSrv.variables) { + for (const variable of this.templateSrv.variables) { let value; value = '$' + variable.name; if (config.templateQuoter && variable.multi === false) { @@ -325,7 +325,7 @@ export class MysqlQueryCtrl extends QueryCtrl { switch (partType) { case 'column': - let parts = _.map(selectParts, function(part: any) { + const parts = _.map(selectParts, (part: any) => { return sqlPart.create({ type: part.def.type, params: _.clone(part.params) }); }); this.selectParts.push(parts); @@ -336,7 +336,7 @@ export class MysqlQueryCtrl extends QueryCtrl { if (this.target.group.length === 0) { this.addGroup('time', '$__interval'); } - let aggIndex = this.findAggregateIndex(selectParts); + const aggIndex = this.findAggregateIndex(selectParts); if (aggIndex !== -1) { // replace current aggregation selectParts[aggIndex] = partModel; @@ -349,12 +349,12 @@ export class MysqlQueryCtrl extends QueryCtrl { break; case 'moving_window': case 'window': - let windowIndex = this.findWindowIndex(selectParts); + const windowIndex = this.findWindowIndex(selectParts); if (windowIndex !== -1) { // replace current window function selectParts[windowIndex] = partModel; } else { - let aggIndex = this.findAggregateIndex(selectParts); + const aggIndex = this.findAggregateIndex(selectParts); if (aggIndex !== -1) { selectParts.splice(aggIndex + 1, 0, partModel); } else { @@ -388,11 +388,11 @@ export class MysqlQueryCtrl extends QueryCtrl { if (part.def.type === 'column') { // remove all parts of column unless its last column if (this.selectParts.length > 1) { - let modelsIndex = _.indexOf(this.selectParts, selectParts); + const modelsIndex = _.indexOf(this.selectParts, selectParts); this.selectParts.splice(modelsIndex, 1); } } else { - let partIndex = _.indexOf(selectParts, part); + const partIndex = _.indexOf(selectParts, part); selectParts.splice(partIndex, 1); } @@ -460,7 +460,7 @@ export class MysqlQueryCtrl extends QueryCtrl { if (partType === 'time') { params = ['$__interval', 'none']; } - let partModel = sqlPart.create({ type: partType, params: params }); + const partModel = sqlPart.create({ type: partType, params: params }); if (partType === 'time') { // put timeGroup at start @@ -470,12 +470,12 @@ export class MysqlQueryCtrl extends QueryCtrl { } // add aggregates when adding group by - for (let selectParts of this.selectParts) { + for (const selectParts of this.selectParts) { if (!selectParts.some(part => part.def.type === 'aggregate')) { - let aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); + const aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); selectParts.splice(1, 0, aggregate); if (!selectParts.some(part => part.def.type === 'alias')) { - let alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] }); + const alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] }); selectParts.push(alias); } } @@ -557,7 +557,7 @@ export class MysqlQueryCtrl extends QueryCtrl { } getWhereOptions() { - var options = []; + const options = []; if (this.queryModel.hasUnixEpochTimecolumn()) { options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__unixEpochFilter' })); } else { @@ -570,7 +570,7 @@ export class MysqlQueryCtrl extends QueryCtrl { addWhereAction(part, index) { switch (this.whereAdd.type) { case 'macro': { - let partModel = sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] }); + 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; @@ -593,11 +593,11 @@ export class MysqlQueryCtrl extends QueryCtrl { return this.datasource .metricFindQuery(this.metaBuilder.buildColumnQuery('group')) .then(tags => { - var options = []; + const options = []; if (!this.queryModel.hasTimeGroup()) { options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time($__interval,none)' })); } - for (let tag of tags) { + for (const tag of tags) { options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text })); } return options; diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index cc1e54ac496..f3fbcd93333 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -13,7 +13,7 @@ describe('MySQLDatasource', () => { from: moment.utc('2018-04-25 10:00'), to: moment.utc('2018-04-25 11:00'), }; - const ctx = { + const ctx = { backendSrv, timeSrvMock: { timeRange: () => ({ @@ -22,7 +22,7 @@ describe('MySQLDatasource', () => { raw: raw, }), }, - }; + } as any; beforeEach(() => { ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv, ctx.timeSrvMock); diff --git a/public/app/plugins/datasource/mysql/sql_part.ts b/public/app/plugins/datasource/mysql/sql_part.ts index 25cdd09baa6..e7984ef1346 100644 --- a/public/app/plugins/datasource/mysql/sql_part.ts +++ b/public/app/plugins/datasource/mysql/sql_part.ts @@ -1,9 +1,9 @@ import { SqlPartDef, SqlPart } from 'app/core/components/sql_part/sql_part'; -let index = []; +const index = []; function createPart(part): any { - let def = index[part.type]; + const def = index[part.type]; if (!def) { return null; } From 215ca50cc1ec94da9a97609819441b84139e3a08 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 19 Oct 2018 10:19:33 +0200 Subject: [PATCH 032/156] make interpolateVariable arrow function --- public/app/plugins/datasource/mysql/datasource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 4b4c3c3a526..f0381d53b70 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -18,7 +18,7 @@ export class MysqlDatasource { this.interval = (instanceSettings.jsonData || {}).timeInterval; } - interpolateVariable(value, variable) { + interpolateVariable = (value, variable) => { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { return this.queryModel.quoteLiteral(value); @@ -35,7 +35,7 @@ export class MysqlDatasource { return this.queryModel.quoteLiteral(v); }); return quotedValues.join(','); - } + }; query(options) { const queries = _.filter(options.targets, target => { From 68c460a957356b606960e18b77f22b6efdde72a6 Mon Sep 17 00:00:00 2001 From: Yuan Liu Date: Fri, 19 Oct 2018 17:17:38 +0800 Subject: [PATCH 033/156] fix cannot receive dingding alert bug --- pkg/services/alerting/notifiers/dingding.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 738e43af2d2..1ef085c82f1 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -57,6 +57,9 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { message := evalContext.Rule.Message picUrl := evalContext.ImagePublicUrl title := evalContext.GetNotificationTitle() + if message == "" { + message = title + } bodyJSON, err := simplejson.NewJson([]byte(`{ "msgtype": "link", From 44ed188c8449ad1b5c6d328a6c0b348243c2b9fd Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 19 Oct 2018 14:32:37 +0300 Subject: [PATCH 034/156] graph legend: review fixes --- .../app/plugins/panel/graph/Legend/Legend.tsx | 83 +++++++++++-------- .../panel/graph/Legend/LegendSeriesItem.tsx | 35 +++++--- public/app/plugins/panel/graph/graph.ts | 17 ++-- 3 files changed, 79 insertions(+), 56 deletions(-) diff --git a/public/app/plugins/panel/graph/Legend/Legend.tsx b/public/app/plugins/panel/graph/Legend/Legend.tsx index f6daf778848..ee9de5b685e 100644 --- a/public/app/plugins/panel/graph/Legend/Legend.tsx +++ b/public/app/plugins/panel/graph/Legend/Legend.tsx @@ -57,6 +57,21 @@ export class GraphLegend extends React.PureComponent { onColorChange: () => {}, }; + onToggleSeries = (series, event) => { + this.props.onToggleSeries(series, event); + this.forceUpdate(); + }; + + onToggleAxis = series => { + this.props.onToggleAxis(series); + this.forceUpdate(); + }; + + onColorChange = (series, color) => { + this.props.onColorChange(series, color); + this.forceUpdate(); + }; + sortLegend() { let seriesList = this.props.seriesList || []; if (this.props.sort) { @@ -74,12 +89,6 @@ export class GraphLegend extends React.PureComponent { return seriesList; } - onToggleSeries(series: TimeSeries, event: Event) { - // const scrollPosition = legendScrollbar.scroller.scrollTop; - this.props.onToggleSeries(series, event); - // legendScrollbar.scroller.scrollTop = scrollPosition; - } - render() { const { optionalClass, hiddenSeries, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.props; const { values, min, max, avg, current, total } = this.props; @@ -101,10 +110,10 @@ export class GraphLegend extends React.PureComponent { const legendProps: GraphLegendProps = { seriesList: seriesList, hiddenSeries: hiddenSeries, - onToggleSeries: (s, e) => this.onToggleSeries(s, e), - onToggleSort: (sortBy, sortDesc) => this.props.onToggleSort(sortBy, sortDesc), - onColorChange: (series, color) => this.props.onColorChange(series, color), - onToggleAxis: series => this.props.onToggleAxis(series), + onToggleSeries: this.onToggleSeries, + onToggleAxis: this.onToggleAxis, + onToggleSort: this.props.onToggleSort, + onColorChange: this.onColorChange, ...seriesValuesProps, ...sortProps, }; @@ -121,23 +130,22 @@ class LegendSeriesList extends React.PureComponent { render() { const { seriesList, hiddenSeries, values, min, max, avg, current, total } = this.props; const seriesValuesProps = { values, min, max, avg, current, total }; - return seriesList.map((series, i) => ( + return seriesList.map(series => (