Merge branch 'master' into alert_prometheus

This commit is contained in:
bergquist 2016-09-22 19:27:24 +02:00
commit d711c0ed35
329 changed files with 5738 additions and 13256 deletions

View File

@ -12,7 +12,7 @@ grunt karma:dev
### Run tests for backend assets before commit
```
test -z "$(gofmt -s -l . | grep -v vendor/src/ | tee /dev/stderr)"
test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
```
### Run tests for frontend assets before commit

View File

@ -12,6 +12,8 @@
* **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
* **OAuth**: Add support for generic oauth, closes [#4718](https://github.com/grafana/grafana/pull/4718)
* **Cloudwatch**: Add support to expand multi select template variable, closes [#5003](https://github.com/grafana/grafana/pull/5003)
* **Graph Panel**: Now supports flexible lower/upper bounds on Y-Max and Y-Min, PR [#5720](https://github.com/grafana/grafana/pull/5720)
### Breaking changes
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
@ -19,6 +21,10 @@
### Bugfixes
* **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999)
* **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467)
* **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070)
* **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769)
* **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887)
# 3.1.2 (unreleased)
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)

View File

@ -9,7 +9,6 @@ module.exports = function (grunt) {
genDir: 'public_gen',
destDir: 'dist',
tempDir: 'tmp',
arch: os.arch(),
platform: process.platform.replace('win32', 'windows'),
};
@ -17,6 +16,10 @@ module.exports = function (grunt) {
config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86';
}
config.arch = grunt.option('arch') || os.arch();
config.phjs = grunt.option('phjsToRelease');
config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
console.log('Version', config.pkg.version);

View File

@ -96,7 +96,7 @@ easily the grafana repository you want to build.
```bash
go get github.com/*your_account*/grafana
mkdir $GOPATH/src/github.com/grafana
ln -s github.com/*your_account*/grafana $GOPATH/src/github.com/grafana/grafana
ln -s $GOPATH/src/github.com/*your_account*/grafana $GOPATH/src/github.com/grafana/grafana
```
### Building the backend

View File

@ -25,11 +25,16 @@ var (
versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
goarch string
goos string
gocc string
gocxx string
cgo string
pkgArch string
version string = "v1"
// deb & rpm does not support semver so have to handle their version a little differently
linuxPackageVersion string = "v1"
linuxPackageIteration string = ""
race bool
phjsToRelease string
workingDir string
binaries []string = []string{"grafana-server", "grafana-cli"}
)
@ -47,6 +52,11 @@ func main() {
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.StringVar(&gocc, "cc", "", "CC")
flag.StringVar(&gocxx, "cxx", "", "CXX")
flag.StringVar(&cgo, "cgo-enabled", "", "CGO_ENABLED")
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
flag.BoolVar(&race, "race", race, "Use race detector")
flag.Parse()
@ -73,15 +83,15 @@ func main() {
grunt("test")
case "package":
grunt("release", fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
grunt(gruntBuildArg("release")...)
createLinuxPackages()
case "pkg-rpm":
grunt("release")
grunt(gruntBuildArg("release")...)
createRpmPackages()
case "pkg-deb":
grunt("release")
grunt(gruntBuildArg("release")...)
createDebPackages()
case "latest":
@ -258,6 +268,10 @@ func createPackage(options linuxPackageOptions) {
"-p", "./dist",
}
if pkgArch != "" {
args = append(args, "-a", pkgArch)
}
if linuxPackageIteration != "" {
args = append(args, "--iteration", linuxPackageIteration)
}
@ -307,11 +321,20 @@ func grunt(params ...string) {
runPrint("./node_modules/.bin/grunt", params...)
}
func gruntBuildArg(task string) []string {
args := []string{task, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)}
if pkgArch != "" {
args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
}
if phjsToRelease != "" {
args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
}
return args
}
func setup() {
runPrint("go", "get", "-v", "github.com/kardianos/govendor")
runPrint("go", "get", "-v", "github.com/blang/semver")
runPrint("go", "get", "-v", "github.com/mattn/go-sqlite3")
runPrint("go", "install", "-v", "github.com/mattn/go-sqlite3")
runPrint("go", "install", "-v", "./pkg/cmd/grafana-server")
}
func test(pkg string) {
@ -382,6 +405,15 @@ func setBuildEnv() {
if goarch == "386" {
os.Setenv("GO386", "387")
}
if cgo != "" {
os.Setenv("CGO_ENABLED", cgo)
}
if gocc != "" {
os.Setenv("CC", gocc)
}
if gocxx != "" {
os.Setenv("CXX", gocxx)
}
}
func getGitSha() string {

View File

@ -404,7 +404,7 @@ url = https://grafana.net
#################################### External image storage ##########################
[external_image_storage]
# You can choose between (s3, webdav or internal)
# You can choose between (s3, webdav)
provider = s3
[external_image_storage.s3]

View File

@ -6,6 +6,10 @@ page_keywords: grafana, admin, http, api, documentation
# Admin API
The admin http API does not currently work with an api token. Api Token's are currently only linked to an organization and organization role. They cannot given
the permission of server admin, only user's can be given that permission. So in order to use these API calls you will have to use basic auth and Grafana user
with Grafana admin permission.
## Settings
`GET /api/admin/settings`
@ -15,7 +19,6 @@ page_keywords: grafana, admin, http, api, documentation
GET /api/admin/settings
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Response**:
@ -171,7 +174,6 @@ page_keywords: grafana, admin, http, api, documentation
GET /api/admin/stats
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Response**:
@ -201,7 +203,6 @@ Create new user
POST /api/admin/users HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name":"User",
@ -228,7 +229,6 @@ Change password for specific user
PUT /api/admin/users/2/password HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Response**:
@ -246,7 +246,6 @@ Change password for specific user
PUT /api/admin/users/2/permissions HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Response**:
@ -264,7 +263,6 @@ Change password for specific user
DELETE /api/admin/users/2 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Response**:

View File

@ -428,7 +428,7 @@ session provider you have configured.
- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
- **memcache:** ex: 127.0.0.1:11211
- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,db=grafana`
- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`
If you use MySQL or Postgres as the session store you need to create the
session table manually.

View File

@ -58,6 +58,7 @@ func Register(r *macaron.Macaron) {
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
r.Get("/dashboard/*", reqSignedIn, Index)
r.Get("/dashboard-solo/snapshot/*", Index)
r.Get("/dashboard-solo/*", reqSignedIn, Index)
r.Get("/import/dashboard", reqSignedIn, Index)
r.Get("/dashboards/*", reqSignedIn, Index)

23
pkg/api/dtos/playlist.go Normal file
View File

@ -0,0 +1,23 @@
package dtos
type PlaylistDashboard struct {
Id int64 `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
Uri string `json:"uri"`
Order int `json:"order"`
}
type PlaylistDashboardsSlice []PlaylistDashboard
func (slice PlaylistDashboardsSlice) Len() int {
return len(slice)
}
func (slice PlaylistDashboardsSlice) Less(i, j int) bool {
return slice[i].Order < slice[j].Order
}
func (slice PlaylistDashboardsSlice) Swap(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
}

View File

@ -3,7 +3,6 @@ package api
import (
"errors"
"fmt"
"net/url"
"golang.org/x/oauth2"
@ -46,9 +45,9 @@ func OAuthLogin(ctx *middleware.Context) {
userInfo, err := connect.UserInfo(token)
if err != nil {
if err == social.ErrMissingTeamMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
} else if err == social.ErrMissingOrganizationMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled"))
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001")
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
@ -60,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) {
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
return
}

View File

@ -1,16 +1,18 @@
package api
import (
"sort"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
_ "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
)
func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
result := make([]m.PlaylistDashboardDto, 0)
func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByIds) > 0 {
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
@ -19,11 +21,12 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
}
for _, item := range dashboardQuery.Result {
result = append(result, m.PlaylistDashboardDto{
result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
Uri: "db/" + item.Slug,
Order: dashboardIdOrder[item.Id],
})
}
}
@ -31,8 +34,8 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
return result, nil
}
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
result := make([]m.PlaylistDashboardDto, 0)
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByTag) > 0 {
for _, tag := range dashboardByTag {
@ -47,10 +50,11 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
if err := bus.Dispatch(&searchQuery); err == nil {
for _, item := range searchQuery.Result {
result = append(result, m.PlaylistDashboardDto{
result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
Title: item.Title,
Uri: item.Uri,
Order: dashboardTagOrder[tag],
})
}
}
@ -60,28 +64,33 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
return result
}
func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := LoadPlaylistItems(playlistId)
dashboardByIds := make([]int64, 0)
dashboardByTag := make([]string, 0)
dashboardIdOrder := make(map[int64]int)
dashboardTagOrder := make(map[string]int)
for _, i := range playlistItems {
if i.Type == "dashboard_by_id" {
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
dashboardByIds = append(dashboardByIds, dashboardId)
dashboardIdOrder[dashboardId] = i.Order
}
if i.Type == "dashboard_by_tag" {
dashboardByTag = append(dashboardByTag, i.Value)
dashboardTagOrder[i.Value] = i.Order
}
}
result := make([]m.PlaylistDashboardDto, 0)
result := make(dtos.PlaylistDashboardsSlice, 0)
var k, _ = populateDashboardsById(dashboardByIds)
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
result = append(result, k...)
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
sort.Sort(sort.Reverse(result))
return result, nil
}

View File

@ -14,7 +14,6 @@ import (
"strings"
"github.com/fatih/color"
"github.com/grafana/grafana-cli/pkg/log"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
@ -94,7 +93,7 @@ func InstallPlugin(pluginName, version string, c CommandLine) error {
res, _ := s.ReadPlugin(pluginFolder, pluginName)
for _, v := range res.Dependencies.Plugins {
InstallPlugin(v.Id, version, c)
log.Infof("Installed dependency: %v ✔\n", v.Id)
logger.Infof("Installed dependency: %v ✔\n", v.Id)
}
return err

View File

@ -141,8 +141,6 @@ func createRequest(repoUrl string, subPaths ...string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
logger.Info("grafanaVersion ", grafanaVersion)
req.Header.Set("grafana-version", grafanaVersion)
req.Header.Set("User-Agent", "grafana "+grafanaVersion)

View File

@ -24,10 +24,10 @@ func NewGauge(meta *MetricMeta) Gauge {
}
}
func RegGauge(meta *MetricMeta) Gauge {
g := NewGauge(meta)
MetricStats.Register(g)
return g
func RegGauge(name string, tagStrings ...string) Gauge {
tr := NewGauge(NewMetricMeta(name, tagStrings))
MetricStats.Register(tr)
return tr
}
// GaugeSnapshot is a read-only copy of another Gauge.

View File

@ -63,6 +63,8 @@ func (this *GraphitePublisher) Publish(metrics []Metric) {
switch metric := m.(type) {
case Counter:
this.addCount(buf, metricName+".count", metric.Count(), now)
case Gauge:
this.addCount(buf, metricName, metric.Value(), now)
case Timer:
percentiles := metric.Percentiles([]float64{0.25, 0.75, 0.90, 0.99})
this.addCount(buf, metricName+".count", metric.Count(), now)

View File

@ -49,6 +49,12 @@ var (
// Timers
M_DataSource_ProxyReq_Timer Timer
M_Alerting_Exeuction_Time Timer
// StatTotals
M_StatTotal_Dashboards Gauge
M_StatTotal_Users Gauge
M_StatTotal_Orgs Gauge
M_StatTotal_Playlists Gauge
)
func initMetricVars(settings *MetricSettings) {
@ -105,4 +111,10 @@ func initMetricVars(settings *MetricSettings) {
// Timers
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
M_Alerting_Exeuction_Time = RegTimer("alerting.execution_time")
// StatTotals
M_StatTotal_Dashboards = RegGauge("stat_totals", "stat", "dashboards")
M_StatTotal_Users = RegGauge("stat_totals", "stat", "users")
M_StatTotal_Orgs = RegGauge("stat_totals", "stat", "orgs")
M_StatTotal_Playlists = RegGauge("stat_totals", "stat", "playlists")
}

View File

@ -15,6 +15,7 @@ import (
)
var metricsLogger log.Logger = log.New("metrics")
var metricPublishCounter int64 = 0
func Init() {
settings := readSettings()
@ -45,12 +46,33 @@ func sendMetrics(settings *MetricSettings) {
return
}
updateTotalStats()
metrics := MetricStats.GetSnapshots()
for _, publisher := range settings.Publishers {
publisher.Publish(metrics)
}
}
func updateTotalStats() {
// every interval also publish totals
metricPublishCounter++
if metricPublishCounter%10 == 0 {
// get stats
statsQuery := m.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
M_StatTotal_Dashboards.Update(statsQuery.Result.DashboardCount)
M_StatTotal_Users.Update(statsQuery.Result.UserCount)
M_StatTotal_Playlists.Update(statsQuery.Result.PlaylistCount)
M_StatTotal_Orgs.Update(statsQuery.Result.OrgCount)
}
}
func sendUsageStats() {
if !setting.ReportingEnabled {
return

View File

@ -57,17 +57,6 @@ func (this PlaylistDashboard) TableName() string {
type Playlists []*Playlist
type PlaylistDashboards []*PlaylistDashboard
//
// DTOS
//
type PlaylistDashboardDto struct {
Id int64 `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
Uri string `json:"uri"`
}
//
// COMMANDS
//

View File

@ -1,10 +1,10 @@
package models
type SystemStats struct {
DashboardCount int
UserCount int
OrgCount int
PlaylistCount int
DashboardCount int64
UserCount int64
OrgCount int64
PlaylistCount int64
}
type DataSourceStats struct {

View File

@ -39,17 +39,16 @@ func StartPluginUpdateChecker() {
}
func getAllExternalPluginSlugs() string {
str := ""
var result []string
for _, plug := range Plugins {
if plug.IsCorePlugin {
continue
}
str += plug.Id + ","
result = append(result, plug.Id)
}
return str
return strings.Join(result, ",")
}
func checkForUpdates() {

View File

@ -72,8 +72,8 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error {
Url: imageUrl,
Width: "800",
Height: "400",
SessionId: "123",
Timeout: "10",
SessionId: "cef0256d482b4293",
Timeout: "30",
}
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {

View File

@ -12,6 +12,7 @@ import (
"net/smtp"
"os"
"strings"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
@ -66,7 +67,7 @@ func sendToSmtpServer(recipients []string, msgContent []byte) error {
tlsconfig.Certificates = []tls.Certificate{cert}
}
conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), time.Second*10)
if err != nil {
return err
}

View File

@ -44,7 +44,7 @@ func sendWebRequest(webhook *Webhook) error {
webhookLog.Debug("Sending webhook", "url", webhook.Url)
client := http.Client{
Timeout: time.Duration(3 * time.Second),
Timeout: time.Duration(10 * time.Second),
}
request, err := http.NewRequest("POST", webhook.Url, bytes.NewReader([]byte(webhook.Body)))

View File

@ -120,4 +120,9 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{
Cols: []string{"org_id", "plugin_id"}, Type: IndexType,
}))
// dashboard_id index for dashboard_tag table
mg.AddMigration("Add index for dashboard_id in dashboard_tag", NewAddIndexMigration(dashboardTagV1, &Index{
Cols: []string{"dashboard_id"}, Type: IndexType,
}))
}

View File

@ -6,6 +6,12 @@ define([
function (angular, coreModule, config) {
'use strict';
var failCodes = {
"1000": "Required Github team membership not fulfilled",
"1001": "Required Github organization membership not fulfilled",
"1002": "Required email domain not fulfilled",
};
coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
$scope.formModel = {
user: '',
@ -31,8 +37,8 @@ function (angular, coreModule, config) {
$scope.$watch("loginMode", $scope.loginModeChanged);
var params = $location.search();
if (params.failedMsg) {
$scope.appEvent('alert-warning', ['Login Failed', params.failedMsg]);
if (params.failCode) {
$scope.appEvent('alert-warning', ['Login Failed', failCodes[params.failCode]]);
delete params.failedMsg;
$location.search(params);
}

View File

@ -9,6 +9,7 @@ export class User {
isGrafanaAdmin: any;
isSignedIn: any;
orgRole: any;
timezone: string;
constructor() {
if (config.bootData.user) {

View File

@ -9,6 +9,10 @@ function($, _, moment) {
var kbn = {};
kbn.valueFormats = {};
kbn.regexEscape = function(value) {
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
};
///// HELPER FUNCTIONS /////
kbn.round_interval = function(interval) {

View File

@ -2,7 +2,7 @@ define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/annotations_srv',
'./templating/templateSrv',
'./templating/all',
'./dashboard/all',
'./playlist/all',
'./snapshot/all',

View File

@ -29,7 +29,7 @@ export class AnnotationsSrv {
this.getGlobalAnnotations(options),
this.getPanelAnnotations(options)
]).then(allResults => {
return _.flatten(allResults);
return _.flattenDeep(allResults);
}).catch(err => {
this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
});

View File

@ -0,0 +1,171 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
import coreModule from 'app/core/core_module';
export class AdHocFiltersCtrl {
segments: any;
variable: any;
removeTagFilterSegment: any;
/** @ngInject */
constructor(private uiSegmentSrv, private datasourceSrv, private $q, private templateSrv, private $rootScope) {
this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove filter --'});
this.buildSegmentModel();
}
buildSegmentModel() {
this.segments = [];
if (this.variable.value && !_.isArray(this.variable.value)) {
}
for (let tag of this.variable.filters) {
if (this.segments.length > 0) {
this.segments.push(this.uiSegmentSrv.newCondition('AND'));
}
if (tag.key !== undefined && tag.value !== undefined) {
this.segments.push(this.uiSegmentSrv.newKey(tag.key));
this.segments.push(this.uiSegmentSrv.newOperator(tag.operator));
this.segments.push(this.uiSegmentSrv.newKeyValue(tag.value));
}
}
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
getOptions(segment, index) {
if (segment.type === 'operator') {
return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '>', '=~', '!~']));
}
if (segment.type === 'condition') {
return this.$q.when([this.uiSegmentSrv.newSegment('AND')]);
}
return this.datasourceSrv.get(this.variable.datasource).then(ds => {
var options: any = {};
var promise = null;
if (segment.type !== 'value') {
promise = ds.getTagKeys();
} else {
options.key = this.segments[index-2].value;
promise = ds.getTagValues(options);
}
return promise.then(results => {
results = _.map(results, segment => {
return this.uiSegmentSrv.newSegment({value: segment.text});
});
// add remove option for keys
if (segment.type === 'key') {
results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
}
return results;
});
});
}
segmentChanged(segment, index) {
this.segments[index] = segment;
// handle remove tag condition
if (segment.value === this.removeTagFilterSegment.value) {
this.segments.splice(index, 3);
if (this.segments.length === 0) {
this.segments.push(this.uiSegmentSrv.newPlusButton());
} else if (this.segments.length > 2) {
this.segments.splice(Math.max(index-1, 0), 1);
if (this.segments[this.segments.length-1].type !== 'plus-button') {
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
}
} else {
if (segment.type === 'plus-button') {
if (index > 2) {
this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
}
this.segments.push(this.uiSegmentSrv.newOperator('='));
this.segments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
segment.type = 'key';
segment.cssClass = 'query-segment-key';
}
if ((index+1) === this.segments.length) {
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
}
this.updateVariableModel();
}
updateVariableModel() {
var filters = [];
var filterIndex = -1;
var operator = "";
var hasFakes = false;
this.segments.forEach(segment => {
if (segment.type === 'value' && segment.fake) {
hasFakes = true;
return;
}
switch (segment.type) {
case 'key': {
filters.push({key: segment.value});
filterIndex += 1;
break;
}
case 'value': {
filters[filterIndex].value = segment.value;
break;
}
case 'operator': {
filters[filterIndex].operator = segment.value;
break;
}
case 'condition': {
filters[filterIndex].condition = segment.value;
break;
}
}
});
if (hasFakes) {
return;
}
this.variable.setFilters(filters);
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
}
}
var template = `
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="segment in ctrl.segments">
<metric-segment segment="segment" get-options="ctrl.getOptions(segment, $index)"
on-change="ctrl.segmentChanged(segment, $index)"></metric-segment>
</div>
</div>
`;
export function adHocFiltersComponent() {
return {
restrict: 'E',
template: template,
controller: AdHocFiltersCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
variable: "="
}
};
}
coreModule.directive('adHocFilters', adHocFiltersComponent);

View File

@ -7,7 +7,7 @@ define([
'./rowCtrl',
'./shareModalCtrl',
'./shareSnapshotCtrl',
'./dashboardSrv',
'./dashboard_srv',
'./keybindings',
'./viewStateSrv',
'./timeSrv',
@ -20,4 +20,5 @@ define([
'./import/dash_import',
'./export/export_modal',
'./dash_list_ctrl',
'./ad_hoc_filters',
], function () {});

View File

@ -1,552 +0,0 @@
define([
'angular',
'jquery',
'lodash',
'moment',
],
function (angular, $, _, moment) {
'use strict';
var module = angular.module('grafana.services');
module.factory('dashboardSrv', function(contextSrv) {
function DashboardModel (data, meta) {
if (!data) {
data = {};
}
this.id = data.id || null;
this.title = data.title || 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || "dark";
this.timezone = data.timezone || '';
this.editable = data.editable !== false;
this.hideControls = data.hideControls || false;
this.sharedCrosshair = data.sharedCrosshair || false;
this.rows = data.rows || [];
this.time = data.time || { from: 'now-6h', to: 'now' };
this.timepicker = data.timepicker || {};
this.templating = this._ensureListExist(data.templating);
this.annotations = this._ensureListExist(data.annotations);
this.refresh = data.refresh;
this.snapshot = data.snapshot;
this.schemaVersion = data.schemaVersion || 0;
this.version = data.version || 0;
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this._updateSchema(data);
this._initMeta(meta);
}
var p = DashboardModel.prototype;
p._initMeta = function(meta) {
meta = meta || {};
meta.canShare = meta.canShare !== false;
meta.canSave = meta.canSave !== false;
meta.canStar = meta.canStar !== false;
meta.canEdit = meta.canEdit !== false;
if (!this.editable) {
meta.canEdit = false;
meta.canDelete = false;
meta.canSave = false;
this.hideControls = true;
}
this.meta = meta;
};
// cleans meta data and other non peristent state
p.getSaveModelClone = function() {
var copy = $.extend(true, {}, this);
delete copy.meta;
return copy;
};
p._ensureListExist = function (data) {
if (!data) { data = {}; }
if (!data.list) { data.list = []; }
return data;
};
p.getNextPanelId = function() {
var i, j, row, panel, max = 0;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (panel.id > max) { max = panel.id; }
}
}
return max + 1;
};
p.forEachPanel = function(callback) {
var i, j, row;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
callback(row.panels[j], j, row, i);
}
}
};
p.getPanelById = function(id) {
for (var i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (var j = 0; j < row.panels.length; j++) {
var panel = row.panels[j];
if (panel.id === id) {
return panel;
}
}
}
return null;
};
p.rowSpan = function(row) {
return _.reduce(row.panels, function(p,v) {
return p + v.span;
},0);
};
p.addPanel = function(panel, row) {
var rowSpan = this.rowSpan(row);
var panelCount = row.panels.length;
var space = (12 - rowSpan) - panel.span;
panel.id = this.getNextPanelId();
// try to make room of there is no space left
if (space <= 0) {
if (panelCount === 1) {
row.panels[0].span = 6;
panel.span = 6;
}
else if (panelCount === 2) {
row.panels[0].span = 4;
row.panels[1].span = 4;
panel.span = 4;
}
}
row.panels.push(panel);
};
p.isSubmenuFeaturesEnabled = function() {
var visableTemplates = _.filter(this.templating.list, function(template) {
return template.hideVariable === undefined || template.hideVariable === false;
});
return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
};
p.getPanelInfoById = function(panelId) {
var result = {};
_.each(this.rows, function(row) {
_.each(row.panels, function(panel, index) {
if (panel.id === panelId) {
result.panel = panel;
result.row = row;
result.index = index;
}
});
});
if (!result.panel) {
return null;
}
return result;
};
p.duplicatePanel = function(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId();
delete newPanel.repeat;
delete newPanel.repeatIteration;
delete newPanel.repeatPanelId;
delete newPanel.scopedVars;
var currentRow = this.rows[rowIndex];
currentRow.panels.push(newPanel);
return newPanel;
};
p.formatDate = function(date, format) {
date = moment.isMoment(date) ? date : moment(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
this.timezone = this.getTimezone();
return this.timezone === 'browser' ?
moment(date).format(format) :
moment.utc(date).format(format);
};
p.getRelativeTime = function(date) {
date = moment.isMoment(date) ? date : moment(date);
return this.timezone === 'browser' ?
moment(date).fromNow() :
moment.utc(date).fromNow();
};
p.getNextQueryLetter = function(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
};
p.isTimezoneUtc = function() {
return this.getTimezone() === 'utc';
};
p.getTimezone = function() {
return this.timezone ? this.timezone : contextSrv.user.timezone;
};
p._updateSchema = function(old) {
var i, j, k;
var oldVersion = this.schemaVersion;
var panelUpgrades = [];
this.schemaVersion = 13;
if (oldVersion === this.schemaVersion) {
return;
}
// version 2 schema changes
if (oldVersion < 2) {
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
this.templating.list = old.services.filter.list || [];
}
delete this.services;
}
panelUpgrades.push(function(panel) {
// rename panel type
if (panel.type === 'graphite') {
panel.type = 'graph';
}
if (panel.type !== 'graph') {
return;
}
if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
if (panel.grid) {
if (panel.grid.min) {
panel.grid.leftMin = panel.grid.min;
delete panel.grid.min;
}
if (panel.grid.max) {
panel.grid.leftMax = panel.grid.max;
delete panel.grid.max;
}
}
if (panel.y_format) {
panel.y_formats[0] = panel.y_format;
delete panel.y_format;
}
if (panel.y2_format) {
panel.y_formats[1] = panel.y2_format;
delete panel.y2_format;
}
});
}
// schema version 3 changes
if (oldVersion < 3) {
// ensure panel ids
var maxId = this.getNextPanelId();
panelUpgrades.push(function(panel) {
if (!panel.id) {
panel.id = maxId;
maxId += 1;
}
});
}
// schema version 4 changes
if (oldVersion < 4) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
_.each(panel.aliasYAxis, function(value, key) {
panel.seriesOverrides = [{ alias: key, yaxis: value }];
});
delete panel.aliasYAxis;
});
}
if (oldVersion < 6) {
// move pulldowns to new schema
var annotations = _.find(old.pulldowns, { type: 'annotations' });
if (annotations) {
this.annotations = {
list: annotations.annotations || [],
};
}
// update template variables
for (i = 0 ; i < this.templating.list.length; i++) {
var variable = this.templating.list[i];
if (variable.datasource === void 0) { variable.datasource = null; }
if (variable.type === 'filter') { variable.type = 'query'; }
if (variable.type === void 0) { variable.type = 'query'; }
if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
}
}
if (oldVersion < 7) {
if (old.nav && old.nav.length) {
this.timepicker = old.nav[0];
delete this.nav;
}
// ensure query refIds
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
if (!target.refId) {
target.refId = this.getNextQueryLetter(panel);
}
}.bind(this));
});
}
if (oldVersion < 8) {
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
// update old influxdb query schema
if (target.fields && target.tags && target.groupBy) {
if (target.rawQuery) {
delete target.fields;
delete target.fill;
} else {
target.select = _.map(target.fields, function(field) {
var parts = [];
parts.push({type: 'field', params: [field.name]});
parts.push({type: field.func, params: []});
if (field.mathExpr) {
parts.push({type: 'math', params: [field.mathExpr]});
}
if (field.asExpr) {
parts.push({type: 'alias', params: [field.asExpr]});
}
return parts;
});
delete target.fields;
_.each(target.groupBy, function(part) {
if (part.type === 'time' && part.interval) {
part.params = [part.interval];
delete part.interval;
}
if (part.type === 'tag' && part.key) {
part.params = [part.key];
delete part.key;
}
});
if (target.fill) {
target.groupBy.push({type: 'fill', params: [target.fill]});
delete target.fill;
}
}
}
});
});
}
// schema version 9 changes
if (oldVersion < 9) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
if (panel.thresholds) {
var k = panel.thresholds.split(",");
if (k.length >= 3) {
k.shift();
panel.thresholds = k.join(",");
}
}
});
}
// schema version 10 changes
if (oldVersion < 10) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'table') { return; }
_.each(panel.styles, function(style) {
if (style.thresholds && style.thresholds.length >= 3) {
var k = style.thresholds;
k.shift();
style.thresholds = k;
}
});
});
}
if (oldVersion < 12) {
// update template variables
_.each(this.templating.list, function(templateVariable) {
if (templateVariable.refresh) { templateVariable.refresh = 1; }
if (!templateVariable.refresh) { templateVariable.refresh = 0; }
if (templateVariable.hideVariable) {
templateVariable.hide = 2;
} else if (templateVariable.hideLabel) {
templateVariable.hide = 1;
} else {
templateVariable.hide = 0;
}
});
}
if (oldVersion < 12) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
if (!panel.grid) { return; }
if (!panel.yaxes) {
panel.yaxes = [
{
show: panel['y-axis'],
min: panel.grid.leftMin,
max: panel.grid.leftMax,
logBase: panel.grid.leftLogBase,
format: panel.y_formats[0],
label: panel.leftYAxisLabel,
},
{
show: panel['y-axis'],
min: panel.grid.rightMin,
max: panel.grid.rightMax,
logBase: panel.grid.rightLogBase,
format: panel.y_formats[1],
label: panel.rightYAxisLabel,
}
];
panel.xaxis = {
show: panel['x-axis'],
};
delete panel.grid.leftMin;
delete panel.grid.leftMax;
delete panel.grid.leftLogBase;
delete panel.grid.rightMin;
delete panel.grid.rightMax;
delete panel.grid.rightLogBase;
delete panel.y_formats;
delete panel.leftYAxisLabel;
delete panel.rightYAxisLabel;
delete panel['y-axis'];
delete panel['x-axis'];
}
});
}
if (oldVersion < 13) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
panel.thresholds = [];
var t1 = {}, t2 = {};
if (panel.grid.threshold1 !== null) {
t1.value = panel.grid.threshold1;
if (panel.grid.thresholdLine) {
t1.line = true;
t1.lineColor = panel.grid.threshold1Color;
} else {
t1.fill = true;
t1.fillColor = panel.grid.threshold1Color;
}
}
if (panel.grid.threshold2 !== null) {
t2.value = panel.grid.threshold2;
if (panel.grid.thresholdLine) {
t2.line = true;
t2.lineColor = panel.grid.threshold2Color;
} else {
t2.fill = true;
t2.fillColor = panel.grid.threshold2Color;
}
}
if (_.isNumber(t1.value)) {
if (_.isNumber(t2.value)) {
if (t1.value > t2.value) {
t1.op = t2.op = '<';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
} else {
t1.op = t2.op = '>';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
}
} else {
t1.op = '>';
panel.thresholds.push(t1);
}
}
delete panel.grid.threshold1;
delete panel.grid.threshold1Color;
delete panel.grid.threshold2;
delete panel.grid.threshold2Color;
delete panel.grid.thresholdLine;
});
}
if (panelUpgrades.length === 0) {
return;
}
for (i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
for (k = 0; k < panelUpgrades.length; k++) {
panelUpgrades[k].call(this, row.panels[j]);
}
}
}
};
return {
create: function(dashboard, meta) {
return new DashboardModel(dashboard, meta);
},
setCurrent: function(dashboard) {
this.currentDashboard = dashboard;
},
getCurrent: function() {
return this.currentDashboard;
},
};
});
});

