Commit dda1cf1a by Torkel Ödegaard

Merge branch 'ace-editor'

parents 78ea1ea7 bdb1cfb0
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"ace-builds": "^1.2.8",
"eventemitter3": "^2.0.2", "eventemitter3": "^2.0.2",
"gaze": "^1.1.2", "gaze": "^1.1.2",
"grunt-jscs": "3.0.1", "grunt-jscs": "3.0.1",
......
/**
* codeEditor directive based on Ace code editor
* https://github.com/ajaxorg/ace
*
* Basic usage:
* <code-editor content="ctrl.target.query" on-change="ctrl.panelCtrl.refresh()"
* data-mode="sql" data-show-gutter>
* </code-editor>
*
* Params:
* content: Editor content.
* onChange: Function called on content change (invoked on editor blur, ctrl+enter, not on every change).
* getCompleter: Function returned external completer. Completer is an object implemented getCompletions() method,
* see Prometheus Data Source implementation for details.
*
* Some Ace editor options available via data-* attributes:
* data-mode - Language mode (text, sql, javascript, etc.). Default is 'text'.
* data-theme - Editor theme (eg 'solarized_dark').
* data-max-lines - Max editor height in lines. Editor grows automatically from 1 to maxLines.
* data-show-gutter - Show gutter (contains line numbers and additional info).
* data-tab-size - Tab size, default is 2.
* data-behaviours-enabled - Specifies whether to use behaviors or not. "Behaviors" in this case is the auto-pairing of
* special characters, like quotation marks, parenthesis, or brackets.
*
* Keybindings:
* Ctrl-Enter (Command-Enter): run onChange() function
*/
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import config from 'app/core/config';
import ace from 'ace';
const ACE_SRC_BASE = "public/vendor/npm/ace-builds/src-noconflict/";
const DEFAULT_THEME_DARK = "grafana-dark";
const DEFAULT_THEME_LIGHT = "textmate";
const DEFAULT_MODE = "text";
const DEFAULT_MAX_LINES = 10;
const DEFAULT_TAB_SIZE = 2;
const DEFAULT_BEHAVIOURS = true;
const GRAFANA_MODULES = ['mode-prometheus', 'snippets-prometheus', 'theme-grafana-dark'];
const GRAFANA_MODULE_BASE = "public/app/core/components/code_editor/";
// Trick for loading additional modules
function setModuleUrl(moduleType, name) {
let baseUrl = ACE_SRC_BASE;
let aceModeName = `ace/${moduleType}/${name}`;
let moduleName = `${moduleType}-${name}`;
let componentName = `${moduleName}.js`;
if (_.includes(GRAFANA_MODULES, moduleName)) {
baseUrl = GRAFANA_MODULE_BASE;
}
if (moduleType === 'snippets') {
componentName = `${moduleType}/${name}.js`;
}
ace.config.setModuleUrl(aceModeName, baseUrl + componentName);
}
setModuleUrl("ext", "language_tools");
setModuleUrl("mode", "text");
setModuleUrl("snippets", "text");
let editorTemplate = `<div></div>`;
function link(scope, elem, attrs) {
let lightTheme = config.bootData.user.lightTheme;
let default_theme = lightTheme ? DEFAULT_THEME_LIGHT : DEFAULT_THEME_DARK;
// Options
let langMode = attrs.mode || DEFAULT_MODE;
let maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
let showGutter = attrs.showGutter !== undefined;
let theme = attrs.theme || default_theme;
let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS;
// Initialize editor
let aceElem = elem.get(0);
let codeEditor = ace.edit(aceElem);
let editorSession = codeEditor.getSession();
let editorOptions = {
maxLines: maxLines,
showGutter: showGutter,
tabSize: tabSize,
behavioursEnabled: behavioursEnabled,
highlightActiveLine: false,
showPrintMargin: false,
autoScrollEditorIntoView: true // this is needed if editor is inside scrollable page
};
// Set options
codeEditor.setOptions(editorOptions);
// disable depreacation warning
codeEditor.$blockScrolling = Infinity;
// Padding hacks
codeEditor.renderer.setScrollMargin(15, 15);
codeEditor.renderer.setPadding(10);
setThemeMode(theme);
setLangMode(langMode);
setEditorContent(scope.content);
// Add classes
elem.addClass("gf-code-editor");
let textarea = elem.find("textarea");
textarea.addClass('gf-form-input');
// Event handlers
editorSession.on('change', (e) => {
scope.$apply(() => {
let newValue = codeEditor.getValue();
scope.content = newValue;
});
});
// Sync with outer scope - update editor content if model has been changed from outside of directive.
scope.$watch('content', (newValue, oldValue) => {
let editorValue = codeEditor.getValue();
if (newValue !== editorValue && newValue !== oldValue) {
scope.$$postDigest(function() {
setEditorContent(newValue);
});
}
});
codeEditor.on('blur', () => {
scope.onChange();
});
scope.$on("$destroy", () => {
codeEditor.destroy();
});
// Keybindings
codeEditor.commands.addCommand({
name: 'executeQuery',
bindKey: {win: 'Ctrl-Enter', mac: 'Command-Enter'},
exec: () => {
scope.onChange();
}
});
function setLangMode(lang) {
let aceModeName = `ace/mode/${lang}`;
setModuleUrl("mode", lang);
setModuleUrl("snippets", lang);
editorSession.setMode(aceModeName);
ace.config.loadModule("ace/ext/language_tools", (language_tools) => {
codeEditor.setOptions({
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true
});
console.log('getting completer', lang);
if (scope.getCompleter()) {
// make copy of array as ace seems to share completers array between instances
codeEditor.completers = codeEditor.completers.slice();
codeEditor.completers.push(scope.getCompleter());
}
});
}
function setThemeMode(theme) {
setModuleUrl("theme", theme);
let themeModule = `ace/theme/${theme}`;
ace.config.loadModule(themeModule, (theme_module) => {
// Check is theme light or dark and fix if needed
let lightTheme = config.bootData.user.lightTheme;
let fixedTheme = theme;
if (lightTheme && theme_module.isDark) {
fixedTheme = DEFAULT_THEME_LIGHT;
} else if (!lightTheme && !theme_module.isDark) {
fixedTheme = DEFAULT_THEME_DARK;
}
setModuleUrl("theme", fixedTheme);
themeModule = `ace/theme/${fixedTheme}`;
codeEditor.setTheme(themeModule);
elem.addClass("gf-code-editor--theme-loaded");
});
}
function setEditorContent(value) {
codeEditor.setValue(value);
codeEditor.clearSelection();
}
}
export function codeEditorDirective() {
return {
restrict: 'E',
template: editorTemplate,
scope: {
content: "=",
onChange: "&",
getCompleter: "&"
},
link: link
};
}
coreModule.directive('codeEditor', codeEditorDirective);
// jshint ignore: start
// jscs: disable
ace.define("ace/snippets/prometheus",["require","exports","module"], function(require, exports, module) {
"use strict";
// exports.snippetText = "# rate\n\
// snippet r\n\
// rate(${1:metric}[${2:range}])\n\
// ";
exports.snippets = [
{
"content": "rate(${1:metric}[${2:range}])",
"name": "rate()",
"scope": "prometheus",
"tabTrigger": "r"
}
];
exports.scope = "prometheus";
});
/* jshint ignore:start */
ace.define("ace/theme/grafana-dark",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
"use strict";
exports.isDark = true;
exports.cssClass = "gf-code-dark";
exports.cssText = ".gf-code-dark .ace_gutter {\
background: #2f3129;\
color: #8f908a\
}\
.gf-code-dark .ace_print-margin {\
width: 1px;\
background: #555651\
}\
.gf-code-dark {\
background-color: #111;\
color: #e0e0e0\
}\
.gf-code-dark .ace_cursor {\
color: #f8f8f0\
}\
.gf-code-dark .ace_marker-layer .ace_selection {\
background: #49483e\
}\
.gf-code-dark.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px #272822;\
}\
.gf-code-dark .ace_marker-layer .ace_step {\
background: rgb(102, 82, 0)\
}\
.gf-code-dark .ace_marker-layer .ace_bracket {\
margin: -1px 0 0 -1px;\
border: 1px solid #49483e\
}\
.gf-code-dark .ace_marker-layer .ace_active-line {\
background: #202020\
}\
.gf-code-dark .ace_gutter-active-line {\
background-color: #272727\
}\
.gf-code-dark .ace_marker-layer .ace_selected-word {\
border: 1px solid #49483e\
}\
.gf-code-dark .ace_invisible {\
color: #52524d\
}\
.gf-code-dark .ace_entity.ace_name.ace_tag,\
.gf-code-dark .ace_keyword,\
.gf-code-dark .ace_meta.ace_tag,\
.gf-code-dark .ace_storage {\
color: #66d9ef\
}\
.gf-code-dark .ace_punctuation,\
.gf-code-dark .ace_punctuation.ace_tag {\
color: #fff\
}\
.gf-code-dark .ace_constant.ace_character,\
.gf-code-dark .ace_constant.ace_language,\
.gf-code-dark .ace_constant.ace_numeric,\
.gf-code-dark .ace_constant.ace_other {\
color: #fe85fc\
}\
.gf-code-dark .ace_invalid {\
color: #f8f8f0;\
background-color: #f92672\
}\
.gf-code-dark .ace_invalid.ace_deprecated {\
color: #f8f8f0;\
background-color: #ae81ff\
}\
.gf-code-dark .ace_support.ace_constant,\
.gf-code-dark .ace_support.ace_function {\
color: #59e6e3\
}\
.gf-code-dark .ace_fold {\
background-color: #a6e22e;\
border-color: #f8f8f2\
}\
.gf-code-dark .ace_storage.ace_type,\
.gf-code-dark .ace_support.ace_class,\
.gf-code-dark .ace_support.ace_type {\
font-style: italic;\
color: #66d9ef\
}\
.gf-code-dark .ace_entity.ace_name.ace_function,\
.gf-code-dark .ace_entity.ace_other,\
.gf-code-dark .ace_entity.ace_other.ace_attribute-name,\
.gf-code-dark .ace_variable {\
color: #a6e22e\
}\
.gf-code-dark .ace_variable.ace_parameter {\
font-style: italic;\
color: #fd971f\
}\
.gf-code-dark .ace_string {\
color: #74e680\
}\
.gf-code-dark .ace_paren {\
color: #f0a842\
}\
.gf-code-dark .ace_operator {\
color: #FFF\
}\
.gf-code-dark .ace_comment {\
color: #75715e\
}\
.gf-code-dark .ace_indent-guide {\
background: url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaaaccayaaaczgbynaaaaekleqvqimwpq0fd0zxbzd/wpaajvaoxesgneaaaaaelftksuqmcc) right repeat-y\
}";
var dom = require("../lib/dom");
dom.importCssString(exports.cssText, exports.cssClass);
});
/* jshint ignore:end */
...@@ -19,6 +19,7 @@ import "./directives/diff-view"; ...@@ -19,6 +19,7 @@ import "./directives/diff-view";
import './jquery_extended'; import './jquery_extended';
import './partials'; import './partials';
import './components/jsontree/jsontree'; import './components/jsontree/jsontree';
import './components/code_editor/code_editor';
import {grafanaAppDirective} from './components/grafana_app'; import {grafanaAppDirective} from './components/grafana_app';
import {sideMenuDirective} from './components/sidemenu/sidemenu'; import {sideMenuDirective} from './components/sidemenu/sidemenu';
......
...@@ -72,3 +72,8 @@ declare module 'd3' { ...@@ -72,3 +72,8 @@ declare module 'd3' {
var d3: any; var d3: any;
export default d3; export default d3;
} }
declare module 'ace' {
var ace: any;
export default ace;
}
<query-editor-row query-ctrl="ctrl" can-collapse="false"> <query-editor-row query-ctrl="ctrl" can-collapse="false">
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<textarea rows="10" class="gf-form-input" ng-model="ctrl.target.rawSql" spellcheck="false" placeholder="query expression" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"></textarea> <code-editor content="ctrl.target.rawSql" on-change="ctrl.panelCtrl.refresh()" data-mode="sql">
</code-editor>
</div> </div>
</div> </div>
......
///<reference path="../../../headers/common.d.ts" />
import {PrometheusDatasource} from "./datasource";
export class PromCompleter {
identifierRegexps = [/[\[\]a-zA-Z_0-9=]/];
constructor(private datasource: PrometheusDatasource) {
}
getCompletions(editor, session, pos, prefix, callback) {
if (prefix === '[') {
var vectors = [];
for (let unit of ['s', 'm', 'h']) {
for (let value of [1,5,10,30]) {
vectors.push({caption: value+unit, value: '['+value+unit, meta: 'range vector'});
}
}
callback(null, vectors);
return;
}
var query = prefix;
var line = editor.session.getLine(pos.row);
return this.datasource.performSuggestQuery(query).then(metricNames => {
callback(null, metricNames.map(name => {
let value = name;
if (prefix === '(') {
value = '(' + name;
}
return {
caption: name,
value: value,
meta: 'metric',
};
}));
});
}
}
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false"> <query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<textarea rows="3" class="gf-form-input" ng-model="ctrl.target.expr" spellcheck="false" placeholder="query expression" data-min-length=0 data-items=100 give-focus="ctrl.target.refId == 'A'" ng-model-onblur ng-change="ctrl.refreshMetricData()"></textarea> <code-editor content="ctrl.target.expr" on-change="ctrl.refreshMetricData()"
get-completer="ctrl.getCompleter()" data-mode="prometheus">
</code-editor>
</div> </div>
</div> </div>
...@@ -38,17 +40,6 @@ ...@@ -38,17 +40,6 @@
</div> </div>
</div> </div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-26">
<label class="gf-form-label width-8">Metric lookup</label>
<input type="text" class="gf-form-input" ng-model="ctrl.target.metric" spellcheck='false' bs-typeahead="ctrl.suggestMetrics" placeholder="metric name" data-min-length=0 data-items=100>
</div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-6">Format as</label> <label class="gf-form-label width-6">Format as</label>
<div class="gf-form-select-wrapper width-8"> <div class="gf-form-select-wrapper width-8">
...@@ -66,4 +57,5 @@ ...@@ -66,4 +57,5 @@
</div> </div>
</div> </div>
</query-editor-row> </query-editor-row>
...@@ -6,6 +6,7 @@ import moment from 'moment'; ...@@ -6,6 +6,7 @@ import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath'; import * as dateMath from 'app/core/utils/datemath';
import {QueryCtrl} from 'app/plugins/sdk'; import {QueryCtrl} from 'app/plugins/sdk';
import {PromCompleter} from './completer';
class PrometheusQueryCtrl extends QueryCtrl { class PrometheusQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html'; static templateUrl = 'partials/query.editor.html';
...@@ -15,6 +16,7 @@ class PrometheusQueryCtrl extends QueryCtrl { ...@@ -15,6 +16,7 @@ class PrometheusQueryCtrl extends QueryCtrl {
formats: any; formats: any;
oldTarget: any; oldTarget: any;
suggestMetrics: any; suggestMetrics: any;
getMetricsAutocomplete: any;
linkToPrometheus: any; linkToPrometheus: any;
/** @ngInject */ /** @ngInject */
...@@ -36,24 +38,19 @@ class PrometheusQueryCtrl extends QueryCtrl { ...@@ -36,24 +38,19 @@ class PrometheusQueryCtrl extends QueryCtrl {
{text: 'Table', value: 'table'}, {text: 'Table', value: 'table'},
]; ];
$scope.$on('typeahead-updated', () => {
this.$scope.$apply(() => {
this.target.expr += this.target.metric;
this.metric = '';
this.refreshMetricData();
});
});
// called from typeahead so need this
// here in order to ensure this ref
this.suggestMetrics = (query, callback) => {
console.log(this);
this.datasource.performSuggestQuery(query).then(callback);
};
this.updateLink(); this.updateLink();
} }
getCompleter(query) {
return new PromCompleter(this.datasource);
// console.log('getquery);
// return this.datasource.performSuggestQuery(query).then(res => {
// return res.map(item => {
// return {word: item, type: 'metric'};
// });
// });
}
getDefaultFormat() { getDefaultFormat() {
if (this.panelCtrl.panel.type === 'table') { if (this.panelCtrl.panel.type === 'table') {
return 'table'; return 'table';
......
...@@ -33,6 +33,7 @@ System.config({ ...@@ -33,6 +33,7 @@ System.config({
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
"d3": "vendor/d3/d3.js", "d3": "vendor/d3/d3.js",
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
"ace": "vendor/npm/ace-builds/src-noconflict/ace"
}, },
packages: { packages: {
...@@ -73,5 +74,9 @@ System.config({ ...@@ -73,5 +74,9 @@ System.config({
format: 'global', format: 'global',
exports: 'Mousetrap' exports: 'Mousetrap'
}, },
'vendor/npm/ace-builds/src-noconflict/ace.js': {
format: 'global',
exports: 'ace'
}
} }
}); });
...@@ -77,6 +77,7 @@ ...@@ -77,6 +77,7 @@
@import "components/row.scss"; @import "components/row.scss";
@import "components/json_explorer.scss"; @import "components/json_explorer.scss";
@import "components/collapse_box.scss"; @import "components/collapse_box.scss";
@import "components/code_editor.scss";
// PAGES // PAGES
@import "pages/login"; @import "pages/login";
......
.gf-code-editor {
min-height: 2.60rem;
min-width: 20rem;
flex-grow: 1;
margin-right: 0.25rem;
visibility: hidden;
&.ace_editor {
@include font-family-monospace();
font-size: 1rem;
min-height: 2.60rem;
@include border-radius($input-border-radius-sm);
border: $input-btn-border-width solid $input-border-color;
}
&--theme-loaded {
visibility: visible;
}
}
.ace_editor.ace_autocomplete {
@include font-family-monospace();
font-size: 1rem;
// Ace editor adds <style> tag at the end of <head>, after grafana.css, so !important
// is used for overriding styles with the same CSS specificity.
background-color: $dropdownBackground !important;
color: $dropdownLinkColor !important;
border: 1px solid $dropdownBorder !important;
width: 320px !important;
.ace_scroller {
.ace_selected, .ace_active-line, .ace_line-hover {
color: $dropdownLinkColorHover;
background-color: $dropdownLinkBackgroundHover !important;
}
.ace_line-hover {
border-color: transparent;
}
.ace_completion-highlight {
color: $yellow;
}
.ace_rightAlignedText {
color: $text-muted;
z-index: 0;
}
}
}
$doc-font-size: $font-size-sm;
.ace_tooltip.ace_doc-tooltip {
@include font-family-monospace();
font-size: $doc-font-size;
background-color: $popover-help-bg;
color: $popover-help-color;
background-image: none;
border: 1px solid $dropdownBorder;
padding: 0.5rem 1rem;
hr {
background-color: $popover-help-color;
margin: 0.5rem 0rem;
}
code {
padding: 0px 1px;
margin: 0px;
}
}
.ace_tooltip {
border-radius: 3px;
}
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
"d3": "vendor/d3/d3.js", "d3": "vendor/d3/d3.js",
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
"ace": "vendor/npm/ace-builds/src-noconflict/ace",
}, },
packages: { packages: {
...@@ -73,6 +74,10 @@ ...@@ -73,6 +74,10 @@
format: 'global', format: 'global',
exports: 'Mousetrap' exports: 'Mousetrap'
}, },
'vendor/npm/ace-builds/src-noconflict/ace.js': {
format: 'global',
exports: 'ace'
},
} }
}); });
......
...@@ -19,6 +19,7 @@ module.exports = function(config) { ...@@ -19,6 +19,7 @@ module.exports = function(config) {
cwd: './node_modules', cwd: './node_modules',
expand: true, expand: true,
src: [ src: [
'ace-builds/src-noconflict/**/*',
'eventemitter3/*.js', 'eventemitter3/*.js',
'systemjs/dist/*.js', 'systemjs/dist/*.js',
'es6-promise/**/*', 'es6-promise/**/*',
......
...@@ -21,7 +21,10 @@ module.exports = function(config, grunt) { ...@@ -21,7 +21,10 @@ module.exports = function(config, grunt) {
return; return;
} }
gaze(config.srcDir + '/**/*', function(err, watcher) { gaze([
config.srcDir + '/app/**/*',
config.srcDir + '/sass/**/*',
], function(err, watcher) {
console.log('Gaze watchers setup'); console.log('Gaze watchers setup');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment