Compare commits

...

16 Commits

Author SHA1 Message Date
bergquist
3b510b7581 release: 4.1.2 2017-02-13 13:13:31 +01:00
Daniel Lee
0c7d101a97 admin: adds paging to global user list
Currently there is a limit of 1000 users in the global
user list. This change introduces paging so that an
admin can see all users and not just the first 1000.

Adds a new route to the api - /api/users/search that
returns a list of users and a total count. It takes
two parameters perpage and page that enable paging.

Fixes #7469
2017-02-13 13:11:09 +01:00
Daniel Lee
4c192714e4 api: removes import alias + some unused fields 2017-02-13 13:04:59 +01:00
Daniel Lee
d01cf16dea fix(panel): case insensitive sort metric sources
Sorts the list of metric sources that is used in dropdown for Panel
Data Source on the Metrics tab so that it is case insensitive and
so that the built data sources are last in the list.
2017-02-13 13:04:51 +01:00
Daniel Lee
9f0b6f5aca fix(api): case insensitive sort for datasources
The data source list is case sensitive when sorted. This changes the
sort to be case insensitive. The test only tests the handler, not the
routing or database query.
2017-02-13 13:04:42 +01:00
bergquist
46c1f72c03 fix(table): fixes broken annotation rendering
closes #7268
2017-01-17 12:20:25 +01:00
bergquist
ec033840bc updates package cloud publish script 2017-01-12 11:05:18 +01:00
bergquist
f09c114019 updates version to 4.1.1 2017-01-12 09:48:03 +01:00
Torkel Ödegaard
225bf094b2 fix(graph): fixed table legend min-height, fixes #7221 2017-01-11 15:22:31 +01:00
Torkel Ödegaard
98b648dabe fix(graph): fix for table legend and scroll, fixes #7204, fixes #6628 2017-01-11 10:38:10 +01:00
bergquist
da7f1f29de updates packagecloud script 2017-01-10 15:09:01 +01:00
bergquist
0719a66d47 tech(build): updates circle ci trigger scripts 2017-01-10 15:06:48 +01:00
bergquist
65ac0bc68b tech(build): upgrades appveyour nodejs version 2017-01-10 14:59:06 +01:00
bergquist
8862ee1ae6 tech(build): changes version to 4.1.0 2017-01-10 13:33:04 +01:00
Mark Theisen
3205d1e754 Fix webhook username mismatch
Fixing variable name mismatch so that the Authorization HTTP header is added to webhook notifications.
2017-01-10 10:34:51 +01:00
Mitsuhiro Tanda
eac75ec303 add cloudwatch region (#7161) 2017-01-06 12:17:04 +01:00
31 changed files with 1231 additions and 227 deletions

View File

@@ -5,7 +5,7 @@ os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\grafana\grafana
environment:
nodejs_version: "5"
nodejs_version: "6"
GOPATH: c:\gopath
install:

View File

@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "4.1.0-beta1",
"version": "4.1.2",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"

View File

@@ -1,6 +1,6 @@
#! /usr/bin/env bash
deb_ver=4.0.2-1481203731
rpm_ver=4.0.2-1481203731
deb_ver=4.1.1-1484211277
rpm_ver=4.1.1-1484211277
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb

View File

@@ -124,6 +124,7 @@ func Register(r *macaron.Macaron) {
// users (admin permission required)
r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/search", wrap(SearchUsersWithPaging))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
@@ -194,7 +195,7 @@ func Register(r *macaron.Macaron) {
// Data sources
r.Group("/datasources", func() {
r.Get("/", GetDataSources)
r.Get("/", wrap(GetDataSources))
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
r.Delete("/:id", DeleteDataSource)

View File

@@ -140,8 +140,8 @@ func init() {
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
func handleGetRegions(req *cwRequest, c *middleware.Context) {
regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ca-central-1", "cn-north-1",
"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1",
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
}
result := []interface{}{}

View File

@@ -11,12 +11,11 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func GetDataSources(c *middleware.Context) {
func GetDataSources(c *middleware.Context) Response {
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to query datasources", err)
return
return ApiError(500, "Failed to query datasources", err)
}
result := make(dtos.DataSourceList, 0)
@@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) {
}
sort.Sort(result)
c.JSON(200, result)
return Json(200, &result)
}
func GetDataSourceById(c *middleware.Context) Response {

132
pkg/api/datasources_test.go Normal file
View File

@@ -0,0 +1,132 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/models"
macaron "gopkg.in/macaron.v1"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
. "github.com/smartystreets/goconvey/convey"
)
const (
TestOrgID = 1
TestUserID = 1
)
func TestDataSourcesProxy(t *testing.T) {
Convey("Given a user is logged in", t, func() {
loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) {
// Stubs the database query
bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error {
So(query.OrgId, ShouldEqual, TestOrgID)
query.Result = []*models.DataSource{
{Name: "mmm"},
{Name: "ZZZ"},
{Name: "BBB"},
{Name: "aaa"},
}
return nil
})
// handler func being tested
sc.handlerFunc = GetDataSources
sc.fakeReq("GET", "/api/datasources").exec()
respJSON := []map[string]interface{}{}
err := json.NewDecoder(sc.resp.Body).Decode(&respJSON)
So(err, ShouldBeNil)
Convey("should return list of datasources for org sorted alphabetically and case insensitively", func() {
So(respJSON[0]["name"], ShouldEqual, "aaa")
So(respJSON[1]["name"], ShouldEqual, "BBB")
So(respJSON[2]["name"], ShouldEqual, "mmm")
So(respJSON[3]["name"], ShouldEqual, "ZZZ")
})
})
})
}
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = models.ROLE_EDITOR
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
}
return nil
})
sc.m.Get(url, sc.defaultHandler)
fn(sc)
})
}
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
So(err, ShouldBeNil)
sc.req = req
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
So(err, ShouldBeNil)
sc.req = req
return sc
}
type scenarioContext struct {
m *macaron.Macaron
context *middleware.Context
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
}
func (sc *scenarioContext) exec() {
sc.m.ServeHTTP(sc.resp, sc.req)
}
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *middleware.Context) Response

