Преглед изворни кода

Merge branch 'develop' of github.com:grafana/grafana into dashboard_snapshot_poc

Conflicts:
	src/app/features/dashboard/partials/shareDashboard.html
Torkel Ödegaard пре 10 година
родитељ
комит
49a0ea53c7

+ 7 - 0
conf/defaults.ini

@@ -1,6 +1,13 @@
 app_name = Grafana
 app_mode = production
 
+# Once every 1 hour Grafana will report anonymous data to
+# stats.grafana.org (https). No ip addresses are being tracked.
+# only simple counters to track running instances, dashboard
+# count and errors. It is very helpful to us.
+# Change this option to false to disable reporting.
+reporting-enabled = true
+
 [server]
 ; protocol (http or https)
 protocol = http

+ 7 - 0
conf/sample.ini

@@ -5,6 +5,13 @@
 
 app_mode = production
 
+# Once every 1 hour Grafana will report anonymous data to
+# stats.grafana.org (https). No ip addresses are being tracked.
+# only simple counters to track running instances, dashboard
+# counts and errors. It is very helpful to us.
+# Change this option to false to disable reporting.
+reporting-enabled = true
+
 [server]
 ; protocol (http or https)
 protocol = http

+ 1 - 11
main.go

@@ -39,17 +39,7 @@ func main() {
 	app.Name = "Grafana Backend"
 	app.Usage = "grafana web"
 	app.Version = version
-	app.Commands = []cli.Command{
-		cmd.ListOrgs,
-		cmd.CreateOrg,
-		cmd.DeleteOrg,
-		cmd.ExportDashboard,
-		cmd.ImportDashboard,
-		cmd.ListDataSources,
-		cmd.CreateDataSource,
-		cmd.DescribeDataSource,
-		cmd.DeleteDataSource,
-		cmd.Web}
+	app.Commands = []cli.Command{cmd.ImportDashboard, cmd.Web}
 	app.Flags = append(app.Flags, []cli.Flag{
 		cli.StringFlag{
 			Name:  "config",

+ 3 - 0
pkg/api/admin_users.go

@@ -3,6 +3,7 @@ package api
 import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/util"
@@ -64,6 +65,8 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
 		return
 	}
 
+	metrics.M_Api_Admin_User_Create.Inc(1)
+
 	c.JsonOK("User created")
 }
 

+ 5 - 0
pkg/api/dashboard.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
@@ -27,6 +28,8 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
 }
 
 func GetDashboard(c *middleware.Context) {
+	metrics.M_Api_Dashboard_Get.Inc(1)
+
 	slug := c.Params(":slug")
 
 	query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
@@ -88,6 +91,8 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
 		return
 	}
 
+	metrics.M_Api_Dashboard_Post.Inc(1)
+
 	c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
 }
 

+ 1 - 1
pkg/api/index.go

@@ -47,7 +47,7 @@ func Index(c *middleware.Context) {
 
 func NotFound(c *middleware.Context) {
 	if c.IsApiRequest() {
-		c.JsonApiErr(200, "Not found", nil)
+		c.JsonApiErr(404, "Not found", nil)
 		return
 	}
 

+ 3 - 1
pkg/api/login.go

@@ -6,6 +6,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
@@ -75,7 +76,6 @@ func LoginView(c *middleware.Context) {
 }
 
 func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {
-
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.User}
 	err := bus.Dispatch(&userQuery)
 
@@ -112,6 +112,8 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {
 		c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
 	}
 
+	metrics.M_Api_Login_Post.Inc(1)
+
 	c.JSON(200, result)
 }
 

+ 3 - 0
pkg/api/login_oauth.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
@@ -81,5 +82,7 @@ func OAuthLogin(ctx *middleware.Context) {
 	// login
 	loginUserWithUser(userQuery.Result, ctx)
 
+	metrics.M_Api_Login_OAuth.Inc(1)
+
 	ctx.Redirect(setting.AppSubUrl + "/")
 }

+ 3 - 0
pkg/api/org.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 )
@@ -35,6 +36,8 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) {
 		return
 	}
 