View File

@ -15,7 +15,7 @@ export class DashboardCtrl {
private $rootScope,
dashboardKeybindings,
timeSrv,
templateValuesSrv,
variableSrv,
dashboardSrv,
unsavedChangesSrv,
dynamicDashboardSrv,
@ -46,7 +46,7 @@ export class DashboardCtrl {
// template values service needs to initialize completely before
// the rest of the dashboard can load
templateValuesSrv.init(dashboard)
variableSrv.init(dashboard)
// template values failes are non fatal
.catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
// continue
@ -87,7 +87,6 @@ export class DashboardCtrl {
};
$scope.templateVariableUpdated = function() {
console.log('dynamic update');
dynamicDashboardSrv.update($scope.dashboard);
};

View File

@ -0,0 +1,590 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import moment from 'moment';
import _ from 'lodash';
import $ from 'jquery';
import {Emitter} from 'app/core/core';
import {contextSrv} from 'app/core/services/context_srv';
import coreModule from 'app/core/core_module';
export class DashboardModel {
id: any;
title: any;
autoUpdate: any;
description: any;
tags: any;
style: any;
timezone: any;
editable: any;
hideControls: any;
sharedCrosshair: any;
rows: any;
time: any;
timepicker: any;
templating: any;
annotations: any;
refresh: any;
snapshot: any;
schemaVersion: number;
version: number;
links: any;
gnetId: any;
meta: any;
events: any;
constructor(data, meta) {
if (!data) {
data = {};
}
this.events = new Emitter();
this.id = data.id || null;
this.title = data.title || 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || "dark";
this.timezone = data.timezone || '';
this.editable = data.editable !== false;
this.hideControls = data.hideControls || false;
this.sharedCrosshair = data.sharedCrosshair || false;
this.rows = data.rows || [];
this.time = data.time || { from: 'now-6h', to: 'now' };
this.timepicker = data.timepicker || {};
this.templating = this.ensureListExist(data.templating);
this.annotations = this.ensureListExist(data.annotations);
this.refresh = data.refresh;
this.snapshot = data.snapshot;
this.schemaVersion = data.schemaVersion || 0;
this.version = data.version || 0;
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this.updateSchema(data);
this.initMeta(meta);
}
private initMeta(meta) {
meta = meta || {};
meta.canShare = meta.canShare !== false;
meta.canSave = meta.canSave !== false;
meta.canStar = meta.canStar !== false;
meta.canEdit = meta.canEdit !== false;
if (!this.editable) {
meta.canEdit = false;
meta.canDelete = false;
meta.canSave = false;
this.hideControls = true;
}
this.meta = meta;
}
// cleans meta data and other non peristent state
getSaveModelClone() {
// temp remove stuff
var events = this.events;
var meta = this.meta;
delete this.events;
delete this.meta;
events.emit('prepare-save-model');
var copy = $.extend(true, {}, this);
// restore properties
this.events = events;
this.meta = meta;
return copy;
}
private ensureListExist(data) {
if (!data) { data = {}; }
if (!data.list) { data.list = []; }
return data;
}
getNextPanelId() {
var i, j, row, panel, max = 0;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (panel.id > max) { max = panel.id; }
}
}
return max + 1;
}
forEachPanel(callback) {
var i, j, row;
for (i = 0; i < this.rows.length; i++) {
row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
callback(row.panels[j], j, row, i);
}
}
}
getPanelById(id) {
for (var i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (var j = 0; j < row.panels.length; j++) {
var panel = row.panels[j];
if (panel.id === id) {
return panel;
}
}
}
return null;
}
rowSpan(row) {
return _.reduce(row.panels, function(p,v) {
return p + v.span;
},0);
};
addPanel(panel, row) {
var rowSpan = this.rowSpan(row);
var panelCount = row.panels.length;
var space = (12 - rowSpan) - panel.span;
panel.id = this.getNextPanelId();
// try to make room of there is no space left
if (space <= 0) {
if (panelCount === 1) {
row.panels[0].span = 6;
panel.span = 6;
} else if (panelCount === 2) {
row.panels[0].span = 4;
row.panels[1].span = 4;
panel.span = 4;
}
}
row.panels.push(panel);
}
isSubmenuFeaturesEnabled() {
var visableTemplates = _.filter(this.templating.list, function(template) {
return template.hideVariable === undefined || template.hideVariable === false;
});
return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
}
getPanelInfoById(panelId) {
var result: any = {};
_.each(this.rows, function(row) {
_.each(row.panels, function(panel, index) {
if (panel.id === panelId) {
result.panel = panel;
result.row = row;
result.index = index;
}
});
});
if (!result.panel) {
return null;
}
return result;
}
duplicatePanel(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId();
delete newPanel.repeat;
delete newPanel.repeatIteration;
delete newPanel.repeatPanelId;
delete newPanel.scopedVars;
var currentRow = this.rows[rowIndex];
currentRow.panels.push(newPanel);
return newPanel;
}
formatDate(date, format) {
date = moment.isMoment(date) ? date : moment(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
this.timezone = this.getTimezone();
return this.timezone === 'browser' ?
moment(date).format(format) :
moment.utc(date).format(format);
}
getRelativeTime(date) {
date = moment.isMoment(date) ? date : moment(date);
return this.timezone === 'browser' ?
moment(date).fromNow() :
moment.utc(date).fromNow();
}
getNextQueryLetter(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
}
isTimezoneUtc() {
return this.getTimezone() === 'utc';
}
getTimezone() {
return this.timezone ? this.timezone : contextSrv.user.timezone;
}
private updateSchema(old) {
var i, j, k;
var oldVersion = this.schemaVersion;
var panelUpgrades = [];
this.schemaVersion = 13;
if (oldVersion === this.schemaVersion) {
return;
}
// version 2 schema changes
if (oldVersion < 2) {
if (old.services) {
if (old.services.filter) {
this.time = old.services.filter.time;
this.templating.list = old.services.filter.list || [];
}
}
panelUpgrades.push(function(panel) {
// rename panel type
if (panel.type === 'graphite') {
panel.type = 'graph';
}
if (panel.type !== 'graph') {
return;
}
if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
if (panel.grid) {
if (panel.grid.min) {
panel.grid.leftMin = panel.grid.min;
delete panel.grid.min;
}
if (panel.grid.max) {
panel.grid.leftMax = panel.grid.max;
delete panel.grid.max;
}
}
if (panel.y_format) {
panel.y_formats[0] = panel.y_format;
delete panel.y_format;
}
if (panel.y2_format) {
panel.y_formats[1] = panel.y2_format;
delete panel.y2_format;
}
});
}
// schema version 3 changes
if (oldVersion < 3) {
// ensure panel ids
var maxId = this.getNextPanelId();
panelUpgrades.push(function(panel) {
if (!panel.id) {
panel.id = maxId;
maxId += 1;
}
});
}
// schema version 4 changes
if (oldVersion < 4) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
_.each(panel.aliasYAxis, function(value, key) {
panel.seriesOverrides = [{ alias: key, yaxis: value }];
});
delete panel.aliasYAxis;
});
}
if (oldVersion < 6) {
// move pulldowns to new schema
var annotations = _.find(old.pulldowns, { type: 'annotations' });
if (annotations) {
this.annotations = {
list: annotations.annotations || [],
};
}
// update template variables
for (i = 0 ; i < this.templating.list.length; i++) {
var variable = this.templating.list[i];
if (variable.datasource === void 0) { variable.datasource = null; }
if (variable.type === 'filter') { variable.type = 'query'; }
if (variable.type === void 0) { variable.type = 'query'; }
if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
}
}
if (oldVersion < 7) {
if (old.nav && old.nav.length) {
this.timepicker = old.nav[0];
}
// ensure query refIds
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
if (!target.refId) {
target.refId = this.getNextQueryLetter(panel);
}
}.bind(this));
});
}
if (oldVersion < 8) {
panelUpgrades.push(function(panel) {
_.each(panel.targets, function(target) {
// update old influxdb query schema
if (target.fields && target.tags && target.groupBy) {
if (target.rawQuery) {
delete target.fields;
delete target.fill;
} else {
target.select = _.map(target.fields, function(field) {
var parts = [];
parts.push({type: 'field', params: [field.name]});
parts.push({type: field.func, params: []});
if (field.mathExpr) {
parts.push({type: 'math', params: [field.mathExpr]});
}
if (field.asExpr) {
parts.push({type: 'alias', params: [field.asExpr]});
}
return parts;
});
delete target.fields;
_.each(target.groupBy, function(part) {
if (part.type === 'time' && part.interval) {
part.params = [part.interval];
delete part.interval;
}
if (part.type === 'tag' && part.key) {
part.params = [part.key];
delete part.key;
}
});
if (target.fill) {
target.groupBy.push({type: 'fill', params: [target.fill]});
delete target.fill;
}
}
}
});
});
}
// schema version 9 changes
if (oldVersion < 9) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
if (panel.thresholds) {
var k = panel.thresholds.split(",");
if (k.length >= 3) {
k.shift();
panel.thresholds = k.join(",");
}
}
});
}
// schema version 10 changes
if (oldVersion < 10) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'table') { return; }
_.each(panel.styles, function(style) {
if (style.thresholds && style.thresholds.length >= 3) {
var k = style.thresholds;
k.shift();
style.thresholds = k;
}
});
});
}
if (oldVersion < 12) {
// update template variables
_.each(this.templating.list, function(templateVariable) {
if (templateVariable.refresh) { templateVariable.refresh = 1; }
if (!templateVariable.refresh) { templateVariable.refresh = 0; }
if (templateVariable.hideVariable) {
templateVariable.hide = 2;
} else if (templateVariable.hideLabel) {
templateVariable.hide = 1;
} else {
templateVariable.hide = 0;
}
});
}
if (oldVersion < 12) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
if (!panel.grid) { return; }
if (!panel.yaxes) {
panel.yaxes = [
{
show: panel['y-axis'],
min: panel.grid.leftMin,
max: panel.grid.leftMax,
logBase: panel.grid.leftLogBase,
format: panel.y_formats[0],
label: panel.leftYAxisLabel,
},
{
show: panel['y-axis'],
min: panel.grid.rightMin,
max: panel.grid.rightMax,
logBase: panel.grid.rightLogBase,
format: panel.y_formats[1],
label: panel.rightYAxisLabel,
}
];
panel.xaxis = {
show: panel['x-axis'],
};
delete panel.grid.leftMin;
delete panel.grid.leftMax;
delete panel.grid.leftLogBase;
delete panel.grid.rightMin;
delete panel.grid.rightMax;
delete panel.grid.rightLogBase;
delete panel.y_formats;
delete panel.leftYAxisLabel;
delete panel.rightYAxisLabel;
delete panel['y-axis'];
delete panel['x-axis'];
}
});
}
if (oldVersion < 13) {
// update graph yaxes changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'graph') { return; }
panel.thresholds = [];
var t1: any = {}, t2: any = {};
if (panel.grid.threshold1 !== null) {
t1.value = panel.grid.threshold1;
if (panel.grid.thresholdLine) {
t1.line = true;
t1.lineColor = panel.grid.threshold1Color;
} else {
t1.fill = true;
t1.fillColor = panel.grid.threshold1Color;
}
}
if (panel.grid.threshold2 !== null) {
t2.value = panel.grid.threshold2;
if (panel.grid.thresholdLine) {
t2.line = true;
t2.lineColor = panel.grid.threshold2Color;
} else {
t2.fill = true;
t2.fillColor = panel.grid.threshold2Color;
}
}
if (_.isNumber(t1.value)) {
if (_.isNumber(t2.value)) {
if (t1.value > t2.value) {
t1.op = t2.op = '<';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
} else {
t1.op = t2.op = '>';
panel.thresholds.push(t2);
panel.thresholds.push(t1);
}
} else {
t1.op = '>';
panel.thresholds.push(t1);
}
}
delete panel.grid.threshold1;
delete panel.grid.threshold1Color;
delete panel.grid.threshold2;
delete panel.grid.threshold2Color;
delete panel.grid.thresholdLine;
});
}
if (panelUpgrades.length === 0) {
return;
}
for (i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
for (k = 0; k < panelUpgrades.length; k++) {
panelUpgrades[k].call(this, row.panels[j]);
}
}
}
}
}
export class DashboardSrv {
currentDashboard: any;
create(dashboard, meta) {
return new DashboardModel(dashboard, meta);
}
setCurrent(dashboard) {
this.currentDashboard = dashboard;
}
getCurrent() {
return this.currentDashboard;
}
}
coreModule.service('dashboardSrv', DashboardSrv);

View File

