Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Alexander Zobnin
2017-06-18 20:59:05 +03:00
15 changed files with 536 additions and 122 deletions

View File

@@ -0,0 +1,74 @@
+++
title = "API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization"
type = "docs"
keywords = ["grafana", "tutorials", "API", "Token", "Org", "Organization"]
[menu.docs]
parent = "tutorials"
weight = 10
+++
# API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization
A common scenario is to want to via the Grafana API setup new Grafana organizations or to add dynamically generated dashboards to an existing organization.
## Authentication
There are two ways to authenticate against the API: basic authentication and API Tokens.
Some parts of the API are only available through basic authentication and these parts of the API usually require that the user is a Grafana Admin. But all organization actions are accessed via an API Token. An API Token is tied to an organization and can be used to create dashboards etc but only for that organization.
## How To Create A New Organization and an API Token
The task is to create a new organization and then add a Token that can be used by other users. In the examples below which use basic auth, the user is `admin` and the password is `admin`.
1. [Create the org](http://docs.grafana.org/http_api/org/#create-organisation). Here is an example using curl:
```
curl -X POST -H "Content-Type: application/json" -d '{"name":"apiorg"}' http://admin:admin@localhost:3000/api/orgs
```
This should return a response: `{"message":"Organization created","orgId":6}`. Use the orgId for the next steps.
2. Optional step. If the org was created previously and/or step 3 fails then first [add your Admin user to the org](http://docs.grafana.org/http_api/org/#add-user-in-organisation):
```
curl -X POST -H "Content-Type: application/json" -d '{"loginOrEmail":"admin", "role": "Admin"}' http://admin:admin@localhost:3000/api/orgs/<org id of new org>/users
```
3. [Switch the org context for the Admin user to the new org](http://docs.grafana.org/http_api/user/#switch-user-context):
```
curl -X POST http://admin:admin@localhost:3000/api/user/using/<id of new org>
```
4. [Create the API token](http://docs.grafana.org/http_api/auth/#create-api-key):
```
curl -X POST -H "Content-Type: application/json" -d '{"name":"apikeycurl", "role": "Admin"}' http://admin:admin@localhost:3000/api/auth/keys
```
This should return a response: `{"name":"apikeycurl","key":"eyJrIjoiR0ZXZmt1UFc0OEpIOGN5RWdUalBJTllUTk83VlhtVGwiLCJuIjoiYXBpa2V5Y3VybCIsImlkIjo2fQ=="}`.
Save the key returned here in your password manager as it is not possible to fetch again it in the future.
## How To Add A Dashboard
Using the Token that was created in the previous step, you can create a dashboard or carry out other actions without having to switch organizations.
1. [Add a dashboard](http://docs.grafana.org/http_api/dashboard/#create-update-dashboard) using the key (or bearer token as it is also called):
```
curl -X POST --insecure -H "Authorization: Bearer eyJrIjoiR0ZXZmt1UFc0OEpIOGN5RWdUalBJTllUTk83VlhtVGwiLCJuIjoiYXBpa2V5Y3VybCIsImlkIjo2fQ==" -H "Content-Type: application/json" -d '{
"dashboard": {
"id": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
"rows": [
{
}
],
"schemaVersion": 6,
"version": 0
},
"overwrite": false
}' http://localhost:3000/api/dashboards/db
```
This import will not work if you exported the dashboard via the Share -> Export menu in the Grafana UI (it strips out data source names etc.). View the JSON and save it to a file instead or fetch the dashboard JSON via the API.

View File

@@ -61,7 +61,7 @@ func (hs *HttpServer) Start(ctx context.Context) error {
return nil return nil
} }
case setting.HTTPS: case setting.HTTPS:
err = hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile) err = hs.listenAndServeTLS(setting.CertFile, setting.KeyFile)
if err == http.ErrServerClosed { if err == http.ErrServerClosed {
hs.log.Debug("server was shutdown gracefully") hs.log.Debug("server was shutdown gracefully")
return nil return nil
@@ -92,7 +92,7 @@ func (hs *HttpServer) Shutdown(ctx context.Context) error {
return err return err
} }
func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) error { func (hs *HttpServer) listenAndServeTLS(certfile, keyfile string) error {
if certfile == "" { if certfile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS") return fmt.Errorf("cert_file cannot be empty when using HTTPS")
} }
@@ -127,14 +127,11 @@ func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) er
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
}, },
} }
srv := &http.Server{
Addr: listenAddr,
Handler: hs.macaron,
TLSConfig: tlsCfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
return srv.ListenAndServeTLS(setting.CertFile, setting.KeyFile) hs.httpSrv.TLSConfig = tlsCfg
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
} }
func (hs *HttpServer) newMacaron() *macaron.Macaron { func (hs *HttpServer) newMacaron() *macaron.Macaron {

View File

@@ -0,0 +1,250 @@
///<reference path="../../../headers/common.d.ts" />
import config from 'app/core/config';
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../../core_module';
function typeaheadMatcher(item) {
var str = this.query;
if (str[0] === '/') { str = str.substring(1); }
if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
return item.toLowerCase().match(str.toLowerCase());
}
export class FormDropdownCtrl {
inputElement: any;
linkElement: any;
model: any;
display: any;
text: any;
options: any;
cssClass: any;
cssClasses: any;
allowCustom: any;
labelMode: boolean;
linkMode: boolean;
cancelBlur: any;
onChange: any;
getOptions: any;
optionCache: any;
lookupText: boolean;
constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
this.inputElement = $element.find('input').first();
this.linkElement = $element.find('a').first();
this.linkMode = true;
this.cancelBlur = null;
// listen to model changes
$scope.$watch("ctrl.model", this.modelChanged.bind(this));
if (this.labelMode) {
this.cssClasses = 'gf-form-label ' + this.cssClass;
} else {
this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
}
this.inputElement.attr('data-provide', 'typeahead');
this.inputElement.typeahead({
source: this.typeaheadSource.bind(this),
minLength: 0,
items: 10000,
updater: this.typeaheadUpdater.bind(this),
matcher: typeaheadMatcher,
});
// modify typeahead lookup
// this = typeahead
var typeahead = this.inputElement.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
this.linkElement.keydown(evt => {
// trigger typeahead on down arrow or enter key
if (evt.keyCode === 40 || evt.keyCode === 13) {
this.linkElement.click();
}
});
this.inputElement.keydown(evt => {
if (evt.keyCode === 13) {
setTimeout(() => {
this.inputElement.blur();
}, 100);
}
});
this.inputElement.blur(this.inputBlur.bind(this));
}
getOptionsInternal(query) {
var result = this.getOptions({$query: query});
if (this.isPromiseLike(result)) {
return result;
}
return this.$q.when(result);
}
isPromiseLike(obj) {
return obj && (typeof obj.then === 'function');
}
modelChanged() {
if (_.isObject(this.model)) {
this.updateDisplay(this.model.text);
} else {
// if we have text use it
if (this.lookupText) {
this.getOptionsInternal("").then(options => {
var item = _.find(options, {value: this.model});
this.updateDisplay(item ? item.text : this.model);
});
} else {
this.updateDisplay(this.model);
}
}
}
typeaheadSource(query, callback) {
this.getOptionsInternal(query).then(options => {
this.optionCache = options;
// extract texts
let optionTexts = _.map(options, 'text');
// add custom values
if (this.allowCustom) {
if (_.indexOf(optionTexts, this.text) === -1) {
options.unshift(this.text);
}
}
callback(optionTexts);
});
}
typeaheadUpdater(text) {
if (text === this.text) {
clearTimeout(this.cancelBlur);
this.inputElement.focus();
return text;
}
this.inputElement.val(text);
this.switchToLink(true);
return text;
}
switchToLink(fromClick) {
if (this.linkMode && !fromClick) { return; }
clearTimeout(this.cancelBlur);
this.cancelBlur = null;
this.linkMode = true;
this.inputElement.hide();
this.linkElement.show();
this.updateValue(this.inputElement.val());
}
inputBlur() {
// happens long before the click event on the typeahead options
// need to have long delay because the blur
this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200);
}
updateValue(text) {
if (text === '' || this.text === text) {
return;
}
this.$scope.$apply(() => {
var option = _.find(this.optionCache, {text: text});
if (option) {
if (_.isObject(this.model)) {
this.model = option;
} else {
this.model = option.value;
}
this.text = option.text;
} else if (this.allowCustom) {
if (_.isObject(this.model)) {
this.model.text = this.model.value = text;
} else {
this.model = text;
}
this.text = text;
}
// needs to call this after digest so
// property is synced with outerscope
this.$scope.$$postDigest(() => {
this.$scope.$apply(() => {
this.onChange({$option: option});
});
});
});
}
updateDisplay(text) {
this.text = text;
this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
}
open() {
this.inputElement.show();
this.inputElement.css('width', (Math.max(this.linkElement.width(), 80) + 16) + 'px');
this.inputElement.focus();
this.linkElement.hide();
this.linkMode = false;
var typeahead = this.inputElement.data('typeahead');
if (typeahead) {
this.inputElement.val('');
typeahead.lookup();
}
}
}
const template = `
<input type="text"
data-provide="typeahead"
class="gf-form-input"
spellcheck="false"
style="display:none">
</input>
<a ng-class="ctrl.cssClasses"
tabindex="1"
ng-click="ctrl.open()"
give-focus="ctrl.focus"
ng-bind-html="ctrl.display">
</a>
`;
export function formDropdownDirective() {
return {
restrict: 'E',
template: template,
controller: FormDropdownCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
model: "=",
getOptions: "&",
onChange: "&",
cssClass: "@",
allowCustom: "@",
labelMode: "@",
lookupText: "@",
},
};
}
coreModule.directive('gfFormDropdown', formDropdownDirective);