+	metrics.M_Api_Org_Create.Inc(1)
+
 	c.JsonOK("Organization created")
 }
 

+ 5 - 4
pkg/api/render.go

@@ -12,12 +12,13 @@ import (
 
 func RenderToPng(c *middleware.Context) {
 	queryReader := util.NewUrlQueryReader(c.Req.URL)
-	queryParams := fmt.Sprintf("?render=1&%s=%d&%s", middleware.SESS_KEY_USERID, c.UserId, c.Req.URL.RawQuery)
+	queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
 
 	renderOpts := &renderer.RenderOpts{
-		Url:    c.Params("*") + queryParams,
-		Width:  queryReader.Get("width", "800"),
-		Height: queryReader.Get("height", "400"),
+		Url:       c.Params("*") + queryParams,
+		Width:     queryReader.Get("width", "800"),
+		Height:    queryReader.Get("height", "400"),
+		SessionId: c.Session.ID(),
 	}
 
 	renderOpts.Url = setting.ToAbsUrl(renderOpts.Url)

+ 3 - 0
pkg/api/signup.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
@@ -26,4 +27,6 @@ func SignUp(c *middleware.Context, cmd m.CreateUserCommand) {
 	loginUserWithUser(&user, c)
 
 	c.JsonOK("User created and logged in")
+
+	metrics.M_Api_User_SignUp.Inc(1)
 }

+ 5 - 0
pkg/cmd/web.go

@@ -21,6 +21,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/api"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/services/eventpublisher"
@@ -88,6 +89,10 @@ func runWeb(c *cli.Context) {
 	m := newMacaron()
 	api.Register(m)
 
+	if setting.ReportingEnabled {
+		go metrics.StartUsageReportLoop()
+	}
+
 	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
 	log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl)
 	switch setting.Protocol {

+ 7 - 4
pkg/components/renderer/renderer.go

@@ -14,9 +14,10 @@ import (
 )
 
 type RenderOpts struct {
-	Url    string
-	Width  string
-	Height string
+	Url       string
+	Width     string
+	Height    string
+	SessionId string
 }
 
 func RenderToPng(params *RenderOpts) (string, error) {
@@ -26,7 +27,9 @@ func RenderToPng(params *RenderOpts) (string, error) {
 	pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, getHash(params.Url)))
 	pngPath = pngPath + ".png"
 
-	cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width, "height="+params.Height, "png="+pngPath)
+	cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width,
+		"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
+		"domain="+setting.Domain, "sessionid="+params.SessionId)
 	stdout, err := cmd.StdoutPipe()
 
 	if err != nil {

+ 72 - 0
pkg/metrics/counter.go

@@ -0,0 +1,72 @@
+package metrics
+
+import "sync/atomic"
+
+// Counters hold an int64 value that can be incremented and decremented.
+type Counter interface {
+	Clear()
+	Count() int64
+	Dec(int64)
+	Inc(int64)
+	Snapshot() Counter
+}
+
+// NewCounter constructs a new StandardCounter.
+func NewCounter() Counter {
+	return &StandardCounter{0}
+}
+
+// CounterSnapshot is a read-only copy of another Counter.
+type CounterSnapshot int64
+
+// Clear panics.
+func (CounterSnapshot) Clear() {
+	panic("Clear called on a CounterSnapshot")
+}
+
+// Count returns the count at the time the snapshot was taken.
+func (c CounterSnapshot) Count() int64 { return int64(c) }
+
+// Dec panics.
+func (CounterSnapshot) Dec(int64) {
+	panic("Dec called on a CounterSnapshot")
+}
+
+// Inc panics.
+func (CounterSnapshot) Inc(int64) {
+	panic("Inc called on a CounterSnapshot")
+}
+
+// Snapshot returns the snapshot.
+func (c CounterSnapshot) Snapshot() Counter { return c }
+
+// StandardCounter is the standard implementation of a Counter and uses the
+// sync/atomic package to manage a single int64 value.
+type StandardCounter struct {
+	count int64
+}
+
+// Clear sets the counter to zero.
+func (c *StandardCounter) Clear() {
+	atomic.StoreInt64(&c.count, 0)
+}
+
+// Count returns the current count.
+func (c *StandardCounter) Count() int64 {
+	return atomic.LoadInt64(&c.count)
+}
+
+// Dec decrements the counter by the given amount.
+func (c *StandardCounter) Dec(i int64) {
+	atomic.AddInt64(&c.count, -i)
+}
+
+// Inc increments the counter by the given amount.
+func (c *StandardCounter) Inc(i int64) {
+	atomic.AddInt64(&c.count, i)
+}
+
+// Snapshot returns a read-only copy of the counter.
+func (c *StandardCounter) Snapshot() Counter {
+	return CounterSnapshot(c.Count())
+}

+ 39 - 0
pkg/metrics/metric_ref.go

@@ -0,0 +1,39 @@
+package metrics
+
+type comboCounterRef struct {
+	usageCounter  Counter
+	metricCounter Counter
+}
+
+func NewComboCounterRef(name string) Counter {
+	cr := &comboCounterRef{}
+	cr.usageCounter = UsageStats.GetOrRegister(name, NewCounter).(Counter)
+	cr.metricCounter = MetricStats.GetOrRegister(name, NewCounter).(Counter)
+	return cr
+}
+
+func (c comboCounterRef) Clear() {
+	c.usageCounter.Clear()
+	c.metricCounter.Clear()
+}
+
+func (c comboCounterRef) Count() int64 {
+	panic("Count called on a combocounter ref")
+}
+
+// Dec panics.
+func (c comboCounterRef) Dec(i int64) {
+	c.usageCounter.Dec(i)
+	c.metricCounter.Dec(i)
+}
+
+// Inc panics.
+func (c comboCounterRef) Inc(i int64) {
+	c.usageCounter.Inc(i)
+	c.metricCounter.Inc(i)
+}
+
+// Snapshot returns the snapshot.
+func (c comboCounterRef) Snapshot() Counter {
+	panic("snapshot called on a combocounter ref")
+}

+ 25 - 0
pkg/metrics/metrics.go

@@ -0,0 +1,25 @@
+package metrics
+
+var UsageStats = NewRegistry()
+var MetricStats = NewRegistry()
+
+var (
+	M_Instance_Start = NewComboCounterRef("instance.start")
+
+	M_Page_Status_200 = NewComboCounterRef("page.status.200")
+	M_Page_Status_500 = NewComboCounterRef("page.status.500")
+	M_Page_Status_404 = NewComboCounterRef("page.status.404")
+
+	M_Api_Status_500 = NewComboCounterRef("api.status.500")
+	M_Api_Status_404 = NewComboCounterRef("api.status.404")
+
+	M_Api_User_SignUp       = NewComboCounterRef("api.user.signup")
+	M_Api_Dashboard_Get     = NewComboCounterRef("api.dashboard.get")
+	M_Api_Dashboard_Post    = NewComboCounterRef("api.dashboard.post")
+	M_Api_Admin_User_Create = NewComboCounterRef("api.admin.user_create")
+	M_Api_Login_Post        = NewComboCounterRef("api.login.post")
+	M_Api_Login_OAuth       = NewComboCounterRef("api.login.oauth")
+	M_Api_Org_Create        = NewComboCounterRef("api.org.create")
+
+	M_Models_Dashboard_Insert = NewComboCounterRef("models.dashboard.insert")
+)

+ 102 - 0
pkg/metrics/registry.go

@@ -0,0 +1,102 @@
+package metrics
+
+import (
+	"fmt"
+	"reflect"
+	"sync"
+)
+
+// DuplicateMetric is the error returned by Registry.Register when a metric
+// already exists.  If you mean to Register that metric you must first
+// Unregister the existing metric.
+type DuplicateMetric string
+
+func (err DuplicateMetric) Error() string {
+	return fmt.Sprintf("duplicate metric: %s", string(err))
+}
+
+type Registry interface {
+	// Call the given function for each registered metric.
+	Each(func(string, interface{}))
+
+	// Get the metric by the given name or nil if none is registered.
+	Get(string) interface{}
+
+	// Gets an existing metric or registers the given one.
+	// The interface can be the metric to register if not found in registry,
+	// or a function returning the metric for lazy instantiation.
+	GetOrRegister(string, interface{}) interface{}
+
+	// Register the given metric under the given name.
+	Register(string, interface{}) error
+}
+
+// The standard implementation of a Registry is a mutex-protected map
+// of names to metrics.
+type StandardRegistry struct {
+	metrics map[string]interface{}
+	mutex   sync.Mutex
+}
+
+// Create a new registry.
+func NewRegistry() Registry {
+	return &StandardRegistry{metrics: make(map[string]interface{})}
+}
+
+// Call the given function for each registered metric.
+func (r *StandardRegistry) Each(f func(string, interface{})) {
+	for name, i := range r.registered() {
+		f(name, i)
+	}
+}
+
+// Get the metric by the given name or nil if none is registered.
+func (r *StandardRegistry) Get(name string) interface{} {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+	return r.metrics[name]
+}
+
+// Gets an existing metric or creates and registers a new one. Threadsafe
+// alternative to calling Get and Register on failure.
+// The interface can be the metric to register if not found in registry,
+// or a function returning the metric for lazy instantiation.
+func (r *StandardRegistry) GetOrRegister(name string, i interface{}) interface{} {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+	if metric, ok := r.metrics[name]; ok {
+		return metric
+	}
+	if v := reflect.ValueOf(i); v.Kind() == reflect.Func {
+		i = v.Call(nil)[0].Interface()
+	}
+	r.register(name, i)
+	return i
+}
+
+// Register the given metric under the given name.  Returns a DuplicateMetric
+// if a metric by the given name is already registered.
+func (r *StandardRegistry) Register(name string, i interface{}) error {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+	return r.register(name, i)
+}
+
+func (r *StandardRegistry) register(name string, i interface{}) error {
+	if _, ok := r.metrics[name]; ok {
+		return DuplicateMetric(name)
+	}
+
+	r.metrics[name] = i
+	return nil
+}
+
+func (r *StandardRegistry) registered() map[string]interface{} {
+	metrics := make(map[string]interface{}, len(r.metrics))
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+	for name, i := range r.metrics {
+		metrics[name] = i
+	}
+	return metrics
+}

+ 63 - 0
pkg/metrics/report_usage.go

@@ -0,0 +1,63 @@
+package metrics
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+func StartUsageReportLoop() chan struct{} {
+	M_Instance_Start.Inc(1)
+
+	ticker := time.NewTicker(time.Hour)
+	for {
+		select {
+		case <-ticker.C:
+			sendUsageStats()
+		}
+	}
+}
+
+func sendUsageStats() {
+	log.Trace("Sending anonymous usage stats to stats.grafana.org")
+
+	version := strings.Replace(setting.BuildVersion, ".", "_", -1)
+
+	metrics := map[string]interface{}{}
+	report := map[string]interface{}{
+		"version": version,
+		"metrics": metrics,
+	}
+
+	// statsQuery := m.GetSystemStatsQuery{}
+	// if err := bus.Dispatch(&statsQuery); err != nil {
+	// 	log.Error(3, "Failed to get system stats", err)
+	// 	return
+	// }
+
+	UsageStats.Each(func(name string, i interface{}) {
+		switch metric := i.(type) {
+		case Counter:
+			if metric.Count() > 0 {
+				metrics[name+".count"] = metric.Count()
+				metric.Clear()
+			}
+		}
+	})
+
+	// metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount
+	// metrics["stats.users.count"] = statsQuery.Result.UserCount
+	// metrics["stats.orgs.count"] = statsQuery.Result.OrgCount
+
+	out, _ := json.Marshal(report)
+	data := bytes.NewBuffer(out)
+
+	client := http.Client{Timeout: time.Duration(5 * time.Second)}
+
+	go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data)
+}

+ 0 - 7
pkg/middleware/auth.go

@@ -22,13 +22,6 @@ func getRequestUserId(c *Context) int64 {
 		return userId.(int64)
 	}
 
-	// TODO: figure out a way to secure this
-	if c.Req.URL.Query().Get("render") == "1" {
-		userId := c.QueryInt64(SESS_KEY_USERID)
-		c.Session.Set(SESS_KEY_USERID, userId)
-		return userId
-	}
-
 	return 0
 }
 