@ -24,6 +24,10 @@ export class DashboardExporter {
var templateizeDatasourceUsage = obj => {
promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
if (ds.meta.builtIn) {
return;
}
var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
datasources[refName] = {
name: refName,
@ -46,11 +50,19 @@ export class DashboardExporter {
// check up panel data sources
for (let row of dash.rows) {
_.each(row.panels, (panel) => {
for (let panel of row.panels) {
if (panel.datasource !== undefined) {
templateizeDatasourceUsage(panel);
}
if (panel.targets) {
for (let target of panel.targets) {
if (target.datasource !== undefined) {
templateizeDatasourceUsage(target);
}
}
}
var panelDef = config.panels[panel.type];
if (panelDef) {
requires['panel' + panelDef.id] = {
@ -60,7 +72,7 @@ export class DashboardExporter {
version: panelDef.info.version,
};
}
});
}
}
// templatize template vars

View File

@ -8,7 +8,7 @@ function (angular, _, require, config) {
var module = angular.module('grafana.controllers');
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) {
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
$scope.editor = { index: $scope.tabIndex || 0};

View File

@ -0,0 +1,379 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {DashboardSrv} from '../dashboard_srv';
describe('dashboardSrv', function() {
var _dashboardSrv;
beforeEach(() => {
_dashboardSrv = new DashboardSrv();
});
describe('when creating new dashboard with defaults only', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({}, {});
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have meta', function() {
expect(model.meta.canSave).to.be(true);
expect(model.meta.canShare).to.be(true);
});
it('should have default properties', function() {
expect(model.rows.length).to.be(0);
});
});
describe('when getting next panel id', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{ panels: [{ id: 5 }]}]
});
});
it('should return max id + 1', function() {
expect(model.getNextPanelId()).to.be(6);
});
});
describe('row and panel manipulation', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({});
});
it('row span should sum spans', function() {
var spanLeft = dashboard.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
expect(spanLeft).to.be(5);
});
it('adding default should split span in half', function() {
dashboard.rows = [{ panels: [{ span: 12, id: 7 }] }];
dashboard.addPanel({span: 4}, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(6);
expect(dashboard.rows[0].panels[1].span).to.be(6);
expect(dashboard.rows[0].panels[1].id).to.be(8);
});
it('duplicate panel should try to add it to same row', function() {
var panel = { span: 4, attr: '123', id: 10 };
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(4);
expect(dashboard.rows[0].panels[1].span).to.be(4);
expect(dashboard.rows[0].panels[1].attr).to.be('123');
expect(dashboard.rows[0].panels[1].id).to.be(11);
});
it('duplicate panel should remove repeat data', function() {
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined);
});
});
describe('when creating dashboard with editable false', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false
});
});
it('should set editable false', function() {
expect(model.editable).to.be(false);
});
});
describe('when creating dashboard with old schema', function() {
var model;
var graph;
var singlestat;
var table;
beforeEach(function() {
model = _dashboardSrv.create({
services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }},
pulldowns: [
{type: 'filtering', enable: true},
{type: 'annotations', enable: true, annotations: [{name: 'old'}]}
],
rows: [
{
panels: [
{
type: 'graph', legend: true, aliasYAxis: { test: 2 },
y_formats: ['kbyte', 'ms'],
grid: {
min: 1,
max: 10,
rightMin: 5,
rightMax: 15,
leftLogBase: 1,
rightLogBase: 2,
threshold1: 200,
threshold2: 400,
threshold1Color: 'yellow',
threshold2Color: 'red',
},
leftYAxisLabel: 'left label',
targets: [{refId: 'A'}, {}],
},
{
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
targets: [{refId: 'A'}, {}],
},
{
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
targets: [{refId: 'A'}, {}],
}
]
}
]
});
graph = model.rows[0].panels[0];
singlestat = model.rows[0].panels[1];
table = model.rows[0].panels[2];
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have panel id', function() {
expect(graph.id).to.be(1);
});
it('should move time and filtering list', function() {
expect(model.time.from).to.be('now-1d');
expect(model.templating.list[0].allFormat).to.be('glob');
});
it('graphite panel should change name too graph', function() {
expect(graph.type).to.be('graph');
});
it('single stat panel should have two thresholds', function() {
expect(singlestat.thresholds).to.be('20,30');
});
it('queries without refId should get it', function() {
expect(graph.targets[1].refId).to.be('B');
});
it('update legend setting', function() {
expect(graph.legend.show).to.be(true);
});
it('move aliasYAxis to series override', function() {
expect(graph.seriesOverrides[0].alias).to.be("test");
expect(graph.seriesOverrides[0].yaxis).to.be(2);
});
it('should move pulldowns to new schema', function() {
expect(model.annotations.list[0].name).to.be('old');
});
it('table panel should only have two thresholds values', function() {
expect(table.styles[0].thresholds[0]).to.be("20");
expect(table.styles[0].thresholds[1]).to.be("30");
expect(table.styles[1].thresholds[0]).to.be("200");
expect(table.styles[1].thresholds[1]).to.be("300");
});
it('graph grid to yaxes options', function() {
expect(graph.yaxes[0].min).to.be(1);
expect(graph.yaxes[0].max).to.be(10);
expect(graph.yaxes[0].format).to.be('kbyte');
expect(graph.yaxes[0].label).to.be('left label');
expect(graph.yaxes[0].logBase).to.be(1);
expect(graph.yaxes[1].min).to.be(5);
expect(graph.yaxes[1].max).to.be(15);
expect(graph.yaxes[1].format).to.be('ms');
expect(graph.yaxes[1].logBase).to.be(2);
expect(graph.grid.rightMax).to.be(undefined);
expect(graph.grid.rightLogBase).to.be(undefined);
expect(graph.y_formats).to.be(undefined);
});
it('dashboard schema version should be set to latest', function() {
expect(model.schemaVersion).to.be(13);
});
it('graph thresholds should be migrated', function() {
expect(graph.thresholds.length).to.be(2);
expect(graph.thresholds[0].op).to.be('>');
expect(graph.thresholds[0].value).to.be(400);
expect(graph.thresholds[0].fillColor).to.be('red');
expect(graph.thresholds[1].value).to.be(200);
expect(graph.thresholds[1].fillColor).to.be('yellow');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Given editable false dashboard', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false,
});
});
it('Should set meta canEdit and canSave to false', function() {
expect(model.meta.canSave).to.be(false);
expect(model.meta.canEdit).to.be(false);
});
it('getSaveModelClone should remove meta', function() {
var clone = model.getSaveModelClone();
expect(clone.meta).to.be(undefined);
});
});
describe('when loading dashboard with old influxdb query schema', function() {
var model;
var target;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{
panels: [{
type: 'graph',
grid: {},
yaxes: [{}, {}],
targets: [{
"alias": "$tag_datacenter $tag_source $col",
"column": "value",
"measurement": "logins.count",
"fields": [
{
"func": "mean",
"name": "value",
"mathExpr": "*2",
"asExpr": "value"
},
{
"name": "one-minute",
"func": "mean",
"mathExpr": "*3",
"asExpr": "one-minute"
}
],
"tags": [],
"fill": "previous",
"function": "mean",
"groupBy": [
{
"interval": "auto",
"type": "time"
},
{
"key": "source",
"type": "tag"
},
{
"type": "tag",
"key": "datacenter"
}
],
}]
}]
}]
});
target = model.rows[0].panels[0].targets[0];
});
it('should update query schema', function() {
expect(target.fields).to.be(undefined);
expect(target.select.length).to.be(2);
expect(target.select[0].length).to.be(4);
expect(target.select[0][0].type).to.be('field');
expect(target.select[0][1].type).to.be('mean');
expect(target.select[0][2].type).to.be('math');
expect(target.select[0][3].type).to.be('alias');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Formatting epoch timestamp when timezone is set as utc', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({
timezone: 'utc',
});
});
it('Should format timestamp with second resolution by default', function() {
expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with second resolution even if second format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with millisecond resolution if format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007');
});
});
});

View File