View File

@@ -8,9 +8,9 @@
<i class="fa fa-chevron-left"></i> <i class="fa fa-chevron-left"></i>
</a> </a>
<a class="navbar-page-btn navbar-page-btn--search" ng-click="ctrl.showSearch()"> <!-- <a class="navbar&#45;page&#45;btn navbar&#45;page&#45;btn&#45;&#45;search" ng&#45;click="ctrl.showSearch()"> -->
<i class="fa fa-search"></i> <!-- <i class="fa fa&#45;search"></i> -->
</a> <!-- </a> -->
<div ng-if="::!ctrl.hasMenu"> <div ng-if="::!ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn"> <a href="{{::ctrl.section.url}}" class="navbar-page-btn">

View File

@@ -34,6 +34,7 @@ import {switchDirective} from './components/switch';
import {dashboardSelector} from './components/dashboard_selector'; import {dashboardSelector} from './components/dashboard_selector';
import {queryPartEditorDirective} from './components/query_part/query_part_editor'; import {queryPartEditorDirective} from './components/query_part/query_part_editor';
import {WizardFlow} from './components/wizard/wizard'; import {WizardFlow} from './components/wizard/wizard';
import {formDropdownDirective} from './components/form_dropdown/form_dropdown';
import 'app/core/controllers/all'; import 'app/core/controllers/all';
import 'app/core/services/all'; import 'app/core/services/all';
import 'app/core/routes/routes'; import 'app/core/routes/routes';
@@ -68,6 +69,7 @@ export {
queryPartEditorDirective, queryPartEditorDirective,
WizardFlow, WizardFlow,
colors, colors,
formDropdownDirective,
assignModelProperties, assignModelProperties,
contextSrv, contextSrv,
KeybindingSrv, KeybindingSrv,

View File

@@ -35,7 +35,7 @@ function ($, angular, coreModule) {
options.html = editViewMap[options.editview].html; options.html = editViewMap[options.editview].html;
} }
if (lastEditView === options.editview) { if (lastEditView && lastEditView === options.editview) {
hideEditorPane(false); hideEditorPane(false);
return; return;
} }

