plugin change: make interval, cache timeout & max data points options in plugin.json, remove query.options component feature, add help markdown feature and toggle for data sources

This commit is contained in:
Torkel Ödegaard 2017-08-31 14:05:52 +02:00
parent 9b60a63778
commit 84d4958a3c
15 changed files with 231 additions and 75 deletions

View File

@ -209,7 +209,7 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/plugins", wrap(GetPluginList)) r.Get("/plugins", wrap(GetPluginList))
r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme)) r.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
r.Group("/plugins", func() { r.Group("/plugins", func() {
r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards)) r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))

View File

@ -147,15 +147,16 @@ func GetPluginDashboards(c *middleware.Context) Response {
} }
} }
func GetPluginReadme(c *middleware.Context) Response { func GetPluginMarkdown(c *middleware.Context) Response {
pluginId := c.Params(":pluginId") pluginId := c.Params(":pluginId")
name := c.Params(":name")
if content, err := plugins.GetPluginReadme(pluginId); err != nil { if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil {
if notfound, ok := err.(plugins.PluginNotFoundError); ok { if notfound, ok := err.(plugins.PluginNotFoundError); ok {
return ApiError(404, notfound.Error(), nil) return ApiError(404, notfound.Error(), nil)
} }
return ApiError(500, "Could not get readme", err) return ApiError(500, "Could not get markdown file", err)
} else { } else {
return Respond(200, content) return Respond(200, content)
} }

View File

@ -1,15 +1,24 @@
package plugins package plugins
import "encoding/json" import (
"encoding/json"
"os"
"path/filepath"
)
type DataSourcePlugin struct { type DataSourcePlugin struct {
FrontendPluginBase FrontendPluginBase
Annotations bool `json:"annotations"` Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"` Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"` Alerting bool `json:"alerting"`
BuiltIn bool `json:"builtIn"` MinInterval bool `json:"minInterval,omitempty"`
Mixed bool `json:"mixed"` CacheTimeout bool `json:"cacheTimeout,omitempty"`
Routes []*AppPluginRoute `json:"routes"` MaxDataPoints bool `json:"maxDataPoints,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"`
HasHelp bool `json:"hasHelp,omitempty"`
Routes []*AppPluginRoute `json:"-"`
} }
func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error { func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
@ -21,6 +30,15 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
return err return err
} }
// look for help markdown
helpPath := filepath.Join(p.PluginDir, "HELP.md")
if _, err := os.Stat(helpPath); os.IsNotExist(err) {
helpPath = filepath.Join(p.PluginDir, "help.md")
}
if _, err := os.Stat(helpPath); err == nil {
p.HasHelp = true
}
DataSources[p.Id] = p DataSources[p.Id] = p
return nil return nil
} }

View File

@ -38,8 +38,8 @@ type PluginBase struct {
Includes []*PluginInclude `json:"includes"` Includes []*PluginInclude `json:"includes"`
Module string `json:"module"` Module string `json:"module"`
BaseUrl string `json:"baseUrl"` BaseUrl string `json:"baseUrl"`
HideFromList bool `json:"hideFromList"` HideFromList bool `json:"hideFromList,omitempty"`
State string `json:"state"` State string `json:"state,omitempty"`
IncludedInAppId string `json:"-"` IncludedInAppId string `json:"-"`
PluginDir string `json:"-"` PluginDir string `json:"-"`
@ -48,9 +48,6 @@ type PluginBase struct {
GrafanaNetVersion string `json:"-"` GrafanaNetVersion string `json:"-"`
GrafanaNetHasUpdate bool `json:"-"` GrafanaNetHasUpdate bool `json:"-"`
// cache for readme file contents
Readme []byte `json:"-"`
} }
func (pb *PluginBase) registerPlugin(pluginDir string) error { func (pb *PluginBase) registerPlugin(pluginDir string) error {

View File

@ -3,6 +3,7 @@ package plugins
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
@ -166,30 +167,24 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
return loader.Load(jsonParser, currentDir) return loader.Load(jsonParser, currentDir)
} }
func GetPluginReadme(pluginId string) ([]byte, error) { func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
plug, exists := Plugins[pluginId] plug, exists := Plugins[pluginId]
if !exists { if !exists {
return nil, PluginNotFoundError{pluginId} return nil, PluginNotFoundError{pluginId}
} }
if plug.Readme != nil { path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
return plug.Readme, nil if _, err := os.Stat(path); os.IsNotExist(err) {
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
} }
readmePath := filepath.Join(plug.PluginDir, "README.md") if _, err := os.Stat(path); os.IsNotExist(err) {
if _, err := os.Stat(readmePath); os.IsNotExist(err) { return make([]byte, 0), nil
readmePath = filepath.Join(plug.PluginDir, "readme.md")
} }
if _, err := os.Stat(readmePath); os.IsNotExist(err) { if data, err := ioutil.ReadFile(path); err != nil {
plug.Readme = make([]byte, 0)
return plug.Readme, nil
}
if readmeBytes, err := ioutil.ReadFile(readmePath); err != nil {
return nil, err return nil, err
} else { } else {
plug.Readme = readmeBytes return data, nil
return plug.Readme, nil
} }
} }

View File

@ -2,6 +2,7 @@
import _ from 'lodash'; import _ from 'lodash';
import {DashboardModel} from '../dashboard/model'; import {DashboardModel} from '../dashboard/model';
import Remarkable from 'remarkable';
export class MetricsTabCtrl { export class MetricsTabCtrl {
dsName: string; dsName: string;
@ -14,9 +15,16 @@ export class MetricsTabCtrl {
panelDsValue: any; panelDsValue: any;
addQueryDropdown: any; addQueryDropdown: any;
queryTroubleshooterOpen: boolean; queryTroubleshooterOpen: boolean;
helpOpen: boolean;
hasHelp: boolean;
helpHtml: string;
hasMinInterval: boolean;
hasCacheTimeout: boolean;
hasMaxDataPoints: boolean;
animateStart: boolean;
/** @ngInject */ /** @ngInject */
constructor($scope, private uiSegmentSrv, private datasourceSrv) { constructor($scope, private $sce, private datasourceSrv, private backendSrv, private $timeout) {
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl;
$scope.ctrl = this; $scope.ctrl = this;
@ -34,6 +42,14 @@ export class MetricsTabCtrl {
this.addQueryDropdown = {text: 'Add Query', value: null, fake: true}; this.addQueryDropdown = {text: 'Add Query', value: null, 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);
this.updateDatasourceOptions();
}
updateDatasourceOptions() {
this.hasHelp = this.current.meta.hasHelp;
this.hasMinInterval = this.current.meta.minInterval === true;
this.hasCacheTimeout = this.current.meta.cacheTimeout === true;
this.hasMaxDataPoints = this.current.meta.maxDataPoints === true;
} }
getOptions(includeBuiltin) { getOptions(includeBuiltin) {
@ -51,6 +67,7 @@ export class MetricsTabCtrl {
this.current = option.datasource; this.current = option.datasource;
this.panelCtrl.setDatasource(option.datasource); this.panelCtrl.setDatasource(option.datasource);
this.updateDatasourceOptions();
} }
addMixedQuery(option) { addMixedQuery(option) {
@ -67,6 +84,19 @@ export class MetricsTabCtrl {
this.panelCtrl.addQuery({isNew: true}); this.panelCtrl.addQuery({isNew: true});
} }
toggleHelp() {
this.animateStart = false;
this.helpOpen = !this.helpOpen;
this.backendSrv.get(`/api/plugins/${this.current.meta.id}/markdown/help`).then(res => {
var md = new Remarkable();
this.helpHtml = this.$sce.trustAsHtml(md.render(res));
this.$timeout(() => {
this.animateStart = true;
}, 1);
});
}
toggleQueryTroubleshooter() { toggleQueryTroubleshooter() {
this.queryTroubleshooterOpen = !this.queryTroubleshooterOpen; this.queryTroubleshooterOpen = !this.queryTroubleshooterOpen;
} }

View File

@ -11,41 +11,90 @@
on-change="ctrl.datasourceChanged($option)"> on-change="ctrl.datasourceChanged($option)">
</gf-form-dropdown> </gf-form-dropdown>
</div> </div>
<div class="gf-form"> <div class="gf-form" ng-if="ctrl.hasMinInterval">
<label class="gf-form-label">Min auto interval</label> <label class="gf-form-label">
<input type="text" class="gf-form-input width-7" placeholder="1s" /> Min auto interval
</label>
<input type="text"
class="gf-form-input width-6"
placeholder="1s"
ng-model="ctrl.panel.interval"
spellcheck="false"
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
/>
<info-popover mode="right-absolute"> <info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency, A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute. Access auto interval via variable <code>$__interval</code> for time range for example <code>1m</code> if your data is written every minute. Access auto interval via variable <code>$__interval</code> for time range
string and <code>$__interval_ms</code> for numeric variable that can be used in math expressions. string and <code>$__interval_ms</code> for numeric variable that can be used in math expressions.
</info-popover> </info-popover>
</div> </div>
<div class="gf-form" ng-if="ctrl.hasCacheTimeout">
<label class="gf-form-label">
Cache timeout
</label>
<input type="text"
class="gf-form-input width-6"
placeholder="60"
ng-model="ctrl.panel.cacheTimeout"
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
spellcheck="false"
/>
<info-popover mode="right-absolute">
If your time series store has a query cache this option can override the default
cache timeout. Specify a numeric value in seconds.
</info-popover>
</div>
<div class="gf-form" ng-if="ctrl.hasMaxDataPoints">
<label class="gf-form-label">
Max data points
</label>
<input type="text"
class="gf-form-input width-6"
placeholder="auto"
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
ng-model="ctrl.panel.maxDataPoints"
spellcheck="false" />
<info-popover mode="right-absolute">
The maximum data points the query should return. For graphs this
is automatically set to one data point per pixel.
</info-popover>
</div>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<label class="gf-form-label gf-form-label--grow"></label> <label class="gf-form-label gf-form-label--grow"></label>
</div> </div>
<div class="gf-form"> <div class="gf-form" ng-if="ctrl.hasHelp">
<label class="gf-form-label"> <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.toggleHelp()">
<i class="fa fa-question-circle"></i> <i class="fa fa-chevron-right" ng-hide="ctrl.helpOpen"></i>
<a href="http://google.com">Help &amp; Docs</a> <i class="fa fa-chevron-down" ng-show="ctrl.helpOpen"></i>
</label> Help
</button>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.toggleQueryTroubleshooter()"> <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.toggleQueryTroubleshooter()" bs-tooltip="'Display data query request & response'">
<i class="fa fa-chevron-right" ng-hide="ctrl.queryTroubleshooterOpen"></i> <i class="fa fa-chevron-right" ng-hide="ctrl.queryTroubleshooterOpen"></i>
<i class="fa fa-chevron-down" ng-show="ctrl.queryTroubleshooterOpen"></i> <i class="fa fa-chevron-down" ng-show="ctrl.queryTroubleshooterOpen"></i>
Query Inspector Query Inspector
</button> </button>
</div> </div>
</div> </div>
<div class="grafana-info-box grafana-info-box--animate" ng-if="ctrl.helpOpen" ng-class="{'grafana-info-box--animate-open': ctrl.animateStart}">
<div class="markdown-html" ng-bind-html="ctrl.helpHtml"></div>
<a class="grafana-info-box__close" ng-click="ctrl.toggleHelp()">
<i class="fa fa-chevron-up"></i>
</a>
</div>
<query-troubleshooter panel-ctrl="ctrl.panelCtrl" is-open="ctrl.queryTroubleshooterOpen"></query-troubleshooter> <query-troubleshooter panel-ctrl="ctrl.panelCtrl" is-open="ctrl.queryTroubleshooterOpen"></query-troubleshooter>
</div> </div>
<div class="query-editor-rows gf-form-group"> <div class="query-editor-rows gf-form-group">
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}"> <div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true"> <rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
<plugin-component type="query-ctrl"> <plugin-component type="query-ctrl">
</plugin-component> </plugin-component>
</rebuild-on-change> </rebuild-on-change>
</div> </div>
<div class="gf-form-query"> <div class="gf-form-query">
@ -56,16 +105,16 @@
</span> </span>
<span class="gf-form-query-letter-cell-letter">{{ctrl.panelCtrl.nextRefId}}</span> <span class="gf-form-query-letter-cell-letter">{{ctrl.panelCtrl.nextRefId}}</span>
</label> </label>
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed"> <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed">
Add Query Add Query
</button> </button>
<div class="dropdown" ng-if="ctrl.current.meta.mixed"> <div class="dropdown" ng-if="ctrl.current.meta.mixed">
<gf-form-dropdown model="ctrl.addQueryDropdown" <gf-form-dropdown model="ctrl.addQueryDropdown"
get-options="ctrl.getOptions(false)" get-options="ctrl.getOptions(false)"
on-change="ctrl.addMixedQuery($option)"> on-change="ctrl.addMixedQuery($option)">
</gf-form-dropdown> </gf-form-dropdown>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash'; import _ from 'lodash';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import Remarkable from 'remarkable';
export class PluginEditCtrl { export class PluginEditCtrl {
model: any; model: any;
@ -67,11 +68,9 @@ export class PluginEditCtrl {
} }
initReadme() { initReadme() {
return this.backendSrv.get(`/api/plugins/${this.pluginId}/readme`).then(res => { return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
return System.import('remarkable').then(Remarkable => { var md = new Remarkable();
var md = new Remarkable(); this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
});
}); });
} }

View File

@ -16,6 +16,19 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
this.withCredentials = instanceSettings.withCredentials; this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST'; this.render_method = instanceSettings.render_method || 'POST';
this.getQueryOptionsInfo = function() {
return {
"maxDataPoints": true,
"cacheTimeout": true,
"links": [
{
text: "Help",
url: "http://docs.grafana.org/features/datasources/graphite/#using-graphite-in-grafana"
}
]
};
};
this.query = function(options) { this.query = function(options) {
var graphOptions = { var graphOptions = {
from: this.translateTime(options.rangeRaw.from, false), from: this.translateTime(options.rangeRaw.from, false),

View File

@ -0,0 +1,32 @@
#### Get Shorter legend names
- alias() function to specify a custom series name<
- aliasByNode(2) to alias by a specific part of your metric path
- aliasByNode(2, -1) you can add multiple segment paths, and use negative index
- groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by
#### Series as parameter
- Some graphite functions allow you to have many series arguments
- Use #[A-Z] to use a graphite query as parameter to a function
- Examples:
- asPercent(#A, #B)
- prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query
- prod.srv-01.counters.count - sumSeries(#A) : sum count and series A
- divideSeries(#A, #B)
If a query is added only to be used as a parameter, hide it from the graph with the eye icon
#### Max data points
- Every graphite request is issued with a maxDataPoints parameter
- Graphite uses this parameter to consolidate the real number of values down to this number
- If there are more real values, then by default they will be consolidated using averages
- This could hide real peaks and max values in your series
- You can change how point consolidation is made using the consolidateBy graphite function
- Point consolidation will effect series legend values (min,max,total,current)
- if you override maxDataPoint and set a high value performance can be severely effected
#### Documentation links:
- [Grafana's Graphite Documentation](http://docs.grafana.org/features/datasources/graphite)
- [Official Graphite Documentation](https://graphite.readthedocs.io)

View File

@ -10,6 +10,8 @@
"metrics": true, "metrics": true,
"alerting": true, "alerting": true,
"annotations": true, "annotations": true,
"maxDataPoints": true,
"cacheTimeout": true,
"info": { "info": {
"author": { "author": {

View File

@ -7,6 +7,7 @@
"metrics": true, "metrics": true,
"annotations": true, "annotations": true,
"alerting": true, "alerting": true,
"minInterval": true,
"info": { "info": {
"author": { "author": {

View File

@ -276,7 +276,7 @@ $card-background-hover: linear-gradient(135deg, #343434, #262626);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3); $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
// info box // info box
$info-box-background: linear-gradient(120deg, #142749, #0e203e); $info-box-background: linear-gradient(177deg, #006e95, #412078);
// footer // footer
$footer-link-color: $gray-1; $footer-link-color: $gray-1;

View File

@ -277,7 +277,7 @@ $gf-form-margin: 0.25rem;
&--right-absolute { &--right-absolute {
position: absolute; position: absolute;
right: $spacer; right: $spacer;
top: 8px; top: 10px;
} }
&--right-normal { &--right-normal {

View File

@ -1,12 +1,12 @@
.grafana-info-box::before { // .grafana-info-box::before {
content: "\f05a"; // content: "\f05a";
font-family:'FontAwesome'; // font-family:'FontAwesome';
position: absolute; // position: absolute;
top: -13px; // top: -13px;
left: -8px; // left: -8px;
font-size: 20px; // font-size: 20px;
color: $text-color; // color: $text-color;
} // }
.grafana-info-box { .grafana-info-box {
position: relative; position: relative;
@ -15,6 +15,7 @@
padding: 1rem; padding: 1rem;
border-radius: 4px; border-radius: 4px;
margin-bottom: $spacer; margin-bottom: $spacer;
margin-right: $gf-form-margin;
h5 { h5 {
margin-bottom: $spacer; margin-bottom: $spacer;
@ -26,5 +27,23 @@
a { a {
@extend .external-link; @extend .external-link;
} }
&--animate {
max-height: 0;
overflow: hidden;
}
&--animate-open {
max-height: 1000px;
transition: max-height 250ms ease-in-out;
}
} }
.grafana-info-box__close {
text-align: center;
display: block;
color: $link-color !important;
height: 0;
position: relative;
top: -9px;
}