+ 12 - 0
pkg/middleware/middleware.go

@@ -10,6 +10,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 )
@@ -99,6 +100,15 @@ func (ctx *Context) Handle(status int, title string, err error) {
 		}
 	}
 
+	switch status {
+	case 200:
+		metrics.M_Page_Status_200.Inc(1)
+	case 404:
+		metrics.M_Page_Status_404.Inc(1)
+	case 500:
+		metrics.M_Page_Status_500.Inc(1)
+	}
+
 	ctx.Data["Title"] = title
 	ctx.HTML(status, strconv.Itoa(status))
 }
@@ -128,7 +138,9 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
 	switch status {
 	case 404:
 		resp["message"] = "Not Found"
+		metrics.M_Api_Status_500.Inc(1)
 	case 500:
+		metrics.M_Api_Status_404.Inc(1)
 		resp["message"] = "Internal Server Error"
 	}
 

+ 11 - 0
pkg/models/stats.go

@@ -0,0 +1,11 @@
+package models
+
+type SystemStats struct {
+	DashboardCount int
+	UserCount      int
+	OrgCount       int
+}
+
+type GetSystemStatsQuery struct {
+	Result *SystemStats
+}

+ 2 - 0
pkg/services/sqlstore/dashboard.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/go-xorm/xorm"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 )
 