@ -1,6 +1,6 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import 'app/features/dashboard/dashboardSrv';
import {DashboardSrv} from '../dashboard_srv';
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
function dynamicDashScenario(desc, func) {
@ -10,6 +10,7 @@ function dynamicDashScenario(desc, func) {
ctx.setup = function (setupFunc) {
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module(function($provide) {
$provide.value('contextSrv', {

View File

@ -42,21 +42,34 @@ describe('given dashboard with repeated panels', function() {
repeat: 'test',
panels: [
{id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'},
{id: 2, repeat: null, repeatPanelId: 2},
{id: 3, repeat: null, repeatPanelId: 2},
{
id: 4,
datasource: '-- Mixed --',
targets: [{datasource: 'other'}],
},
]
});
dash.rows.push({
repeat: null,
repeatRowId: 1,
panels: [],
});
var datasourceSrvStub = {
get: sinon.stub().returns(Promise.resolve({
name: 'gfdb',
meta: {id: "testdb", info: {version: "1.2.1"}, name: "TestDB"}
}))
};
var datasourceSrvStub = {get: sinon.stub()};
datasourceSrvStub.get.withArgs('gfdb').returns(Promise.resolve({
name: 'gfdb',
meta: {id: "testdb", info: {version: "1.2.1"}, name: "TestDB"}
}));
datasourceSrvStub.get.withArgs('other').returns(Promise.resolve({
name: 'other',
meta: {id: "other", info: {version: "1.2.1"}, name: "OtherDB"}
}));
datasourceSrvStub.get.withArgs('-- Mixed --').returns(Promise.resolve({
name: 'mixed',
meta: {id: "mixed", info: {version: "1.2.1"}, name: "Mixed", builtIn: true}
}));
config.panels['graph'] = {
id: "graph",
@ -72,7 +85,7 @@ describe('given dashboard with repeated panels', function() {
});
it('exported dashboard should not contain repeated panels', function() {
expect(exported.rows[0].panels.length).to.be(1);
expect(exported.rows[0].panels.length).to.be(2);
});
it('exported dashboard should not contain repeated rows', function() {
@ -109,6 +122,16 @@ describe('given dashboard with repeated panels', function() {
expect(require.version).to.be("1.2.1");
});
it('should not add built in datasources to required', function() {
var require = _.find(exported.__requires, {name: 'Mixed'});
expect(require).to.be(undefined);
});
it('should add datasources used in mixed mode', function() {
var require = _.find(exported.__requires, {name: 'OtherDB'});
expect(require).to.not.be(undefined);
});
it('should add panel to required', function() {
var require = _.find(exported.__requires, {name: 'Graph'});
expect(require.name).to.be("Graph");

View File

@ -1,27 +1,26 @@
<div class="submenu-controls">
<ul ng-if="ctrl.dashboard.templating.list.length > 0">
<li ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item">
<span class="submenu-item-label template-variable " ng-hide="variable.hide === 1">
{{variable.label || variable.name}}:
</span>
<value-select-dropdown variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
</li>
</ul>
<div class="submenu-controls gf-form-query">
<div ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
<div class="gf-form">
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
{{variable.label || variable.name}}
</label>
<value-select-dropdown ng-if="variable.type !== 'adhoc'" variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
</div>
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
</div>
<ul ng-if="ctrl.dashboard.annotations.list.length > 0">
<li ng-repeat="annotation in ctrl.dashboard.annotations.list" class="submenu-item annotation-segment" ng-class="{'annotation-disabled': !annotation.enable}">
<a ng-click="ctrl.disableAnnotation(annotation)">
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
{{annotation.name}}
<input class="cr1" id="hideYAxis" type="checkbox" ng-model="annotation.enable" ng-checked="annotation.enable">
<label for="hideYAxis" class="cr1"></label>
</a>
</li>
</ul>
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
<div ng-repeat="annotation in ctrl.dashboard.annotations.list" class="submenu-item" ng-class="{'annotation-disabled': !annotation.enable}">
<gf-form-switch class="gf-form" label="{{annotation.name}}" checked="annotation.enable" on-change="ctrl.annotationStateChanged()"></gf-form-switch>
</div>
</div>
<ul class="pull-right" ng-if="ctrl.dashboard.links.length > 0">
<dash-links-container links="ctrl.dashboard.links"></dash-links-container>
</ul>
<div class="gf-form gf-form--grow">
</div>
<div class="clearfix"></div>
<div ng-if="ctrl.dashboard.links.length > 0" >
<dash-links-container links="ctrl.dashboard.links" class="gf-form-inline"></dash-links-container>
</div>
<div class="clearfix"></div>
</div>

View File

@ -10,24 +10,23 @@ export class SubmenuCtrl {
/** @ngInject */
constructor(private $rootScope,
private templateValuesSrv,
private variableSrv,
private templateSrv,
private $location) {
this.annotations = this.dashboard.templating.list;
this.variables = this.dashboard.templating.list;
this.variables = this.variableSrv.variables;
}
disableAnnotation(annotation) {
annotation.enable = !annotation.enable;
annotationStateChanged() {
this.$rootScope.$broadcast('refresh');
}
getValuesForTag(variable, tagKey) {
return this.templateValuesSrv.getValuesForTag(variable, tagKey);
return this.variableSrv.getValuesForTag(variable, tagKey);
}
variableUpdated(variable) {
this.templateValuesSrv.variableUpdated(variable).then(() => {
this.variableSrv.variableUpdated(variable).then(() => {
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
});

View File

@ -34,10 +34,6 @@ function (angular, _, $) {
$location.search(urlParams);
});
$scope.onAppEvent('template-variable-value-updated', function() {
self.updateUrlParamsWithCurrentVariables();
});
$scope.onAppEvent('$routeUpdate', function() {
var urlState = self.getQueryStringState();
if (self.needsSync(urlState)) {
@ -57,22 +53,6 @@ function (angular, _, $) {
this.expandRowForPanel();
}
DashboardViewState.prototype.updateUrlParamsWithCurrentVariables = function() {
// update url
var params = $location.search();
// remove variable params
_.each(params, function(value, key) {
if (key.indexOf('var-') === 0) {
delete params[key];
}
});
// add new values
templateSrv.fillVariableValuesForUrl(params);
// update url
$location.search(params);
};
DashboardViewState.prototype.expandRowForPanel = function() {
if (!this.state.panelId) { return; }
@ -185,7 +165,7 @@ function (angular, _, $) {
DashboardViewState.prototype.enterFullscreen = function(panelScope) {
var ctrl = panelScope.ctrl;
ctrl.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit;
ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
ctrl.fullscreen = true;
this.oldTimeRange = ctrl.range;

View File

@ -44,17 +44,19 @@ function (angular, _) {
restrict: 'E',
link: function(scope, elem) {
var link = scope.link;
var template = '<div class="submenu-item dropdown">' +
'<a class="pointer dash-nav-link" data-placement="bottom"' +
var template = '<div class="gf-form">' +
'<a class="pointer gf-form-label" data-placement="bottom"' +
(link.asDropdown ? ' ng-click="fillDropdown(link)" data-toggle="dropdown"' : "") + '>' +
'<i></i> <span></span></a>';
if (link.asDropdown) {
template += '<ul class="dropdown-menu" role="menu">' +
'<li ng-repeat="dash in link.searchHits"><a href="{{dash.url}}"><i class="fa fa-th-large"></i> {{dash.title}}</a></li>' +
'<li ng-repeat="dash in link.searchHits"><a href="{{dash.url}}">{{dash.title}}</a></li>' +
'</ul>';
}
template += '</div>';
elem.html(template);
$compile(elem.contents())(scope);

View File

@ -25,7 +25,7 @@
</div>
<div class="row">
<div class="col-md-6">
<div class="col-lg-6">
<div class="playlist-search-containerwrapper">
<div class="max-width-32">
<h5 class="page-headering playlist-column-header">Available</h5>
@ -72,7 +72,7 @@
</div>
</div>
<div class="col-md-6">
<div class="col-lg-6">
<h5 class="page headering playlist-column-header">Selected</h5>
<table class="grafana-options-table playlist-available-list">
<tr ng-repeat="playlistItem in ctrl.playlistItems">

View File

@ -14,7 +14,7 @@ export class PlaylistSearchCtrl {
/** @ngInject */
constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv) {
this.query = { query: '', tag: [], starred: false };
this.query = {query: '', tag: [], starred: false, limit: 30};
$timeout(() => {
this.query.query = '';

View File

@ -3,7 +3,9 @@
<div class="page-container">
<div class="page-header">
<h1>Plugins</h1>
<h1>
Plugins <span class="muted small">(currently installed)</span>
</h1>
<div class="page-header-tabs">
<ul class="gf-tabs">
@ -25,7 +27,7 @@
</ul>
<a class="get-more-plugins-link" href="https://grafana.net/plugins?utm_source=grafana_plugin_list" target="_blank">
Find plugins on
Find more plugins on
</a>
</div>
</div>

View File

@ -13,7 +13,7 @@ class StyleGuideCtrl {
pages = ['colors', 'buttons'];
/** @ngInject **/
constructor(private $http, $routeParams) {
constructor(private $http, private $routeParams, private $location) {
this.theme = config.bootData.user.lightTheme ? 'light': 'dark';
this.page = {};
@ -37,8 +37,11 @@ class StyleGuideCtrl {
}
switchTheme() {
var other = this.theme === 'dark' ? 'light' : 'dark';
window.location.href = window.location.href + '?theme=' + other;
this.$routeParams.theme = this.theme === 'dark' ? 'light' : 'dark';
this.$location.search(this.$routeParams);
setTimeout(() => {
window.location.href = window.location.href;
});
}
}

View File

@ -0,0 +1,74 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class AdhocVariable implements Variable {
filters: any[];
defaults = {
type: 'adhoc',
name: '',
label: '',
hide: 0,
datasource: null,
filters: [],
};
/** @ngInject **/
constructor(private model) {
assignModelProperties(this, model, this.defaults);
}
setValue(option) {
return Promise.resolve();
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
updateOptions() {
return Promise.resolve();
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
if (!_.isArray(urlValue)) {
urlValue = [urlValue];
}
this.filters = urlValue.map(item => {
var values = item.split('|');
return {
key: values[0],
operator: values[1],
value: values[2],
};
});
return Promise.resolve();
}
getValueForUrl() {
return this.filters.map(filter => {
return filter.key + '|' + filter.operator + '|' + filter.value;
});
}
setFilters(filters: any[]) {
this.filters = filters;
}
}
variableTypes['adhoc'] = {
name: 'Ad hoc filters',
ctor: AdhocVariable,
description: 'Add key/value filters on the fly',
};

View File

@ -0,0 +1,20 @@
import './templateSrv';
import './editor_ctrl';
import {VariableSrv} from './variable_srv';
import {IntervalVariable} from './interval_variable';
import {QueryVariable} from './query_variable';
import {DatasourceVariable} from './datasource_variable';
import {CustomVariable} from './custom_variable';
import {ConstantVariable} from './constant_variable';
import {AdhocVariable} from './adhoc_variable';
export {
VariableSrv,
IntervalVariable,
QueryVariable,
DatasourceVariable,
CustomVariable,
ConstantVariable,
AdhocVariable,
}

View File

@ -0,0 +1,59 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class ConstantVariable implements Variable {
query: string;
options: any[];
current: any;
defaults = {
type: 'constant',
name: '',
hide: 2,
label: '',
query: '',
current: {},
};
/** @ngInject **/
constructor(private model, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option) {
this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
this.options = [{text: this.query.trim(), value: this.query.trim()}];
this.setValue(this.options[0]);
return Promise.resolve();
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['constant'] = {
name: 'Constant',
ctor: ConstantVariable,
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share' ,
};

View File

@ -0,0 +1,80 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class CustomVariable implements Variable {
query: string;
options: any;
includeAll: boolean;
multi: boolean;
current: any;
defaults = {
type: 'custom',
name: '',
label: '',
hide: 0,
options: [],
current: {},
query: '',
includeAll: false,
multi: false,
allValue: null,
};
/** @ngInject **/
constructor(private model, private timeSrv, private templateSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}
setValue(option) {
return this.variableSrv.setOptionAsCurrent(this, option);
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
updateOptions() {
// extract options in comma separated string
this.options = _.map(this.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() };
});
if (this.includeAll) {
this.addAllOption();
}
return this.variableSrv.validateVariableSelectionState(this);
}
addAllOption() {
this.options.unshift({text: 'All', value: "$__all"});
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
if (this.current.text === 'All') {
return 'All';
}
return this.current.value;
}
}
variableTypes['custom'] = {
name: 'Custom',
ctor: CustomVariable,
description: 'Define variable values manually' ,
supportsMulti: true,
};

View File

@ -0,0 +1,87 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class DatasourceVariable implements Variable {
regex: any;
query: string;
options: any;
current: any;
defaults = {
type: 'datasource',
name: '',
hide: 0,
label: '',
current: {},
regex: '',
options: [],
query: '',
};
/** @ngInject **/
constructor(private model, private datasourceSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option) {
return this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
var options = [];
var sources = this.datasourceSrv.getMetricSources({skipVariables: true});
var regex;
if (this.regex) {
regex = kbn.stringToJsRegex(this.regex);
}
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// must match on type
if (source.meta.id !== this.query) {
continue;
}
if (regex && !regex.exec(source.name)) {
continue;
}
options.push({text: source.name, value: source.name});
}
if (options.length === 0) {
options.push({text: 'No data sources found', value: ''});
}
this.options = options;
return this.variableSrv.validateVariableSelectionState(this);
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['datasource'] = {
name: 'Datasource',
ctor: DatasourceVariable,
description: 'Enabled you to dynamically switch the datasource for multiple panels',
};

View File

@ -1,198 +0,0 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('TemplateEditorCtrl', function($scope, datasourceSrv, templateSrv, templateValuesSrv) {
var replacementDefaults = {
type: 'query',
datasource: null,
refresh: 0,
sort: 1,
name: '',
hide: 0,
options: [],
includeAll: false,
multi: false,
};
$scope.variableTypes = [
{value: "query", text: "Query"},
{value: "interval", text: "Interval"},
{value: "datasource", text: "Data source"},
{value: "custom", text: "Custom"},
{value: "constant", text: "Constant"},
];
$scope.refreshOptions = [
{value: 0, text: "Never"},
{value: 1, text: "On Dashboard Load"},
{value: 2, text: "On Time Range Change"},
];
$scope.sortOptions = [
{value: 0, text: "Without Sort"},
{value: 1, text: "Alphabetical (asc)"},
{value: 2, text: "Alphabetical (desc)"},
{value: 3, text: "Numerical (asc)"},
{value: 4, text: "Numerical (desc)"},
];
$scope.hideOptions = [
{value: 0, text: ""},
{value: 1, text: "Label"},
{value: 2, text: "Variable"},
];
$scope.init = function() {
$scope.mode = 'list';
$scope.datasourceTypes = {};
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
$scope.datasourceTypes[ds.meta.id] = {text: ds.meta.name, value: ds.meta.id};
return !ds.meta.builtIn;
});
$scope.datasourceTypes = _.map($scope.datasourceTypes, function(value) {
return value;
});
$scope.variables = templateSrv.variables;
$scope.reset();
$scope.$watch('mode', function(val) {
if (val === 'new') {
$scope.reset();
}
});
$scope.$watch('current.datasource', function(val) {
if ($scope.mode === 'new') {
datasourceSrv.get(val).then(function(ds) {
if (ds.meta.defaultMatchFormat) {
$scope.current.allFormat = ds.meta.defaultMatchFormat;
$scope.current.multiFormat = ds.meta.defaultMatchFormat;
}
});
}
});
};
$scope.add = function() {
if ($scope.isValid()) {
$scope.variables.push($scope.current);
$scope.update();
$scope.updateSubmenuVisibility();
}
};
$scope.isValid = function() {
if (!$scope.current.name) {
$scope.appEvent('alert-warning', ['Validation', 'Template variable requires a name']);
return false;
}
if (!$scope.current.name.match(/^\w+$/)) {
$scope.appEvent('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
return false;
}
var sameName = _.find($scope.variables, { name: $scope.current.name });
if (sameName && sameName !== $scope.current) {
$scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
return false;
}
return true;
};
$scope.runQuery = function() {
return templateValuesSrv.updateOptions($scope.current).then(null, function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Templating', 'Template variables could not be initialized: ' + err.message]);
});
};
$scope.edit = function(variable) {
$scope.current = variable;
$scope.currentIsNew = false;
$scope.mode = 'edit';
$scope.current.sort = $scope.current.sort || replacementDefaults.sort;
if ($scope.current.datasource === void 0) {
$scope.current.datasource = null;
$scope.current.type = 'query';
$scope.current.allFormat = 'glob';
}
};
$scope.duplicate = function(variable) {
$scope.current = angular.copy(variable);
$scope.variables.push($scope.current);
$scope.current.name = 'copy_of_'+variable.name;
$scope.updateSubmenuVisibility();
};
$scope.update = function() {
if ($scope.isValid()) {
$scope.runQuery().then(function() {
$scope.reset();
$scope.mode = 'list';
});
}
};
$scope.reset = function() {
$scope.currentIsNew = true;
$scope.current = angular.copy(replacementDefaults);
};
$scope.showSelectionOptions = function() {
if ($scope.current) {
if ($scope.current.type === 'query') {
return true;
}
if ($scope.current.type === 'custom') {
return true;
}
}
return false;
};
$scope.typeChanged = function () {
if ($scope.current.type === 'interval') {
$scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
$scope.current.refresh = 0;
}
if ($scope.current.type === 'query') {
$scope.current.query = '';
}
if ($scope.current.type === 'constant') {
$scope.current.query = '';
$scope.current.refresh = 0;
$scope.current.hide = 2;
}
if ($scope.current.type === 'datasource') {
$scope.current.query = $scope.datasourceTypes[0].value;
$scope.current.regex = '';
$scope.current.refresh = 1;
}
};
$scope.removeVariable = function(variable) {
var index = _.indexOf($scope.variables, variable);
$scope.variables.splice(index, 1);
$scope.updateSubmenuVisibility();
};
});
});

View File

@ -0,0 +1,155 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import {variableTypes} from './variable';
export class VariableEditorCtrl {
/** @ngInject **/
constructor(private $scope, private datasourceSrv, private variableSrv, templateSrv) {
$scope.variableTypes = variableTypes;
$scope.ctrl = {};
$scope.refreshOptions = [
{value: 0, text: "Never"},
{value: 1, text: "On Dashboard Load"},
{value: 2, text: "On Time Range Change"},
];
$scope.sortOptions = [
{value: 0, text: "Disabled"},
{value: 1, text: "Alphabetical (asc)"},
{value: 2, text: "Alphabetical (desc)"},
{value: 3, text: "Numerical (asc)"},
{value: 4, text: "Numerical (desc)"},
];
$scope.hideOptions = [
{value: 0, text: ""},
{value: 1, text: "Label"},
{value: 2, text: "Variable"},
];
$scope.init = function() {
$scope.mode = 'list';
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
return !ds.meta.builtIn && ds.value !== null;
});
$scope.datasourceTypes = _($scope.datasources).uniqBy('meta.id').map(function(ds) {
return {text: ds.meta.name, value: ds.meta.id};
}).value();
$scope.variables = variableSrv.variables;
$scope.reset();
$scope.$watch('mode', function(val) {
if (val === 'new') {
$scope.reset();
}
});
};
$scope.add = function() {
if ($scope.isValid()) {
$scope.variables.push($scope.current);
$scope.update();
$scope.updateSubmenuVisibility();
}
};
$scope.isValid = function() {
if (!$scope.ctrl.form.$valid) {
return;
}
if (!$scope.current.name.match(/^\w+$/)) {
$scope.appEvent('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
return false;
}
var sameName = _.find($scope.variables, { name: $scope.current.name });
if (sameName && sameName !== $scope.current) {
$scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
return false;
}
return true;
};
$scope.validate = function() {
$scope.infoText = '';
if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) {
$scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource';
datasourceSrv.get($scope.current.datasource).then(ds => {
if (!ds.getTagKeys) {
$scope.infoText = 'This datasource does not support adhoc filters yet.';
}
});
}
};
$scope.runQuery = function() {
return variableSrv.updateOptions($scope.current).then(null, function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Templating', 'Template variables could not be initialized: ' + err.message]);
});
};
$scope.edit = function(variable) {
$scope.current = variable;
$scope.currentIsNew = false;
$scope.mode = 'edit';
$scope.validate();
};
$scope.duplicate = function(variable) {
var clone = _.cloneDeep(variable.getModel());
$scope.current = variableSrv.createVariableFromModel(clone);
$scope.variables.push($scope.current);
$scope.current.name = 'copy_of_'+variable.name;
$scope.updateSubmenuVisibility();
};
$scope.update = function() {
if ($scope.isValid()) {
$scope.runQuery().then(function() {
$scope.reset();
$scope.mode = 'list';
templateSrv.updateTemplateData();
});
}
};
$scope.reset = function() {
$scope.currentIsNew = true;
$scope.current = variableSrv.createVariableFromModel({type: 'query'});
};
$scope.typeChanged = function() {
var old = $scope.current;
$scope.current = variableSrv.createVariableFromModel({type: $scope.current.type});
$scope.current.name = old.name;
$scope.current.hide = old.hide;
$scope.current.label = old.label;
var oldIndex = _.indexOf(this.variables, old);
if (oldIndex !== -1) {
this.variables[oldIndex] = $scope.current;
}
$scope.validate();
};
$scope.removeVariable = function(variable) {
var index = _.indexOf($scope.variables, variable);
$scope.variables.splice(index, 1);
$scope.updateSubmenuVisibility();
};
}
}
coreModule.controller('VariableEditorCtrl', VariableEditorCtrl);

View File

@ -0,0 +1,89 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
export class IntervalVariable implements Variable {
auto_count: number;
auto_min: number;
options: any;
auto: boolean;
query: string;
refresh: number;
current: any;
defaults = {
type: 'interval',
name: '',
hide: 0,
label: '',
refresh: 2,
options: [],
current: {},
query: '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d',
auto: false,
auto_min: '10s',
auto_count: 30,
};
/** @ngInject **/
constructor(private model, private timeSrv, private templateSrv, private variableSrv) {
assignModelProperties(this, model, this.defaults);
this.refresh = 2;
}
getModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option) {
this.updateAutoValue();
return this.variableSrv.setOptionAsCurrent(this, option);
}
updateAutoValue() {
if (!this.auto) {
return;
}
// add auto option if missing
if (this.options.length && this.options[0].text !== 'auto') {
this.options.unshift({ text: 'auto', value: '$__auto_interval' });
}
var interval = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
this.templateSrv.setGrafanaVariable('$__auto_interval', interval);
}
updateOptions() {
// extract options in comma separated string
this.options = _.map(this.query.split(/[,]+/), function(text) {
return {text: text.trim(), value: text.trim()};
});
this.updateAutoValue();
return this.variableSrv.validateVariableSelectionState(this);
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
this.updateAutoValue();
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['interval'] = {
name: 'Interval',
ctor: IntervalVariable,
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
};

View File

@ -1,4 +1,4 @@
<div ng-controller="TemplateEditorCtrl" ng-init="init()">
<div ng-controller="VariableEditorCtrl" ng-init="init()">
<div class="tabbed-view-header">
<h2 class="tabbed-view-title">
Templating
@ -70,33 +70,23 @@
</div>
</div>
<div ng-if="mode === 'edit' || mode === 'new'">
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form">
<h5 class="section-heading">Variable</h5>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">Name</span>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.name'></input>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.name' required></input>
</div>
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">
Type
<info-popover mode="right-normal">
<dl>
<dt>Query</dt>
<dd>Variable values are fetched from a metric names query to a data source</dd>
<dt>Interval</dt>
<dd>Timespan variable type</dd>
<dt>Datasource</dt>
<dd>Dynamically switch data sources using this type of variable</dd>
<dt>Custom</dt>
<dd>Define variable values manually</dd>
</dl>
<a href="http://docs.grafana.org/reference/templating" target="_blank">Templating docs</a>
{{variableTypes[current.type].description}}
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-17">
<select class="gf-form-input" ng-model="current.type" ng-options="f.value as f.text for f in variableTypes" ng-change="typeChanged()"></select>
<select class="gf-form-input" ng-model="current.type" ng-options="k as v.name for (k, v) in variableTypes" ng-change="typeChanged()"></select>
</div>
</div>
</div>
@ -112,15 +102,14 @@
</div>
</div>
</div>
</div>
<div ng-show="current.type === 'interval'" class="gf-form-group">
<div ng-if="current.type === 'interval'" class="gf-form-group">
<h5 class="section-heading">Interval Options</h5>
<div class="gf-form">
<span class="gf-form-label width-9">Values</span>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()"></input>
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Auto option</span>
@ -144,15 +133,15 @@
</div>
</div>
<div ng-show="current.type === 'custom'" class="gf-form-group">
<div ng-if="current.type === 'custom'" class="gf-form-group">
<h5 class="section-heading">Custom Options</h5>
<div class="gf-form">
<span class="gf-form-label width-13">Values separated by comma</span>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
</div>
</div>
<div ng-show="current.type === 'constant'" class="gf-form-group">
<div ng-if="current.type === 'constant'" class="gf-form-group">
<h5 class="section-heading">Constant options</h5>
<div class="gf-form">
<span class="gf-form-label">Value</span>
@ -160,14 +149,14 @@
</div>
</div>
<div ng-show="current.type === 'query'" class="gf-form-group">
<div ng-if="current.type === 'query'" class="gf-form-group">
<h5 class="section-heading">Query Options</h5>
<div class="gf-form-inline">
<div class="gf-form max-width-21">
<span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
<span class="gf-form-label width-7">Data source</span>
<div class="gf-form-select-wrapper max-width-14">
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required></select>
</div>
</div>
<div class="gf-form max-width-21">
@ -181,21 +170,10 @@
<select class="gf-form-input" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
</div>
</div>
<div class="gf-form max-width-21">
<span class="gf-form-label width-7">
Sort
<info-popover mode="right-normal">
How to sort the values of this variable.
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-14">
<select class="gf-form-input" ng-model="current.sort" ng-options="f.value as f.text for f in sortOptions" ng-change="runQuery()"></select>
</div>
</div>
</div>
<div class="gf-form">
</div>
<div class="gf-form">
<span class="gf-form-label width-7">Query</span>
<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()"></input>
<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">
@ -206,15 +184,26 @@
</span>
<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
</div>
</div>
<div class="gf-form max-width-21">
<span class="gf-form-label width-7">
Sort
<info-popover mode="right-normal">
How to sort the values of this variable.
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-14">
<select class="gf-form-input" ng-model="current.sort" ng-options="f.value as f.text for f in sortOptions" ng-change="runQuery()"></select>
</div>
</div>
</div>
<div ng-show="current.type === 'datasource'" class="gf-form-group">
<h5 class="section-heading">Data source options</h5>
<div ng-show="current.type === 'datasource'" class="gf-form-group">
<h5 class="section-heading">Data source options</h5>
<div class="gf-form">
<label class="gf-form-label width-12">Type</label>
<div class="gf-form-select-wrapper max-width-18">
<select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes" ng-change="runQuery()"></select>
<div class="gf-form">
<label class="gf-form-label width-12">Type</label>
<div class="gf-form-select-wrapper max-width-18">
<select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes" ng-change="runQuery()"></select>
</div>
</div>
@ -233,8 +222,18 @@
</div>
</div>
<div class="section gf-form-group" ng-show="showSelectionOptions()">
<h5 class="section-heading">Selection Options</h5>
<div ng-if="current.type === 'adhoc'" class="gf-form-group">
<h5 class="section-heading">Options</h5>
<div class="gf-form max-width-21">
<span class="gf-form-label width-8">Data source</span>
<div class="gf-form-select-wrapper max-width-14">
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required ng-change="validate()"></select>
</div>
</div>
</div>
<div class="section gf-form-group" ng-show="variableTypes[current.type].supportsMulti">
<h5 class="section-heading">Selection Options</h5>
<div class="section">
<gf-form-switch class="gf-form"
label="Multi-value"
@ -271,7 +270,7 @@
</div>
</div>
<div class="gf-form-group">
<div class="gf-form-group" ng-show="current.options.length">
<h5>Preview of values (shows max 20)</h5>
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
@ -279,12 +278,17 @@
</div>
</div>
</div>
</div>
<div class="gf-form-button-row p-y-0">
<button type="button" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
<button type="button" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
</div>
</div>
<div class="alert alert-info gf-form-group" ng-if="infoText">
{{infoText}}
</div>
<div class="gf-form-button-row p-y-0">
<button type="submit" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
<button type="submit" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,168 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {Variable, containsVariable, assignModelProperties, variableTypes} from './variable';
import {VariableSrv} from './variable_srv';
function getNoneOption() {
return { text: 'None', value: '', isNone: true };
}
export class QueryVariable implements Variable {
datasource: any;
query: any;
regex: any;
sort: any;
options: any;
current: any;
refresh: number;
hide: number;
name: string;
multi: boolean;
includeAll: boolean;
defaults = {
type: 'query',
query: '',
regex: '',
sort: 0,
datasource: null,
refresh: 0,
hide: 0,
name: '',
multi: false,
includeAll: false,
allValue: null,
options: [],
current: {},
tagsQuery: null,
tagValuesQuery: null,
};
/** @ngInject **/
constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) {
// copy model properties to this instance
assignModelProperties(this, model, this.defaults);
}
getModel() {
// copy back model properties to model
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option){
return this.variableSrv.setOptionAsCurrent(this, option);
}
setValueFromUrl(urlValue) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
if (this.current.text === 'All') {
return 'All';
}
return this.current.value;
}
updateOptions() {
return this.datasourceSrv.get(this.datasource)
.then(this.updateOptionsFromMetricFindQuery.bind(this))
.then(this.variableSrv.validateVariableSelectionState.bind(this.variableSrv, this));
}
updateOptionsFromMetricFindQuery(datasource) {
return datasource.metricFindQuery(this.query).then(results => {
this.options = this.metricNamesToVariableValues(results);
if (this.includeAll) {
this.addAllOption();
}
if (!this.options.length) {
this.options.push(getNoneOption());
}
return datasource;
});
}
addAllOption() {
this.options.unshift({text: 'All', value: "$__all"});
}
metricNamesToVariableValues(metricNames) {
var regex, options, i, matches;
options = [];
if (this.regex) {
regex = kbn.stringToJsRegex(this.templateSrv.replace(this.regex));
}
for (i = 0; i < metricNames.length; i++) {
var item = metricNames[i];
var value = item.value || item.text;
var text = item.text || item.value;
if (_.isNumber(value)) {
value = value.toString();
}
if (_.isNumber(text)) {
text = text.toString();
}
if (regex) {
matches = regex.exec(value);
if (!matches) { continue; }
if (matches.length > 1) {
value = matches[1];
text = matches[1];
}
}
options.push({text: text, value: value});
}
options = _.uniqBy(options, 'value');
return this.sortVariableValues(options, this.sort);
}
sortVariableValues(options, sortOrder) {
if (sortOrder === 0) {
return options;
}
var sortType = Math.ceil(sortOrder / 2);
var reverseSort = (sortOrder % 2 === 0);
if (sortType === 1) {
options = _.sortBy(options, 'text');
} else if (sortType === 2) {
options = _.sortBy(options, function(opt) {
var matches = opt.text.match(/.*?(\d+).*/);
if (!matches) {
return 0;
} else {
return parseInt(matches[1], 10);
}
});
}
if (reverseSort) {
options = options.reverse();
}
return options;
}
dependsOn(variable) {
return containsVariable(this.query, this.datasource, variable.name);
}
}
variableTypes['query'] = {
name: 'Query',
ctor: QueryVariable,
description: 'Variable values are fetched from a datasource query',
supportsMulti: true,
};

View File

@ -0,0 +1,40 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {AdhocVariable} from '../adhoc_variable';
describe('AdhocVariable', function() {
describe('when serializing to url', function() {
it('should set return key value and op seperated by pipe', function() {
var variable = new AdhocVariable({
filters: [
{key: 'key1', operator: '=', value: 'value1'},
{key: 'key2', operator: '!=', value: 'value2'},
]
});
var urlValue = variable.getValueForUrl();
expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2"]);
});
});
describe('when deserializing from url', function() {
it('should restore filters', function() {
var variable = new AdhocVariable({});
variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2"]);
expect(variable.filters[0].key).to.be('key1');
expect(variable.filters[0].operator).to.be('=');
expect(variable.filters[0].value).to.be('value1');
expect(variable.filters[1].key).to.be('key2');
expect(variable.filters[1].operator).to.be('!=');
expect(variable.filters[1].value).to.be('value2');
});
});
});

View File

@ -0,0 +1,39 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {QueryVariable} from '../query_variable';
describe('QueryVariable', function() {
describe('when creating from model', function() {
it('should set defaults', function() {
var variable = new QueryVariable({}, null, null, null, null);
expect(variable.datasource).to.be(null);
expect(variable.refresh).to.be(0);
expect(variable.sort).to.be(0);
expect(variable.name).to.be('');
expect(variable.hide).to.be(0);
expect(variable.options.length).to.be(0);
expect(variable.multi).to.be(false);
expect(variable.includeAll).to.be(false);
});
it('get model should copy changes back to model', () => {
var variable = new QueryVariable({}, null, null, null, null);
variable.options = [{text: 'test'}];
variable.datasource = 'google';
variable.regex = 'asd';
variable.sort = 50;
var model = variable.getModel();
expect(model.options.length).to.be(1);
expect(model.options[0].text).to.be('test');
expect(model.datasource).to.be('google');
expect(model.regex).to.be('asd');
expect(model.sort).to.be(50);
});
});
});

View File

@ -0,0 +1,237 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import '../all';
import {Emitter} from 'app/core/core';
describe('templateSrv', function() {
var _templateSrv, _variableSrv;
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.inject(function(variableSrv, templateSrv) {
_templateSrv = templateSrv;
_variableSrv = variableSrv;
}));
function initTemplateSrv(variables) {
_variableSrv.init({
templating: {list: variables},
events: new Emitter(),
});
}
describe('init', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle'}}]);
});
it('should initialize template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.oogle.filters');
});
});
describe('replace can pass scoped vars', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle' }}]);
});
it('should replace $test with scoped value', function() {
var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.mupp.filters');
});
it('should replace $test with scoped text', function() {
var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.asd.filters');
});
});
describe('replace can pass multi / all format', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: ['value1', 'value2'] }}]);
});
it('should replace $test with globbed value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
});
describe('variable with all option', function() {
beforeEach(function() {
initTemplateSrv([{
type: 'query',
name: 'test',
current: {value: '$__all' },
options: [
{value: '$__all'}, {value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
});
describe('variable with all option and custom value', function() {
beforeEach(function() {
initTemplateSrv([{
type: 'query',
name: 'test',
current: {value: '$__all' },
allValue: '*',
options: [
{value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.*.filters');
});
it('should not escape custom all value', function() {
var target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).to.be('this.*');
});
});
describe('lucene format', function() {
it('should properly escape $test with lucene escape sequences', function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: 'value/4' }}]);
var target = _templateSrv.replace('this:$test', {}, 'lucene');
expect(target).to.be("this:value\\\/4");
});
});
describe('format variable to string values', function() {
it('single value should return value', function() {
var result = _templateSrv.formatValue('test');
expect(result).to.be('test');
});
it('multi value and glob format should render glob string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'glob');
expect(result).to.be('{test,test2}');
});
it('multi value and lucene should render as lucene expr', function() {
var result = _templateSrv.formatValue(['test','test2'], 'lucene');
expect(result).to.be('("test" OR "test2")');
});
it('multi value and regex format should render regex string', function() {
var result = _templateSrv.formatValue(['test.','test2'], 'regex');
expect(result).to.be('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'pipe');
expect(result).to.be('test|test2');
});
it('slash should be properly escaped in regex format', function() {
var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14');
});
});
describe('can check if variable exists', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should return true if exists', function() {
var result = _templateSrv.variableExists('$test');
expect(result).to.be(true);
});
});
describe('can hightlight variables in string', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should insert html', function() {
var result = _templateSrv.highlightVariablesAsHtml('$test');
expect(result).to.be('<span class="template-variable">$test</span>');
});
it('should insert html anywhere in string', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $test ok');
expect(result).to.be('this <span class="template-variable">$test</span> ok');
});
it('should ignore if variables does not exist', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $google ok');
expect(result).to.be('this $google ok');
});
});
describe('updateTemplateData with simple value', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: 'muuuu' } }]);
});
it('should set current value and update template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.muuuu.filters');
});
});
describe('fillVariableValuesForUrl with multi value', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set multiple url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).to.eql(['val1', 'val2']);
});
});
describe('fillVariableValuesForUrl with multi value and scopedVars', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set scoped value as url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}});
expect(params['var-test']).to.eql('val1');
});
});
describe('replaceWithText', function() {
beforeEach(function() {
initTemplateSrv([
{type: 'query', name: 'server', current: { value: '{asd,asd2}', text: 'All' } },
{type: 'interval', name: 'period', current: { value: '$__auto_interval', text: 'auto' } }
]);
_templateSrv.setGrafanaVariable('$__auto_interval', '13m');
_templateSrv.updateTemplateData();
});
it('should replace with text except for grafanaVariables', function() {
var target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).to.be('Server: All, period: 13m');
});
});
});

View File

@ -0,0 +1,59 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {containsVariable, assignModelProperties} from '../variable';
describe('containsVariable', function() {
describe('when checking if a string contains a variable', function() {
it('should find it with $var syntax', function() {
var contains = containsVariable('this.$test.filters', 'test');
expect(contains).to.be(true);
});
it('should not find it if only part matches with $var syntax', function() {
var contains = containsVariable('this.$ServerDomain.filters', 'Server');
expect(contains).to.be(false);
});
it('should find it with [[var]] syntax', function() {
var contains = containsVariable('this.[[test]].filters', 'test');
expect(contains).to.be(true);
});
it('should find it when part of segment', function() {
var contains = containsVariable('metrics.$env.$group-*', 'group');
expect(contains).to.be(true);
});
it('should find it its the only thing', function() {
var contains = containsVariable('$env', 'env');
expect(contains).to.be(true);
});
it('should be able to pass in multiple test strings', function() {
var contains = containsVariable('asd','asd2.$env', 'env');
expect(contains).to.be(true);
});
});
});
describe('assignModelProperties', function() {
it('only set properties defined in defaults', function() {
var target: any = {test: 'asd'};
assignModelProperties(target, {propA: 1, propB: 2}, {propB: 0});
expect(target.propB).to.be(2);
expect(target.test).to.be('asd');
});
it('use default value if not found on source', function() {
var target: any = {test: 'asd'};
assignModelProperties(target, {propA: 1, propB: 2}, {propC: 10});
expect(target.propC).to.be(10);
});
});

View File

