From 3626b71c2229eec5e0d9fa3c97ba1aa78b8de92e Mon Sep 17 00:00:00 2001 From: Johannes 'fish' Ziemke Date: Thu, 21 Mar 2013 19:12:04 +0100 Subject: [PATCH 1/6] Improve graph UI. - resize graphs on browser resize - move status field to upper right corner to save some space - align the legend width to the graph's width --- web/static/css/prometheus.css | 19 +++++++++++++++++++ web/static/graph.html | 5 ++--- web/static/js/graph.js | 15 +++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/web/static/css/prometheus.css b/web/static/css/prometheus.css index 2a563b085f..27c45ae755 100644 --- a/web/static/css/prometheus.css +++ b/web/static/css/prometheus.css @@ -23,3 +23,22 @@ input:not([type=submit]):not([type=file]):not([type=button]) { -moz-border-radius: 4px; border-radius: 4px; } + +.graph { + min-height: 400px; + overflow-x: hidden; +} + +div.legend { + display: block; +} + +form { + display: inline-block; +} + +.eval_stats { + display: inline; + vertical-align: top; +} + diff --git a/web/static/graph.html b/web/static/graph.html index 46360e674d..da6f6d7c11 100644 --- a/web/static/graph.html +++ b/web/static/graph.html @@ -56,10 +56,9 @@ - ajax_spinner - -
+ ajax_spinner +
diff --git a/web/static/js/graph.js b/web/static/js/graph.js index aa87e9a0f7..d226b00c18 100644 --- a/web/static/js/graph.js +++ b/web/static/js/graph.js @@ -321,8 +321,8 @@ Prometheus.Graph.prototype.showGraph = function() { var self = this; self.rickshawGraph = new Rickshaw.Graph({ element: self.graph[0], - height: Math.max($(window).height() - 200, 100), - width: Math.max($(window).width() - 200, 200), + height: Math.max(self.graph.innerHeight(), 100), + width: Math.max(self.graph.innerWidth(), 200), renderer: (self.stacked.is(":checked") ? "stack" : "line"), interpolation: "linear", series: self.data @@ -370,6 +370,14 @@ Prometheus.Graph.prototype.updateGraph = function(reloadGraph) { self.changeHandler(); }; +Prometheus.Graph.prototype.resizeGraph = function() { + var self = this; + self.rickshawGraph.configure({ + height: Math.max(self.graph.innerHeight(), 100), + width: Math.max(self.graph.innerWidth(), 200), + }); + self.rickshawGraph.render(); +}; function parseGraphOptionsFromUrl() { var hashOptions = window.location.hash.slice(1); @@ -395,6 +403,9 @@ function addGraph(options) { graph.onChange(function() { storeGraphOptionsInUrl(); }); + $(window).resize(function() { + graph.resizeGraph(); + }); } function init() { From 24b3a6d2cc991a7e7fbab5123c9d02de27db5d0d Mon Sep 17 00:00:00 2001 From: Johannes 'fish' Ziemke Date: Mon, 25 Mar 2013 16:01:52 +0100 Subject: [PATCH 2/6] Improve inserting of metrics in graph UI. - Metric will inserted at cursor position. - Selected text will get replaced. - Press to jump to metrics and to jump back. --- web/static/js/graph.js | 13 +- web/static/vendor/js/jquery.hotkeys.js | 100 +++++++ web/static/vendor/js/jquery.selection.js | 360 +++++++++++++++++++++++ 3 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 web/static/vendor/js/jquery.hotkeys.js create mode 100644 web/static/vendor/js/jquery.selection.js diff --git a/web/static/js/graph.js b/web/static/js/graph.js index d226b00c18..5b410ce400 100644 --- a/web/static/js/graph.js +++ b/web/static/js/graph.js @@ -71,6 +71,14 @@ Prometheus.Graph.prototype.initialize = function() { self.stacked = self.queryForm.find("input[name=stacked]"); self.insertMetric = self.queryForm.find("select[name=insert_metric]"); + // Return moves focus back to expr instead of submitting. + self.insertMetric.bind('keydown', 'return', function(e) { + self.expr.focus(); + self.expr.val(self.expr.val()); + + return e.preventDefault(); + }) + self.graph = graphWrapper.find(".graph"); self.legend = graphWrapper.find(".legend"); self.spinner = graphWrapper.find(".spinner"); @@ -96,8 +104,11 @@ Prometheus.Graph.prototype.initialize = function() { self.queryForm.find("input[name=dec_end]").click(function() { self.decreaseEnd(); }); self.insertMetric.change(function() { - self.expr.val(self.expr.val() + self.insertMetric.val()); + self.expr.selection('replace', {text: self.insertMetric.val(), mode: 'before'}) + self.insertMetric.focus(); // refocusing }); + self.insertMetric.click(function() { self.expr.focus() }) + self.expr.focus(); // TODO: move to external Graph method. self.populateInsertableMetrics(); diff --git a/web/static/vendor/js/jquery.hotkeys.js b/web/static/vendor/js/jquery.hotkeys.js new file mode 100644 index 0000000000..5905f9db2a --- /dev/null +++ b/web/static/vendor/js/jquery.hotkeys.js @@ -0,0 +1,100 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "), + textAcceptingInputTypes = ["text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color"]; + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1 ) ) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); \ No newline at end of file diff --git a/web/static/vendor/js/jquery.selection.js b/web/static/vendor/js/jquery.selection.js new file mode 100644 index 0000000000..86b100c602 --- /dev/null +++ b/web/static/vendor/js/jquery.selection.js @@ -0,0 +1,360 @@ +/*! + * jQuery.selection - jQuery Plugin + * + * Copyright (c) 2010-2012 IWASAKI Koji (@madapaja). + * http://blog.madapaja.net/ + * Under The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +(function($, win, doc) { + /** + * 要素の文字列選択状態を取得します + * + * @param {Element} element 対象要素 + * @return {Object} return + * @return {String} return.text 選択されている文字列 + * @return {Integer} return.start 選択開始位置 + * @return {Integer} return.end 選択終了位置 + */ + var _getCaretInfo = function(element){ + var res = { + text: '', + start: 0, + end: 0 + }; + + if (!element.value) { + /* 値がない、もしくは空文字列 */ + return res; + } + + try { + if (win.getSelection) { + /* IE 以外 */ + res.start = element.selectionStart; + res.end = element.selectionEnd; + res.text = element.value.slice(res.start, res.end); + } else if (doc.selection) { + /* for IE */ + element.focus(); + + var range = doc.selection.createRange(), + range2 = doc.body.createTextRange(), + tmpLength; + + res.text = range.text; + + try { + range2.moveToElementText(element); + range2.setEndPoint('StartToStart', range); + } catch (e) { + range2 = element.createTextRange(); + range2.setEndPoint('StartToStart', range); + } + + res.start = element.value.length - range2.text.length; + res.end = res.start + range.text.length; + } + } catch (e) { + /* あきらめる */ + } + + return res; + }; + + /** + * 要素に対するキャレット操作 + * @type {Object} + */ + var _CaretOperation = { + /** + * 要素のキャレット位置を取得します + * + * @param {Element} element 対象要素 + * @return {Object} return + * @return {Integer} return.start 選択開始位置 + * @return {Integer} return.end 選択終了位置 + */ + getPos: function(element) { + var tmp = _getCaretInfo(element); + return {start: tmp.start, end: tmp.end}; + }, + + /** + * 要素のキャレット位置を設定します + * + * @param {Element} element 対象要素 + * @param {Object} toRange 設定するキャレット位置 + * @param {Integer} toRange.start 選択開始位置 + * @param {Integer} toRange.end 選択終了位置 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + setPos: function(element, toRange, caret) { + caret = this._caretMode(caret); + + if (caret == 'start') { + toRange.end = toRange.start; + } else if (caret == 'end') { + toRange.start = toRange.end; + } + + element.focus(); + try { + if (element.createTextRange) { + var range = element.createTextRange(); + + if (win.navigator.userAgent.toLowerCase().indexOf("msie") >= 0) { + toRange.start = element.value.substr(0, toRange.start).replace(/\r/g, '').length; + toRange.end = element.value.substr(0, toRange.end).replace(/\r/g, '').length; + } + + range.collapse(true); + range.moveStart('character', toRange.start); + range.moveEnd('character', toRange.end - toRange.start); + + range.select(); + } else if (element.setSelectionRange) { + element.setSelectionRange(toRange.start, toRange.end); + } + } catch (e) { + /* あきらめる */ + } + }, + + /** + * 要素内の選択文字列を取得します + * + * @param {Element} element 対象要素 + * @return {String} return 選択文字列 + */ + getText: function(element) { + return _getCaretInfo(element).text; + }, + + /** + * キャレットモードを選択します + * + * @param {String} caret キャレットモード + * @return {String} return "keep" | "start" | "end" のいずれか + */ + _caretMode: function(caret) { + caret = caret || "keep"; + if (caret == false) { + caret = 'end'; + } + + switch (caret) { + case 'keep': + case 'start': + case 'end': + break; + + default: + caret = 'keep'; + } + + return caret; + }, + + /** + * 選択文字列を置き換えます + * + * @param {Element} element 対象要素 + * @param {String} text 置き換える文字列 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + replace: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start, end: tmp.start + text.length}; + + element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.end); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + }, + + /** + * 文字列を選択文字列の前に挿入します + * + * @param {Element} element 対象要素 + * @param {String} text 挿入文字列 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + insertBefore: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start + text.length, end: tmp.end + text.length}; + + element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.start); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + }, + + /** + * 文字列を選択文字列の後に挿入します + * + * @param {Element} element 対象要素 + * @param {String} text 挿入文字列 + * @param {String} caret キャレットモード "keep" | "start" | "end" のいずれか + */ + insertAfter: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start, end: tmp.end}; + + element.value = orig.substr(0, tmp.end) + text + orig.substr(tmp.end); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + } + }; + + /* jQuery.selection を追加 */ + $.extend({ + /** + * ウィンドウの選択されている文字列を取得 + * + * @param {String} mode 選択モード "text" | "html" のいずれか + * @return {String} return + */ + selection: function(mode) { + var getText = ((mode || 'text').toLowerCase() == 'text'); + + try { + if (win.getSelection) { + if (getText) { + // get text + return win.getSelection().toString(); + } else { + // get html + var sel = win.getSelection(), range; + + if (sel.getRangeAt) { + range = sel.getRangeAt(0); + } else { + range = doc.createRange(); + range.setStart(sel.anchorNode, sel.anchorOffset); + range.setEnd(sel.focusNode, sel.focusOffset); + } + + return $('
').append(range.cloneContents()).html(); + } + } else if (doc.selection) { + if (getText) { + // get text + return doc.selection.createRange().text; + } else { + // get html + return doc.selection.createRange().htmlText; + } + } + } catch (e) { + /* あきらめる */ + } + + return ''; + } + }); + + /* selection を追加 */ + $.fn.extend({ + selection: function(mode, opts) { + opts = opts || {}; + + switch (mode) { + /** + * selection('getPos') + * キャレット位置を取得します + * + * @return {Object} return + * @return {Integer} return.start 選択開始位置 + * @return {Integer} return.end 選択終了位置 + */ + case 'getPos': + return _CaretOperation.getPos(this[0]); + break; + + /** + * selection('setPos', opts) + * キャレット位置を設定します + * + * @param {Integer} opts.start 選択開始位置 + * @param {Integer} opts.end 選択終了位置 + */ + case 'setPos': + return this.each(function() { + _CaretOperation.setPos(this, opts); + }); + break; + + /** + * selection('replace', opts) + * 選択文字列を置き換えます + * + * @param {String} opts.text 置き換える文字列 + * @param {String} opts.caret キャレットモード "keep" | "start" | "end" のいずれか + */ + case 'replace': + return this.each(function() { + _CaretOperation.replace(this, opts.text, opts.caret); + }); + break; + + /** + * selection('insert', opts) + * 選択文字列の前、もしくは後に文字列を挿入えます + * + * @param {String} opts.text 挿入文字列 + * @param {String} opts.caret キャレットモード "keep" | "start" | "end" のいずれか + * @param {String} opts.mode 挿入モード "before" | "after" のいずれか + */ + case 'insert': + return this.each(function() { + if (opts.mode == 'before') { + _CaretOperation.insertBefore(this, opts.text, opts.caret); + } else { + _CaretOperation.insertAfter(this, opts.text, opts.caret); + } + }); + + break; + + /** + * selection('get') + * 選択されている文字列を取得 + * + * @return {String} return + */ + case 'get': + default: + return _CaretOperation.getText(this[0]); + break; + } + + return this; + } + }); +})(jQuery, window, window.document); From 07c76747f0e06c1ae8c74e02389a0f988a908878 Mon Sep 17 00:00:00 2001 From: Johannes 'fish' Ziemke Date: Mon, 25 Mar 2013 16:06:36 +0100 Subject: [PATCH 3/6] Clean up of graph UI's form. - Removed unnecessary spaces and labels. - Aligned elements for cleaner look. --- web/static/css/prometheus.css | 27 +++++++++++++--- web/static/graph.html | 61 ++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/web/static/css/prometheus.css b/web/static/css/prometheus.css index 27c45ae755..c085d0f868 100644 --- a/web/static/css/prometheus.css +++ b/web/static/css/prometheus.css @@ -4,6 +4,8 @@ body { line-height: 20px; color: #333333; background-color: #eee; + margin: 0px; + padding: 0px; } input:not([type=submit]):not([type=file]):not([type=button]) { @@ -24,6 +26,15 @@ input:not([type=submit]):not([type=file]):not([type=button]) { border-radius: 4px; } +.grouping_box .head, .eval_stats { + display: inline-block; + vertical-align: top; +} + +.grouping_box .head .opts { + float: right; +} + .graph { min-height: 400px; overflow-x: hidden; @@ -31,14 +42,20 @@ input:not([type=submit]):not([type=file]):not([type=button]) { div.legend { display: block; + overflow: scroll; } -form { - display: inline-block; +input { + margin: 0; + border: 1px solid black; } -.eval_stats { - display: inline; - vertical-align: top; +select { + z-index: 10; + width: 150px; } +input[name=end_input], input[name=range_input] { + margin-left: -4px; + margin-right: -4px; +} diff --git a/web/static/graph.html b/web/static/graph.html index da6f6d7c11..f1fd52fc26 100644 --- a/web/static/graph.html +++ b/web/static/graph.html @@ -20,6 +20,8 @@ + + @@ -27,38 +29,37 @@
- - - - -
- - - - - - - - - - - - - - - - - - - - - - +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
+
+
ajax_spinner -
From ecb8331df151f477038066c87eda84b440ae9c96 Mon Sep 17 00:00:00 2001 From: Johannes 'fish' Ziemke Date: Mon, 25 Mar 2013 16:15:29 +0100 Subject: [PATCH 4/6] Update jQuery to 1.9.1. --- web/static/graph.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/static/graph.html b/web/static/graph.html index f1fd52fc26..ca2ad0edae 100644 --- a/web/static/graph.html +++ b/web/static/graph.html @@ -4,8 +4,8 @@ Prometheus Expression Browser - - + + From 0a87618733d74bca5be8c9f2958422221fc33eea Mon Sep 17 00:00:00 2001 From: Johannes 'fish' Ziemke Date: Mon, 25 Mar 2013 16:15:54 +0100 Subject: [PATCH 5/6] Add autocompletion for metrics. --- web/static/js/graph.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/static/js/graph.js b/web/static/js/graph.js index 5b410ce400..799bc1c9b5 100644 --- a/web/static/js/graph.js +++ b/web/static/js/graph.js @@ -125,9 +125,12 @@ Prometheus.Graph.prototype.populateInsertableMetrics = function() { url: "/api/metrics", dataType: "json", success: function(json, textStatus) { + var availableMetrics = [] for (var i = 0; i < json.length; i++) { self.insertMetric[0].options.add(new Option(json[i], json[i])); + availableMetrics.push(json[i]) } + self.expr.autocomplete({source: availableMetrics}) }, error: function() { alert("Error loading available metrics!"); From b0d1864146be6544f9eae1328f1878df07eaf3cd Mon Sep 17 00:00:00 2001 From: Johannes 'fish' Ziemke Date: Tue, 26 Mar 2013 14:07:56 +0100 Subject: [PATCH 6/6] Move css for graphs to graph.css and fix minor/fomatting issues. --- web/static/css/graph.css | 3 ++- web/static/css/prometheus.css | 12 +----------- web/static/js/graph.js | 1 + 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/web/static/css/graph.css b/web/static/css/graph.css index 5a84cbaabe..0e2718ccad 100644 --- a/web/static/css/graph.css +++ b/web/static/css/graph.css @@ -8,6 +8,8 @@ body { .graph { position: relative; + min-height: 400px; + overflow-x: hidden; } svg { @@ -19,5 +21,4 @@ svg { display: inline-block; vertical-align: top; margin: 0 0 0 0px; - background- } diff --git a/web/static/css/prometheus.css b/web/static/css/prometheus.css index c085d0f868..96a8ca84b5 100644 --- a/web/static/css/prometheus.css +++ b/web/static/css/prometheus.css @@ -35,19 +35,9 @@ input:not([type=submit]):not([type=file]):not([type=button]) { float: right; } -.graph { - min-height: 400px; - overflow-x: hidden; -} - -div.legend { - display: block; - overflow: scroll; -} - input { margin: 0; - border: 1px solid black; + border: 1px solid gray; } select { diff --git a/web/static/js/graph.js b/web/static/js/graph.js index 799bc1c9b5..e2bc92fce5 100644 --- a/web/static/js/graph.js +++ b/web/static/js/graph.js @@ -384,6 +384,7 @@ Prometheus.Graph.prototype.updateGraph = function(reloadGraph) { self.changeHandler(); }; + Prometheus.Graph.prototype.resizeGraph = function() { var self = this; self.rickshawGraph.configure({