Commit cdabe503 by Torkel Ödegaard

Merge branch 'master' of github.com:torkelo/grafana-pro

Conflicts:
	grafana
parents f133a9de a9a06ad5
Subproject commit 9b2476451ef341285e1387c6eefe97c7995e300a Subproject commit c62ee78cba92ce2733196a824b0f0b70e4c40bdb
No preview for this file type
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/torkelo/grafana-pro/pkg/components" "github.com/torkelo/grafana-pro/pkg/components"
"github.com/torkelo/grafana-pro/pkg/models"
"github.com/torkelo/grafana-pro/pkg/stores" "github.com/torkelo/grafana-pro/pkg/stores"
) )
...@@ -53,12 +54,20 @@ func (self *HttpServer) ListenAndServe() { ...@@ -53,12 +54,20 @@ func (self *HttpServer) ListenAndServe() {
// register default route // register default route
self.router.GET("/", self.auth(), self.index) self.router.GET("/", self.auth(), self.index)
self.router.GET("/dashboard/*_", self.auth(), self.index) self.router.GET("/dashboard/*_", self.auth(), self.index)
self.router.GET("/admin/*_", self.auth(), self.index)
self.router.GET("/account/*_", self.auth(), self.index)
self.router.Run(":" + self.port) self.router.Run(":" + self.port)
} }
func (self *HttpServer) index(c *gin.Context) { func (self *HttpServer) index(c *gin.Context) {
c.HTML(200, "index.html", &indexViewModel{title: "hello from go"}) viewModel := &IndexDto{}
userAccount, _ := c.Get("userAccount")
if userAccount != nil {
viewModel.User.Login = userAccount.(*models.UserAccount).Login
}
c.HTML(200, "index.html", viewModel)
} }
func CacheHeadersMiddleware() gin.HandlerFunc { func CacheHeadersMiddleware() gin.HandlerFunc {
...@@ -66,12 +75,3 @@ func CacheHeadersMiddleware() gin.HandlerFunc { ...@@ -66,12 +75,3 @@ func CacheHeadersMiddleware() gin.HandlerFunc {
c.Writer.Header().Add("Cache-Control", "max-age=0, public, must-revalidate, proxy-revalidate") c.Writer.Header().Add("Cache-Control", "max-age=0, public, must-revalidate, proxy-revalidate")
} }
} }
// Api Handler Registration
var routeHandlers = make([]routeHandlerRegisterFn, 0)
type routeHandlerRegisterFn func(self *HttpServer)
func addRoutes(fn routeHandlerRegisterFn) {
routeHandlers = append(routeHandlers, fn)
}
package api
import "github.com/gin-gonic/gin"
func init() {
addRoutes(func(self *HttpServer) {
self.addRoute("POST", "/api/account/collaborators/add", self.addCollaborator)
})
}
type addCollaboratorDto struct {
Email string `json:"email" binding:"required"`
}
func (self *HttpServer) addCollaborator(c *gin.Context, auth *authContext) {
var model addCollaboratorDto
if !c.EnsureBody(&model) {
c.JSON(400, gin.H{"status": "Collaborator not found"})
return
}
collaborator, err := self.store.GetUserAccountLogin(model.Email)
if err != nil {
c.JSON(404, gin.H{"status": "Collaborator not found"})
return
}
userAccount := auth.userAccount
if collaborator.Id == userAccount.Id {
c.JSON(400, gin.H{"status": "Cannot add yourself as collaborator"})
return
}
err = userAccount.AddCollaborator(collaborator.Id)
if err != nil {
c.JSON(400, gin.H{"status": err.Error()})
return
}
self.store.SaveUserAccount(userAccount)
c.JSON(200, gin.H{"status": "Collaborator added"})
}
package api
import (
"github.com/gin-gonic/gin"
"github.com/torkelo/grafana-pro/pkg/models"
)
type authContext struct {
account *models.UserAccount
userAccount *models.UserAccount
}
func (auth *authContext) getAccountId() int {
return auth.account.Id
}
func (self *HttpServer) authDenied(c *gin.Context) {
c.Writer.Header().Set("Location", "/login")
c.Abort(302)
}
func (self *HttpServer) auth() gin.HandlerFunc {
return func(c *gin.Context) {
session, _ := sessionStore.Get(c.Request, "grafana-session")
if c.Request.URL.Path != "/login" && session.Values["userAccountId"] == nil {
self.authDenied(c)
return
}
account, err := self.store.GetAccount(session.Values["userAccountId"].(int))
if err != nil {
self.authDenied(c)
return
}
usingAccount, err := self.store.GetAccount(session.Values["usingAccountId"].(int))
if err != nil {
self.authDenied(c)
return
}
c.Set("userAccount", account)
c.Set("usingAccount", usingAccount)
session.Save(c.Request, c.Writer)
}
}
...@@ -8,29 +8,51 @@ import ( ...@@ -8,29 +8,51 @@ import (
func init() { func init() {
addRoutes(func(self *HttpServer) { addRoutes(func(self *HttpServer) {
self.router.GET("/api/dashboards/:id", self.auth(), self.getDashboard) self.addRoute("GET", "/api/dashboards/:slug", self.getDashboard)
self.router.GET("/api/search/", self.auth(), self.search) self.addRoute("GET", "/api/search/", self.search)
self.router.POST("/api/dashboard", self.auth(), self.postDashboard) self.addRoute("POST", "/api/dashboard/", self.postDashboard)
self.addRoute("DELETE", "/api/dashboard/:slug", self.deleteDashboard)
}) })
} }
func (self *HttpServer) getDashboard(c *gin.Context) { func (self *HttpServer) getDashboard(c *gin.Context, auth *authContext) {
id := c.Params.ByName("id") slug := c.Params.ByName("slug")
accountId, err := c.Get("accountId")
dash, err := self.store.GetDashboard(id, accountId.(int)) dash, err := self.store.GetDashboard(slug, auth.getAccountId())
if err != nil { if err != nil {
c.JSON(404, newErrorResponse("Dashboard not found")) c.JSON(404, newErrorResponse("Dashboard not found"))
return return
} }
dash.Data["id"] = dash.Id
c.JSON(200, dash.Data) c.JSON(200, dash.Data)
} }
func (self *HttpServer) search(c *gin.Context) { func (self *HttpServer) deleteDashboard(c *gin.Context, auth *authContext) {
slug := c.Params.ByName("slug")
dash, err := self.store.GetDashboard(slug, auth.getAccountId())
if err != nil {
c.JSON(404, newErrorResponse("Dashboard not found"))
return
}
err = self.store.DeleteDashboard(slug, auth.getAccountId())
if err != nil {
c.JSON(500, newErrorResponse("Failed to delete dashboard: "+err.Error()))
return
}
var resp = map[string]interface{}{"title": dash.Title}
c.JSON(200, resp)
}
func (self *HttpServer) search(c *gin.Context, auth *authContext) {
query := c.Params.ByName("q") query := c.Params.ByName("q")
results, err := self.store.Query(query) results, err := self.store.Query(query, auth.getAccountId())
if err != nil { if err != nil {
log.Error("Store query error: %v", err) log.Error("Store query error: %v", err)
c.JSON(500, newErrorResponse("Failed")) c.JSON(500, newErrorResponse("Failed"))
...@@ -40,14 +62,14 @@ func (self *HttpServer) search(c *gin.Context) { ...@@ -40,14 +62,14 @@ func (self *HttpServer) search(c *gin.Context) {
c.JSON(200, results) c.JSON(200, results)
} }
func (self *HttpServer) postDashboard(c *gin.Context) { func (self *HttpServer) postDashboard(c *gin.Context, auth *authContext) {
var command saveDashboardCommand var command saveDashboardCommand
if c.EnsureBody(&command) { if c.EnsureBody(&command) {
dashboard := models.NewDashboard("test") dashboard := models.NewDashboard("test")
dashboard.Data = command.Dashboard dashboard.Data = command.Dashboard
dashboard.Title = dashboard.Data["title"].(string) dashboard.Title = dashboard.Data["title"].(string)
dashboard.AccountId = 1 dashboard.AccountId = auth.getAccountId()
dashboard.UpdateSlug() dashboard.UpdateSlug()
if dashboard.Data["id"] != nil { if dashboard.Data["id"] != nil {
......
...@@ -35,34 +35,21 @@ func (self *HttpServer) loginPost(c *gin.Context) { ...@@ -35,34 +35,21 @@ func (self *HttpServer) loginPost(c *gin.Context) {
} }
session, _ := sessionStore.Get(c.Request, "grafana-session") session, _ := sessionStore.Get(c.Request, "grafana-session")
session.Values["login"] = true session.Values["userAccountId"] = account.Id
session.Values["accountId"] = account.DatabaseId session.Values["usingAccountId"] = account.UsingAccountId
session.Save(c.Request, c.Writer) session.Save(c.Request, c.Writer)
c.JSON(200, gin.H{"status": "you are logged in"}) var resp = &LoginResultDto{}
resp.Status = "Logged in"
resp.User.Login = account.Login
c.JSON(200, resp)
} }
func (self *HttpServer) logoutPost(c *gin.Context) { func (self *HttpServer) logoutPost(c *gin.Context) {
session, _ := sessionStore.Get(c.Request, "grafana-session") session, _ := sessionStore.Get(c.Request, "grafana-session")
session.Values["login"] = nil session.Values = nil
session.Save(c.Request, c.Writer) session.Save(c.Request, c.Writer)
c.JSON(200, gin.H{"status": "logged out"}) c.JSON(200, gin.H{"status": "logged out"})
} }
func (self *HttpServer) auth() gin.HandlerFunc {
return func(c *gin.Context) {
session, _ := sessionStore.Get(c.Request, "grafana-session")
if c.Request.URL.Path != "/login" && session.Values["login"] == nil {
c.Writer.Header().Set("Location", "/login")
c.Abort(302)
return
}
c.Set("accountId", session.Values["accountId"])
session.Save(c.Request, c.Writer)
}
}
...@@ -10,8 +10,17 @@ type errorResponse struct { ...@@ -10,8 +10,17 @@ type errorResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
type indexViewModel struct { type IndexDto struct {
title string User CurrentUserDto
}
type CurrentUserDto struct {
Login string `json:"login"`
}
type LoginResultDto struct {
Status string `json:"status"`
User CurrentUserDto `json:"user"`
} }
func newErrorResponse(message string) *errorResponse { func newErrorResponse(message string) *errorResponse {
......
package api
import (
"github.com/gin-gonic/gin"
"github.com/torkelo/grafana-pro/pkg/models"
)
type routeHandlerRegisterFn func(self *HttpServer)
type routeHandlerFn func(c *gin.Context, auth *authContext)
var routeHandlers = make([]routeHandlerRegisterFn, 0)
func getRouteHandlerWrapper(handler routeHandlerFn) gin.HandlerFunc {
return func(c *gin.Context) {
authContext := authContext{
account: c.MustGet("usingAccount").(*models.UserAccount),
userAccount: c.MustGet("userAccount").(*models.UserAccount),
}
handler(c, &authContext)
}
}
func (self *HttpServer) addRoute(method string, path string, handler routeHandlerFn) {
switch method {
case "GET":
self.router.GET(path, self.auth(), getRouteHandlerWrapper(handler))
case "POST":
self.router.POST(path, self.auth(), getRouteHandlerWrapper(handler))
case "DELETE":
self.router.DELETE(path, self.auth(), getRouteHandlerWrapper(handler))
}
}
func addRoutes(fn routeHandlerRegisterFn) {
routeHandlers = append(routeHandlers, fn)
}
package models
import (
"errors"
"time"
)
type CollaboratorLink struct {
AccountId int
Role string
ModifiedOn time.Time
CreatedOn time.Time
}
type UserAccount struct {
Id int `gorethink:"id"`
UserName string
Login string
Email string
Password string
NextDashboardId int
UsingAccountId int
Collaborators []CollaboratorLink
CreatedOn time.Time
ModifiedOn time.Time
}
func (account *UserAccount) AddCollaborator(accountId int) error {
for _, collaborator := range account.Collaborators {
if collaborator.AccountId == accountId {
return errors.New("Collaborator already exists")
}
}
account.Collaborators = append(account.Collaborators, CollaboratorLink{
AccountId: accountId,
Role: "admin",
CreatedOn: time.Now(),
ModifiedOn: time.Now(),
})
return nil
}
...@@ -21,31 +21,6 @@ type Dashboard struct { ...@@ -21,31 +21,6 @@ type Dashboard struct {
Data map[string]interface{} Data map[string]interface{}
} }
type UserAccountLink struct {
UserId int
Role string
ModifiedOn time.Time
CreatedOn time.Time
}
type UserAccount struct {
DatabaseId int `gorethink:"id"`
UserName string
Login string
Email string
Password string
NextDashboardId int
UsingAccountId int
GrantedAccess []UserAccountLink
CreatedOn time.Time
ModifiedOn time.Time
}
type UserContext struct {
UserId string
AccountId string
}
type SearchResult struct { type SearchResult struct {
Id string `json:"id"` Id string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
......
package stores package stores
import ( import (
"errors"
"time" "time"
log "github.com/alecthomas/log4go" log "github.com/alecthomas/log4go"
...@@ -44,6 +45,10 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore { ...@@ -44,6 +45,10 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore {
return []interface{}{row.Field("AccountId"), row.Field("Slug")} return []interface{}{row.Field("AccountId"), row.Field("Slug")}
}).Exec(session) }).Exec(session)
r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountId", func(row r.Term) interface{} {
return []interface{}{row.Field("AccountId")}
}).Exec(session)
r.Db(config.DatabaseName).Table("accounts").IndexCreateFunc("AccountLogin", func(row r.Term) interface{} { r.Db(config.DatabaseName).Table("accounts").IndexCreateFunc("AccountLogin", func(row r.Term) interface{} {
return []interface{}{row.Field("Login")} return []interface{}{row.Field("Login")}
}).Exec(session) }).Exec(session)
...@@ -59,7 +64,7 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore { ...@@ -59,7 +64,7 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore {
} }
func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error { func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Upsert: true}).RunWrite(self.session) resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(self.session)
if err != nil { if err != nil {
return err return err
} }
...@@ -88,9 +93,25 @@ func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dash ...@@ -88,9 +93,25 @@ func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dash
return &dashboard, nil return &dashboard, nil
} }
func (self *rethinkStore) Query(query string) ([]*models.SearchResult, error) { func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error {
resp, err := r.Table("dashboards").
GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
Delete().RunWrite(self.session)
if err != nil {
return err
}
if resp.Deleted != 1 {
return errors.New("Did not find dashboard to delete")
}
return nil
}
func (self *rethinkStore) Query(query string, accountId int) ([]*models.SearchResult, error) {
docs, err := r.Table("dashboards").GetAllByIndex("AccountId", []interface{}{accountId}).Filter(r.Row.Field("Title").Match(".*")).Run(self.session)
docs, err := r.Table("dashboards").Filter(r.Row.Field("Title").Match(".*")).Run(self.session)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -10,17 +10,19 @@ import ( ...@@ -10,17 +10,19 @@ import (
func (self *rethinkStore) getNextAccountId() (int, error) { func (self *rethinkStore) getNextAccountId() (int, error) {
resp, err := r.Table("master").Get("ids").Update(map[string]interface{}{ resp, err := r.Table("master").Get("ids").Update(map[string]interface{}{
"NextAccountId": r.Row.Field("NextAccountId").Add(1), "NextAccountId": r.Row.Field("NextAccountId").Add(1),
}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session) }, r.UpdateOpts{ReturnChanges: true}).RunWrite(self.session)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if resp.NewValue == nil { change := resp.Changes[0]
if change.NewValue == nil {
return 0, errors.New("Failed to get new value after incrementing account id") return 0, errors.New("Failed to get new value after incrementing account id")
} }
return int(resp.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil return int(change.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil
} }
func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error { func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error {
...@@ -29,7 +31,8 @@ func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error { ...@@ -29,7 +31,8 @@ func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error {
return err return err
} }
account.DatabaseId = accountId account.Id = accountId
account.UsingAccountId = accountId
resp, err := r.Table("accounts").Insert(account).RunWrite(self.session) resp, err := r.Table("accounts").Insert(account).RunWrite(self.session)
if err != nil { if err != nil {
...@@ -59,18 +62,36 @@ func (self *rethinkStore) GetUserAccountLogin(emailOrName string) (*models.UserA ...@@ -59,18 +62,36 @@ func (self *rethinkStore) GetUserAccountLogin(emailOrName string) (*models.UserA
return &account, nil return &account, nil
} }
func (self *rethinkStore) GetAccount(id int) (*models.UserAccount, error) {
resp, err := r.Table("accounts").Get(id).Run(self.session)
if err != nil {
return nil, err
}
var account models.UserAccount
err = resp.One(&account)
if err != nil {
return nil, errors.New("Not found")
}
return &account, nil
}
func (self *rethinkStore) getNextDashboardNumber(accountId int) (int, error) { func (self *rethinkStore) getNextDashboardNumber(accountId int) (int, error) {
resp, err := r.Table("accounts").Get(accountId).Update(map[string]interface{}{ resp, err := r.Table("accounts").Get(accountId).Update(map[string]interface{}{
"NextDashboardId": r.Row.Field("NextDashboardId").Add(1), "NextDashboardId": r.Row.Field("NextDashboardId").Add(1),
}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session) }, r.UpdateOpts{ReturnChanges: true}).RunWrite(self.session)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if resp.NewValue == nil { change := resp.Changes[0]
if change.NewValue == nil {
return 0, errors.New("Failed to get next dashboard id, no new value after update") return 0, errors.New("Failed to get next dashboard id, no new value after update")
} }
return int(resp.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil return int(change.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil
} }
...@@ -38,17 +38,17 @@ func TestRethinkStore(t *testing.T) { ...@@ -38,17 +38,17 @@ func TestRethinkStore(t *testing.T) {
account := &models.UserAccount{UserName: "torkelo", Email: "mupp", Login: "test@test.com"} account := &models.UserAccount{UserName: "torkelo", Email: "mupp", Login: "test@test.com"}
err := store.SaveUserAccount(account) err := store.SaveUserAccount(account)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(account.DatabaseId, ShouldNotEqual, 0) So(account.Id, ShouldNotEqual, 0)
read, err := store.GetUserAccountLogin("test@test.com") read, err := store.GetUserAccountLogin("test@test.com")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(read.DatabaseId, ShouldEqual, account.DatabaseId) So(read.Id, ShouldEqual, account.DatabaseId)
}) })
Convey("can get next dashboard id", t, func() { Convey("can get next dashboard id", t, func() {
account := &models.UserAccount{UserName: "torkelo", Email: "mupp"} account := &models.UserAccount{UserName: "torkelo", Email: "mupp"}
err := store.SaveUserAccount(account) err := store.SaveUserAccount(account)
dashId, err := store.getNextDashboardNumber(account.DatabaseId) dashId, err := store.getNextDashboardNumber(account.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(dashId, ShouldEqual, 1) So(dashId, ShouldEqual, 1)
}) })
......
...@@ -5,11 +5,13 @@ import ( ...@@ -5,11 +5,13 @@ import (
) )
type Store interface { type Store interface {
GetDashboard(title string, accountId int) (*models.Dashboard, error) GetDashboard(slug string, accountId int) (*models.Dashboard, error)
SaveDashboard(dash *models.Dashboard) error SaveDashboard(dash *models.Dashboard) error
Query(query string) ([]*models.SearchResult, error) DeleteDashboard(slug string, accountId int) error
Query(query string, acccountId int) ([]*models.SearchResult, error)
SaveUserAccount(acccount *models.UserAccount) error SaveUserAccount(acccount *models.UserAccount) error
GetUserAccountLogin(emailOrName string) (*models.UserAccount, error) GetUserAccountLogin(emailOrName string) (*models.UserAccount, error)
GetAccount(id int) (*models.UserAccount, error)
Close() Close()
} }
......
<!DOCTYPE html> <!DOCTYPE html>
<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]--> <html lang="en">
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
...@@ -8,6 +7,7 @@ ...@@ -8,6 +7,7 @@
<title>Grafana</title> <title>Grafana</title>
<link rel="stylesheet" href="/public/css/grafana.dark.min.css" title="Dark"> <link rel="stylesheet" href="/public/css/grafana.dark.min.css" title="Dark">
<link rel="icon" type="image/png" href="img/fav32.png">
<base href="/"> <base href="/">
<!-- build:js app/app.js --> <!-- build:js app/app.js -->
...@@ -20,18 +20,22 @@ ...@@ -20,18 +20,22 @@
</head> </head>
<body ng-cloak ng-controller="GrafanaCtrl"> <body ng-cloak ng-controller="GrafanaCtrl">
<link rel="stylesheet" href="/public/css/grafana.light.min.css" ng-if="grafana.style === 'light'"> <link rel="stylesheet" href="/public/css/grafana.light.min.css" ng-if="grafana.style === 'light'">
<div class="pro-container" ng-class="{'pro-sidemenu-open': showProSideMenu}"> <div class="pro-container" ng-class="{'pro-sidemenu-open': grafana.sidemenu}">
<aside class="pro-sidemenu" ng-if="showProSideMenu"> <aside class="pro-sidemenu" ng-if="grafana.sidemenu">
<div ng-include="'app/partials/pro/sidemenu.html'"></div> <div ng-include="'app/partials/pro/sidemenu.html'"></div>
</aside> </aside>
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} dashboard-notice" ng-show="$last"> <div class="page-alert-list">
<button type="button" class="close" ng-click="dashAlerts.clear(alert)" style="padding-right:50px">&times;</button> <div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
<strong>{{alert.title}}</strong> <span ng-bind-html='alert.text'></span> <div style="padding-right:10px" class='pull-right small'> {{$index + 1}} alert(s) </div> <button type="button" class="alert-close" ng-click="dashAlerts.clear(alert)">
<i class="icon-remove-sign"></i>
</button>
<div class="alert-title">{{alert.title}}</div>
<div ng-bind-html='alert.text'></div>
</div>
</div> </div>
<div ng-view class="pro-main-view"></div> <div ng-view class="pro-main-view"></div>
...@@ -39,4 +43,12 @@ ...@@ -39,4 +43,12 @@
</div> </div>
</body> </body>
<script>
window.grafanaBootData = {
user: {
login: [[.User.Login]]
}
};
</script>
</html> </html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment