mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into alert_prometheus
This commit is contained in:
commit
d711c0ed35
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
44
build.go
44
build.go
@ -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 {
|
||||
|
@ -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]
|
||||
|
@ -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**:
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
23
pkg/api/dtos/playlist.go
Normal 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]
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
//
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export class User {
|
||||
isGrafanaAdmin: any;
|
||||
isSignedIn: any;
|
||||
orgRole: any;
|
||||
timezone: string;
|
||||
|
||||
constructor() {
|
||||
if (config.bootData.user) {
|
||||
|
@ -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) {
|
||||
|
@ -2,7 +2,7 @@ define([
|
||||
'./panellinks/module',
|
||||
'./dashlinks/module',
|
||||
'./annotations/annotations_srv',
|
||||
'./templating/templateSrv',
|
||||
'./templating/all',
|
||||
'./dashboard/all',
|
||||
'./playlist/all',
|
||||
'./snapshot/all',
|
||||
|
@ -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)]);
|
||||
});
|
||||
|
171
public/app/features/dashboard/ad_hoc_filters.ts
Normal file
171
public/app/features/dashboard/ad_hoc_filters.ts
Normal 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);
|
@ -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 () {});
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
@ -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);
|
||||
};
|
||||
|
||||
|
590
public/app/features/dashboard/dashboard_srv.ts
Normal file
590
public/app/features/dashboard/dashboard_srv.ts
Normal 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);
|
||||
|
@ -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
|
||||
|
@ -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};
|
||||
|
379
public/app/features/dashboard/specs/dashboard_srv_specs.ts
Normal file
379
public/app/features/dashboard/specs/dashboard_srv_specs.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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', {
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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 = '';
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
74
public/app/features/templating/adhoc_variable.ts
Normal file
74
public/app/features/templating/adhoc_variable.ts
Normal 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',
|
||||
};
|
20
public/app/features/templating/all.ts
Normal file
20
public/app/features/templating/all.ts
Normal 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,
|
||||
}
|
59
public/app/features/templating/constant_variable.ts
Normal file
59
public/app/features/templating/constant_variable.ts
Normal 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' ,
|
||||
};
|
80
public/app/features/templating/custom_variable.ts
Normal file
80
public/app/features/templating/custom_variable.ts
Normal 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,
|
||||
};
|
87
public/app/features/templating/datasource_variable.ts
Normal file
87
public/app/features/templating/datasource_variable.ts
Normal 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',
|
||||
};
|
@ -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();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
155
public/app/features/templating/editor_ctrl.ts
Normal file
155
public/app/features/templating/editor_ctrl.ts
Normal 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);
|
||||
|
89
public/app/features/templating/interval_variable.ts
Normal file
89
public/app/features/templating/interval_variable.ts
Normal 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)',
|
||||
};
|
@ -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>
|
||||
|
||||
|
168
public/app/features/templating/query_variable.ts
Normal file
168
public/app/features/templating/query_variable.ts
Normal 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,
|
||||
};
|
40
public/app/features/templating/specs/adhoc_variable_specs.ts
Normal file
40
public/app/features/templating/specs/adhoc_variable_specs.ts
Normal 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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
39
public/app/features/templating/specs/query_variable_specs.ts
Normal file
39
public/app/features/templating/specs/query_variable_specs.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
237
public/app/features/templating/specs/template_srv_specs.ts
Normal file
237
public/app/features/templating/specs/template_srv_specs.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
59
public/app/features/templating/specs/variable_specs.ts
Normal file
59
public/app/features/templating/specs/variable_specs.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
142
public/app/features/templating/specs/variable_srv_init_specs.ts
Normal file
142
public/app/features/templating/specs/variable_srv_init_specs.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
40
public/app/features/templating/variable.ts
Normal file
40
public/app/features/templating/variable.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
234
public/app/features/templating/variable_srv.ts
Normal file
234
public/app/features/templating/variable_srv.ts
Normal 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);
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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": {
|
||||
|
@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 '';}
|
||||
|
||||
|
@ -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(' ');
|
||||
}
|
||||
}
|
||||
|
@ -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\'');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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('|');
|
||||
};
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
<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
|
||||
<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<br /> 50,80 will produce: <50 = Green, 50:80 = Yellow, >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
|
||||
<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
|
||||
<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
|
||||
<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">
|
||||
<i class="fa fa-warning"></i>
|
||||
Min value is bigger than max.
|
||||
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<li class="tight-form-item">
|
||||
Threshold labels
|
||||
<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
|
||||
<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<br /> 50,80 will produce: <50 = Green, 50:80 = Yellow, >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">
|
||||
<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>
|
||||
|
@ -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> Add column style rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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};
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -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; };
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -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);
|
||||
|
6
public/vendor/flot/jquery.flot.js
vendored
6
public/vendor/flot/jquery.flot.js
vendored
@ -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;
|
||||
|
8
scripts/import_many_dashboards.sh
Executable file
8
scripts/import_many_dashboards.sh
Executable file
File diff suppressed because one or more lines are too long
@ -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')
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
163
vendor/github.com/aws/aws-sdk-go/awstesting/assert.go
generated
vendored
163
vendor/github.com/aws/aws-sdk-go/awstesting/assert.go
generated
vendored
@ -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
|
||||
}
|
64
vendor/github.com/aws/aws-sdk-go/awstesting/assert_test.go
generated
vendored
64
vendor/github.com/aws/aws-sdk-go/awstesting/assert_test.go
generated
vendored
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
20
vendor/github.com/aws/aws-sdk-go/awstesting/client.go
generated
vendored
20
vendor/github.com/aws/aws-sdk-go/awstesting/client.go
generated
vendored
@ -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)
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user