@@ -48,6 +49,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
 		}
 
 		if dash.Id == 0 {
+			metrics.M_Models_Dashboard_Insert.Inc(1)
 			_, err = sess.Insert(dash)
 		} else {
 			dash.Version += 1

+ 36 - 0
pkg/services/sqlstore/stats.go

@@ -0,0 +1,36 @@
+package sqlstore
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", GetSystemStats)
+}
+
+func GetSystemStats(query *m.GetSystemStatsQuery) error {
+	var rawSql = `SELECT
+			(
+				SELECT COUNT(*)
+        FROM ` + dialect.Quote("user") + `
+      ) AS user_count,
+			(
+				SELECT COUNT(*)
+        FROM ` + dialect.Quote("org") + `
+      ) AS org_count,
+      (
+        SELECT COUNT(*)
+        FROM ` + dialect.Quote("dashboard") + `
+      ) AS dashboard_count
+			`
+
+	var stats m.SystemStats
+	_, err := x.Sql(rawSql).Get(&stats)
+	if err != nil {
+		return err
+	}
+
+	query.Result = &stats
+	return err
+}

+ 4 - 0
pkg/setting/setting.go

@@ -96,6 +96,8 @@ var (
 	PhantomDir string
 
 	configFiles []string
+
+	ReportingEnabled bool
 )
 
 func init() {
@@ -233,6 +235,8 @@ func NewConfigContext(config string) {
 	ImagesDir = "data/png"
 	PhantomDir = "vendor/phantomjs"
 
+	ReportingEnabled = Cfg.Section("").Key("reporting-enabled").MustBool(true)
+
 	readSessionConfig()
 }
 

+ 1 - 0
src/app/features/dashboard/partials/shareDashboard.html

@@ -85,3 +85,4 @@
 	</div>
 
 </div>
+

+ 18 - 8
src/app/features/panellinkeditor/module.html

@@ -2,23 +2,23 @@
   <div class="section">
 		<h5>Drilldown / detail link<tip>These links appear in the dropdown menu in the panel menu. </tip></h5>
 
-		<div class="tight-form" ng-repeat="link in panel.links"j>
+		<div class="tight-form" ng-repeat="link in panel.links">
 			<ul class="tight-form-list">
 				<li class="tight-form-item">
 					<i class="fa fa-remove pointer" ng-click="deleteLink(link)"></i>
 				</li>
 
-				<li class="tight-form-item">title</li>
+				<li class="tight-form-item" style="width: 80px;">Link title</li>
 				<li>
 					<input type="text" ng-model="link.title" class="input-medium tight-form-input">
 				</li>
 
-				<li class="tight-form-item">type</li>
+				<li class="tight-form-item">Type</li>
 				<li>
 					<select class="input-medium tight-form-input" style="width: 101px;" ng-model="link.type" ng-options="f for f in ['dashboard','absolute']"></select>
 				</li>
 
-				<li class="tight-form-item" ng-show="link.type === 'dashboard'">dashboard</li>
+				<li class="tight-form-item" ng-show="link.type === 'dashboard'">Dashboard</li>
 				<li ng-show="link.type === 'dashboard'">
 					<input type="text"
 					ng-model="link.dashboard"
@@ -26,20 +26,30 @@
 					class="input-large tight-form-input">
 				</li>
 
-				<li class="tight-form-item" ng-show="link.type === 'absolute'">url</li>
+				<li class="tight-form-item" ng-show="link.type === 'absolute'">Url</li>
 				<li ng-show="link.type === 'absolute'">
-					<input type="text" ng-model="link.url" class="input-large tight-form-input">
+					<input type="text" ng-model="link.url" class="input-xlarge tight-form-input">
 				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
 
-				<li class="tight-form-item">params
+		<div class="tight-form">
+			<ul class="tight-form-list" role="menu">
+				<li class="tight-form-item">
+					<i class="fa fa-remove invisible"></i>
+				</li>
+				<li class="tight-form-item" style="width: 80px;">
+					Params
 					<tip>Use var-variableName=value to pass templating variables.</tip>
 				</li>
 				<li>
-					<input type="text" ng-model="link.params" class="input-medium tight-form-input">
+					<input type="text" ng-model="link.params" class="input-xxlarge tight-form-input">
 				</li>
 			</ul>
 			<div class="clearfix"></div>
 		</div>
+
 	</div>
 </div>
 

+ 4 - 4
src/app/panels/graph/graph.js

@@ -396,8 +396,8 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
             }
           }
 