@ -0,0 +1,142 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import '../all';
import _ from 'lodash';
import helpers from 'test/specs/helpers';
import {Emitter} from 'app/core/core';
describe('VariableSrv init', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
beforeEach(angularMocks.inject(($rootScope, $q, $location, $injector) => {
ctx.$q = $q;
ctx.$rootScope = $rootScope;
ctx.$location = $location;
ctx.variableSrv = $injector.get('variableSrv');
ctx.$rootScope.$digest();
}));
function describeInitScenario(desc, fn) {
describe(desc, function() {
var scenario: any = {
urlParams: {},
setup: setupFn => {
scenario.setupFn = setupFn;
}
};
beforeEach(function() {
scenario.setupFn();
ctx.datasource = {};
ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ctx.datasource));
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
ctx.$location.search = sinon.stub().returns(scenario.urlParams);
ctx.dashboard = {templating: {list: scenario.variables}, events: new Emitter()};
ctx.variableSrv.init(ctx.dashboard);
ctx.$rootScope.$digest();
scenario.variables = ctx.variableSrv.variables;
});
fn(scenario);
});
}
['query', 'interval', 'custom', 'datasource'].forEach(type => {
describeInitScenario('when setting ' + type + ' variable via url', scenario => {
scenario.setup(() => {
scenario.variables = [{
name: 'apps',
type: type,
current: {text: "test", value: "test"},
options: [{text: "test", value: "test"}]
}];
scenario.urlParams["var-apps"] = "new";
});
it('should update current value', () => {
expect(scenario.variables[0].current.value).to.be("new");
expect(scenario.variables[0].current.text).to.be("new");
});
});
});
describe('given dependent variables', () => {
var variableList = [
{
name: 'app',
type: 'query',
query: '',
current: {text: "app1", value: "app1"},
options: [{text: "app1", value: "app1"}]
},
{
name: 'server',
type: 'query',
refresh: 1,
query: '$app.*',
current: {text: "server1", value: "server1"},
options: [{text: "server1", value: "server1"}]
},
];
describeInitScenario('when setting parent var from url', scenario => {
scenario.setup(() => {
scenario.variables = _.cloneDeep(variableList);
scenario.urlParams["var-app"] = "google";
scenario.queryResult = [{text: 'google-server1'}, {text: 'google-server2'}];
});
it('should update child variable', () => {
expect(scenario.variables[1].options.length).to.be(2);
expect(scenario.variables[1].current.text).to.be("google-server1");
});
it('should only update it once', () => {
expect(ctx.datasource.metricFindQuery.callCount).to.be(1);
});
});
});
describeInitScenario('when template variable is present in url multiple times', scenario => {
scenario.setup(() => {
scenario.variables = [{
name: 'apps',
type: 'query',
multi: true,
current: {text: "val1", value: "val1"},
options: [{text: "val1", value: "val1"}, {text: 'val2', value: 'val2'}, {text: 'val3', value: 'val3', selected: true}]
}];
scenario.urlParams["var-apps"] = ["val2", "val1"];
});
it('should update current value', function() {
var variable = ctx.variableSrv.variables[0];
expect(variable.current.value.length).to.be(2);
expect(variable.current.value[0]).to.be("val2");
expect(variable.current.value[1]).to.be("val1");
expect(variable.current.text).to.be("val2 + val1");
expect(variable.options[0].selected).to.be(true);
expect(variable.options[1].selected).to.be(true);
});
it('should set options that are not in value to selected false', function() {
var variable = ctx.variableSrv.variables[0];
expect(variable.options[2].selected).to.be(false);
});
});
});

View File

@ -1,134 +1,114 @@
define([
'../mocks/dashboard-mock',
'./helpers',
'moment',
'app/features/templating/templateValuesSrv'
], function(dashboardMock, helpers, moment) {
'use strict';
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
describe('templateValuesSrv', function() {
var ctx = new helpers.ServiceTestContext();
import '../all';
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
beforeEach(ctx.createService('templateValuesSrv'));
import moment from 'moment';
import helpers from 'test/specs/helpers';
import {Emitter} from 'app/core/core';
describe('update interval variable options', function() {
var variable = { type: 'interval', query: 'auto,1s,2h,5h,1d', name: 'test' };
describe('VariableSrv', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
beforeEach(angularMocks.inject(($rootScope, $q, $location, $injector) => {
ctx.$q = $q;
ctx.$rootScope = $rootScope;
ctx.$location = $location;
ctx.variableSrv = $injector.get('variableSrv');
ctx.variableSrv.init({
templating: {list: []},
events: new Emitter(),
});
ctx.$rootScope.$digest();
}));
function describeUpdateVariable(desc, fn) {
describe(desc, function() {
var scenario: any = {};
scenario.setup = function(setupFn) {
scenario.setupFn = setupFn;
};
beforeEach(function() {
ctx.service.updateOptions(variable);
});
scenario.setupFn();
var ds: any = {};
ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
it('should update options array', function() {
expect(variable.options.length).to.be(5);
expect(variable.options[1].text).to.be('1s');
expect(variable.options[1].value).to.be('1s');
});
});
describe('when template variable is present in url', function() {
var variable = {
name: 'apps',
current: {text: "test", value: "test"},
options: [{text: "test", value: "test"}]
};
beforeEach(function(done) {
var dashboard = { templating: { list: [variable] } };
var urlParams = {};
urlParams["var-apps"] = "new";
ctx.$location.search = sinon.stub().returns(urlParams);
ctx.service.init(dashboard).then(function() { done(); });
scenario.variable = ctx.variableSrv.addVariable(scenario.variableModel);
ctx.variableSrv.updateOptions(scenario.variable);
ctx.$rootScope.$digest();
});
it('should update current value', function() {
expect(variable.current.value).to.be("new");
expect(variable.current.text).to.be("new");
});
fn(scenario);
});
}
describeUpdateVariable('interval variable without auto', scenario => {
scenario.setup(() => {
scenario.variableModel = {type: 'interval', query: '1s,2h,5h,1d', name: 'test'};
});
describe('when template variable is present in url multiple times', function() {
var variable = {
name: 'apps',
multi: true,
current: {text: "val1", value: "val1"},
options: [{text: "val1", value: "val1"}, {text: 'val2', value: 'val2'}, {text: 'val3', value: 'val3', selected: true}]
it('should update options array', () => {
expect(scenario.variable.options.length).to.be(4);
expect(scenario.variable.options[0].text).to.be('1s');
expect(scenario.variable.options[0].value).to.be('1s');
});
});
//
// Interval variable update
//
describeUpdateVariable('interval variable with auto', scenario => {
scenario.setup(() => {
scenario.variableModel = {type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 };
var range = {
from: moment(new Date()).subtract(7, 'days').toDate(),
to: new Date()
};
beforeEach(function(done) {
var dashboard = { templating: { list: [variable] } };
var urlParams = {};
urlParams["var-apps"] = ["val2", "val1"];
ctx.$location.search = sinon.stub().returns(urlParams);
ctx.service.init(dashboard).then(function() { done(); });
ctx.$rootScope.$digest();
});
it('should update current value', function() {
expect(variable.current.value.length).to.be(2);
expect(variable.current.value[0]).to.be("val2");
expect(variable.current.value[1]).to.be("val1");
expect(variable.current.text).to.be("val2 + val1");
expect(variable.options[0].selected).to.be(true);
expect(variable.options[1].selected).to.be(true);
});
it('should set options that are not in value to selected false', function() {
expect(variable.options[2].selected).to.be(false);
});
ctx.timeSrv.timeRange = sinon.stub().returns(range);
ctx.templateSrv.setGrafanaVariable = sinon.spy();
});
function describeUpdateVariable(desc, fn) {
describe(desc, function() {
var scenario = {};
scenario.setup = function(setupFn) {
scenario.setupFn = setupFn;
};
beforeEach(function() {
scenario.setupFn();
var ds = {};
ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
ctx.service.updateOptions(scenario.variable);
ctx.$rootScope.$digest();
});
fn(scenario);
});
}
describeUpdateVariable('interval variable without auto', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(4);
expect(scenario.variable.options[0].text).to.be('1s');
expect(scenario.variable.options[0].value).to.be('1s');
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(5);
expect(scenario.variable.options[0].text).to.be('auto');
expect(scenario.variable.options[0].value).to.be('$__auto_interval');
});
describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: '', name: 'test', current: {} };
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
it('should set $__auto_interval', function() {
var call = ctx.templateSrv.setGrafanaVariable.getCall(0);
expect(call.args[0]).to.be('$__auto_interval');
expect(call.args[1]).to.be('12h');
});
});
it('should set current value to first option', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.current.value).to.be('backend1');
});
//
// Query variable update
//
describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: '', name: 'test', current: {}};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
describeUpdateVariable('query variable with multi select and new options does not contain some selected values', function(scenario) {
it('should set current value to first option', function() {
expect(scenario.variable.options.length).to.be(2);
expect(scenario.variable.current.value).to.be('backend1');
});
});
describeUpdateVariable('query variable with multi select and new options does not contain some selected values', function(scenario) {
scenario.setup(function() {
scenario.variable = {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
@ -148,7 +128,7 @@ define([
describeUpdateVariable('query variable with multi select and new options does not contain any selected values', function(scenario) {
scenario.setup(function() {
scenario.variable = {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
@ -168,7 +148,7 @@ define([
describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
scenario.setup(function() {
scenario.variable = {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
@ -189,7 +169,7 @@ define([
describeUpdateVariable('query variable with numeric results', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: '', name: 'test', current: {} };
scenario.variableModel = { type: 'query', query: '', name: 'test', current: {} };
scenario.queryResult = [{text: 12, value: 12}];
});
@ -200,65 +180,9 @@ define([
});
});
describeUpdateVariable('interval variable without auto', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(4);
expect(scenario.variable.options[0].text).to.be('1s');
expect(scenario.variable.options[0].value).to.be('1s');
});
});
describeUpdateVariable('interval variable with auto', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 };
var range = {
from: moment(new Date()).subtract(7, 'days').toDate(),
to: new Date()
};
ctx.timeSrv.timeRange = sinon.stub().returns(range);
ctx.templateSrv.setGrafanaVariable = sinon.spy();
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(5);
expect(scenario.variable.options[0].text).to.be('auto');
expect(scenario.variable.options[0].value).to.be('$__auto_interval');
});
it('should set $__auto_interval', function() {
var call = ctx.templateSrv.setGrafanaVariable.getCall(0);
expect(call.args[0]).to.be('$__auto_interval');
expect(call.args[1]).to.be('12h');
});
});
describeUpdateVariable('update custom variable', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'custom', query: 'hej, hop, asd', name: 'test'};
});
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(3);
expect(scenario.variable.options[0].text).to.be('hej');
expect(scenario.variable.options[1].value).to.be('hop');
});
it('should set $__auto_interval', function() {
var call = ctx.templateSrv.setGrafanaVariable.getCall(0);
expect(call.args[0]).to.be('$__auto_interval');
expect(call.args[1]).to.be('12h');
});
});
describeUpdateVariable('basic query variable', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
@ -276,8 +200,8 @@ define([
describeUpdateVariable('and existing value still exists in options', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.current = { value: 'backend2', text: 'backend2'};
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.current = { value: 'backend2', text: 'backend2'};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}];
});
@ -288,8 +212,8 @@ define([
describeUpdateVariable('and regex pattern exists', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = '/apps.*(backend_[0-9]+)/';
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
@ -300,8 +224,8 @@ define([
describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = '/apps.*(backendasd[0-9]+)/';
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
@ -313,8 +237,8 @@ define([
describeUpdateVariable('regex pattern without slashes', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = 'backend_01';
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = 'backend_01';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}];
});
@ -325,8 +249,8 @@ define([
describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variable.regex = 'backend_01';
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test'};
scenario.variableModel.regex = '/backend_01/';
scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_01.counters.req'}];
});
@ -337,7 +261,7 @@ define([
describeUpdateVariable('with include All', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', includeAll: true};
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', includeAll: true};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
@ -349,7 +273,7 @@ define([
describeUpdateVariable('with include all and custom value', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allValue: '*' };
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', includeAll: true, allValue: '*'};
scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
@ -358,9 +282,77 @@ define([
});
});
describeUpdateVariable('without sort', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 0};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options without sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('aaa10');
expect(scenario.variable.options[2].text).to.be('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 1};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 2};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('ccc3');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 3};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variableModel = {type: 'query', query: 'apps.*', name: 'test', sort: 4};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('bbb2');
});
});
//
// datasource variable update
//
describeUpdateVariable('datasource variable with regex filter', function(scenario) {
scenario.setup(function() {
scenario.variable = {
scenario.variableModel = {
type: 'datasource',
query: 'graphite',
name: 'test',
@ -386,69 +378,18 @@ define([
});
});
describeUpdateVariable('without sort', function(scenario) {
//
// Custom variable update
//
describeUpdateVariable('update custom variable', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 0};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
scenario.variableModel = {type: 'custom', query: 'hej, hop, asd', name: 'test'};
});
it('should return options without sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('aaa10');
expect(scenario.variable.options[2].text).to.be('ccc3');
it('should update options array', function() {
expect(scenario.variable.options.length).to.be(3);
expect(scenario.variable.options[0].text).to.be('hej');
expect(scenario.variable.options[1].value).to.be('hop');
});
});
describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 1};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 2};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with alphabetical sort', function() {
expect(scenario.variable.options[0].text).to.be('ccc3');
expect(scenario.variable.options[1].text).to.be('bbb2');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (asc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 3};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('bbb2');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('aaa10');
});
});
describeUpdateVariable('with numerical sort (desc)', function(scenario) {
scenario.setup(function() {
scenario.variable = {type: 'query', query: 'apps.*', name: 'test', sort: 4};
scenario.queryResult = [{text: 'bbb2'}, {text: 'aaa10'}, { text: 'ccc3'}];
});
it('should return options with numerical sort', function() {
expect(scenario.variable.options[0].text).to.be('aaa10');
expect(scenario.variable.options[1].text).to.be('ccc3');
expect(scenario.variable.options[2].text).to.be('bbb2');
});
});
});
});

View File

@ -1,10 +1,9 @@
define([
'angular',
'lodash',
'./editorCtrl',
'./templateValuesSrv',
'app/core/utils/kbn',
],
function (angular, _) {
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
@ -16,6 +15,7 @@ function (angular, _) {
this._index = {};
this._texts = {};
this._grafanaVariables = {};
this._adhocVariables = {};
this.init = function(variables) {
this.variables = variables;
@ -24,19 +24,32 @@ function (angular, _) {
this.updateTemplateData = function() {
this._index = {};
this._filters = {};
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
// add adhoc filters to it's own index
if (variable.type === 'adhoc') {
this._adhocVariables[variable.datasource] = variable;
continue;
}
if (!variable.current || !variable.current.isNone && !variable.current.value) {
continue;
}
this._index[variable.name] = variable;
}
};
function regexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
}
this.getAdhocFilters = function(datasourceName) {
var variable = this._adhocVariables[datasourceName];
if (variable) {
return variable.filters || [];
}
return [];
};
function luceneEscape(value) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
@ -63,10 +76,10 @@ function (angular, _) {
switch(format) {
case "regex": {
if (typeof value === 'string') {
return regexEscape(value);
return kbn.regexEscape(value);
}
var escapedValues = _.map(value, regexEscape);
var escapedValues = _.map(value, kbn.regexEscape);
return '(' + escapedValues.join('|') + ')';
}
case "lucene": {
@ -97,17 +110,6 @@ function (angular, _) {
return match && (self._index[match[1] || match[2]] !== void 0);
};
this.containsVariable = function(str, variableName) {
if (!str) {
return false;
}
variableName = regexEscape(variableName);
var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
var match = findVarRegex.exec(str);
return match !== null;
};
this.highlightVariablesAsHtml = function(str) {
if (!str || !_.isString(str)) { return str; }
@ -196,18 +198,11 @@ function (angular, _) {
this.fillVariableValuesForUrl = function(params, scopedVars) {
_.each(this.variables, function(variable) {
var current = variable.current;
var value = current.value;
if (current.text === 'All') {
value = 'All';
}
if (scopedVars && scopedVars[variable.name] !== void 0) {
value = scopedVars[variable.name].value;
params['var-' + variable.name] = scopedVars[variable.name].value;
} else {
params['var-' + variable.name] = variable.getValueForUrl();
}
params['var-' + variable.name] = value;
});
};

View File

@ -1,412 +0,0 @@
define([
'angular',
'lodash',
'jquery',
'app/core/utils/kbn',
],
function (angular, _, $, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.service('templateValuesSrv', function($q, $rootScope, datasourceSrv, $location, templateSrv, timeSrv) {
var self = this;
this.variableLock = {};
function getNoneOption() { return { text: 'None', value: '', isNone: true }; }
// update time variant variables
$rootScope.onAppEvent('refresh', function() {
// look for interval variables
var intervalVariable = _.find(self.variables, { type: 'interval' });
if (intervalVariable) {
self.updateAutoInterval(intervalVariable);
}
// update variables with refresh === 2
var promises = self.variables
.filter(function(variable) {
return variable.refresh === 2;
}).map(function(variable) {
var previousOptions = variable.options.slice();
return self.updateOptions(variable).then(function () {
return self.variableUpdated(variable).then(function () {
// check if current options changed due to refresh
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
$rootScope.appEvent('template-variable-value-updated');
}
});
});
});
return $q.all(promises);
}, $rootScope);
this.init = function(dashboard) {
this.dashboard = dashboard;
this.variables = dashboard.templating.list;
templateSrv.init(this.variables);
var queryParams = $location.search();
var promises = [];
// use promises to delay processing variables that
// depend on other variables.
this.variableLock = {};
_.forEach(this.variables, function(variable) {
self.variableLock[variable.name] = $q.defer();
});
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
promises.push(this.processVariable(variable, queryParams));
}
return $q.all(promises);
};
this.processVariable = function(variable, queryParams) {
var dependencies = [];
var lock = self.variableLock[variable.name];
// determine our dependencies.
if (variable.type === "query") {
_.forEach(this.variables, function(v) {
// both query and datasource can contain variable
if (templateSrv.containsVariable(variable.query, v.name) ||
templateSrv.containsVariable(variable.datasource, v.name)) {
dependencies.push(self.variableLock[v.name].promise);
}
});
}
return $q.all(dependencies).then(function() {
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
return self.setVariableFromUrl(variable, urlValue).then(lock.resolve);
}
else if (variable.refresh === 1 || variable.refresh === 2) {
return self.updateOptions(variable).then(function() {
if (_.isEmpty(variable.current) && variable.options.length) {
self.setVariableValue(variable, variable.options[0]);
}
lock.resolve();
});
}
else if (variable.type === 'interval') {
self.updateAutoInterval(variable);
lock.resolve();
} else {
lock.resolve();
}
}).finally(function() {
delete self.variableLock[variable.name];
});
};
this.setVariableFromUrl = function(variable, urlValue) {
var promise = $q.when(true);
if (variable.refresh) {
promise = this.updateOptions(variable);
}
return promise.then(function() {
var option = _.find(variable.options, function(op) {
return op.text === urlValue || op.value === urlValue;
});
option = option || { text: urlValue, value: urlValue };
self.updateAutoInterval(variable);
return self.setVariableValue(variable, option, true);
});
};
this.updateAutoInterval = function(variable) {
if (!variable.auto) { return; }
// add auto option if missing
if (variable.options.length && variable.options[0].text !== 'auto') {
variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
}
var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count, (variable.auto_min ? ">"+variable.auto_min : null));
templateSrv.setGrafanaVariable('$__auto_interval', interval);
};
this.setVariableValue = function(variable, option) {
variable.current = angular.copy(option);
if (_.isArray(variable.current.text)) {
variable.current.text = variable.current.text.join(' + ');
}
self.selectOptionsForCurrentValue(variable);
templateSrv.updateTemplateData();
return this.updateOptionsInChildVariables(variable);
};
this.variableUpdated = function(variable) {
templateSrv.updateTemplateData();
return self.updateOptionsInChildVariables(variable);
};
this.updateOptionsInChildVariables = function(updatedVariable) {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
if (self.variableLock[updatedVariable.name]) {
return $q.when();
}
var promises = _.map(self.variables, function(otherVariable) {
if (otherVariable === updatedVariable) {
return;
}
if ((otherVariable.type === "datasource" &&
templateSrv.containsVariable(otherVariable.regex, updatedVariable.name)) ||
templateSrv.containsVariable(otherVariable.query, updatedVariable.name) ||
templateSrv.containsVariable(otherVariable.datasource, updatedVariable.name)) {
return self.updateOptions(otherVariable);
}
});
return $q.all(promises);
};
this._updateNonQueryVariable = function(variable) {
if (variable.type === 'datasource') {
self.updateDataSourceVariable(variable);
return;
}
if (variable.type === 'constant') {
variable.options = [{text: variable.query, value: variable.query}];
return;
}
// extract options in comma separated string
variable.options = _.map(variable.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() };
});
if (variable.type === 'interval') {
self.updateAutoInterval(variable);
return;
}
if (variable.type === 'custom' && variable.includeAll) {
self.addAllOption(variable);
}
};
this.updateDataSourceVariable = function(variable) {
var options = [];
var sources = datasourceSrv.getMetricSources({skipVariables: true});
var regex;
if (variable.regex) {
regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
}
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// must match on type
if (source.meta.id !== variable.query) {
continue;
}
if (regex && !regex.exec(source.name)) {
continue;
}
options.push({text: source.name, value: source.name});
}
if (options.length === 0) {
options.push({text: 'No data sources found', value: ''});
}
variable.options = options;
};
this.updateOptions = function(variable) {
if (variable.type !== 'query') {
self._updateNonQueryVariable(variable);
return self.validateVariableSelectionState(variable);
}
return datasourceSrv.get(variable.datasource)
.then(_.partial(this.updateOptionsFromMetricFindQuery, variable))
.then(_.partial(this.updateTags, variable))
.then(_.partial(this.validateVariableSelectionState, variable));
};
this.selectOptionsForCurrentValue = function(variable) {
var i, y, value, option;
var selected = [];
for (i = 0; i < variable.options.length; i++) {
option = variable.options[i];
option.selected = false;
if (_.isArray(variable.current.value)) {
for (y = 0; y < variable.current.value.length; y++) {
value = variable.current.value[y];
if (option.value === value) {
option.selected = true;
selected.push(option);
}
}
} else if (option.value === variable.current.value) {
option.selected = true;
selected.push(option);
}
}
return selected;
};
this.validateVariableSelectionState = function(variable) {
if (!variable.current) {
if (!variable.options.length) { return; }
return self.setVariableValue(variable, variable.options[0], false);
}
if (_.isArray(variable.current.value)) {
var selected = self.selectOptionsForCurrentValue(variable);
// if none pick first
if (selected.length === 0) {
selected = variable.options[0];
} else {
selected = {
value: _.map(selected, function(val) {return val.value;}),
text: _.map(selected, function(val) {return val.text;}).join(' + '),
};
}
return self.setVariableValue(variable, selected, false);
} else {
var currentOption = _.find(variable.options, {text: variable.current.text});
if (currentOption) {
return self.setVariableValue(variable, currentOption, false);
} else {
if (!variable.options.length) { return $q.when(null); }
return self.setVariableValue(variable, variable.options[0]);
}
}
};
this.updateTags = function(variable, datasource) {
if (variable.useTags) {
return datasource.metricFindQuery(variable.tagsQuery).then(function (results) {
variable.tags = [];
for (var i = 0; i < results.length; i++) {
variable.tags.push(results[i].text);
}
return datasource;
});
} else {
delete variable.tags;
}
return datasource;
};
this.updateOptionsFromMetricFindQuery = function(variable, datasource) {
return datasource.metricFindQuery(variable.query).then(function (results) {
variable.options = self.metricNamesToVariableValues(variable, results);
if (variable.includeAll) {
self.addAllOption(variable);
}
if (!variable.options.length) {
variable.options.push(getNoneOption());
}
return datasource;
});
};
this.getValuesForTag = function(variable, tagKey) {
return datasourceSrv.get(variable.datasource).then(function(datasource) {
var query = variable.tagValuesQuery.replace('$tag', tagKey);
return datasource.metricFindQuery(query).then(function (results) {
return _.map(results, function(value) {
return value.text;
});
});
});
};
this.metricNamesToVariableValues = function(variable, metricNames) {
var regex, options, i, matches;
options = [];
if (variable.regex) {
regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
}
for (i = 0; i < metricNames.length; i++) {
var item = metricNames[i];
var value = item.value || item.text;
var text = item.text || item.value;
if (_.isNumber(value)) {
value = value.toString();
}
if (_.isNumber(text)) {
text = text.toString();
}
if (regex) {
matches = regex.exec(value);
if (!matches) { continue; }
if (matches.length > 1) {
value = matches[1];
text = value;
}
}
options.push({text: text, value: value});
}
options = _.uniq(options, 'value');
return this.sortVariableValues(options, variable.sort);
};
this.addAllOption = function(variable) {
variable.options.unshift({text: 'All', value: "$__all"});
};
this.sortVariableValues = function(options, sortOrder) {
if (sortOrder === 0) {
return options;
}
var sortType = Math.ceil(sortOrder / 2);
var reverseSort = (sortOrder % 2 === 0);
if (sortType === 1) {
options = _.sortBy(options, 'text');
} else if (sortType === 2) {
options = _.sortBy(options, function(opt) {
var matches = opt.text.match(/.*?(\d+).*/);
if (!matches) {
return 0;
} else {
return parseInt(matches[1], 10);
}
});
}
if (reverseSort) {
options = options.reverse();
}
return options;
};
});
});

View File

@ -0,0 +1,40 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
export interface Variable {
setValue(option);
updateOptions();
dependsOn(variable);
setValueFromUrl(urlValue);
getValueForUrl();
getModel();
}
export var variableTypes = {};
export function assignModelProperties(target, source, defaults) {
_.forEach(defaults, function(value, key) {
target[key] = source[key] === undefined ? value : source[key];
});
}
export function containsVariable(...args: any[]) {
var variableName = args[args.length-1];
var str = args[0] || '';
for (var i = 1; i < args.length-1; i++) {
str += args[i] || '';
}
variableName = kbn.regexEscape(variableName);
var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
var match = findVarRegex.exec(str);
return match !== null;
}

View File

@ -0,0 +1,234 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import {Variable, variableTypes} from './variable';
export class VariableSrv {
dashboard: any;
variables: any;
variableLock: any;
/** @ngInject */
constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
// update time variant variables
$rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope);
$rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
}
init(dashboard) {
this.variableLock = {};
this.dashboard = dashboard;
// create working class models representing variables
this.variables = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
this.templateSrv.init(this.variables);
// register event to sync back to persisted model
this.dashboard.events.on('prepare-save-model', this.syncToDashboardModel.bind(this));
// init variables
for (let variable of this.variables) {
this.variableLock[variable.name] = this.$q.defer();
}
var queryParams = this.$location.search();
return this.$q.all(this.variables.map(variable => {
return this.processVariable(variable, queryParams);
})).then(() => {
this.templateSrv.updateTemplateData();
});
}
onDashboardRefresh() {
var promises = this.variables
.filter(variable => variable.refresh === 2)
.map(variable => {
var previousOptions = variable.options.slice();
return variable.updateOptions()
.then(this.variableUpdated.bind(this, variable))
.then(() => {
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
this.$rootScope.$emit('template-variable-value-updated');
}
});
});
return this.$q.all(promises);
}
processVariable(variable, queryParams) {
var dependencies = [];
var lock = this.variableLock[variable.name];
for (let otherVariable of this.variables) {
if (variable.dependsOn(otherVariable)) {
dependencies.push(this.variableLock[otherVariable.name].promise);
}
}
return this.$q.all(dependencies).then(() => {
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
return variable.setValueFromUrl(urlValue).then(lock.resolve);
}
if (variable.refresh === 1 || variable.refresh === 2) {
return variable.updateOptions().then(lock.resolve);
}
lock.resolve();
}).finally(() => {
delete this.variableLock[variable.name];
});
}
createVariableFromModel(model) {
var ctor = variableTypes[model.type].ctor;
if (!ctor) {
throw "Unable to find variable constructor for " + model.type;
}
var variable = this.$injector.instantiate(ctor, {model: model});
return variable;
}
addVariable(model) {
var variable = this.createVariableFromModel(model);
this.variables.push(this.createVariableFromModel(variable));
return variable;
}
syncToDashboardModel() {
this.dashboard.templating.list = this.variables.map(variable => {
return variable.getModel();
});
}
updateOptions(variable) {
return variable.updateOptions();
}
variableUpdated(variable) {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
if (this.variableLock[variable.name]) {
return this.$q.when();
}
// cascade updates to variables that use this variable
var promises = _.map(this.variables, otherVariable => {
if (otherVariable === variable) {
return;
}
if (otherVariable.dependsOn(variable)) {
return this.updateOptions(otherVariable);
}
});
return this.$q.all(promises);
}
selectOptionsForCurrentValue(variable) {
var i, y, value, option;
var selected: any = [];
for (i = 0; i < variable.options.length; i++) {
option = variable.options[i];
option.selected = false;
if (_.isArray(variable.current.value)) {
for (y = 0; y < variable.current.value.length; y++) {
value = variable.current.value[y];
if (option.value === value) {
option.selected = true;
selected.push(option);
}
}
} else if (option.value === variable.current.value) {
option.selected = true;
selected.push(option);
}
}
return selected;
}
validateVariableSelectionState(variable) {
if (!variable.current) {
variable.current = {};
}
if (_.isArray(variable.current.value)) {
var selected = this.selectOptionsForCurrentValue(variable);
// if none pick first
if (selected.length === 0) {
selected = variable.options[0];
} else {
selected = {
value: _.map(selected, function(val) {return val.value;}),
text: _.map(selected, function(val) {return val.text;}).join(' + '),
};
}
return variable.setValue(selected);
} else {
var currentOption = _.find(variable.options, {text: variable.current.text});
if (currentOption) {
return variable.setValue(currentOption);
} else {
if (!variable.options.length) { return Promise.resolve(); }
return variable.setValue(variable.options[0]);
}
}
}
setOptionFromUrl(variable, urlValue) {
var promise = this.$q.when();
if (variable.refresh) {
promise = variable.updateOptions();
}
return promise.then(() => {
var option = _.find(variable.options, op => {
return op.text === urlValue || op.value === urlValue;
});
option = option || {text: urlValue, value: urlValue};
return variable.setValue(option);
});
}
setOptionAsCurrent(variable, option) {
variable.current = _.cloneDeep(option);
if (_.isArray(variable.current.text)) {
variable.current.text = variable.current.text.join(' + ');
}
this.selectOptionsForCurrentValue(variable);
return this.variableUpdated(variable);
}
updateUrlParamsWithCurrentVariables() {
// update url
var params = this.$location.search();
// remove variable params
_.each(params, function(value, key) {
if (key.indexOf('var-') === 0) {
delete params[key];
}
});
// add new values
this.templateSrv.fillVariableValuesForUrl(params);
// update url
this.$location.search(params);
}
}
coreModule.service('variableSrv', VariableSrv);

View File

@ -1,5 +1,5 @@
<div class="variable-link-wrapper">
<a ng-click="vm.show()" class="variable-value-link">
<a ng-click="vm.show()" class="gf-form-label variable-value-link">
{{vm.linkText}}
<span ng-repeat="tag in vm.selectedTags" bs-tooltip='tag.valuesText' data-placement="bottom">
<span class="label-tag"tag-color-from-name="tag.text">
@ -10,7 +10,7 @@
<i class="fa fa-caret-down"></i>
</a>
<input type="text" class="hidden-input input-small" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
<input type="text" class="hidden-input input-small gf-form-input" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
<div class="variable-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
<div class="variable-options-wrapper">

View File

@ -23,6 +23,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
var queries = [];
options = angular.copy(options);
options.targets = this.expandTemplateVariable(options.targets, templateSrv);
_.each(options.targets, function(target) {
if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) {
return;
@ -337,6 +338,37 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
});
}
this.getExpandedVariables = function(target, dimensionKey, variable) {
return _.chain(variable.options)
.filter(function(v) {
return v.selected;
})
.map(function(v) {
var t = angular.copy(target);
t.dimensions[dimensionKey] = v.value;
return t;
}).value();
};
this.expandTemplateVariable = function(targets, templateSrv) {
var self = this;
return _.chain(targets)
.map(function(target) {
var dimensionKey = _.findKey(target.dimensions, function(v) {
return templateSrv.variableExists(v);
});
if (dimensionKey) {
var variable = _.find(templateSrv.variables, function(variable) {
return templateSrv.containsVariable(target.dimensions[dimensionKey], variable.name);
});
return self.getExpandedVariables(target, dimensionKey, variable);
} else {
return [target];
}
}).flatten().value();
};
this.convertToCloudWatchTime = function(date, roundUp) {
if (_.isString(date)) {
date = dateMath.parse(date, roundUp);

View File

@ -98,6 +98,38 @@ describe('CloudWatchDatasource', function() {
});
ctx.$rootScope.$apply();
});
it('should generate the correct targets by expanding template variables', function() {
var templateSrv = {
variables: [
{
name: 'instance_id',
options: [
{ value: 'i-23456789', selected: false },
{ value: 'i-34567890', selected: true }
]
}
],
variableExists: function (e) { return true; },
containsVariable: function (str, variableName) { return str.indexOf('$' + variableName) !== -1; }
};
var targets = [
{
region: 'us-east-1',
namespace: 'AWS/EC2',
metricName: 'CPUUtilization',
dimensions: {
InstanceId: '$instance_id'
},
statistics: ['Average'],
period: 300
}
];
var result = ctx.ds.expandTemplateVariable(targets, templateSrv);
expect(result[0].dimensions.InstanceId).to.be('i-34567890');
});
});
function describeMetricFindQuery(query, func) {

View File

@ -177,11 +177,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var target;
var sentTargets = [];
// add global adhoc filters to timeFilter
var adhocFilters = templateSrv.getAdhocFilters(this.name);
for (var i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (target.hide) {continue;}
var queryObj = this.queryBuilder.build(target);
var queryObj = this.queryBuilder.build(target, adhocFilters);
var esQuery = angular.toJson(queryObj);
var luceneQuery = target.query || '*';
luceneQuery = templateSrv.replace(luceneQuery, options.scopedVars, 'lucene');
@ -213,11 +216,6 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
});
};
function escapeForJson(value) {
var luceneQuery = JSON.stringify(value);
return luceneQuery.substr(1, luceneQuery.length - 2);
}
this.getFields = function(query) {
return this._get('/_mapping').then(function(result) {
var typeMap = {
@ -247,7 +245,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
// Hide meta-fields and check field type
if (key[0] !== '_' &&
(!query.type ||
query.type && typeMap[subObj.type] === query.type)) {
query.type && typeMap[subObj.type] === query.type)) {
fields[fieldName] = {
text: fieldName,
@ -282,12 +280,15 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var header = this.getQueryHeader('count', range.from, range.to);
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
esQuery = esQuery.replace("$lucene_query", escapeForJson(queryDef.query));
esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
esQuery = header + '\n' + esQuery + '\n';
return this._post('_msearch?search_type=count', esQuery).then(function(res) {
if (!res.responses[0].aggregations) {
return [];
}
var buckets = res.responses[0].aggregations["1"].buckets;
return _.map(buckets, function(bucket) {
return {text: bucket.key, value: bucket.key};
@ -310,6 +311,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
return this.getTerms(query);
}
};
this.getTagKeys = function() {
return this.getFields({});
};
this.getTagValues = function(options) {
return this.getTerms({field: options.key, query: '*'});
};
}
return {

View File

@ -98,7 +98,23 @@ function (queryDef) {
return query;
};
ElasticQueryBuilder.prototype.build = function(target) {
ElasticQueryBuilder.prototype.addAdhocFilters = function(query, adhocFilters) {
if (!adhocFilters) {
return;
}
var i, filter, condition;
var must = query.query.filtered.filter.bool.must;
for (i = 0; i < adhocFilters.length; i++) {
filter = adhocFilters[i];
condition = {};
condition[filter.key] = filter.value;
must.push({"term": condition});
}
};
ElasticQueryBuilder.prototype.build = function(target, adhocFilters) {
// make sure query has defaults;
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
target.dsType = 'elasticsearch';
@ -125,6 +141,8 @@ function (queryDef) {
}
};
this.addAdhocFilters(query, adhocFilters);
// handle document query
if (target.bucketAggs.length === 0) {
metric = target.metrics[0];
@ -203,12 +221,6 @@ function (queryDef) {
"size": 0,
"query": {
"filtered": {
"query": {
"query_string": {
"analyze_wildcard": true,
"query": '$lucene_query',
}
},
"filter": {
"bool": {
"must": [{"range": this.getRangeFilter()}]
@ -217,6 +229,16 @@ function (queryDef) {
}
}
};
if (queryDef.query) {
query.query.filtered.query = {
"query_string": {
"analyze_wildcard": true,
"query": queryDef.query,
}
};
}
query.aggs = {
"1": {
"terms": {

View File

@ -238,4 +238,16 @@ describe('ElasticQueryBuilder', function() {
expect(firstLevel.aggs["2"].derivative.buckets_path).to.be("3");
});
it('with adhoc filters', function() {
var query = builder.build({
metrics: [{type: 'Count', id: '0'}],
timeField: '@timestamp',
bucketAggs: [{type: 'date_histogram', field: '@timestamp', id: '3'}],
}, [
{key: 'key1', operator: '=', value: 'value1'}
]);
expect(query.query.filtered.filter.bool.must[1].term["key1"]).to.be("value1");
});
});

View File

@ -134,13 +134,7 @@ for (var i = 0; i < 128; i++) {
i >= 97 && i <= 122; // a-z
}
var identifierPartTable = [];
for (var i2 = 0; i2 < 128; i2++) {
identifierPartTable[i2] =
identifierStartTable[i2] || // $, _, A-Z, a-z
i2 >= 48 && i2 <= 57; // 0-9
}
var identifierPartTable = identifierStartTable;
export function Lexer(expression) {
this.input = expression;
@ -423,256 +417,260 @@ Lexer.prototype = {
if (char === '-') {
value += char;
index += 1;
char = this.peek(index);
}
char = this.peek(index);
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
while (index < length) {
char = this.peek(index);
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 16,
isMalformed: false,
pos: this.char
};
}
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} else if (!isOctalDigit(char)) {
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: false
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(+value)
};
},
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: ch1,
value: ch1,
pos: this.char
};
}
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
type: 'number',
value: value,
isUnclosed: true,
quote: quote,
base: 16,
isMalformed: false,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
value += char;
this.skip(jump);
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} if (!isOctalDigit(char)) {
// if the char is a non punctuator then its not a valid number
if (!this.isPunctuator(char)) {
return null;
}
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: bad
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
this.skip();
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(+value)
};
},
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
type: ch1,
value: ch1,
pos: this.char
};
},
}
};
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
value: value,
isUnclosed: true,
quote: quote,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
value += char;
this.skip(jump);
}
this.skip();
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
pos: this.char
};
},
};

