mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 16:45:43 -06:00
Merge branch 'master' of github.com:torkelo/grafana-pro
Conflicts: grafana
This commit is contained in:
commit
cdabe50320
2
grafana
2
grafana
@ -1 +1 @@
|
||||
Subproject commit 9b2476451ef341285e1387c6eefe97c7995e300a
|
||||
Subproject commit c62ee78cba92ce2733196a824b0f0b70e4c40bdb
|
BIN
grafana-pro
BIN
grafana-pro
Binary file not shown.
@ -7,6 +7,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/torkelo/grafana-pro/pkg/components"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
"github.com/torkelo/grafana-pro/pkg/stores"
|
||||
)
|
||||
|
||||
@ -53,12 +54,20 @@ func (self *HttpServer) ListenAndServe() {
|
||||
// register default route
|
||||
self.router.GET("/", 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -66,12 +75,3 @@ func CacheHeadersMiddleware() gin.HandlerFunc {
|
||||
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)
|
||||
}
|
||||
|
45
pkg/api/api_account.go
Normal file
45
pkg/api/api_account.go
Normal file
@ -0,0 +1,45 @@
|
||||
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"})
|
||||
}
|
48
pkg/api/api_auth.go
Normal file
48
pkg/api/api_auth.go
Normal file
@ -0,0 +1,48 @@
|
||||
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 (
|
||||
|
||||
func init() {
|
||||
addRoutes(func(self *HttpServer) {
|
||||
self.router.GET("/api/dashboards/:id", self.auth(), self.getDashboard)
|
||||
self.router.GET("/api/search/", self.auth(), self.search)
|
||||
self.router.POST("/api/dashboard", self.auth(), self.postDashboard)
|
||||
self.addRoute("GET", "/api/dashboards/:slug", self.getDashboard)
|
||||
self.addRoute("GET", "/api/search/", self.search)
|
||||
self.addRoute("POST", "/api/dashboard/", self.postDashboard)
|
||||
self.addRoute("DELETE", "/api/dashboard/:slug", self.deleteDashboard)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *HttpServer) getDashboard(c *gin.Context) {
|
||||
id := c.Params.ByName("id")
|
||||
accountId, err := c.Get("accountId")
|
||||
func (self *HttpServer) getDashboard(c *gin.Context, auth *authContext) {
|
||||
slug := c.Params.ByName("slug")
|
||||
|
||||
dash, err := self.store.GetDashboard(id, accountId.(int))
|
||||
dash, err := self.store.GetDashboard(slug, auth.getAccountId())
|
||||
if err != nil {
|
||||
c.JSON(404, newErrorResponse("Dashboard not found"))
|
||||
return
|
||||
}
|
||||
|
||||
dash.Data["id"] = dash.Id
|
||||
|
||||
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")
|
||||
|
||||
results, err := self.store.Query(query)
|
||||
results, err := self.store.Query(query, auth.getAccountId())
|
||||
if err != nil {
|
||||
log.Error("Store query error: %v", err)
|
||||
c.JSON(500, newErrorResponse("Failed"))
|
||||
@ -40,14 +62,14 @@ func (self *HttpServer) search(c *gin.Context) {
|
||||
c.JSON(200, results)
|
||||
}
|
||||
|
||||
func (self *HttpServer) postDashboard(c *gin.Context) {
|
||||
func (self *HttpServer) postDashboard(c *gin.Context, auth *authContext) {
|
||||
var command saveDashboardCommand
|
||||
|
||||
if c.EnsureBody(&command) {
|
||||
dashboard := models.NewDashboard("test")
|
||||
dashboard.Data = command.Dashboard
|
||||
dashboard.Title = dashboard.Data["title"].(string)
|
||||
dashboard.AccountId = 1
|
||||
dashboard.AccountId = auth.getAccountId()
|
||||
dashboard.UpdateSlug()
|
||||
|
||||
if dashboard.Data["id"] != nil {
|
||||
|
@ -35,34 +35,21 @@ func (self *HttpServer) loginPost(c *gin.Context) {
|
||||
}
|
||||
|
||||
session, _ := sessionStore.Get(c.Request, "grafana-session")
|
||||
session.Values["login"] = true
|
||||
session.Values["accountId"] = account.DatabaseId
|
||||
|
||||
session.Values["userAccountId"] = account.Id
|
||||
session.Values["usingAccountId"] = account.UsingAccountId
|
||||
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) {
|
||||
session, _ := sessionStore.Get(c.Request, "grafana-session")
|
||||
session.Values["login"] = nil
|
||||
session.Values = nil
|
||||
session.Save(c.Request, c.Writer)
|
||||
|
||||
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 {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type indexViewModel struct {
|
||||
title string
|
||||
type IndexDto struct {
|
||||
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 {
|
||||
|
36
pkg/api/api_routing.go
Normal file
36
pkg/api/api_routing.go
Normal file
@ -0,0 +1,36 @@
|
||||
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)
|
||||
}
|
43
pkg/models/account.go
Normal file
43
pkg/models/account.go
Normal file
@ -0,0 +1,43 @@
|
||||
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 {
|
||||
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 {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
|
@ -1,6 +1,7 @@
|
||||
package stores
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
log "github.com/alecthomas/log4go"
|
||||
@ -44,6 +45,10 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore {
|
||||
return []interface{}{row.Field("AccountId"), row.Field("Slug")}
|
||||
}).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{} {
|
||||
return []interface{}{row.Field("Login")}
|
||||
}).Exec(session)
|
||||
@ -59,7 +64,7 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore {
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -88,9 +93,25 @@ func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dash
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -10,17 +10,19 @@ import (
|
||||
func (self *rethinkStore) getNextAccountId() (int, error) {
|
||||
resp, err := r.Table("master").Get("ids").Update(map[string]interface{}{
|
||||
"NextAccountId": r.Row.Field("NextAccountId").Add(1),
|
||||
}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
|
||||
}, r.UpdateOpts{ReturnChanges: true}).RunWrite(self.session)
|
||||
|
||||
if err != nil {
|
||||
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 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 {
|
||||
@ -29,7 +31,8 @@ func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error {
|
||||
return err
|
||||
}
|
||||
|
||||
account.DatabaseId = accountId
|
||||
account.Id = accountId
|
||||
account.UsingAccountId = accountId
|
||||
|
||||
resp, err := r.Table("accounts").Insert(account).RunWrite(self.session)
|
||||
if err != nil {
|
||||
@ -59,18 +62,36 @@ func (self *rethinkStore) GetUserAccountLogin(emailOrName string) (*models.UserA
|
||||
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) {
|
||||
resp, err := r.Table("accounts").Get(accountId).Update(map[string]interface{}{
|
||||
"NextDashboardId": r.Row.Field("NextDashboardId").Add(1),
|
||||
}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
|
||||
}, r.UpdateOpts{ReturnChanges: true}).RunWrite(self.session)
|
||||
|
||||
if err != nil {
|
||||
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 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) {
|
||||
account := &models.UserAccount{UserName: "torkelo", Email: "mupp", Login: "test@test.com"}
|
||||
err := store.SaveUserAccount(account)
|
||||
So(err, ShouldBeNil)
|
||||
So(account.DatabaseId, ShouldNotEqual, 0)
|
||||
So(account.Id, ShouldNotEqual, 0)
|
||||
|
||||
read, err := store.GetUserAccountLogin("test@test.com")
|
||||
So(err, ShouldBeNil)
|
||||
So(read.DatabaseId, ShouldEqual, account.DatabaseId)
|
||||
So(read.Id, ShouldEqual, account.DatabaseId)
|
||||
})
|
||||
|
||||
Convey("can get next dashboard id", t, func() {
|
||||
account := &models.UserAccount{UserName: "torkelo", Email: "mupp"}
|
||||
err := store.SaveUserAccount(account)
|
||||
dashId, err := store.getNextDashboardNumber(account.DatabaseId)
|
||||
dashId, err := store.getNextDashboardNumber(account.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(dashId, ShouldEqual, 1)
|
||||
})
|
||||
|
@ -5,11 +5,13 @@ import (
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
GetDashboard(title string, accountId int) (*models.Dashboard, error)
|
||||
GetDashboard(slug string, accountId int) (*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
|
||||
GetUserAccountLogin(emailOrName string) (*models.UserAccount, error)
|
||||
GetAccount(id int) (*models.UserAccount, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
@ -8,6 +7,7 @@
|
||||
|
||||
<title>Grafana</title>
|
||||
<link rel="stylesheet" href="/public/css/grafana.dark.min.css" title="Dark">
|
||||
<link rel="icon" type="image/png" href="img/fav32.png">
|
||||
<base href="/">
|
||||
|
||||
<!-- build:js app/app.js -->
|
||||
@ -20,18 +20,22 @@
|
||||
</head>
|
||||
|
||||
<body ng-cloak ng-controller="GrafanaCtrl">
|
||||
|
||||
<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>
|
||||
</aside>
|
||||
|
||||
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} dashboard-notice" ng-show="$last">
|
||||
<button type="button" class="close" ng-click="dashAlerts.clear(alert)" style="padding-right:50px">×</button>
|
||||
<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>
|
||||
<div class="page-alert-list">
|
||||
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
|
||||
<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 ng-view class="pro-main-view"></div>
|
||||
@ -39,4 +43,12 @@
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
<script>
|
||||
window.grafanaBootData = {
|
||||
user: {
|
||||
login: [[.User.Login]]
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user