-          axis.min = axis.min !== null ? axis.min : 1;
-          axis.ticks = [1];
+          axis.min = axis.min !== null ? axis.min : 0;
+          axis.ticks = [0, 1];
           var nextTick = 1;
 
           while (true) {
@@ -409,10 +409,10 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
           }
 
           if (axis.logBase === 10) {
-            axis.transform = function(v) { return Math.log(v+0.0001); };
+            axis.transform = function(v) { return Math.log(v+0.1); };
             axis.inverseTransform  = function (v) { return Math.pow(10,v); };
           } else {
-            axis.transform = function(v) { return Math.log(v+0.0001) / Math.log(axis.logBase); };
+            axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); };
             axis.inverseTransform  = function (v) { return Math.pow(axis.logBase,v); };
           }
         }

+ 3 - 3
src/test/specs/graph-specs.js

@@ -153,9 +153,9 @@ define([
 
       it('should apply axis transform and ticks', function() {
         var axis = ctx.plotOptions.yaxes[0];
-        expect(axis.transform(100)).to.be(Math.log(100+0.0001));
-        expect(axis.ticks[0]).to.be(1);
-        expect(axis.ticks[1]).to.be(10);
+        expect(axis.transform(100)).to.be(Math.log(100+0.1));
+        expect(axis.ticks[0]).to.be(0);
+        expect(axis.ticks[1]).to.be(1);
       });
     });
 

+ 8 - 2
vendor/phantomjs/render.js

@@ -9,13 +9,19 @@ args.forEach(function(arg) {
   params[parts[1]] = parts[2];
 });
 
-var usage = "url=<url> png=<filename> width=<width> height=<height>";
+var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
 
-if (!params.url || !params.png) {
+if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) {
   console.log(usage);
   phantom.exit();
 }
 
+phantom.addCookie({
+  'name': params.cookiename,
+  'value': params.sessionid,
+  'domain': params.domain
+});
+
 page.viewportSize = {
   width: params.width || '800',
   height: params.height || '400'