View File

@@ -841,7 +841,7 @@ function($, _) {
{ {
text: 'temperature', text: 'temperature',
submenu: [ submenu: [
{text: 'Celcius (°C)', value: 'celsius' }, {text: 'Celsius (°C)', value: 'celsius' },
{text: 'Farenheit (°F)', value: 'farenheit' }, {text: 'Farenheit (°F)', value: 'farenheit' },
{text: 'Kelvin (K)', value: 'kelvin' }, {text: 'Kelvin (K)', value: 'kelvin' },
] ]

View File

@@ -1,63 +1,95 @@
<navbar model="ctrl.navModel"> <div class="navbar">
<div class="navbar-inner">
<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying"> <a class="navbar-brand-btn pointer" ng-click="ctrl.toggleSideMenu()">
<li> <span class="navbar-brand-btn-background">
<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a> <img src="public/img/grafana_icon.svg"></img>
</li> </span>
<li> <i class="icon-gf icon-gf-grafana_wordmark"></i>
<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a> <i class="fa fa-caret-down"></i>
</li> <i class="fa fa-chevron-left"></i>
<li>
<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
</li>
</ul>
<ul class="nav pull-left dashnav-action-icons">
<li ng-show="::ctrl.dashboard.meta.canStar">
<a class="pointer" ng-click="ctrl.starDashboard()">
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a> </a>
</li>
<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown"> <div class="navbar-section-wrapper">
<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a> <a class="navbar-page-btn" ng-click="ctrl.showSearch()">
<ul class="dropdown-menu"> <i class="icon-gf icon-gf-dashboard"></i>
{{ctrl.dashboard.title}}
<i class="fa fa-caret-down"></i>
</a>
</div>
<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
<li> <li>
<a class="pointer" ng-click="ctrl.shareDashboard(0)"> <a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
</li> </li>
<li> <li>
<a class="pointer" ng-click="ctrl.shareDashboard(1)"> <a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
</a>
</li> </li>
<li> <li>
<a class="pointer" ng-click="ctrl.shareDashboard(2)"> <a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
<i class="fa fa-cloud-upload"></i>Export
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
</a>
</li> </li>
</ul> </ul>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
</ul>
<ul class="nav pull-right"> <ul class="nav pull-left dashnav-action-icons">
<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard"> <li ng-show="::ctrl.dashboard.meta.canStar">
<a ng-click="ctrl.exitFullscreen()"> <a class="pointer" ng-click="ctrl.starDashboard()">
Back to dashboard <i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a> </a>
</li> </li>
<li> <li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown">
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker> <a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
</li> <ul class="dropdown-menu">
</ul> <li>
<a class="pointer" ng-click="ctrl.shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(1)">
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(2)">
<i class="fa fa-cloud-upload"></i>Export
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
</a>
</li>
</ul>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
<li class="dropdown">
<a class="pointer" data-toggle="dropdown">
<i class="fa fa-cog"></i>
</a>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="navItem in ::ctrl.navModel.menu" ng-class="{active: navItem.active}">
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
{{::navItem.title}}
</a>
</li>
</ul>
</li>
</ul>
</navbar> <ul class="nav pull-right">
<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
<a ng-click="ctrl.exitFullscreen()">
Back to dashboard
</a>
</li>
<li>
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
</li>
</ul>
</div>
</div>
<dashboard-search></dashboard-search>

View File

@@ -22,8 +22,8 @@ export class DashNavCtrl {
private backendSrv, private backendSrv,
private $timeout, private $timeout,
private datasourceSrv, private datasourceSrv,
private navModelSrv) { private navModelSrv,
private contextSrv) {
this.navModel = navModelSrv.getDashboardNav(this.dashboard, this); this.navModel = navModelSrv.getDashboardNav(this.dashboard, this);
appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope); appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope);
@@ -38,6 +38,10 @@ export class DashNavCtrl {
} }
} }
toggleSideMenu() {
this.contextSrv.toggleSideMenu();
}
openEditView(editview) { openEditView(editview) {
var search = _.extend(this.$location.search(), {editview: editview}); var search = _.extend(this.$location.search(), {editview: editview});
this.$location.search(search); this.$location.search(search);
@@ -135,6 +139,17 @@ export class DashNavCtrl {
var uri = "data:application/json;charset=utf-8," + encodeURIComponent(html); var uri = "data:application/json;charset=utf-8," + encodeURIComponent(html);
var newWindow = window.open(uri); var newWindow = window.open(uri);
} }
showSearch() {
this.$rootScope.appEvent('show-dash-search');
}
navItemClicked(navItem, evt) {
if (navItem.clickHandler) {
navItem.clickHandler();
evt.preventDefault();
}
}
} }
export function dashNavDirective() { export function dashNavDirective() {

View File

@@ -5,8 +5,6 @@ import _ from 'lodash';
import {DashboardModel} from '../dashboard/model'; import {DashboardModel} from '../dashboard/model';
export class MetricsTabCtrl { export class MetricsTabCtrl {
dsSegment: any;
mixedDsSegment: any;
dsName: string; dsName: string;
panel: any; panel: any;
panelCtrl: any; panelCtrl: any;
@@ -14,30 +12,26 @@ export class MetricsTabCtrl {
current: any; current: any;
nextRefId: string; nextRefId: string;
dashboard: DashboardModel; dashboard: DashboardModel;
panelDsValue: any;
addQueryDropdown: any;
/** @ngInject */ /** @ngInject */
constructor($scope, private uiSegmentSrv, datasourceSrv) { constructor($scope, private uiSegmentSrv, private datasourceSrv) {
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl;
$scope.ctrl = this; $scope.ctrl = this;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.dashboard = this.panelCtrl.dashboard; this.dashboard = this.panelCtrl.dashboard;
this.datasources = datasourceSrv.getMetricSources(); this.datasources = datasourceSrv.getMetricSources();
this.panelDsValue = this.panelCtrl.panel.datasource || null;
var dsValue = this.panelCtrl.panel.datasource || null;
for (let ds of this.datasources) { for (let ds of this.datasources) {
if (ds.value === dsValue) { if (ds.value === this.panelDsValue) {
this.current = ds; this.current = ds;
} }
} }
if (!this.current) { this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
this.current = {name: dsValue + ' not found', value: null};
}
this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true});
this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true});
// update next ref id // update next ref id
this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel); this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
@@ -46,33 +40,28 @@ export class MetricsTabCtrl {
getOptions(includeBuiltin) { getOptions(includeBuiltin) {
return Promise.resolve(this.datasources.filter(value => { return Promise.resolve(this.datasources.filter(value => {
return includeBuiltin || !value.meta.builtIn; return includeBuiltin || !value.meta.builtIn;
}).map(value => { }).map(ds => {
return this.uiSegmentSrv.newSegment(value.name); return {value: ds.value, text: ds.name, datasource: ds};
})); }));
} }
datasourceChanged() { datasourceChanged(option) {
var ds = _.find(this.datasources, {name: this.dsSegment.value}); if (!option) {
if (ds) { return;
this.current = ds;
this.panelCtrl.setDatasource(ds);
} }
this.current = option.datasource;
this.panelCtrl.setDatasource(option.datasource);
} }
mixedDatasourceChanged() { addMixedQuery(option) {
var target: any = {isNew: true}; if (!option) {
var ds = _.find(this.datasources, {name: this.mixedDsSegment.value}); return;
if (ds) {
target.datasource = ds.name;
this.panelCtrl.addQuery(target);
} }
// metric segments are really bad, requires hacks to update var target: any = {isNew: true};
const segment = this.uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true}); this.panelCtrl.addQuery({isNew: true, datasource: option.datasource.name});
this.mixedDsSegment.value = segment.value; this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
this.mixedDsSegment.html = segment.html;
this.mixedDsSegment.text = segment.text;
} }
addQuery() { addQuery() {

View File

@@ -19,7 +19,10 @@
</button> </button>
<div class="dropdown" ng-if="ctrl.current.meta.mixed"> <div class="dropdown" ng-if="ctrl.current.meta.mixed">
<metric-segment segment="ctrl.mixedDsSegment" get-options="ctrl.getOptions(false)" on-change="ctrl.mixedDatasourceChanged()"></metric-segment> <gf-form-dropdown model="ctrl.addQueryDropdown"
get-options="ctrl.getOptions(false)"
on-change="ctrl.addMixedQuery($option)">
</gf-form-dropdown>
</div> </div>
</div> </div>
</div> </div>
@@ -30,10 +33,12 @@
<div class="gf-form-group"> <div class="gf-form-group">
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label"> <label class="gf-form-label">Panel Data Source</label>
Panel Data Source <gf-form-dropdown model="ctrl.panelDsValue"
</label> lookup-text="true"
<metric-segment segment="ctrl.dsSegment" get-options="ctrl.getOptions(true)" on-change="ctrl.datasourceChanged()"></metric-segment> get-options="ctrl.getOptions(true)"
on-change="ctrl.datasourceChanged($option)">
</gf-form-dropdown>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -26,13 +26,21 @@ function (angular, _, queryDef) {
var bucketAggs = $scope.target.bucketAggs; var bucketAggs = $scope.target.bucketAggs;
$scope.orderByOptions = []; $scope.orderByOptions = [];
$scope.bucketAggTypes = queryDef.bucketAggTypes;
$scope.orderOptions = queryDef.orderOptions; $scope.getBucketAggTypes = function() {
$scope.sizeOptions = queryDef.sizeOptions; return queryDef.bucketAggTypes;
};
$scope.getOrderOptions = function() {
return queryDef.orderOptions;
};
$scope.getSizeOptions = function() {
return queryDef.sizeOptions;
};
$rootScope.onAppEvent('elastic-query-updated', function() { $rootScope.onAppEvent('elastic-query-updated', function() {
$scope.validateModel(); $scope.validateModel();
$scope.updateOrderByOptions();
}, $scope); }, $scope);
$scope.init = function() { $scope.init = function() {
@@ -166,11 +174,10 @@ function (angular, _, queryDef) {
$scope.toggleOptions = function() { $scope.toggleOptions = function() {
$scope.showOptions = !$scope.showOptions; $scope.showOptions = !$scope.showOptions;
$scope.updateOrderByOptions();
}; };
$scope.updateOrderByOptions = function() { $scope.getOrderByOptions = function() {
$scope.orderByOptions = queryDef.getOrderByOptions($scope.target); return queryDef.getOrderByOptions($scope.target);
}; };
$scope.getFieldsInternal = function() { $scope.getFieldsInternal = function() {

View File

@@ -5,8 +5,22 @@
<span ng-hide="isFirst">Then by</span> <span ng-hide="isFirst">Then by</span>
</label> </label>
<metric-segment-model property="agg.type" options="bucketAggTypes" on-change="onTypeChanged()" custom="false" css-class="width-10"></metric-segment-model> <gf-form-dropdown model="agg.type"
<metric-segment-model ng-if="agg.field" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model> lookup-text="true"
get-options="getBucketAggTypes()"
on-change="onTypeChanged()"
allow-custom="false"
label-mode="true"
css-class="width-10">
</gf-form-dropdown>
<gf-form-dropdown ng-if="agg.field"
model="agg.field"
get-options="getFieldsInternal()"
on-change="onChange()"
allow-custom="false"
label-mode="true"
css-class="width-12">
</gf-form-dropdown>
</div> </div>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
@@ -33,7 +47,13 @@
<div ng-if="agg.type === 'date_histogram'"> <div ng-if="agg.type === 'date_histogram'">
<div class="gf-form offset-width-7"> <div class="gf-form offset-width-7">
<label class="gf-form-label width-10">Interval</label> <label class="gf-form-label width-10">Interval</label>
<metric-segment-model property="agg.settings.interval" get-options="getIntervalOptions()" on-change="onChangeInternal()" css-class="width-12" custom="true"></metric-segment-model> <gf-form-dropdown model="agg.settings.interval"
get-options="getIntervalOptions()"
on-change="onChangeInternal()"
allow-custom="true"
label-mode="true"
css-class="width-12">
</gf-form-dropdown>
</div> </div>
<div class="gf-form offset-width-7"> <div class="gf-form offset-width-7">
@@ -66,11 +86,23 @@
<div ng-if="agg.type === 'terms'"> <div ng-if="agg.type === 'terms'">
<div class="gf-form offset-width-7"> <div class="gf-form offset-width-7">
<label class="gf-form-label width-10">Order</label> <label class="gf-form-label width-10">Order</label>
<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model> <gf-form-dropdown model="agg.settings.order"
lookup-text="true"
get-options="getOrderOptions()"
on-change="onChangeInternal()"
label-mode="true"
css-class="width-12">
</gf-form-dropdown>
</div> </div>
<div class="gf-form offset-width-7"> <div class="gf-form offset-width-7">
<label class="gf-form-label width-10">Size</label> <label class="gf-form-label width-10">Size</label>
<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model> <gf-form-dropdown model="agg.settings.size"
lookup-text="true"
get-options="getSizeOptions()"
on-change="onChangeInternal()"
label-mode="true"
css-class="width-12">
</gf-form-dropdown>
</div> </div>
<div class="gf-form offset-width-7"> <div class="gf-form offset-width-7">
<label class="gf-form-label width-10">Min Doc Count</label> <label class="gf-form-label width-10">Min Doc Count</label>
@@ -78,7 +110,13 @@
</div> </div>
<div class="gf-form offset-width-7"> <div class="gf-form offset-width-7">
<label class="gf-form-label width-10">Order By</label> <label class="gf-form-label width-10">Order By</label>
<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model> <gf-form-dropdown model="agg.settings.orderBy"
lookup-text="true"
get-options="getOrderByOptions()"
on-change="onChangeInternal()"
label-mode="true"
css-class="width-12">
</gf-form-dropdown>
</div> </div>
<div class="gf-form offset-width-7"> <div class="gf-form offset-width-7">
<label class="gf-form-label width-10"> <label class="gf-form-label width-10">

View File

@@ -31,11 +31,11 @@ export class ElasticQueryCtrl extends QueryCtrl {
queryUpdated() { queryUpdated() {
var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true); var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true);
if (newJson !== this.rawQueryOld) { if (this.rawQueryOld && newJson !== this.rawQueryOld) {
this.rawQueryOld = newJson;
this.refresh(); this.refresh();
} }
this.rawQueryOld = newJson;
this.$rootScope.appEvent('elastic-query-updated'); this.$rootScope.appEvent('elastic-query-updated');
} }

View File

@@ -207,15 +207,20 @@ function pushToXBuckets(buckets, point, bucketNum, seriesName) {
} }
function pushToYBuckets(buckets, bucketNum, value, point, bounds) { function pushToYBuckets(buckets, bucketNum, value, point, bounds) {
var count = 1;
// Use the 3rd argument as scale/count
if (point.length > 2) {
count = parseInt(point[2]);
}
if (buckets[bucketNum]) { if (buckets[bucketNum]) {
buckets[bucketNum].values.push(value); buckets[bucketNum].values.push(value);
buckets[bucketNum].count += 1; buckets[bucketNum].count += count;
} else { } else {
buckets[bucketNum] = { buckets[bucketNum] = {
y: bucketNum, y: bucketNum,
bounds: bounds, bounds: bounds,
values: [value], values: [value],
count: 1, count: count,
}; };
} }
} }