View File

@@ -91,7 +91,7 @@ func (slice DataSourceList) Len() int {
}
func (slice DataSourceList) Less(i, j int) bool {
return slice[i].Name < slice[j].Name
return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
}
func (slice DataSourceList) Swap(i, j int) {

View File

@@ -186,14 +186,46 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result.Users)
}
// GET /api/paged-users
func SearchUsersWithPaging(c *middleware.Context) Response {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
}
func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
query := &m.SearchUsersQuery{Query: "", Page: page, Limit: perPage}
if err := bus.Dispatch(query); err != nil {
return nil, err
}
query.Result.Page = page
query.Result.PerPage = perPage
return query, nil
}
func SetHelpFlag(c *middleware.Context) Response {
flag := c.ParamsInt64(":id")

109
pkg/api/user_test.go Normal file
View File

@@ -0,0 +1,109 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserApiEndpoint(t *testing.T) {
Convey("Given a user is logged in", t, func() {
mockResult := models.SearchUserQueryResult{
Users: []*models.UserSearchHitDTO{
{Name: "user1"},
{Name: "user2"},
},
TotalCount: 2,
}
loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsers
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 2)
})
loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsers
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsersWithPaging
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2)
})
loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsersWithPaging
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
})
}

View File

@@ -130,7 +130,14 @@ type SearchUsersQuery struct {
Page int
Limit int
Result []*UserSearchHitDTO
Result SearchUserQueryResult
}
type SearchUserQueryResult struct {
TotalCount int64 `json:"totalCount"`
Users []*UserSearchHitDTO `json:"users"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
type GetUserOrgListQuery struct {

View File

@@ -22,7 +22,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return &WebhookNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url,
User: model.Settings.Get("user").MustString(),
User: model.Settings.Get("username").MustString(),
Password: model.Settings.Get("password").MustString(),
HttpMethod: model.Settings.Get("httpMethod").MustString("POST"),
log: log.New("alerting.notifier.webhook"),

View File

@@ -63,8 +63,8 @@ func TestAccountDataAccess(t *testing.T) {
err := SearchUsers(&query)
So(err, ShouldBeNil)
So(query.Result[0].Email, ShouldEqual, "ac1@test.com")
So(query.Result[1].Email, ShouldEqual, "ac2@test.com")
So(query.Result.Users[0].Email, ShouldEqual, "ac1@test.com")
So(query.Result.Users[1].Email, ShouldEqual, "ac2@test.com")
})
Convey("Given an added org user", func() {

View File

@@ -344,12 +344,21 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
}
func SearchUsers(query *m.SearchUsersQuery) error {
query.Result = make([]*m.UserSearchHitDTO, 0)
query.Result = m.SearchUserQueryResult{
Users: make([]*m.UserSearchHitDTO, 0),
}
sess := x.Table("user")
sess.Where("email LIKE ?", query.Query+"%")
sess.Limit(query.Limit, query.Limit*query.Page)
offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset)
sess.Cols("id", "email", "name", "login", "is_admin")
err := sess.Find(&query.Result)
if err := sess.Find(&query.Result.Users); err != nil {
return err
}
user := m.User{}
count, err := x.Count(&user)
query.Result.TotalCount = count
return err
}

View File

@@ -0,0 +1,45 @@
package sqlstore
import (
"fmt"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/models"
)
func TestUserDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
var err error
for i := 0; i < 5; i++ {
err = CreateUser(&models.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("user", i),
})
So(err, ShouldBeNil)
}
Convey("Can return the first page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
})
Convey("Can return the second page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 5)
})
})
}

View File

@@ -113,6 +113,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
.when('/admin/users', {
templateUrl: 'public/app/features/admin/partials/users.html',
controller : 'AdminListUsersCtrl',
controllerAs: 'ctrl',
resolve: loadAdminBundle,
})
.when('/admin/users/create', {

View File

@@ -97,10 +97,19 @@ function (angular, _, coreModule, config) {
}
metricSources.sort(function(a, b) {
if (a.meta.builtIn || a.name > b.name) {
if (a.meta.builtIn) {
return 1;
}
if (a.name < b.name) {
if (b.meta.builtIn) {
return -1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
}
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
}
return 0;

View File

@@ -1,4 +1,4 @@
import './adminListUsersCtrl';
import AdminListUsersCtrl from './admin_list_users_ctrl';
import './adminListOrgsCtrl';
import './adminEditOrgCtrl';
import './adminEditUserCtrl';
@@ -37,3 +37,4 @@ export class AdminStatsCtrl {
coreModule.controller('AdminSettingsCtrl', AdminSettingsCtrl);
coreModule.controller('AdminHomeCtrl', AdminHomeCtrl);
coreModule.controller('AdminStatsCtrl', AdminStatsCtrl);
coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl);

View File

@@ -1,38 +0,0 @@
define([
'angular',
],
function (angular) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('AdminListUsersCtrl', function($scope, backendSrv) {
$scope.init = function() {
$scope.getUsers();
};
$scope.getUsers = function() {
backendSrv.get('/api/users').then(function(users) {
$scope.users = users;
});
};
$scope.deleteUser = function(user) {
$scope.appEvent('confirm-modal', {
title: 'Delete',
text: 'Do you want to delete ' + user.login + '?',
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: function() {
backendSrv.delete('/api/admin/users/' + user.id).then(function() {
$scope.getUsers();
});
}
});
};
$scope.init();
});
});

View File

@@ -0,0 +1,49 @@
///<reference path="../../headers/common.d.ts" />
export default class AdminListUsersCtrl {
users: any;
pages = [];
perPage = 1000;
page = 1;
totalPages: number;
showPaging = false;
/** @ngInject */
constructor(private $scope, private backendSrv) {
this.getUsers();
}
getUsers() {
this.backendSrv.get(`/api/users/search?perpage=${this.perPage}&page=${this.page}`).then((result) => {
this.users = result.users;
this.page = result.page;
this.perPage = result.perPage;
this.totalPages = Math.ceil(result.totalCount / result.perPage);
this.showPaging = this.totalPages > 1;
this.pages = [];
for (var i = 1; i < this.totalPages+1; i++) {
this.pages.push({ page: i, current: i === this.page});
}
});
}
navigateToPage(page) {
this.page = page.page;
this.getUsers();
}
deleteUser(user) {
this.$scope.appEvent('confirm-modal', {
title: 'Delete',
text: 'Do you want to delete ' + user.login + '?',
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
this.backendSrv.delete('/api/admin/users/' + user.id).then(() => {
this.getUsers();
});
}
});
}
}

View File

@@ -1,49 +1,62 @@
<navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
<a href="admin/users" class="navbar-page-btn">
<i class="icon-gf icon-gf-users"></i>
Users
</a>
<a href="admin/users" class="navbar-page-btn">
<i class="icon-gf icon-gf-users"></i>
Users
</a>
</navbar>
<div class="page-container">
<div class="page-header">
<h1>Users</h1>
<div class="page-header">
<h1>Users</h1>
<a class="btn btn-success" href="admin/users/create">
<i class="fa fa-plus"></i>
Add new user
</a>
</div>
<a class="btn btn-success" href="admin/users/create">
<i class="fa fa-plus"></i>
Add new user
</a>
</div>
<div class="admin-list-table">
<table class="filter-table form-inline">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Login</th>
<th>Email</th>
<th style="white-space: nowrap">Grafana Admin</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in ctrl.users">
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.login}}</td>
<td>{{user.email}}</td>
<td>{{user.isAdmin}}</td>
<td class="text-right">
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
<i class="fa fa-edit"></i>
Edit
</a>
&nbsp;&nbsp;
<a ng-click="ctrl.deleteUser(user)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</tbody>
<table class="filter-table form-inline">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Login</th>
<th>Email</th>
<th style="white-space: nowrap">Grafana Admin</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.login}}</td>
<td>{{user.email}}</td>
<td>{{user.isAdmin}}</td>
<td class="text-right">
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
<i class="fa fa-edit"></i>
Edit
</a>
&nbsp;&nbsp;
<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</tbody>
</table>
</table>
</div>
<div class="admin-list-paging" ng-if="ctrl.showPaging">
<ol>
<li ng-repeat="page in ctrl.pages">
<button
class="btn btn-small"
ng-class="{'btn-secondary': page.current, 'btn-inverse': !page.current}"
ng-click="ctrl.navigateToPage(page)">{{page.page}}</button>
</li>
</ol>
</div>
</div>

View File

@@ -1,58 +1,17 @@
{
"revision": 6,
"title": "TestData - Graph Panel Last 1h",
"tags": [
"grafana-test"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"sharedCrosshair": false,
"hideControls": false,
"time": {
"from": "2016-11-16T16:59:38.294Z",
"to": "2016-11-16T17:09:01.532Z"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 13,
"version": 4,
"links": [],
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"hideControls": false,
"links": [],
"refresh": false,
"revision": 8,
"rows": [
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
@@ -63,7 +22,6 @@
"error": false,
"fill": 1,
"id": 1,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -137,7 +95,6 @@
"error": false,
"fill": 1,
"id": 2,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -211,7 +168,6 @@
"error": false,
"fill": 1,
"id": 3,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -278,17 +234,15 @@
]
}
],
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"repeatIteration": null
"showTitle": false,
"title": "New row",
"titleSize": "h6"
},
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
@@ -299,7 +253,6 @@
"error": false,
"fill": 1,
"id": 4,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -370,7 +323,6 @@
"editable": true,
"error": false,
"id": 6,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
@@ -378,17 +330,15 @@
"type": "text"
}
],
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"repeatIteration": null
"showTitle": false,
"title": "New row",
"titleSize": "h6"
},
{
"collapse": false,
"editable": true,
"height": 336,
"panels": [
{
@@ -399,7 +349,6 @@
"error": false,
"fill": 1,
"id": 5,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -481,7 +430,6 @@
"editable": true,
"error": false,
"id": 7,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
@@ -489,17 +437,15 @@
"type": "text"
}
],
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"repeatIteration": null
"showTitle": false,
"title": "New row",
"titleSize": "h6"
},
{
"collapse": false,
"editable": true,
"height": "250px",
"panels": [
{
@@ -510,7 +456,6 @@
"error": false,
"fill": 1,
"id": 8,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -584,7 +529,6 @@
"error": false,
"fill": 1,
"id": 10,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -655,7 +599,6 @@
"editable": true,
"error": false,
"id": 13,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
@@ -663,17 +606,16 @@
"type": "text"
}
],
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"repeatIteration": null
"showTitle": false,
"title": "New row",
"titleSize": "h6"
},
{
"isNew": false,
"title": "Dashboard Row",
"collapse": false,
"height": 250,
"panels": [
{
"aliasColors": {},
@@ -683,7 +625,6 @@
"error": false,
"fill": 1,
"id": 9,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -776,7 +717,6 @@
"editable": true,
"error": false,
"id": 14,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
@@ -784,17 +724,16 @@
"type": "text"
}
],
"showTitle": false,
"titleSize": "h6",
"height": 250,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
},
{
"isNew": false,
"title": "Dashboard Row",
"collapse": false,
"height": 250,
"panels": [
{
"aliasColors": {},
@@ -804,7 +743,6 @@
"error": false,
"fill": 1,
"id": 12,
"isNew": true,
"legend": {
"avg": false,
"current": false,
@@ -833,12 +771,12 @@
"steppedLine": false,
"targets": [
{
"alias": "",
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
"target": "",
"alias": ""
"target": ""
},
{
"alias": "",
@@ -898,7 +836,6 @@
"editable": true,
"error": false,
"id": 15,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
@@ -906,13 +843,606 @@
"type": "text"
}
],
"showTitle": false,
"titleSize": "h6",
"height": 250,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
},
{
"collapse": false,
"height": 250,
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"decimals": 3,
"fill": 1,
"id": 20,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"show": true,
"total": true,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 12,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Legend Table Single Series Should Take Minium Height",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
},
{
"collapse": false,
"height": 250,
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"decimals": 3,
"fill": 1,
"id": 16,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"show": true,
"total": true,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Legend Table No Scroll Visible",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"decimals": 3,
"fill": 1,
"id": 17,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"show": true,
"total": true,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "E",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "F",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "G",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "I",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "J",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Legend Table Should Scroll",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
},
{
"collapse": false,
"height": 250,
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"decimals": 3,
"fill": 1,
"id": 18,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": true,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Legend Table No Scroll Visible",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"decimals": 3,
"fill": 1,
"id": 19,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": true,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "E",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "F",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "G",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "I",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "J",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "K",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
},
{
"refId": "L",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Legend Table No Scroll Visible",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
}
]
],
"schemaVersion": 14,
"style": "dark",
"tags": [
"grafana-test"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "browser",
"title": "TestData - Graph Panel Last 1h",
"version": 2
}

View File

@@ -9,7 +9,7 @@
"name": "Grafana Project",
"url": "http://grafana.org"
},
"version": "1.0.15",
"version": "1.0.17",
"updated": "2016-09-26"
},

View File

@@ -124,6 +124,7 @@ function (angular, _, $) {
$container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
var tableHeaderElem;
if (panel.legend.alignAsTable) {
var header = '<tr>';
header += '<th colspan="2" style="text-align:left"></th>';
@@ -135,7 +136,7 @@ function (angular, _, $) {
header += getTableHeaderHtml('total');
}
header += '</tr>';
$container.append($(header));
tableHeaderElem = $(header);
}
if (panel.legend.sort) {
@@ -148,6 +149,8 @@ function (angular, _, $) {
}
var seriesShown = 0;
var seriesElements = [];
for (i = 0; i < seriesList.length; i++) {
var series = seriesList[i];
@@ -156,6 +159,7 @@ function (angular, _, $) {
}
var html = '<div class="graph-legend-series';
if (series.yaxis === 2) { html += ' graph-legend-series--right-y'; }
if (ctrl.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
html += '" data-series-index="' + i + '">';
@@ -180,7 +184,7 @@ function (angular, _, $) {
}
html += '</div>';
$container.append($(html));
seriesElements.push($(html));
seriesShown++;
}
@@ -193,9 +197,13 @@ function (angular, _, $) {
}
var topPadding = 6;
$container.css("max-height", maxHeight - topPadding);
var tbodyElem = $('<tbody></tbody>');
tbodyElem.css("max-height", maxHeight - topPadding);
tbodyElem.append(tableHeaderElem);
tbodyElem.append(seriesElements);
$container.append(tbodyElem);
} else {
$container.css("max-height", "");
$container.append(seriesElements);
}
}
}

View File

@@ -153,14 +153,16 @@ describe('when transforming time series table', () => {
describe('Annnotations', () => {
var panel = {transform: 'annotations'};
var rawData = [
{
min: 1000,
text: 'hej',
tags: ['tags', 'asd'],
title: 'title',
}
];
var rawData = {
annotations: [
{
min: 1000,
text: 'hej',
tags: ['tags', 'asd'],
title: 'title',
}
]
};
beforeEach(() => {
table = transformDataToTable(rawData, panel);

View File

@@ -125,12 +125,12 @@ transformers['annotations'] = {
model.columns.push({text: 'Text'});
model.columns.push({text: 'Tags'});
if (!data || data.length === 0) {
if (!data || !data.annotations || data.annotations.length === 0) {
return;
}
for (var i = 0; i < data.length; i++) {
var evt = data[i];
for (var i = 0; i < data.annotations.length; i++) {
var evt = data.annotations[i];
model.rows.push([evt.min, evt.title, evt.text, evt.tags]);
}
}

View File

@@ -85,9 +85,11 @@
}
.graph-legend-table {
overflow-y: auto;
overflow-x: hidden;
display: table;
tbody {
display: block;
overflow-y: auto;
overflow-x: hidden;
}
.graph-legend-series {
display: table-row;

View File

@@ -8,3 +8,15 @@ td.admin-settings-key {
padding-left: 20px;
}
.admin-list-table {
margin-bottom: 20px;
}
.admin-list-paging {
float: right;
li {
display: inline-block;
padding-left: 10px;
margin-bottom: 5px;
}
}

View File

@@ -0,0 +1,58 @@
define([
'app/core/config',
'app/core/services/datasource_srv'
], function(config) {
'use strict';
describe('datasource_srv', function() {
var _datasourceSrv;
var metricSources;
var templateSrv = {};
beforeEach(module('grafana.core'));
beforeEach(module(function($provide) {
$provide.value('templateSrv', templateSrv);
}));
beforeEach(module('grafana.services'));
beforeEach(inject(function(datasourceSrv) {
_datasourceSrv = datasourceSrv;
}));
describe('when loading metric sources', function() {
var unsortedDatasources = {
'mmm': {
type: 'test-db',
meta: { metrics: {m: 1} }
},
'--Mixed--': {
type: 'test-db',
meta: {builtIn: true, metrics: {m: 1} }
},
'ZZZ': {
type: 'test-db',
meta: {metrics: {m: 1} }
},
'aaa': {
type: 'test-db',
meta: { metrics: {m: 1} }
},
'BBB': {
type: 'test-db',
meta: { metrics: {m: 1} }
},
};
beforeEach(function() {
config.datasources = unsortedDatasources;
metricSources = _datasourceSrv.getMetricSources({skipVariables: true});
});
it('should return a list of sources sorted case insensitively with builtin sources last', function() {
expect(metricSources[0].name).to.be('aaa');
expect(metricSources[1].name).to.be('BBB');
expect(metricSources[2].name).to.be('mmm');
expect(metricSources[3].name).to.be('ZZZ');
expect(metricSources[4].name).to.be('--Mixed--');
});
});
});
});

View File

@@ -2,9 +2,31 @@
_circle_token=$1
trigger_build_url=https://circleci.com/api/v1/project/grafana/grafana-packer/tree/master?circle-token=${_circle_token}
trigger_build_url=https://circleci.com/api/v1/project/grafana/grafana-packer/tree/v4.1.x?circle-token=${_circle_token}
post_data=$(cat <<EOF
{
"build_parameters": {
"BRANCH": "v4.1.x"
}
}
EOF
)
echo ${post_data}
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--request POST ${trigger_build_url}
--data "${post_data}" \
--request POST ${trigger_build_url}
#curl \
#--header "Accept: application/json" \
#--header "Content-Type: application/json" \
#-X POST -d '{ "build_parameters": { "BRANCH": "v4.1.x"} }' \
#${trigger_build_url}
#--request POST ${trigger_build_url}

View File

@@ -5,5 +5,5 @@ _token=$1
curl \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${_token}" \
-X POST -d '{ "accountName": "Torkeldegaard", "projectSlug": "grafana","branch": "master","environmentVariables": {}}' \
-X POST -d '{ "accountName": "Torkeldegaard", "projectSlug": "grafana","branch": "v4.1.x","environmentVariables": {}}' \
https://ci.appveyor.com/api/builds