View File

@ -100,10 +100,7 @@ Parser.prototype = {
},
metricExpression: function() {
if (!this.match('templateStart') &&
!this.match('identifier') &&
!this.match('number') &&
!this.match('{')) {
if (!this.match('templateStart') && !this.match('identifier') && !this.match('number') && !this.match('{')) {
return null;
}

View File

@ -62,6 +62,14 @@ describe('when lexing graphite expression', function() {
expect(tokens[4].type).to.be('identifier');
});
it('should tokenize metric expression with segment that start with number', function() {
var lexer = new Lexer("metric.001-server");
var tokens = lexer.tokenize();
expect(tokens[0].type).to.be('identifier');
expect(tokens[2].type).to.be('identifier');
expect(tokens.length).to.be(3);
});
it('should tokenize func call with numbered metric and number arg', function() {
var lexer = new Lexer("scale(metric.10, 15)");
var tokens = lexer.tokenize();

View File

@ -7,6 +7,8 @@ import * as dateMath from 'app/core/utils/datemath';
import InfluxSeries from './influx_series';
import InfluxQuery from './influx_query';
import ResponseParser from './response_parser';
import InfluxQueryBuilder from './query_builder';
export default class InfluxDatasource {
type: string;
@ -43,19 +45,23 @@ export default class InfluxDatasource {
query(options) {
var timeFilter = this.getTimeFilter(options);
var scopedVars = options.scopedVars ? _.cloneDeep(options.scopedVars) : {};
var targets = _.cloneDeep(options.targets);
var queryTargets = [];
var queryModel;
var i, y;
var allQueries = _.map(options.targets, (target) => {
var allQueries = _.map(targets, target => {
if (target.hide) { return ""; }
queryTargets.push(target);
// build query
var queryModel = new InfluxQuery(target, this.templateSrv, options.scopedVars);
var query = queryModel.render(true);
query = query.replace(/\$interval/g, (target.interval || options.interval));
return query;
scopedVars.interval = {value: target.interval || options.interval};
queryModel = new InfluxQuery(target, this.templateSrv, scopedVars);
return queryModel.render(true);
}).reduce((acc, current) => {
if (current !== "") {
acc += ";" + current;
@ -63,11 +69,21 @@ export default class InfluxDatasource {
return acc;
});
if (allQueries === '') {
return this.$q.when({data: []});
}
// add global adhoc filters to timeFilter
var adhocFilters = this.templateSrv.getAdhocFilters(this.name);
if (adhocFilters.length > 0 ) {
timeFilter += ' AND ' + queryModel.renderAdhocFilters(adhocFilters);
}
// replace grafana variables
allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
scopedVars.timeFilter = {value: timeFilter};
// replace templated variables
allQueries = this.templateSrv.replace(allQueries, options.scopedVars);
allQueries = this.templateSrv.replace(allQueries, scopedVars);
return this._seriesQuery(allQueries).then((data): any => {
if (!data || !data.results) {
@ -102,7 +118,7 @@ export default class InfluxDatasource {
}
}
return { data: seriesList };
return {data: seriesList};
});
};
@ -124,16 +140,23 @@ export default class InfluxDatasource {
};
metricFindQuery(query) {
var interpolated;
try {
interpolated = this.templateSrv.replace(query, null, 'regex');
} catch (err) {
return this.$q.reject(err);
}
var interpolated = this.templateSrv.replace(query, null, 'regex');
return this._seriesQuery(interpolated)
.then(_.curry(this.responseParser.parse)(query));
};
}
getTagKeys(options) {
var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
var query = queryBuilder.buildExploreQuery('TAG_KEYS');
return this.metricFindQuery(query);
}
getTagValues(options) {
var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
return this.metricFindQuery(query);
}
_seriesQuery(query) {
if (!query) { return this.$q.when({results: []}); }
@ -141,7 +164,6 @@ export default class InfluxDatasource {
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
}
serializeParams(params) {
if (!params) { return '';}

View File

@ -2,6 +2,7 @@
import _ from 'lodash';
import queryPart from './query_part';
import kbn from 'app/core/utils/kbn';
export default class InfluxQuery {
target: any;
@ -155,7 +156,7 @@ export default class InfluxQuery {
if (operator !== '>' && operator !== '<') {
value = "'" + value.replace(/\\/g, '\\\\') + "'";
}
} else if (interpolate){
} else if (interpolate) {
value = this.templateSrv.replace(value, this.scopedVars, 'regex');
}
@ -181,12 +182,26 @@ export default class InfluxQuery {
return policy + measurement;
}
interpolateQueryStr(value, variable, defaultFormatFn) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return value;
}
if (typeof value === 'string') {
return kbn.regexEscape(value);
}
var escapedValues = _.map(value, kbn.regexEscape);
return escapedValues.join('|');
};
render(interpolate?) {
var target = this.target;
if (target.rawQuery) {
if (interpolate) {
return this.templateSrv.replace(target.query, this.scopedVars, 'regex');
return this.templateSrv.replace(target.query, this.scopedVars, this.interpolateQueryStr);
} else {
return target.query;
}
@ -236,4 +251,11 @@ export default class InfluxQuery {
return query;
}
renderAdhocFilters(filters) {
var conditions = _.map(filters, (tag, index) => {
return this.renderTagCondition(tag, index, false);
});
return conditions.join(' ');
}
}

View File

@ -237,6 +237,19 @@ describe('InfluxQuery', function() {
expect(query.target.select[0][2].type).to.be('math');
});
describe('when render adhoc filters', function() {
it('should generate correct query segment', function() {
var query = new InfluxQuery({measurement: 'cpu', }, templateSrv, {});
var queryText = query.renderAdhocFilters([
{key: 'key1', operator: '=', value: 'value1'},
{key: 'key2', operator: '!=', value: 'value2'},
]);
expect(queryText).to.be('"key1" = \'value1\' AND "key2" != \'value2\'');
});
});
});
});

View File

@ -40,7 +40,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
return backendSrv.datasourceRequest(options);
};
function regexEscape(value) {
function prometheusSpecialRegexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
}
@ -51,10 +51,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
}
if (typeof value === 'string') {
return regexEscape(value);
return prometheusSpecialRegexEscape(value);
}
var escapedValues = _.map(value, regexEscape);
var escapedValues = _.map(value, prometheusSpecialRegexEscape);
return escapedValues.join('|');
};

View File

@ -354,7 +354,8 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) {
function parseThresholdExpr(expr) {
var match, operator, value, precision;
match = expr.match(/\s*([<=>~]*)\W*(\d+(\.\d+)?)/);
expr = String(expr);
match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/);
if (match) {
operator = match[1];
value = parseFloat(match[2]);

View File

@ -312,5 +312,52 @@ describe('grafanaGraph', function() {
expect(ctx.plotOptions.yaxes[0].max).to.be(0);
});
});
describe('and negative values used', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = '-10';
ctrl.panel.yaxes[0].max = '-13.14';
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min and max to negative', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(-10);
expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14);
});
});
});
graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) {
describe('and Y-Min is 0 and Y-Max is 100', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = 0;
ctrl.panel.yaxes[0].max = 100;
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min to 0 and max to 100', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(0);
expect(ctx.plotOptions.yaxes[0].max).to.be(100);
});
});
describe('and Y-Min is -100 and Y-Max is -10.5', function() {
ctx.setup(function(ctrl, data) {
ctrl.panel.yaxes[0].min = -100;
ctrl.panel.yaxes[0].max = -10.5;
data[0] = new TimeSeries({
datapoints: [[120,10],[160,20]],
alias: 'series1',
});
});
it('should set min to -100 and max to -10.5', function() {
expect(ctx.plotOptions.yaxes[0].min).to.be(-100);
expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5);
});
});
});
});

View File

