diff --git a/package.json b/package.json index f47a59799e3..0df426954d3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "grunt-sass": "^1.1.0", "grunt-string-replace": "~1.2.1", "grunt-systemjs-builder": "^0.2.5", - "grunt-tslint": "^3.0.1", + "grunt-tslint": "^3.0.2", "grunt-typescript": "^0.8.0", "grunt-usemin": "3.0.0", "jshint-stylish": "~0.1.5", @@ -74,7 +74,9 @@ "lodash": "^2.4.1", "sinon": "1.16.1", "systemjs-builder": "^0.15.7", - "tslint": "^3.2.1", + "tether": "^1.2.0", + "tether-drop": "^1.4.2", + "tslint": "^3.4.0", "typescript": "^1.7.5" } } diff --git a/pkg/api/api.go b/pkg/api/api.go index 6e7015ed889..c21deae5a59 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -2,6 +2,7 @@ package api import ( "github.com/go-macaron/binding" + "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -224,6 +225,10 @@ func Register(r *macaron.Macaron) { // rendering r.Get("/render/*", reqSignedIn, RenderToPng) + // Gravatar service. + avt := avatar.CacheServer() + r.Get("/avatar/:hash", avt.ServeHTTP) + InitAppPluginRoutes(r) r.NotFound(NotFoundHandler) diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go new file mode 100644 index 00000000000..79aebece349 --- /dev/null +++ b/pkg/api/avatar/avatar.go @@ -0,0 +1,253 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// Code from https://github.com/gogits/gogs/blob/v0.7.0/modules/avatar/avatar.go + +package avatar + +import ( + "bufio" + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" +) + +var gravatarSource string + +func UpdateGravatarSource() { + srcCfg := "//secure.gravatar.com/avatar/" + + gravatarSource = srcCfg + if strings.HasPrefix(gravatarSource, "//") { + gravatarSource = "http:" + gravatarSource + } else if !strings.HasPrefix(gravatarSource, "http://") && + !strings.HasPrefix(gravatarSource, "https://") { + gravatarSource = "http://" + gravatarSource + } +} + +// hash email to md5 string +// keep this func in order to make this package independent +func HashEmail(email string) string { + // https://en.gravatar.com/site/implement/hash/ + email = strings.TrimSpace(email) + email = strings.ToLower(email) + + h := md5.New() + h.Write([]byte(email)) + return hex.EncodeToString(h.Sum(nil)) +} + +// Avatar represents the avatar object. +type Avatar struct { + hash string + reqParams string + data *bytes.Buffer + notFound bool + timestamp time.Time +} + +func New(hash string) *Avatar { + return &Avatar{ + hash: hash, + reqParams: url.Values{ + "d": {"404"}, + "size": {"200"}, + "r": {"pg"}}.Encode(), + } +} + +func (this *Avatar) Expired() bool { + return time.Since(this.timestamp) > (time.Minute * 10) +} + +func (this *Avatar) Encode(wr io.Writer) error { + _, err := wr.Write(this.data.Bytes()) + return err +} + +func (this *Avatar) Update() (err error) { + select { + case <-time.After(time.Second * 3): + err = fmt.Errorf("get gravatar image %s timeout", this.hash) + case err = <-thunder.GoFetch(gravatarSource+this.hash+"?"+this.reqParams, this): + } + return err +} + +type service struct { + notFound *Avatar + cache map[string]*Avatar +} + +func (this *service) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) { + for _, k := range keys { + if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil { + defaultValue = v + } + } + return defaultValue +} + +func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + urlPath := r.URL.Path + hash := urlPath[strings.LastIndex(urlPath, "/")+1:] + + var avatar *Avatar + + if avatar, _ = this.cache[hash]; avatar == nil { + avatar = New(hash) + } + + if avatar.Expired() { + if err := avatar.Update(); err != nil { + log.Trace("avatar update error: %v", err) + } + } + + if avatar.notFound { + avatar = this.notFound + } else { + this.cache[hash] = avatar + } + + w.Header().Set("Content-Type", "image/jpeg") + w.Header().Set("Content-Length", strconv.Itoa(len(avatar.data.Bytes()))) + w.Header().Set("Cache-Control", "private, max-age=3600") + + if err := avatar.Encode(w); err != nil { + log.Warn("avatar encode error: %v", err) + w.WriteHeader(500) + } +} + +func CacheServer() http.Handler { + UpdateGravatarSource() + + return &service{ + notFound: newNotFound(), + cache: make(map[string]*Avatar), + } +} + +func newNotFound() *Avatar { + avatar := &Avatar{} + + // load transparent png into buffer + path := filepath.Join(setting.StaticRootPath, "img", "transparent.png") + + if data, err := ioutil.ReadFile(path); err != nil { + log.Error(3, "Failed to read transparent.png, %v", path) + } else { + avatar.data = bytes.NewBuffer(data) + } + + return avatar +} + +// thunder downloader +var thunder = &Thunder{QueueSize: 10} + +type Thunder struct { + QueueSize int // download queue size + q chan *thunderTask + once sync.Once +} + +func (t *Thunder) init() { + if t.QueueSize < 1 { + t.QueueSize = 1 + } + t.q = make(chan *thunderTask, t.QueueSize) + for i := 0; i < t.QueueSize; i++ { + go func() { + for { + task := <-t.q + task.Fetch() + } + }() + } +} + +func (t *Thunder) Fetch(url string, avatar *Avatar) error { + t.once.Do(t.init) + task := &thunderTask{ + Url: url, + Avatar: avatar, + } + task.Add(1) + t.q <- task + task.Wait() + return task.err +} + +func (t *Thunder) GoFetch(url string, avatar *Avatar) chan error { + c := make(chan error) + go func() { + c <- t.Fetch(url, avatar) + }() + return c +} + +// thunder download +type thunderTask struct { + Url string + Avatar *Avatar + sync.WaitGroup + err error +} + +func (this *thunderTask) Fetch() { + this.err = this.fetch() + this.Done() +} + +var client = &http.Client{} + +func (this *thunderTask) fetch() error { + this.Avatar.timestamp = time.Now() + + log.Debug("avatar.fetch(fetch new avatar): %s", this.Url) + req, _ := http.NewRequest("GET", this.Url, nil) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jpeg,image/png,*/*;q=0.8") + req.Header.Set("Accept-Encoding", "deflate,sdch") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36") + resp, err := client.Do(req) + + if err != nil { + this.Avatar.notFound = true + return fmt.Errorf("gravatar unreachable, %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + this.Avatar.notFound = true + return fmt.Errorf("status code: %d", resp.StatusCode) + } + + this.Avatar.data = &bytes.Buffer{} + writer := bufio.NewWriter(this.Avatar.data) + + if _, err = io.Copy(writer, resp.Body); err != nil { + return err + } + + return nil +} diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 83176e836e5..b810701233e 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -7,6 +7,7 @@ import ( "time" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) type LoginCommand struct { @@ -89,5 +90,5 @@ func GetGravatarUrl(text string) string { hasher := md5.New() hasher.Write([]byte(strings.ToLower(text))) - return fmt.Sprintf("https://secure.gravatar.com/avatar/%x?s=90&default=mm", hasher.Sum(nil)) + return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil)) } diff --git a/pkg/api/index.go b/pkg/api/index.go index cffbf04482e..72277fe301b 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -36,7 +36,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } if setting.DisableGravatar { - data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png" + data.User.GravatarUrl = setting.AppSubUrl + "/public/img/transparent.png" } if len(data.User.Name) == 0 { @@ -105,6 +105,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Text: "Admin", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin", + Children: []*dtos.NavLink{ + {Text: "Global Users", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/users"}, + {Text: "Global Orgs", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/orgs"}, + {Text: "Server Settings", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/settings"}, + {Text: "Server Stats", Icon: "fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/stats"}, + }, }) } diff --git a/public/app/app.ts b/public/app/app.ts index a4e1bebca6e..b30b75a0ca3 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -47,6 +47,20 @@ export class GrafanaApp { this.registerFunctions.factory = $provide.factory; this.registerFunctions.service = $provide.service; this.registerFunctions.filter = $filterProvider.register; + + $provide.decorator("$http", ["$delegate", "$templateCache", function($delegate, $templateCache) { + var get = $delegate.get; + $delegate.get = function(url, config) { + if (url.match(/\.html$/)) { + // some template's already exist in the cache + if (!$templateCache.get(url)) { + url += "?v=" + new Date().getTime(); + } + } + return get(url, config); + }; + return $delegate; + }]); }); this.ngModuleDependencies = [ diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index 41757d54cc1..37a4f2a448e 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -1,19 +1,16 @@ diff --git a/public/app/core/components/navbar/navbar.ts b/public/app/core/components/navbar/navbar.ts index 9332f43542c..baca7721fe8 100644 --- a/public/app/core/components/navbar/navbar.ts +++ b/public/app/core/components/navbar/navbar.ts @@ -30,28 +30,4 @@ export function navbarDirective() { }; } -var navButtonTemplate = ` -
- - - {{::title}} - -
-`; - -function navButton() { - return { - restrict: 'E', - template: navButtonTemplate, - scope: { - title: "@", - titleUrl: "@", - }, - link: function(scope, elem, attrs, ctrl) { - scope.icon = attrs.icon; - } - }; -} - coreModule.directive('navbar', navbarDirective); -coreModule.directive('navButton', navButton); diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index f4221367fdf..6e460e813ce 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -3,7 +3,10 @@
  • - + + + +
    {{ctrl.user.name}} diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 71e21f19ae0..604dc9f9876 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -186,7 +186,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) { .when('/global-alerts', { templateUrl: 'public/app/features/dashboard/partials/globalAlerts.html', }) - .when('/styleguide', { + .when('/styleguide/:page?', { controller: 'StyleGuideCtrl', controllerAs: 'ctrl', templateUrl: 'public/app/features/styleguide/styleguide.html', diff --git a/public/app/features/admin/partials/admin_home.html b/public/app/features/admin/partials/admin_home.html index aa667ddd2d0..eed8f503ed8 100644 --- a/public/app/features/admin/partials/admin_home.html +++ b/public/app/features/admin/partials/admin_home.html @@ -24,5 +24,9 @@ View Server Stats + + Style guide + +
    diff --git a/public/app/features/apps/partials/list.html b/public/app/features/apps/partials/list.html index 98305ff7696..bab4ca5c687 100644 --- a/public/app/features/apps/partials/list.html +++ b/public/app/features/apps/partials/list.html @@ -2,14 +2,14 @@
    -
    + +
    + No apps defined +
    -
    - No apps defined -
    - -
      +
        • @@ -43,6 +43,5 @@
      • -
      -
    +
    diff --git a/public/app/features/apps/partials/page.html b/public/app/features/apps/partials/page.html index 0b3f983124d..c65053b47fe 100644 --- a/public/app/features/apps/partials/page.html +++ b/public/app/features/apps/partials/page.html @@ -1,14 +1,13 @@ - +
    -
    + -
    - - -
    - +
    + +
    diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 971bb39a09c..155037ca124 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -1,22 +1,18 @@ - + + + {{dashboard.title}} + + - + + + + {{dashboard.title}} +   (snapshot) + +