@ -1,207 +1,125 @@
<div class="editor-row">
<div class="section tight-form-container" style="margin-bottom: 20px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<strong>Big value</strong>
</li>
<li class="tight-form-item">
Prefix
</li>
<li>
<input type="text" class="input-small tight-form-input"
ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
</li>
<li class="tight-form-item">
Value
</li>
<li>
<select class="input-small tight-form-input"
ng-model="ctrl.panel.valueName"
ng-options="f for f in ctrl.valueNameOptions"
ng-change="ctrl.render()"></select>
</li>
<li class="tight-form-item">
Postfix
</li>
<li>
<input type="text" class="input-small tight-form-input last"
ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<strong>Font size</strong>
</li>
<li class="tight-form-item">
Prefix
</li>
<li>
<select class="input-small tight-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
</li>
<li class="tight-form-item">
Value
</li>
<li>
<select class="input-small tight-form-input" ng-model="ctrl.panel.valueFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
</li>
<li class="tight-form-item">
Postfix
</li>
<li>
<select class="input-small tight-form-input last" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<strong>Unit</strong>
</li>
<li class="dropdown" style="width: 266px;"
ng-model="ctrl.panel.format"
dropdown-typeahead="ctrl.unitFormats"
dropdown-typeahead-on-select="ctrl.setUnitFormat($subItem)">
</li>
<li class="tight-form-item">Decimals</li>
<li>
<input type="number" class="input-small tight-form-input last" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
ng-model="ctrl.panel.decimals" ng-change="ctrl.refresh()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Value</h5>
<div class="editor-row">
<div class="section" style="margin-bottom: 20px">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<strong>Coloring</strong>
</li>
<li class="tight-form-item">
Background&nbsp;
<input class="cr1" id="ctrl.panel.colorBackground" type="checkbox"
ng-model="ctrl.panel.colorBackground" ng-checked="ctrl.panel.colorBackground" ng-change="ctrl.render()">
<label for="ctrl.panel.colorBackground" class="cr1"></label>
</li>
<li class="tight-form-item">
Value&nbsp;
<input class="cr1" id="ctrl.panel.colorValue" type="checkbox"
ng-model="ctrl.panel.colorValue" ng-checked="ctrl.panel.colorValue" ng-change="ctrl.render()">
<label for="ctrl.panel.colorValue" class="cr1"></label>
</li>
<li class="tight-form-item">
Thresholds<tip>Define two threshold values&lt;br /&gt; 50,80 will produce: &lt;50 = Green, 50:80 = Yellow, &gt;80 = Red</tip>
</li>
<li>
<input type="text" class="input-large tight-form-input" ng-model="ctrl.panel.thresholds" ng-blur="ctrl.render()" placeholder="50,80"></input>
</li>
<li class="tight-form-item">
Colors
</li>
<li class="tight-form-item">
<spectrum-picker ng-model="ctrl.panel.colors[0]" ng-change="ctrl.render()" ></spectrum-picker>
<spectrum-picker ng-model="ctrl.panel.colors[1]" ng-change="ctrl.render()" ></spectrum-picker>
<spectrum-picker ng-model="ctrl.panel.colors[2]" ng-change="ctrl.render()" ></spectrum-picker>
</li>
<li class="tight-form-item last">
<a class="pointer" ng-click="ctrl.invertColorOrder()">invert order</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-4">Stat</label>
<div class="gf-form-select-wrapper width-7">
<select class="gf-form-input" ng-model="ctrl.panel.valueName" ng-options="f for f in ctrl.valueNameOptions" ng-change="ctrl.render()"></select>
</div>
<label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.panel.valueFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
</div>
</div>
</div>
<div class="editor-row">
<div class="section" style="margin-bottom: 20px">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<strong>Spark lines</strong>
</li>
<li class="tight-form-item">
Show&nbsp;
<input class="cr1" id="ctrl.panel.sparkline.show" type="checkbox"
ng-model="ctrl.panel.sparkline.show" ng-checked="ctrl.panel.sparkline.show" ng-change="ctrl.render()">
<label for="ctrl.panel.sparkline.show" class="cr1"></label>
</li>
<li class="tight-form-item">
Background mode&nbsp;
<input class="cr1" id="ctrl.panel.sparkline.full" type="checkbox"
ng-model="ctrl.panel.sparkline.full" ng-checked="ctrl.panel.sparkline.full" ng-change="ctrl.render()">
<label for="ctrl.panel.sparkline.full" class="cr1"></label>
</li>
<li class="tight-form-item">
Line Color
</li>
<li class="tight-form-item">
<spectrum-picker ng-model="ctrl.panel.sparkline.lineColor" ng-change="ctrl.render()" ></spectrum-picker>
</li>
<li class="tight-form-item">
Fill Color
</li>
<li class="tight-form-item last">
<spectrum-picker ng-model="ctrl.panel.sparkline.fillColor" ng-change="ctrl.render()" ></spectrum-picker>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-4">Prefix</label>
<input type="text" class="gf-form-input width-7" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
<label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
</div>
</div>
</div>
<div class="editor-row">
<div class="section" style="margin-bottom: 20px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<strong>Gauge</strong>
</li>
<li class="tight-form-item">
Show&nbsp;
<input class="cr1" id="panel.gauge.show" type="checkbox"
ng-model="ctrl.panel.gauge.show" ng-checked="ctrl.panel.gauge.show" ng-change="ctrl.render()">
<label for="panel.gauge.show" class="cr1"></label>
</li>
<li class="tight-form-item">
Min
</li>
<li>
<input type="number" class="input-small tight-form-input" ng-model="ctrl.panel.gauge.minValue" ng-blur="ctrl.render()" placeholder="0"></input>
</li>
<li class="tight-form-item last">
Max
</li>
<li>
<input type="number" class="input-small tight-form-input last" ng-model="ctrl.panel.gauge.maxValue" ng-blur="ctrl.render()" placeholder="100"></input>
<span class="alert-state-critical" ng-show="ctrl.invalidGaugeRange">
&nbsp; <i class="fa fa-warning"></i>
Min value is bigger than max.
&nbsp;
</span>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<li class="tight-form-item">
Threshold labels&nbsp;
<input class="cr1" id="panel.gauge.thresholdLabels" type="checkbox" ng-model="ctrl.panel.gauge.thresholdLabels" ng-checked="ctrl.panel.gauge.thresholdLabels" ng-change="ctrl.render()">
<label for="panel.gauge.thresholdLabels" class="cr1"></label>
</li>
<li class="tight-form-item">
Threshold markers&nbsp;
<input class="cr1" id="panel.gauge.thresholdMarkers" type="checkbox" ng-model="ctrl.panel.gauge.thresholdMarkers" ng-checked="ctrl.panel.gauge.thresholdMarkers" ng-change="ctrl.render()">
<label for="panel.gauge.thresholdMarkers" class="cr1"></label>
</li>
<div class="clearfix"></div>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-4">Postfix</label>
<input type="text" class="gf-form-input width-7" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
<label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper">
<select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Unit</h5>
<div class="gf-form-inline">
<div class="gf-form">
<div class="gf-form-dropdown-typeahead" ng-model="ctrl.panel.format" dropdown-typeahead2="ctrl.unitFormats" dropdown-typeahead-on-select="ctrl.setUnitFormat($subItem)"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">Decimals</label>
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" ng-model="ctrl.panel.decimals" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Coloring</h5>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-21">
<label class="gf-form-label width-8">Thresholds
<tip>Define two threshold values&lt;br /&gt; 50,80 will produce: &lt;50 = Green, 50:80 = Yellow, &gt;80 = Red</tip>
</label>
<input type="text" class="gf-form-input" ng-model="ctrl.panel.thresholds" ng-blur="ctrl.render()" placeholder="50,80"></input>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Colors</label>
<span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.colors[0]" ng-change="ctrl.render()" ></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.colors[1]" ng-change="ctrl.render()" ></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.colors[2]" ng-change="ctrl.render()" ></spectrum-picker>
</span>
<span class="gf-form-label">
<a ng-click="ctrl.invertColorOrder()">
Invert
</a>
</span>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Spark lines</h5>
<gf-form-switch class="gf-form" label-class="width-9" label="Show" checked="ctrl.panel.sparkline.show" on-change="ctrl.render()"></gf-form-switch>
<div ng-if="ctrl.panel.sparkline.show">
<gf-form-switch class="gf-form" label-class="width-9" label="Background mode" checked="ctrl.panel.sparkline.full" on-change="ctrl.render()"></gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-9">Line Color</label>
<span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.sparkline.lineColor" ng-change="ctrl.render()" ></spectrum-picker>
</span>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Fill Color</label>
<span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.sparkline.fillColor" ng-change="ctrl.render()" ></spectrum-picker>
</span>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Gauge</h5>
<gf-form-switch class="gf-form" label-class="width-9" label="Show" checked="ctrl.panel.gauge.show" on-change="ctrl.render()"></gf-form-switch>
<div ng-if="ctrl.panel.gauge.show">
<div class="gf-form">
<label class="gf-form-label width-9">Min</label>
<input type="number" class="gf-form-input width-6" placeholder="0" data-placement="right" ng-model="ctrl.panel.gauge.minValue" ng-change="ctrl.refresh()" ng-model-onblur>
<label class="gf-form-label alert-state-critical" ng-show="ctrl.invalidGaugeRange">
&nbsp; <i class="fa fa-warning"></i>
Min value is bigger than max.
</label>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Max</label>
<input type="number" class="gf-form-input width-6" placeholder="0" data-placement="right" ng-model="ctrl.panel.gauge.maxValue" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<gf-form-switch class="gf-form" label-class="width-9" label="Threshold labels" checked="ctrl.panel.gauge.thresholdLabels" on-change="ctrl.render()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-9" label="Threshold markers" checked="ctrl.panel.gauge.thresholdMarkers" on-change="ctrl.render()"></gf-form-switch>
</div>
</div>
</div>

View File

@ -1,173 +1,139 @@
<div class="editor-row">
<div class="section">
<h5>Data</h5>
<div class="tight-form-container">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 140px">
To Table Transform
</li>
<li>
<select class="input-large tight-form-input"
ng-model="editor.panel.transform"
ng-options="k as v.description for (k, v) in editor.transformers"
ng-change="editor.transformChanged()"></select>
</li>
</ul>
<div class="clearfix"></div>
<div class="section gf-form-group">
<h5 class="section-heading">Data</h5>
<div class="gf-form">
<label class="gf-form-label width-10">Table Transform</label>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="editor.panel.transform" ng-options="k as v.description for (k, v) in editor.transformers" ng-change="editor.transformChanged()"></select>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-10">Columns</label>
</div>
<div class="gf-form" ng-repeat="column in editor.panel.columns">
<label class="gf-form-label">
<i class="pointer fa fa-remove" ng-click="editor.removeColumn(column)"></i>
<span>{{column.text}}</span>
</label>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 140px">
Columns
</li>
<li class="tight-form-item" ng-repeat="column in editor.panel.columns">
<i class="pointer fa fa-remove" ng-click="editor.removeColumn(column)"></i>
<span>
{{column.text}}
</span>
</li>
<li>
<metric-segment segment="editor.addColumnSegment" get-options="editor.getColumnOptions()" on-change="editor.addColumn()"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
<div class="gf-form">
<metric-segment segment="editor.addColumnSegment" get-options="editor.getColumnOptions()" on-change="editor.addColumn()"></metric-segment>
</div>
</div>
</div>
<div class="section">
<h5>Table Display</h5>
<div class="tight-form-container">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item">
Pagination (Page size)
</li>
<li>
<input type="number" class="input-small tight-form-input" placeholder="100"
empty-to-null ng-model="editor.panel.pageSize" ng-change="editor.render()" ng-model-onblur>
</li>
<li class="tight-form-item">
<editor-checkbox text="Scroll" model="editor.panel.scroll" change="editor.render()"></editor-checkbox>
</li>
<li class="tight-form-item">
Font size
</li>
<li>
<select class="input-small tight-form-input" ng-model="editor.panel.fontSize" ng-options="f for f in editor.fontSizes" ng-change="editor.render()"></select>
</li>
</ul>
<div class="clearfix"></div>
<div class="section gf-form-group">
<h5 class="section-heading">Table Display</h5>
<div class="gf-form-inline">
<div class="gf-form max-width-17">
<label class="gf-form-label width-11">Pagination (Page size)</label>
<input type="number" class="gf-form-input"
placeholder="100" data-placement="right"
ng-model="editor.panel.pageSize"
ng-change="editor.render()"
ng-model-onblur>
</div>
<gf-form-switch class="gf-form" label-class="width-4"
label="Scroll"
checked="editor.panel.scroll"
change="editor.render()"></gf-form-switch>
<div class="gf-form max-width-17">
<label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input"
ng-model="editor.panel.fontSize"
ng-options="f for f in editor.fontSizes"
ng-change="editor.render()"></select>
</div>
</div>
</div>
</div>
</div>
<div class="editor-row" style="margin-top: 20px">
<h5>Column Styles</h5>
<div class="tight-form-container">
<div class="editor-row">
<div class="section gf-form-group">
<h5 class="section-heading">Column Styles</h5>
<div ng-repeat="style in editor.panel.styles">
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li class="tight-form-item last">
<i class="fa fa-remove pointer" ng-click="editor.removeColumnStyle(style)"></i>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item">
Name or regex
</li>
<li>
<input type="text" ng-model="style.pattern" bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 class="input-medium tight-form-input">
</li>
<li class="tight-form-item" style="width: 86px">
Type
</li>
<li>
<select class="input-small tight-form-input"
ng-model="style.type"
ng-options="c.value as c.text for c in editor.columnTypes"
ng-change="editor.render()"
style="width: 150px"
></select>
</li>
</ul>
<ul class="tight-form-list" ng-if="style.type === 'date'">
<li class="tight-form-item">
Format
</li>
<li>
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
</li>
</ul>
<ul class="tight-form-list" ng-if="style.type === 'string'">
<li class="tight-form-item">
<editor-checkbox text="Sanitize HTML" model="style.sanitize" change="editor.render()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="style.type === 'number'">
<ul class="tight-form-list">
<li class="tight-form-item text-right" style="width: 93px">
Coloring
</li>
<li>
<select class="input-small tight-form-input"
ng-model="style.colorMode"
ng-options="c.value as c.text for c in editor.colorModes"
ng-change="editor.render()"
style="width: 150px"
></select>
</li>
<li class="tight-form-item">
Thresholds<tip>Comma separated values</tip>
</li>
<li>
<input type="text" class="input-small tight-form-input" style="width: 150px" ng-model="style.thresholds" ng-blur="editor.render()" placeholder="50,80" array-join></input>
</li>
<li class="tight-form-item" style="width: 60px">
Colors
</li>
<li class="tight-form-item">
<spectrum-picker ng-model="style.colors[0]" ng-change="editor.render()" ></spectrum-picker>
<spectrum-picker ng-model="style.colors[1]" ng-change="editor.render()" ></spectrum-picker>
<spectrum-picker ng-model="style.colors[2]" ng-change="editor.render()" ></spectrum-picker>
</li>
<li class="tight-form-item last">
<a class="pointer" ng-click="editor.invertColorOrder($index)">invert order</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="style.type === 'number'">
<ul class="tight-form-list">
<li class="tight-form-item text-right" style="width: 93px">
Unit
</li>
<li class="dropdown" style="width: 150px"
ng-model="style.unit"
dropdown-typeahead="editor.unitFormats"
dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)">
</li>
<li class="tight-form-item" style="width: 86px">
Decimals
</li>
<li style="width: 105px">
<input type="number" class="input-mini tight-form-input" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label">Name or regex</label>
<input type="text" class="gf-form-input" ng-model="style.pattern" bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label">Type</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form" ng-if="style.type === 'date'">
<label class="gf-form-label">Format</label>
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
</div>
<gf-form-switch class="gf-form" label-class="width-8" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize" change="editor.render()"></gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" ng-click="editor.removeColumnStyle(style)">
<i class="fa fa-trash"></i>
</a>
</label>
</div>
</div>
</div>
</div>
<div class="gf-form-inline" ng-if="style.type === 'number'">
<div class="gf-form offset-width-8">
<label class="gf-form-label width-8">Unit</label>
</div>
<div class="gf-form">
<div class="gf-form-dropdown-typeahead" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">Decimals</label>
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label">Coloring</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="style.colorMode" ng-options="c.value as c.text for c in editor.colorModes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<button class="btn btn-inverse" style="margin-top: 20px" ng-click="editor.addColumnStyle()">
Add column style rule
</button>
<div class="gf-form-inline" ng-if="style.type === 'number'">
<div class="gf-form max-width-17 offset-width-8">
<label class="gf-form-label width-8">Thresholds<tip>Comma separated values</tip></label>
<input type="text" class="gf-form-input" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()" array-join ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-5">Colors</label>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[0]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[1]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[2]" ng-change="editor.render()"></spectrum-picker>
</span>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow">
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
</div>
</div>
</div>
</div>
</div>
<div class="gf-form-button">
<button class="btn btn-inverse" ng-click="editor.addColumnStyle()">
<i class="fa fa-plus"></i>&nbsp;Add column style rule
</button>
</div>
</div>

View File

@ -78,7 +78,8 @@ class TablePanelCtrl extends MetricsPanelCtrl {
if (this.panel.transform === 'annotations') {
this.setTimeQueryStart();
return this.annotationsSrv.getAnnotations(this.dashboard).then(annotations => {
return this.annotationsSrv.getAnnotations({dashboard: this.dashboard, panel: this.panel, range: this.range})
.then(annotations => {
return {data: annotations};
});
}

View File

@ -7,6 +7,7 @@ $gf-form-margin: 0.25rem;
align-items: center;
text-align: left;
position: relative;
font-size: $font-size-sm;
&--offset-1 {
margin-left: $spacer;
@ -48,7 +49,6 @@ $gf-form-margin: 0.25rem;
.gf-form-label {
padding: $input-padding-y $input-padding-x;
margin-right: $gf-form-margin;
line-height: $input-line-height;
flex-shrink: 0;
background-color: $input-label-bg;

View File

@ -1,6 +1,5 @@
.submenu-controls {
margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
font-size: 16px;
}
.annotation-disabled, .annotation-disabled a {
@ -18,22 +17,19 @@
.submenu-item {
margin-right: 20px;
display: inline-block;
border-radius: 3px;
background-color: $panel-bg;
border: $panel-border;
margin-right: 10px;
margin-right: 15px;
display: inline-block;
float: left;
.fa-caret-down {
font-size: 75%;
position: relative;
top: 1px;
top: -1px;
left: 1px;
}
}
.variable-value-link {
font-size: 16px;
padding-right: 10px;
.label-tag {
margin: 0 5px;
@ -42,19 +38,9 @@
padding: 8px 7px;
box-sizing: content-box;
display: inline-block;
font-weight: normal;
display: inline-block;
color: $text-color;
}
.submenu-item-label {
padding: 8px 0px 8px 7px;
box-sizing: content-box;
display: inline-block;
font-weight: normal;
display: inline-block;
}
.variable-link-wrapper {
display: inline-block;
position: relative;

View File

@ -24,6 +24,22 @@ describe("Emitter", () => {
expect(sub2Called).to.be(true);
});
it('when subscribing twice', () => {
var events = new Emitter();
var sub1Called = 0;
function handler() {
sub1Called += 1;
}
events.on('test', handler);
events.on('test', handler);
events.emit('test', null);
expect(sub1Called).to.be(2);
});
it('should handle errors', () => {
var events = new Emitter();
var sub1Called = 0;

View File

@ -1,388 +0,0 @@
define([
'app/features/dashboard/dashboardSrv'
], function() {
'use strict';
describe('dashboardSrv', function() {
var _dashboardSrv;
beforeEach(module('grafana.services'));
beforeEach(module(function($provide) {
$provide.value('contextSrv', {
});
}));
beforeEach(inject(function(dashboardSrv) {
_dashboardSrv = dashboardSrv;
}));
describe('when creating new dashboard with defaults only', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({}, {});
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have meta', function() {
expect(model.meta.canSave).to.be(true);
expect(model.meta.canShare).to.be(true);
});
it('should have default properties', function() {
expect(model.rows.length).to.be(0);
});
});
describe('when getting next panel id', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{ panels: [{ id: 5 }]}]
});
});
it('should return max id + 1', function() {
expect(model.getNextPanelId()).to.be(6);
});
});
describe('row and panel manipulation', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({});
});
it('row span should sum spans', function() {
var spanLeft = dashboard.rowSpan({ panels: [{ span: 2 }, { span: 3 }] });
expect(spanLeft).to.be(5);
});
it('adding default should split span in half', function() {
dashboard.rows = [{ panels: [{ span: 12, id: 7 }] }];
dashboard.addPanel({span: 4}, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(6);
expect(dashboard.rows[0].panels[1].span).to.be(6);
expect(dashboard.rows[0].panels[1].id).to.be(8);
});
it('duplicate panel should try to add it to same row', function() {
var panel = { span: 4, attr: '123', id: 10 };
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(4);
expect(dashboard.rows[0].panels[1].span).to.be(4);
expect(dashboard.rows[0].panels[1].attr).to.be('123');
expect(dashboard.rows[0].panels[1].id).to.be(11);
});
it('duplicate panel should remove repeat data', function() {
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
dashboard.rows = [{ panels: [panel] }];
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined);
});
});
describe('when creating dashboard with editable false', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false
});
});
it('should set editable false', function() {
expect(model.editable).to.be(false);
});
});
describe('when creating dashboard with old schema', function() {
var model;
var graph;
var singlestat;
var table;
beforeEach(function() {
model = _dashboardSrv.create({
services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [{}] }},
pulldowns: [
{type: 'filtering', enable: true},
{type: 'annotations', enable: true, annotations: [{name: 'old'}]}
],
rows: [
{
panels: [
{
type: 'graph', legend: true, aliasYAxis: { test: 2 },
y_formats: ['kbyte', 'ms'],
grid: {
min: 1,
max: 10,
rightMin: 5,
rightMax: 15,
leftLogBase: 1,
rightLogBase: 2,
threshold1: 200,
threshold2: 400,
threshold1Color: 'yellow',
threshold2Color: 'red',
},
leftYAxisLabel: 'left label',
targets: [{refId: 'A'}, {}],
},
{
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
targets: [{refId: 'A'}, {}],
},
{
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
targets: [{refId: 'A'}, {}],
}
]
}
]
});
graph = model.rows[0].panels[0];
singlestat = model.rows[0].panels[1];
table = model.rows[0].panels[2];
});
it('should have title', function() {
expect(model.title).to.be('No Title');
});
it('should have panel id', function() {
expect(graph.id).to.be(1);
});
it('should move time and filtering list', function() {
expect(model.time.from).to.be('now-1d');
expect(model.templating.list[0].allFormat).to.be('glob');
});
it('graphite panel should change name too graph', function() {
expect(graph.type).to.be('graph');
});
it('single stat panel should have two thresholds', function() {
expect(singlestat.thresholds).to.be('20,30');
});
it('queries without refId should get it', function() {
expect(graph.targets[1].refId).to.be('B');
});
it('update legend setting', function() {
expect(graph.legend.show).to.be(true);
});
it('move aliasYAxis to series override', function() {
expect(graph.seriesOverrides[0].alias).to.be("test");
expect(graph.seriesOverrides[0].yaxis).to.be(2);
});
it('should move pulldowns to new schema', function() {
expect(model.annotations.list[0].name).to.be('old');
});
it('table panel should only have two thresholds values', function() {
expect(table.styles[0].thresholds[0]).to.be("20");
expect(table.styles[0].thresholds[1]).to.be("30");
expect(table.styles[1].thresholds[0]).to.be("200");
expect(table.styles[1].thresholds[1]).to.be("300");
});
it('graph grid to yaxes options', function() {
expect(graph.yaxes[0].min).to.be(1);
expect(graph.yaxes[0].max).to.be(10);
expect(graph.yaxes[0].format).to.be('kbyte');
expect(graph.yaxes[0].label).to.be('left label');
expect(graph.yaxes[0].logBase).to.be(1);
expect(graph.yaxes[1].min).to.be(5);
expect(graph.yaxes[1].max).to.be(15);
expect(graph.yaxes[1].format).to.be('ms');
expect(graph.yaxes[1].logBase).to.be(2);
expect(graph.grid.rightMax).to.be(undefined);
expect(graph.grid.rightLogBase).to.be(undefined);
expect(graph.y_formats).to.be(undefined);
});
it('dashboard schema version should be set to latest', function() {
expect(model.schemaVersion).to.be(13);
});
it('graph thresholds should be migrated', function() {
expect(graph.thresholds.length).to.be(2);
expect(graph.thresholds[0].op).to.be('>');
expect(graph.thresholds[0].value).to.be(400);
expect(graph.thresholds[0].fillColor).to.be('red');
expect(graph.thresholds[1].value).to.be(200);
expect(graph.thresholds[1].fillColor).to.be('yellow');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Given editable false dashboard', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false,
});
});
it('Should set meta canEdit and canSave to false', function() {
expect(model.meta.canSave).to.be(false);
expect(model.meta.canEdit).to.be(false);
});
it('getSaveModelClone should remove meta', function() {
var clone = model.getSaveModelClone();
expect(clone.meta).to.be(undefined);
});
});
describe('when loading dashboard with old influxdb query schema', function() {
var model;
var target;
beforeEach(function() {
model = _dashboardSrv.create({
rows: [{
panels: [{
type: 'graph',
grid: {},
yaxes: [{}, {}],
targets: [{
"alias": "$tag_datacenter $tag_source $col",
"column": "value",
"measurement": "logins.count",
"fields": [
{
"func": "mean",
"name": "value",
"mathExpr": "*2",
"asExpr": "value"
},
{
"name": "one-minute",
"func": "mean",
"mathExpr": "*3",
"asExpr": "one-minute"
}
],
"tags": [],
"fill": "previous",
"function": "mean",
"groupBy": [
{
"interval": "auto",
"type": "time"
},
{
"key": "source",
"type": "tag"
},
{
"type": "tag",
"key": "datacenter"
}
],
}]
}]
}]
});
target = model.rows[0].panels[0].targets[0];
});
it('should update query schema', function() {
expect(target.fields).to.be(undefined);
expect(target.select.length).to.be(2);
expect(target.select[0].length).to.be(4);
expect(target.select[0][0].type).to.be('field');
expect(target.select[0][1].type).to.be('mean');
expect(target.select[0][2].type).to.be('math');
expect(target.select[0][3].type).to.be('alias');
});
});
describe('when creating dashboard model with missing list for annoations or templating', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
annotations: {
enable: true,
},
templating: {
enable: true
}
});
});
it('should add empty list', function() {
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
describe('Formatting epoch timestamp when timezone is set as utc', function() {
var dashboard;
beforeEach(function() {
dashboard = _dashboardSrv.create({
timezone: 'utc',
});
});
it('Should format timestamp with second resolution by default', function() {
expect(dashboard.formatDate(1234567890000)).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with second resolution even if second format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss')).to.be('2009-02-13 23:31:30');
});
it('Should format timestamp with millisecond resolution if format is passed as parameter', function() {
expect(dashboard.formatDate(1234567890007,'YYYY-MM-DD HH:mm:ss.SSS')).to.be('2009-02-13 23:31:30.007');
});
});
});
});

View File

@ -158,6 +158,7 @@ define([
return _.template(text, this.templateSettings)(this.data);
};
this.init = function() {};
this.getAdhocFilters = function() { return []; };
this.fillVariableValuesForUrl = function() {};
this.updateTemplateData = function() { };
this.variableExists = function() { return false; };

View File

@ -1,267 +0,0 @@
define([
'../mocks/dashboard-mock',
'lodash',
'app/features/templating/templateSrv'
], function(dashboardMock) {
'use strict';
describe('templateSrv', function() {
var _templateSrv;
var _dashboard;
beforeEach(module('grafana.services'));
beforeEach(module(function() {
_dashboard = dashboardMock.create();
}));
beforeEach(inject(function(templateSrv) {
_templateSrv = templateSrv;
}));
describe('init', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should initialize template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.oogle.filters');
});
});
describe('replace can pass scoped vars', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should replace $test with scoped value', function() {
var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.mupp.filters');
});
it('should replace $test with scoped text', function() {
var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
expect(target).to.be('this.asd.filters');
});
});
describe('replace can pass multi / all format', function() {
beforeEach(function() {
_templateSrv.init([{name: 'test', current: {value: ['value1', 'value2'] }}]);
});
it('should replace $test with globbed value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
it('should replace $test with piped value', function() {
var target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).to.be('this=value1|value2');
});
});
describe('variable with all option', function() {
beforeEach(function() {
_templateSrv.init([{
name: 'test',
current: {value: '$__all' },
options: [
{value: '$__all'}, {value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.{value1,value2}.filters');
});
});
describe('variable with all option and custom value', function() {
beforeEach(function() {
_templateSrv.init([{
name: 'test',
current: {value: '$__all' },
allValue: '*',
options: [
{value: 'value1'}, {value: 'value2'}
]
}]);
});
it('should replace $test with formatted all value', function() {
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).to.be('this.*.filters');
});
it('should not escape custom all value', function() {
var target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).to.be('this.*');
});
});
describe('lucene format', function() {
it('should properly escape $test with lucene escape sequences', function() {
_templateSrv.init([{name: 'test', current: {value: 'value/4' }}]);
var target = _templateSrv.replace('this:$test', {}, 'lucene');
expect(target).to.be("this:value\\\/4");
});
});
describe('format variable to string values', function() {
it('single value should return value', function() {
var result = _templateSrv.formatValue('test');
expect(result).to.be('test');
});
it('multi value and glob format should render glob string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'glob');
expect(result).to.be('{test,test2}');
});
it('multi value and lucene should render as lucene expr', function() {
var result = _templateSrv.formatValue(['test','test2'], 'lucene');
expect(result).to.be('("test" OR "test2")');
});
it('multi value and regex format should render regex string', function() {
var result = _templateSrv.formatValue(['test.','test2'], 'regex');
expect(result).to.be('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', function() {
var result = _templateSrv.formatValue(['test','test2'], 'pipe');
expect(result).to.be('test|test2');
});
it('slash should be properly escaped in regex format', function() {
var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14');
});
});
describe('can check if variable exists', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should return true if exists', function() {
var result = _templateSrv.variableExists('$test');
expect(result).to.be(true);
});
});
describe('can hightlight variables in string', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
});
it('should insert html', function() {
var result = _templateSrv.highlightVariablesAsHtml('$test');
expect(result).to.be('<span class="template-variable">$test</span>');
});
it('should insert html anywhere in string', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $test ok');
expect(result).to.be('this <span class="template-variable">$test</span> ok');
});
it('should ignore if variables does not exist', function() {
var result = _templateSrv.highlightVariablesAsHtml('this $google ok');
expect(result).to.be('this $google ok');
});
});
describe('when checking if a string contains a variable', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
});
it('should find it with $var syntax', function() {
var contains = _templateSrv.containsVariable('this.$test.filters', 'test');
expect(contains).to.be(true);
});
it('should not find it if only part matches with $var syntax', function() {
var contains = _templateSrv.containsVariable('this.$ServerDomain.filters', 'Server');
expect(contains).to.be(false);
});
it('should find it with [[var]] syntax', function() {
var contains = _templateSrv.containsVariable('this.[[test]].filters', 'test');
expect(contains).to.be(true);
});
it('should find it when part of segment', function() {
var contains = _templateSrv.containsVariable('metrics.$env.$group-*', 'group');
expect(contains).to.be(true);
});
it('should find it its the only thing', function() {
var contains = _templateSrv.containsVariable('$env', 'env');
expect(contains).to.be(true);
});
});
describe('updateTemplateData with simple value', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]);
});
it('should set current value and update template data', function() {
var target = _templateSrv.replace('this.[[test]].filters');
expect(target).to.be('this.muuuu.filters');
});
});
describe('fillVariableValuesForUrl with multi value', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set multiple url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).to.eql(['val1', 'val2']);
});
});
describe('fillVariableValuesForUrl with multi value and scopedVars', function() {
beforeEach(function() {
_templateSrv.init([{ name: 'test', current: { value: ['val1', 'val2'] }}]);
});
it('should set multiple url params', function() {
var params = {};
_templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}});
expect(params['var-test']).to.eql('val1');
});
});
describe('replaceWithText', function() {
beforeEach(function() {
_templateSrv.init([
{ name: 'server', current: { value: '{asd,asd2}', text: 'All' } },
{ name: 'period', current: { value: '$__auto_interval', text: 'auto' } }
]);
_templateSrv.setGrafanaVariable('$__auto_interval', '13m');
_templateSrv.updateTemplateData();
});
it('should replace with text except for grafanaVariables', function() {
var target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).to.be('Server: All, period: 13m');
});
});
});
});

View File

@ -1,6 +1,6 @@
define([
'app/features/dashboard/unsavedChangesSrv',
'app/features/dashboard/dashboardSrv'
'app/features/dashboard/dashboard_srv'
], function() {
'use strict';
@ -14,6 +14,7 @@ define([
var dash;
var scope;
beforeEach(module('grafana.core'));
beforeEach(module('grafana.services'));
beforeEach(module(function($provide) {
$provide.value('contextSrv', _contextSrvStub);

View File

@ -1663,8 +1663,10 @@ Licensed under the MIT license.
delta = max - min;
if (delta == 0.0) {
// degenerate case
var widen = max == 0 ? 1 : 0.01;
// Grafana fix: wide Y min and max using increased wideFactor
// when all series values are the same
var wideFactor = 0.25;
var widen = max == 0 ? 1 : max * wideFactor;
if (opts.min == null)
min -= widen;

File diff suppressed because one or more lines are too long

View File

@ -1,34 +1,33 @@
module.exports = function(config,grunt) {
'use strict';
grunt.registerTask('phantomjs', 'Copy phantomjs binary from node', function() {
var dest = './vendor/phantomjs/phantomjs';
var confDir = './node_modules/phantomjs-prebuilt/lib/';
if (!grunt.file.exists(dest)){
var m=grunt.file.read(confDir+"location.js")
var src=/= \"([^\"]*)\"/.exec(m)[1];
if (!grunt.file.isPathAbsolute(src)) {
src = confDir+src;
}
try {
grunt.config('copy.phantom_bin', {
src: src,
dest: dest,
options: { mode: true},
});
grunt.task.run('copy:phantom_bin');
} catch (err) {
grunt.verbose.writeln(err);
grunt.fail.warn('No working Phantomjs binary available')
}
} else {
grunt.log.writeln('Phantomjs already imported from node');
}
});
};
module.exports = function(config,grunt) {
'use strict';
grunt.registerTask('phantomjs', 'Copy phantomjs binary to vendor/', function() {
var dest = './vendor/phantomjs/phantomjs';
var confDir = './node_modules/phantomjs-prebuilt/lib/';
src = config.phjs
if (!src){
var m=grunt.file.read(confDir+"location.js")
var src=/= \"([^\"]*)\"/.exec(m)[1];
if (!grunt.file.isPathAbsolute(src)) {
src = confDir+src;
}
}
try {
grunt.config('copy.phantom_bin', {
src: src,
dest: dest,
options: { mode: true},
});
grunt.task.run('copy:phantom_bin');
} catch (err) {
grunt.verbose.writeln(err);
grunt.fail.warn('No working Phantomjs binary available')
}
});
};

View File

@ -1,163 +0,0 @@
package awstesting
import (
"encoding/json"
"encoding/xml"
"fmt"
"net/url"
"reflect"
"regexp"
"sort"
"testing"
)
// Match is a testing helper to test for testing error by comparing expected
// with a regular expression.
func Match(t *testing.T, regex, expected string) {
if !regexp.MustCompile(regex).Match([]byte(expected)) {
t.Errorf("%q\n\tdoes not match /%s/", expected, regex)
}
}
// AssertURL verifies the expected URL is matches the actual.
func AssertURL(t *testing.T, expect, actual string, msgAndArgs ...interface{}) bool {
expectURL, err := url.Parse(expect)
if err != nil {
t.Errorf(errMsg("unable to parse expected URL", err, msgAndArgs))
return false
}
actualURL, err := url.Parse(actual)
if err != nil {
t.Errorf(errMsg("unable to parse actual URL", err, msgAndArgs))
return false
}
equal(t, expectURL.Host, actualURL.Host, msgAndArgs...)
equal(t, expectURL.Scheme, actualURL.Scheme, msgAndArgs...)
equal(t, expectURL.Path, actualURL.Path, msgAndArgs...)
return AssertQuery(t, expectURL.Query().Encode(), actualURL.Query().Encode(), msgAndArgs...)
}
// AssertQuery verifies the expect HTTP query string matches the actual.
func AssertQuery(t *testing.T, expect, actual string, msgAndArgs ...interface{}) bool {
expectQ, err := url.ParseQuery(expect)
if err != nil {
t.Errorf(errMsg("unable to parse expected Query", err, msgAndArgs))
return false
}
actualQ, err := url.ParseQuery(expect)
if err != nil {
t.Errorf(errMsg("unable to parse actual Query", err, msgAndArgs))
return false
}
// Make sure the keys are the same
if !equal(t, queryValueKeys(expectQ), queryValueKeys(actualQ), msgAndArgs...) {
return false
}
for k, expectQVals := range expectQ {
sort.Strings(expectQVals)
actualQVals := actualQ[k]
sort.Strings(actualQVals)
equal(t, expectQVals, actualQVals, msgAndArgs...)
}
return true
}
// AssertJSON verifies that the expect json string matches the actual.
func AssertJSON(t *testing.T, expect, actual string, msgAndArgs ...interface{}) bool {
expectVal := map[string]interface{}{}
if err := json.Unmarshal([]byte(expect), &expectVal); err != nil {
t.Errorf(errMsg("unable to parse expected JSON", err, msgAndArgs...))
return false
}
actualVal := map[string]interface{}{}
if err := json.Unmarshal([]byte(actual), &actualVal); err != nil {
t.Errorf(errMsg("unable to parse actual JSON", err, msgAndArgs...))
return false
}
return equal(t, expectVal, actualVal, msgAndArgs...)
}
// AssertXML verifies that the expect xml string matches the actual.
func AssertXML(t *testing.T, expect, actual string, container interface{}, msgAndArgs ...interface{}) bool {
expectVal := container
if err := xml.Unmarshal([]byte(expect), &expectVal); err != nil {
t.Errorf(errMsg("unable to parse expected XML", err, msgAndArgs...))
}
actualVal := container
if err := xml.Unmarshal([]byte(actual), &actualVal); err != nil {
t.Errorf(errMsg("unable to parse actual XML", err, msgAndArgs...))
}
return equal(t, expectVal, actualVal, msgAndArgs...)
}
// objectsAreEqual determines if two objects are considered equal.
//
// This function does no assertion of any kind.
//
// Based on github.com/stretchr/testify/assert.ObjectsAreEqual
// Copied locally to prevent non-test build dependencies on testify
func objectsAreEqual(expected, actual interface{}) bool {
if expected == nil || actual == nil {
return expected == actual
}
return reflect.DeepEqual(expected, actual)
}
// Equal asserts that two objects are equal.
//
// assert.Equal(t, 123, 123, "123 and 123 should be equal")
//
// Returns whether the assertion was successful (true) or not (false).
//
// Based on github.com/stretchr/testify/assert.Equal
// Copied locally to prevent non-test build dependencies on testify
func equal(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if !objectsAreEqual(expected, actual) {
t.Errorf("Not Equal:\n\t%#v (expected)\n\t%#v (actual), %s",
expected, actual, messageFromMsgAndArgs(msgAndArgs))
return false
}
return true
}
func errMsg(baseMsg string, err error, msgAndArgs ...interface{}) string {
message := messageFromMsgAndArgs(msgAndArgs)
if message != "" {
message += ", "
}
return fmt.Sprintf("%s%s, %v", message, baseMsg, err)
}
// Based on github.com/stretchr/testify/assert.messageFromMsgAndArgs
// Copied locally to prevent non-test build dependencies on testify
func messageFromMsgAndArgs(msgAndArgs []interface{}) string {
if len(msgAndArgs) == 0 || msgAndArgs == nil {
return ""
}
if len(msgAndArgs) == 1 {
return msgAndArgs[0].(string)
}
if len(msgAndArgs) > 1 {
return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
}
return ""
}
func queryValueKeys(v url.Values) []string {
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}

View File

@ -1,64 +0,0 @@
package awstesting_test
import (
"encoding/xml"
"testing"
"github.com/aws/aws-sdk-go/awstesting"
)
func TestAssertJSON(t *testing.T) {
cases := []struct {
e, a string
asserts bool
}{
{
e: `{"RecursiveStruct":{"RecursiveMap":{"foo":{"NoRecurse":"foo"},"bar":{"NoRecurse":"bar"}}}}`,
a: `{"RecursiveStruct":{"RecursiveMap":{"bar":{"NoRecurse":"bar"},"foo":{"NoRecurse":"foo"}}}}`,
asserts: true,
},
}
for i, c := range cases {
mockT := &testing.T{}
if awstesting.AssertJSON(mockT, c.e, c.a) != c.asserts {
t.Error("Assert JSON result was not expected.", i)
}
}
}
func TestAssertXML(t *testing.T) {
cases := []struct {
e, a string
asserts bool
container struct {
XMLName xml.Name `xml:"OperationRequest"`
NS string `xml:"xmlns,attr"`
RecursiveStruct struct {
RecursiveMap struct {
Entries []struct {
XMLName xml.Name `xml:"entries"`
Key string `xml:"key"`
Value struct {
XMLName xml.Name `xml:"value"`
NoRecurse string
}
}
}
}
}
}{
{
e: `<OperationRequest xmlns="https://foo/"><RecursiveStruct xmlns="https://foo/"><RecursiveMap xmlns="https://foo/"><entry xmlns="https://foo/"><key xmlns="https://foo/">foo</key><value xmlns="https://foo/"><NoRecurse xmlns="https://foo/">foo</NoRecurse></value></entry><entry xmlns="https://foo/"><key xmlns="https://foo/">bar</key><value xmlns="https://foo/"><NoRecurse xmlns="https://foo/">bar</NoRecurse></value></entry></RecursiveMap></RecursiveStruct></OperationRequest>`,
a: `<OperationRequest xmlns="https://foo/"><RecursiveStruct xmlns="https://foo/"><RecursiveMap xmlns="https://foo/"><entry xmlns="https://foo/"><key xmlns="https://foo/">bar</key><value xmlns="https://foo/"><NoRecurse xmlns="https://foo/">bar</NoRecurse></value></entry><entry xmlns="https://foo/"><key xmlns="https://foo/">foo</key><value xmlns="https://foo/"><NoRecurse xmlns="https://foo/">foo</NoRecurse></value></entry></RecursiveMap></RecursiveStruct></OperationRequest>`,
asserts: true,
},
}
for i, c := range cases {
// mockT := &testing.T{}
if awstesting.AssertXML(t, c.e, c.a, c.container) != c.asserts {
t.Error("Assert XML result was not expected.", i)
}
}
}

View File

@ -1,20 +0,0 @@
package awstesting
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/client/metadata"
"github.com/aws/aws-sdk-go/aws/defaults"
)
// NewClient creates and initializes a generic service client for testing.
func NewClient(cfgs ...*aws.Config) *client.Client {
info := metadata.ClientInfo{
Endpoint: "http://endpoint",
SigningName: "",
}
def := defaults.Get()
def.Config.MergeIn(cfgs...)
return client.New(*def.Config, info, def.Handlers)
}

View File

@ -1,124 +0,0 @@
// +build integration
// Package s3_test runs integration tests for S3
package s3_test
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/awstesting/integration"
"github.com/aws/aws-sdk-go/service/s3"
)
var bucketName *string
var svc *s3.S3
func TestMain(m *testing.M) {
setup()
defer teardown() // only called if we panic
result := m.Run()
teardown()
os.Exit(result)
}
// Create a bucket for testing
func setup() {
svc = s3.New(integration.Session)
bucketName = aws.String(
fmt.Sprintf("aws-sdk-go-integration-%d-%s", time.Now().Unix(), integration.UniqueID()))
for i := 0; i < 10; i++ {
_, err := svc.CreateBucket(&s3.CreateBucketInput{Bucket: bucketName})
if err == nil {
break
}
}
for {
_, err := svc.HeadBucket(&s3.HeadBucketInput{Bucket: bucketName})
if err == nil {
break
}
time.Sleep(1 * time.Second)
}
}
// Delete the bucket
func teardown() {
resp, _ := svc.ListObjects(&s3.ListObjectsInput{Bucket: bucketName})
for _, o := range resp.Contents {
svc.DeleteObject(&s3.DeleteObjectInput{Bucket: bucketName, Key: o.Key})
}
svc.DeleteBucket(&s3.DeleteBucketInput{Bucket: bucketName})
}
func TestWriteToObject(t *testing.T) {
_, err := svc.PutObject(&s3.PutObjectInput{
Bucket: bucketName,
Key: aws.String("key name"),
Body: bytes.NewReader([]byte("hello world")),
})
assert.NoError(t, err)
resp, err := svc.GetObject(&s3.GetObjectInput{
Bucket: bucketName,
Key: aws.String("key name"),
})
assert.NoError(t, err)
b, _ := ioutil.ReadAll(resp.Body)
assert.Equal(t, []byte("hello world"), b)
}
func TestPresignedGetPut(t *testing.T) {
putreq, _ := svc.PutObjectRequest(&s3.PutObjectInput{
Bucket: bucketName,
Key: aws.String("presigned-key"),
})
var err error
// Presign a PUT request
var puturl string
puturl, err = putreq.Presign(300 * time.Second)
assert.NoError(t, err)
// PUT to the presigned URL with a body
var puthttpreq *http.Request
buf := bytes.NewReader([]byte("hello world"))
puthttpreq, err = http.NewRequest("PUT", puturl, buf)
assert.NoError(t, err)
var putresp *http.Response
putresp, err = http.DefaultClient.Do(puthttpreq)
assert.NoError(t, err)
assert.Equal(t, 200, putresp.StatusCode)
// Presign a GET on the same URL
getreq, _ := svc.GetObjectRequest(&s3.GetObjectInput{
Bucket: bucketName,
Key: aws.String("presigned-key"),
})
var geturl string
geturl, err = getreq.Presign(300 * time.Second)
assert.NoError(t, err)
// Get the body
var getresp *http.Response
getresp, err = http.Get(geturl)
assert.NoError(t, err)
var b []byte
defer getresp.Body.Close()
b, err = ioutil.ReadAll(getresp.Body)
assert.Equal(t, "hello world", string(b))
}

Some files were not shown because too many files have changed in this diff Show More