Przeglądaj źródła

Merge branch 'pluginlist'

Torkel Ödegaard 9 lat temu
rodzic
commit
497e9343aa
46 zmienionych plików z 867 dodań i 181 usunięć
  1. 7 0
      conf/defaults.ini
  2. 7 0
      conf/sample.ini
  3. 2 2
      latest.json
  4. 3 0
      pkg/api/api.go
  5. 11 6
      pkg/api/dtos/plugins.go
  6. 5 3
      pkg/api/frontendsettings.go
  7. 46 0
      pkg/api/gnetproxy.go
  8. 14 4
      pkg/api/plugins.go
  9. 1 1
      pkg/cmd/grafana-server/main.go
  10. 4 3
      pkg/plugins/frontend_plugin.go
  11. 4 0
      pkg/plugins/models.go
  12. 4 0
      pkg/plugins/plugins.go
  13. 119 0
      pkg/plugins/update_checker.go
  14. 2 0
      pkg/setting/setting.go
  15. 2 5
      public/app/core/components/switch.ts
  16. 3 1
      public/app/core/controllers/login_ctrl.js
  17. 1 1
      public/app/features/panel/partials/panelTime.html
  18. 3 2
      public/app/features/plugins/partials/ds_list.html
  19. 3 0
      public/app/features/plugins/partials/plugin_edit.html
  20. 7 2
      public/app/features/plugins/partials/plugin_list.html
  21. 21 0
      public/app/features/plugins/partials/update_instructions.html
  22. 12 2
      public/app/features/plugins/plugin_edit_ctrl.ts
  23. 3 3
      public/app/partials/login.html
  24. 25 33
      public/app/plugins/panel/dashlist/editor.html
  25. 16 11
      public/app/plugins/panel/dashlist/module.html
  26. 79 25
      public/app/plugins/panel/dashlist/module.ts
  27. 2 2
      public/app/plugins/panel/graph/graph_tooltip.js
  28. 2 0
      public/app/plugins/panel/pluginlist/README.md
  29. 40 0
      public/app/plugins/panel/pluginlist/editor.html
  30. 119 0
      public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg
  31. 30 0
      public/app/plugins/panel/pluginlist/module.html
  32. 72 0
      public/app/plugins/panel/pluginlist/module.ts
  33. 16 0
      public/app/plugins/panel/pluginlist/plugin.json
  34. 30 24
      public/dashboards/home.json
  35. 1 0
      public/sass/_grafana.scss
  36. 11 10
      public/sass/_variables.dark.scss
  37. 9 9
      public/sass/_variables.light.scss
  38. 10 1
      public/sass/base/_code.scss
  39. 1 1
      public/sass/base/_type.scss
  40. 27 2
      public/sass/components/_cards.scss
  41. 8 0
      public/sass/components/_panel_dashlist.scss
  42. 2 0
      public/sass/components/_panel_graph.scss
  43. 75 0
      public/sass/components/_panel_pluginlist.scss
  44. 1 1
      public/sass/components/_tooltip.scss
  45. 2 2
      public/sass/pages/_dashboard.scss
  46. 5 25
      tsconfig.json

+ 7 - 0
conf/defaults.ini

@@ -111,6 +111,13 @@ gc_interval_time = 86400
 # Change this option to false to disable reporting.
 reporting_enabled = true
 
+# Set to false to disable all checks to https://grafana.net
+# for new vesions (grafana itself and plugins), check is used
+# in some UI views to notify that grafana or plugin update exists
+# This option does not cause any auto updates, nor send any information
+# only a GET request to http://grafana.net to get latest versions
+check_for_updates = true
+
 # Google Analytics universal tracking code, only enabled if you specify an id here
 google_analytics_ua_id =
 

+ 7 - 0
conf/sample.ini

@@ -100,6 +100,13 @@
 # Change this option to false to disable reporting.
 ;reporting_enabled = true
 
+# Set to false to disable all checks to https://grafana.net
+# for new vesions (grafana itself and plugins), check is used
+# in some UI views to notify that grafana or plugin update exists
+# This option does not cause any auto updates, nor send any information
+# only a GET request to http://grafana.net to get latest versions
+check_for_updates = true
+
 # Google Analytics universal tracking code, only enabled if you specify an id here
 ;google_analytics_ua_id =
 

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
-	"stable": "2.6.0",
-	"testing": "3.0.0"
+  "stable": "2.6.0",
+	"testing": "3.0.0-beta2"
 }

+ 3 - 0
pkg/api/api.go

@@ -253,6 +253,9 @@ func Register(r *macaron.Macaron) {
 	// rendering
 	r.Get("/render/*", reqSignedIn, RenderToPng)
 
+	// grafana.net proxy
+	r.Any("/api/gnet/*", reqSignedIn, ProxyGnetRequest)
+
 	// Gravatar service.
 	avt := avatar.CacheServer()
 	r.Get("/avatar/:hash", avt.ServeHTTP)

+ 11 - 6
pkg/api/dtos/plugins.go

@@ -15,15 +15,20 @@ type PluginSetting struct {
 	Dependencies  *plugins.PluginDependencies `json:"dependencies"`
 	JsonData      map[string]interface{}      `json:"jsonData"`
 	DefaultNavUrl string                      `json:"defaultNavUrl"`
+
+	LatestVersion string `json:"latestVersion"`
+	HasUpdate     bool   `json:"hasUpdate"`
 }
 
 type PluginListItem struct {
-	Name    string              `json:"name"`
-	Type    string              `json:"type"`
-	Id      string              `json:"id"`
-	Enabled bool                `json:"enabled"`
-	Pinned  bool                `json:"pinned"`
-	Info    *plugins.PluginInfo `json:"info"`
+	Name          string              `json:"name"`
+	Type          string              `json:"type"`
+	Id            string              `json:"id"`
+	Enabled       bool                `json:"enabled"`
+	Pinned        bool                `json:"pinned"`
+	Info          *plugins.PluginInfo `json:"info"`
+	LatestVersion string              `json:"latestVersion"`
+	HasUpdate     bool                `json:"hasUpdate"`
 }
 
 type PluginList []PluginListItem

+ 5 - 3
pkg/api/frontendsettings.go

@@ -137,9 +137,11 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 		"allowOrgCreate":    (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
 		"authProxyEnabled":  setting.AuthProxyEnabled,
 		"buildInfo": map[string]interface{}{
-			"version":    setting.BuildVersion,
-			"commit":     setting.BuildCommit,
-			"buildstamp": setting.BuildStamp,
+			"version":       setting.BuildVersion,
+			"commit":        setting.BuildCommit,
+			"buildstamp":    setting.BuildStamp,
+			"latestVersion": plugins.GrafanaLatestVersion,
+			"hasUpdate":     plugins.GrafanaHasUpdate,
 		},
 	}
 

+ 46 - 0
pkg/api/gnetproxy.go

@@ -0,0 +1,46 @@
+package api
+
+import (
+	"crypto/tls"
+	"net"
+	"net/http"
+	"net/http/httputil"
+	"time"
+
+	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+var gNetProxyTransport = &http.Transport{
+	TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
+	Proxy:           http.ProxyFromEnvironment,
+	Dial: (&net.Dialer{
+		Timeout:   30 * time.Second,
+		KeepAlive: 30 * time.Second,
+	}).Dial,
+	TLSHandshakeTimeout: 10 * time.Second,
+}
+
+func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
+	director := func(req *http.Request) {
+		req.URL.Scheme = "https"
+		req.URL.Host = "grafana.net"
+		req.Host = "grafana.net"
+
+		req.URL.Path = util.JoinUrlFragments("https://grafana.net/api", proxyPath)
+
+		// clear cookie headers
+		req.Header.Del("Cookie")
+		req.Header.Del("Set-Cookie")
+	}
+
+	return &httputil.ReverseProxy{Director: director}
+}
+
+func ProxyGnetRequest(c *middleware.Context) {
+	proxyPath := c.Params("*")
+	proxy := ReverseProxyGnetReq(proxyPath)
+	proxy.Transport = gNetProxyTransport
+	proxy.ServeHTTP(c.Resp, c.Req.Request)
+	c.Resp.Header().Del("Set-Cookie")
+}

+ 14 - 4
pkg/api/plugins.go

@@ -14,6 +14,7 @@ func GetPluginList(c *middleware.Context) Response {
 	typeFilter := c.Query("type")
 	enabledFilter := c.Query("enabled")
 	embeddedFilter := c.Query("embedded")
+	coreFilter := c.Query("core")
 
 	pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
 
@@ -28,16 +29,23 @@ func GetPluginList(c *middleware.Context) Response {
 			continue
 		}
 
+		// filter out core plugins
+		if coreFilter == "0" && pluginDef.IsCorePlugin {
+			continue
+		}
+
 		// filter on type
 		if typeFilter != "" && typeFilter != pluginDef.Type {
 			continue
 		}
 
 		listItem := dtos.PluginListItem{
-			Id:   pluginDef.Id,
-			Name: pluginDef.Name,
-			Type: pluginDef.Type,
-			Info: &pluginDef.Info,
+			Id:            pluginDef.Id,
+			Name:          pluginDef.Name,
+			Type:          pluginDef.Type,
+			Info:          &pluginDef.Info,
+			LatestVersion: pluginDef.GrafanaNetVersion,
+			HasUpdate:     pluginDef.GrafanaNetHasUpdate,
 		}
 
 		if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
@@ -81,6 +89,8 @@ func GetPluginSettingById(c *middleware.Context) Response {
 			BaseUrl:       def.BaseUrl,
 			Module:        def.Module,
 			DefaultNavUrl: def.DefaultNavUrl,
+			LatestVersion: def.GrafanaNetVersion,
+			HasUpdate:     def.GrafanaNetHasUpdate,
 		}
 
 		query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId}

+ 1 - 1
pkg/cmd/grafana-server/main.go

@@ -24,7 +24,7 @@ import (
 	"github.com/grafana/grafana/pkg/social"
 )
 
-var version = "3.0.0-pre1"
+var version = "3.0.0-beta2"
 var commit = "NA"
 var buildstamp string
 var build_date string

+ 4 - 3
pkg/plugins/frontend_plugin.go

@@ -14,7 +14,7 @@ type FrontendPluginBase struct {
 }
 
 func (fp *FrontendPluginBase) initFrontendPlugin() {
-	if isInternalPlugin(fp.PluginDir) {
+	if isExternalPlugin(fp.PluginDir) {
 		StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
 			Directory: fp.PluginDir,
 			PluginId:  fp.Id,
@@ -48,17 +48,18 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
 
 func (fp *FrontendPluginBase) handleModuleDefaults() {
 
-	if isInternalPlugin(fp.PluginDir) {
+	if isExternalPlugin(fp.PluginDir) {
 		fp.Module = path.Join("plugins", fp.Id, "module")
 		fp.BaseUrl = path.Join("public/plugins", fp.Id)
 		return
 	}
 
+	fp.IsCorePlugin = true
 	fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module")
 	fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id)
 }
 
-func isInternalPlugin(pluginDir string) bool {
+func isExternalPlugin(pluginDir string) bool {
 	return !strings.Contains(pluginDir, setting.StaticRootPath)
 }
 

+ 4 - 0
pkg/plugins/models.go

@@ -43,6 +43,10 @@ type PluginBase struct {
 	IncludedInAppId string `json:"-"`
 	PluginDir       string `json:"-"`
 	DefaultNavUrl   string `json:"-"`
+	IsCorePlugin    bool   `json:"-"`
+
+	GrafanaNetVersion   string `json:"-"`
+	GrafanaNetHasUpdate bool   `json:"-"`
 
 	// cache for readme file contents
 	Readme []byte `json:"-"`

+ 4 - 0
pkg/plugins/plugins.go

@@ -22,6 +22,9 @@ var (
 	Apps         map[string]*AppPlugin
 	Plugins      map[string]*PluginBase
 	PluginTypes  map[string]interface{}
+
+	GrafanaLatestVersion string
+	GrafanaHasUpdate     bool
 )
 
 type PluginScanner struct {
@@ -70,6 +73,7 @@ func Init() error {
 		app.initApp()
 	}
 
+	go StartPluginUpdateChecker()
 	return nil
 }
 

+ 119 - 0
pkg/plugins/update_checker.go

@@ -0,0 +1,119 @@
+package plugins
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+type GrafanaNetPlugin struct {
+	Slug    string `json:"slug"`
+	Version string `json:"version"`
+}
+
+type GithubLatest struct {
+	Stable  string `json:"stable"`
+	Testing string `json:"testing"`
+}
+
+func StartPluginUpdateChecker() {
+	if !setting.CheckForUpdates {
+		return
+	}
+
+	// do one check directly
+	go checkForUpdates()
+
+	ticker := time.NewTicker(time.Minute * 10)
+	for {
+		select {
+		case <-ticker.C:
+			checkForUpdates()
+		}
+	}
+}
+
+func getAllExternalPluginSlugs() string {
+	str := ""
+
+	for _, plug := range Plugins {
+		if plug.IsCorePlugin {
+			continue
+		}
+
+		str += plug.Id + ","
+	}
+
+	return str
+}
+
+func checkForUpdates() {
+	log.Trace("Checking for updates")
+
+	client := http.Client{Timeout: time.Duration(5 * time.Second)}
+
+	pluginSlugs := getAllExternalPluginSlugs()
+	resp, err := client.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
+
+	if err != nil {
+		log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error())
+		return
+	}
+
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		log.Trace("Update check failed, reading response from grafana.net, %v", err.Error())
+		return
+	}
+
+	gNetPlugins := []GrafanaNetPlugin{}
+	err = json.Unmarshal(body, &gNetPlugins)
+	if err != nil {
+		log.Trace("Failed to unmarshal plugin repo, reading response from grafana.net, %v", err.Error())
+		return
+	}
+
+	for _, plug := range Plugins {
+		for _, gplug := range gNetPlugins {
+			if gplug.Slug == plug.Id {
+				plug.GrafanaNetVersion = gplug.Version
+				plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
+			}
+		}
+	}
+
+	resp2, err := client.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
+	if err != nil {
+		log.Trace("Failed to get lates.json repo from github: %v", err.Error())
+		return
+	}
+
+	defer resp2.Body.Close()
+	body, err = ioutil.ReadAll(resp2.Body)
+	if err != nil {
+		log.Trace("Update check failed, reading response from github.net, %v", err.Error())
+		return
+	}
+
+	var githubLatest GithubLatest
+	err = json.Unmarshal(body, &githubLatest)
+	if err != nil {
+		log.Trace("Failed to unmarshal github latest, reading response from github: %v", err.Error())
+		return
+	}
+
+	if strings.Contains(setting.BuildVersion, "-") {
+		GrafanaLatestVersion = githubLatest.Testing
+		GrafanaHasUpdate = strings.HasPrefix(setting.BuildVersion, githubLatest.Testing)
+	} else {
+		GrafanaLatestVersion = githubLatest.Stable
+		GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion
+	}
+}

+ 2 - 0
pkg/setting/setting.go

@@ -124,6 +124,7 @@ var (
 	appliedEnvOverrides          []string
 
 	ReportingEnabled   bool
+	CheckForUpdates    bool
 	GoogleAnalyticsId  string
 	GoogleTagManagerId string
 
@@ -475,6 +476,7 @@ func NewConfigContext(args *CommandLineArgs) error {
 
 	analytics := Cfg.Section("analytics")
 	ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
+	CheckForUpdates = analytics.Key("check_for_updates").MustBool(true)
 	GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String()
 	GoogleTagManagerId = analytics.Key("google_tag_manager_id").String()
 

+ 2 - 5
public/app/core/components/switch.ts

@@ -27,11 +27,8 @@ export class SwitchCtrl {
   }
 
   internalOnChange() {
-    return new Promise(resolve => {
-      this.$timeout(() => {
-        this.onChange();
-        resolve();
-      });
+    return this.$timeout(() => {
+      return this.onChange();
     });
   }
 

+ 3 - 1
public/app/core/controllers/login_ctrl.js

@@ -39,7 +39,9 @@ function (angular, coreModule, config) {
     $scope.buildInfo = {
       version: config.buildInfo.version,
       commit: config.buildInfo.commit,
-      buildstamp: new Date(config.buildInfo.buildstamp * 1000)
+      buildstamp: new Date(config.buildInfo.buildstamp * 1000),
+      latestVersion: config.buildInfo.latestVersion,
+      hasUpdate: config.buildInfo.hasUpdate,
     };
 
     $scope.submit = function() {

+ 1 - 1
public/app/features/panel/partials/panelTime.html

@@ -31,7 +31,7 @@
 		</div>
 		<gf-form-switch class="gf-form max-width-30"
 			label="Hide time override info" label-class="width-12"
-			checked="ctrl.panel.hideTimeOverride" switch-class="max-width-6" on-change="ctrl.render()">
+			checked="ctrl.panel.hideTimeOverride" switch-class="max-width-6" on-change="ctrl.refresh()">
 		</gf-form-switch>
 	</div>
 </div>

+ 3 - 2
public/app/features/plugins/partials/ds_list.html

@@ -20,8 +20,9 @@
 			<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
 				<a class="card-item" href="datasources/edit/{{ds.id}}/">
 					<div class="card-item-header">
-						<i class="icon-gf icon-gf-{{ds.type}}"></i>
-						{{ds.type}}
+						<div class="card-item-type">
+							{{ds.type}}
+						</div>
 					</div>
 					<div class="card-item-body">
 						<figure class="card-item-figure">

+ 3 - 0
public/app/features/plugins/partials/plugin_edit.html

@@ -55,6 +55,9 @@
       <section class="page-sidebar-section">
         <h4>Version</h4>
         <span>{{ctrl.model.info.version}}</span>
+				<div ng-show="ctrl.model.hasUpdate">
+          <a ng-click="ctrl.updateAvailable()" bs-tooltip="ctrl.model.latestVersion">Update Available!</a>
+				</div>
       </section>
       <section class="page-sidebar-section" ng-show="ctrl.model.type === 'app'">
         <h5>Includes</h4>

+ 7 - 2
public/app/features/plugins/partials/plugin_list.html

@@ -33,8 +33,13 @@
 			<li class="card-item-wrapper" ng-repeat="plugin in ctrl.plugins">
 				<a class="card-item" href="plugins/{{plugin.id}}/edit">
 					<div class="card-item-header">
-						<i class="icon-gf icon-gf-{{plugin.type}}"></i>
-						{{plugin.type}}
+						<div class="card-item-type">
+							<i class="icon-gf icon-gf-{{plugin.type}}"></i>
+							{{plugin.type}}
+						</div>
+					  <div class="card-item-notice" ng-show="plugin.hasUpdate">
+							<span bs-tooltip="plugin.latestVersion">Update available!</span>
+						</div>
 					</div>
 					<div class="card-item-body">
 						<figure class="card-item-figure">

+ 21 - 0
public/app/features/plugins/partials/update_instructions.html

@@ -0,0 +1,21 @@
+<div class="modal-body">
+	<div class="modal-header">
+		<h2 class="modal-header-title">
+			<i class="fa fa-cloud-download"></i>
+			<span class="p-l-1">Update Plugin</span>
+		</h2>
+
+		<a class="modal-header-close" ng-click="dismiss();">
+			<i class="fa fa-remove"></i>
+		</a>
+	</div>
+
+	<div class="modal-content">
+		<div class="gf-form-group">
+			<p>Type the following on the command line to update {{plugin.name}}.</p>
+			<pre><code>grafana-cli plugins update {{plugin.id}}</code></pre>
+			<span class="small">Check out {{plugin.name}} on <a href="http://grafana/net/plugins/{{plugin.id}}">Grafana.net</a> for README and changelog. If you do not have access to the command line, ask your Grafana administator.</span>
+		</div>
+		<p class="pluginlist-none-installed code--line"><img class="pluginlist-inline-logo" src="public/img/grafana_icon.svg"><strong>Pro tip</strong>: To update all plugins at once, type <code class="code--small">grafana-cli plugins update-all</code> on the command line.</div>
+	</div>
+</div>

+ 12 - 2
public/app/features/plugins/plugin_edit_ctrl.ts

@@ -19,6 +19,7 @@ export class PluginEditCtrl {
 
   /** @ngInject */
   constructor(private $scope,
+              private $rootScope,
               private backendSrv,
               private $routeParams,
               private $sce,
@@ -73,7 +74,7 @@ export class PluginEditCtrl {
       case 'datasource':  return 'icon-gf icon-gf-datasources';
       case 'panel':  return 'icon-gf icon-gf-panel';
       case 'app':  return 'icon-gf icon-gf-apps';
-      case 'page':  return 'icon-gf icon-gf-share';
+      case 'page':  return 'icon-gf icon-gf-endpoint-tiny';
       case 'dashboard':  return 'icon-gf icon-gf-dashboard';
     }
   }
@@ -128,6 +129,16 @@ export class PluginEditCtrl {
     this.postUpdateHook = callback;
   }
 
+  updateAvailable() {
+    var modalScope = this.$scope.$new(true);
+    modalScope.plugin = this.model;
+
+    this.$rootScope.appEvent('show-modal', {
+      src: 'public/app/features/plugins/partials/update_instructions.html',
+      scope: modalScope
+    });
+  }
+
   enable() {
     this.model.enabled = true;
     this.model.pinned = true;
@@ -142,4 +153,3 @@ export class PluginEditCtrl {
 }
 
 angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl);
-

+ 3 - 3
public/app/partials/login.html

@@ -78,9 +78,9 @@
 				Grafana version: {{buildInfo.version}}, commit: {{buildInfo.commit}},
 				build date: {{buildInfo.buildstamp | date: 'yyyy-MM-dd HH:mm:ss' }}
 			</div>
+			<div class="version-footer text-center small" ng-show="buildInfo.hasUpdate">
+				<a class="external-link" target="_blank" href="http://grafana.org/download">New Grafana Version Available ({{buildInfo.latestVersion}})</a>
+			</div>
 		</div>
 	</div>
-
 </div>
-
-

+ 25 - 33
public/app/plugins/panel/dashlist/editor.html

@@ -1,40 +1,32 @@
-<div class="gf-form-group">
-	<div class="gf-form-inline">
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Mode</span>
-			<div class="gf-form-select-wrapper max-width-10">
-				<select class="gf-form-input" ng-model="ctrl.panel.mode" ng-options="f for f in ctrl.modes" ng-change="ctrl.refresh()"></select>
-			</div>
-		</div>
-		<div class="gf-form" ng-show="ctrl.panel.mode === 'recently viewed'">
-			<span class="gf-form-label">
-				<i class="grafana-tip fa fa-question-circle ng-scope" bs-tooltip="'WARNING: This list will be cleared when clearing browser cache'" data-original-title="" title=""></i>
-			</span>
-		</div>
-	</div>
+<div>
+  <div class="section gf-form-group">
+    <h5 class="section-heading">Options</h5>
 
-	<div class="gf-form-inline" ng-if="ctrl.panel.mode === 'search'">
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Search options</span>
-			<span class="gf-form-label">Query</span>
+    <gf-form-switch class="gf-form" label="Starred" label-class="width-9" checked="ctrl.panel.starred" on-change="ctrl.refresh()"></gf-form-switch>
+    <gf-form-switch class="gf-form" label="Recently viewed" label-class="width-9" checked="ctrl.panel.recent" on-change="ctrl.refresh()"></gf-form-switch>
+    <gf-form-switch class="gf-form" label="Search" label-class="width-9" checked="ctrl.panel.search" on-change="ctrl.refresh()"></gf-form-switch>
 
-			<input type="text" class="gf-form-input" placeholder="title query"
-				ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
+    <gf-form-switch class="gf-form" label="Show headings" label-class="width-9" checked="ctrl.panel.headings" on-change="ctrl.refresh()"></gf-form-switch>
 
-		</div>
+    <div class="gf-form">
+      <span class="gf-form-label width-9">Max items</span>
+      <input class="gf-form-input max-width-5" type="number" ng-model="ctrl.panel.limit" ng-model-onblur ng-change="ctrl.refresh()">
+    </div>
+  </div>
 
-		<div class="gf-form">
-			<span class="gf-form-label">Tags</span>
+  <div class="section gf-form-group">
+    <h5 class="section-heading">Search</h5>
 
-			<bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
-			</bootstrap-tagsinput>
-		</div>
-	</div>
+    <div class="gf-form">
+      <span class="gf-form-label width-6">Query</span>
+      <input type="text" class="gf-form-input" placeholder="title query" ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
+    </div>
+
+    <div class="gf-form">
+      <span class="gf-form-label width-6">Tags</span>
+      <bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
+      </bootstrap-tagsinput>
+    </div>
+  </div>
 
-	<div class="gf-form-inline">
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Limit number to</span>
-			<input class="gf-form-input" type="number" ng-model="ctrl.panel.limit" ng-model-onblur ng-change="ctrl.refresh()">
-		</div>
-	</div>
 </div>

+ 16 - 11
public/app/plugins/panel/dashlist/module.html

@@ -1,12 +1,17 @@
-<div class="dashlist">
-	<div class="dashlist-item" ng-repeat="dash in ctrl.dashList">
-		<a class="dashlist-link dashlist-link-{{dash.type}}" href="dashboard/{{dash.uri}}">
-			<span class="dashlist-title">
-				{{dash.title}}
-			</span>
-			<span class="dashlist-star">
-				<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
-			</span>
-		</a>
-	</div>
+<div class="dashlist" ng-repeat="group in ctrl.groups">
+  <div class="dashlist-section" ng-if="group.show">
+    <h6 class="dashlist-section-header" ng-show="ctrl.panel.headings">
+      {{group.header}}
+    </h6>
+    <div class="dashlist-item" ng-repeat="dash in group.list">
+      <a class="dashlist-link dashlist-link-{{dash.type}}" href="dashboard/{{dash.uri}}">
+        <span class="dashlist-title">
+          {{dash.title}}
+        </span>
+        <span class="dashlist-star">
+          <i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
+        </span>
+      </a>
+    </div>
+  </div>
 </div>

+ 79 - 25
public/app/plugins/panel/dashlist/module.ts

@@ -7,16 +7,19 @@ import {impressions} from 'app/features/dashboard/impression_store';
 
  // Set and populate defaults
 var panelDefaults = {
-  mode: 'starred',
   query: '',
   limit: 10,
-  tags: []
+  tags: [],
+  recent: false,
+  search: false,
+  starred: true,
+  headings: true,
 };
 
 class DashListCtrl extends PanelCtrl {
   static templateUrl = 'module.html';
 
-  dashList: any[];
+  groups: any[];
   modes: any[];
 
   /** @ngInject */
@@ -31,6 +34,31 @@ class DashListCtrl extends PanelCtrl {
 
     this.events.on('refresh', this.onRefresh.bind(this));
     this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
+
+    this.groups = [
+      {list: [], show: false, header: "Starred dashboards",},
+      {list: [], show: false, header: "Recently viewed dashboards"},
+      {list: [], show: false, header: "Search"},
+    ];
+
+    // update capability
+    if (this.panel.mode) {
+      if (this.panel.mode === 'starred') {
+        this.panel.starred = true;
+        this.panel.headings = false;
+      }
+      if (this.panel.mode === 'recently viewed') {
+        this.panel.recent = true;
+        this.panel.starred = false;
+        this.panel.headings = false;
+      }
+      if (this.panel.mode === 'search') {
+        this.panel.search = true;
+        this.panel.starred = false;
+        this.panel.headings = false;
+      }
+      delete this.panel.mode;
+    }
   }
 
   onInitEditMode() {
@@ -40,34 +68,60 @@ class DashListCtrl extends PanelCtrl {
   }
 
   onRefresh() {
-    var params: any = {limit: this.panel.limit};
-
-    if (this.panel.mode === 'recently viewed') {
-      var dashIds = _.first(impressions.getDashboardOpened(), this.panel.limit);
-
-      return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => {
-        this.dashList = dashIds.map(orderId => {
-          return _.find(result, dashboard => {
-            return dashboard.id === orderId;
-          });
-        }).filter(el => {
-          return el !== undefined;
-        });
+    var promises = [];
 
-        this.renderingCompleted();
-      });
+    promises.push(this.getRecentDashboards());
+    promises.push(this.getStarred());
+    promises.push(this.getSearch());
+
+    return Promise.all(promises)
+      .then(this.renderingCompleted.bind(this));
+  }
+
+  getSearch() {
+    this.groups[2].show = this.panel.search;
+    if (!this.panel.search) {
+      return Promise.resolve();
     }
 
-    if (this.panel.mode === 'starred') {
-      params.starred = "true";
-    } else {
-      params.query = this.panel.query;
-      params.tag = this.panel.tags;
+    var params = {
+      limit: this.panel.limit,
+      query: this.panel.query,
+      tag: this.panel.tags,
+    };
+
+    return this.backendSrv.search(params).then(result => {
+      this.groups[2].list = result;
+    });
+  }
+
+  getStarred() {
+    this.groups[0].show = this.panel.starred;
+    if (!this.panel.starred) {
+      return Promise.resolve();
     }
 
+    var params = {limit: this.panel.limit, starred: "true"};
     return this.backendSrv.search(params).then(result => {
-      this.dashList = result;
-      this.renderingCompleted();
+      this.groups[0].list = result;
+    });
+  }
+
+  getRecentDashboards() {
+    this.groups[1].show = this.panel.recent;
+    if (!this.panel.recent) {
+      return Promise.resolve();
+    }
+
+    var dashIds = _.first(impressions.getDashboardOpened(), this.panel.limit);
+    return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => {
+      this.groups[1].list = dashIds.map(orderId => {
+        return _.find(result, dashboard => {
+          return dashboard.id === orderId;
+        });
+      }).filter(el => {
+        return el !== undefined;
+      });
     });
   }
 }

+ 2 - 2
public/app/plugins/panel/graph/graph_tooltip.js

@@ -9,7 +9,7 @@ function ($) {
     var ctrl = scope.ctrl;
     var panel = ctrl.panel;
 
-    var $tooltip = $('<div id="tooltip">');
+    var $tooltip = $('<div id="tooltip" class="graph-tooltip">');
 
     this.findHoverIndexFromDataPoints = function(posX, series, last) {
       var ps = series.datapoints.pointsize;
@@ -34,7 +34,7 @@ function ($) {
     };
 
     this.showTooltip = function(absoluteTime, innerHtml, pos) {
-      var body = '<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ absoluteTime + '</div> ';
+      var body = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>';
       body += innerHtml + '</div>';
       $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY);
     };

+ 2 - 0
public/app/plugins/panel/pluginlist/README.md

@@ -0,0 +1,2 @@
+# Plugin List Panel -  Native Plugin
+

+ 40 - 0
public/app/plugins/panel/pluginlist/editor.html

@@ -0,0 +1,40 @@
+<div class="gf-form-group">
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Mode</span>
+			<div class="gf-form-select-wrapper max-width-10">
+				<select class="gf-form-input" ng-model="ctrl.panel.mode" ng-options="f for f in ctrl.modes" ng-change="ctrl.refresh()"></select>
+			</div>
+		</div>
+		<div class="gf-form" ng-show="ctrl.panel.mode === 'recently viewed'">
+			<span class="gf-form-label">
+				<i class="grafana-tip fa fa-question-circle ng-scope" bs-tooltip="'WARNING: This list will be cleared when clearing browser cache'" data-original-title="" title=""></i>
+			</span>
+		</div>
+	</div>
+
+	<div class="gf-form-inline" ng-if="ctrl.panel.mode === 'search'">
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Search options</span>
+			<span class="gf-form-label">Query</span>
+
+			<input type="text" class="gf-form-input" placeholder="title query"
+				ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
+
+		</div>
+
+		<div class="gf-form">
+			<span class="gf-form-label">Tags</span>
+
+			<bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
+			</bootstrap-tagsinput>
+		</div>
+	</div>
+
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Limit number to</span>
+			<input class="gf-form-input" type="number" ng-model="ctrl.panel.limit" ng-model-onblur ng-change="ctrl.refresh()">
+		</div>
+	</div>
+</div>

+ 119 - 0
public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<g>
+	<g>
+		<path style="fill:#666666;" d="M8.842,11.219h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,11.219z"/>
+		<path style="fill:#666666;" d="M0.008,2.113l2.054-2.054C0.966,0.139,0.089,1.016,0.008,2.113z"/>
+		<polygon style="fill:#666666;" points="0,2.998 0,5.533 5.484,0.05 2.948,0.05 		"/>
+		<polygon style="fill:#666666;" points="6.361,0.05 0,6.411 0,8.946 8.896,0.05 		"/>
+		<path style="fill:#666666;" d="M11.169,2.277c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V2.277z"/>
+		<path style="fill:#666666;" d="M9.654,0.169L0.119,9.704c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,0.812,10.247,0.37,9.654,0.169z"/>
+		<polygon style="fill:#666666;" points="11.169,5.479 5.429,11.219 7.964,11.219 11.169,8.014 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,11.031H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,10.212,89.157,11.031,88.146,11.031z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,23.902h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,23.902z"/>
+		<path style="fill:#666666;" d="M0.008,14.796l2.054-2.054C0.966,12.822,0.089,13.699,0.008,14.796z"/>
+		<polygon style="fill:#666666;" points="0,15.681 0,18.216 5.484,12.733 2.948,12.733 		"/>
+		<polygon style="fill:#666666;" points="6.361,12.733 0,19.094 0,21.629 8.896,12.733 		"/>
+		<path style="fill:#666666;" d="M11.169,14.96c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V14.96z"/>
+		<path style="fill:#666666;" d="M9.654,12.852l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,13.495,10.247,13.053,9.654,12.852z"/>
+		<polygon style="fill:#666666;" points="11.169,18.162 5.429,23.902 7.964,23.902 11.169,20.697 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,23.714H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,22.895,89.157,23.714,88.146,23.714z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,36.585h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,36.585z"/>
+		<path style="fill:#666666;" d="M0.008,27.479l2.054-2.054C0.966,25.505,0.089,26.382,0.008,27.479z"/>
+		<polygon style="fill:#666666;" points="0,28.364 0,30.899 5.484,25.416 2.948,25.416 		"/>
+		<polygon style="fill:#666666;" points="6.361,25.416 0,31.777 0,34.312 8.896,25.416 		"/>
+		<path style="fill:#666666;" d="M11.169,27.643c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V27.643z"/>
+		<path style="fill:#666666;" d="M9.654,25.535L0.119,35.07c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,26.178,10.247,25.736,9.654,25.535z"/>
+		<polygon style="fill:#666666;" points="11.169,30.845 5.429,36.585 7.964,36.585 11.169,33.38 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,36.397H14.866c-1.011,0-1.83-0.82-1.83-1.831v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,35.578,89.157,36.397,88.146,36.397z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,49.268h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,49.268z"/>
+		<path style="fill:#666666;" d="M0.008,40.162l2.054-2.054C0.966,38.188,0.089,39.065,0.008,40.162z"/>
+		<polygon style="fill:#666666;" points="0,41.047 0,43.582 5.484,38.099 2.948,38.099 		"/>
+		<polygon style="fill:#666666;" points="6.361,38.099 0,44.46 0,46.995 8.896,38.099 		"/>
+		<path style="fill:#666666;" d="M11.169,40.326c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V40.326z"/>
+		<path style="fill:#666666;" d="M9.654,38.218l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,38.861,10.247,38.419,9.654,38.218z"/>
+		<polygon style="fill:#666666;" points="11.169,43.528 5.429,49.268 7.964,49.268 11.169,46.063 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,49.08H14.866c-1.011,0-1.83-0.82-1.83-1.831v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,48.261,89.157,49.08,88.146,49.08z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,61.951h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,61.951z"/>
+		<path style="fill:#666666;" d="M0.008,52.845l2.054-2.054C0.966,50.871,0.089,51.748,0.008,52.845z"/>
+		<polygon style="fill:#666666;" points="0,53.73 0,56.265 5.484,50.782 2.948,50.782 		"/>
+		<polygon style="fill:#666666;" points="6.361,50.782 0,57.143 0,59.678 8.896,50.782 		"/>
+		<path style="fill:#666666;" d="M11.169,53.009c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V53.009z"/>
+		<path style="fill:#666666;" d="M9.654,50.901l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,51.544,10.247,51.102,9.654,50.901z"/>
+		<polygon style="fill:#666666;" points="11.169,56.211 5.429,61.951 7.964,61.951 11.169,58.746 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,61.763H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,60.944,89.157,61.763,88.146,61.763z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,74.634h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,74.634z"/>
+		<path style="fill:#666666;" d="M0.008,65.528l2.054-2.054C0.966,63.554,0.089,64.431,0.008,65.528z"/>
+		<polygon style="fill:#666666;" points="0,66.413 0,68.948 5.484,63.465 2.948,63.465 		"/>
+		<polygon style="fill:#666666;" points="6.361,63.465 0,69.826 0,72.361 8.896,63.465 		"/>
+		<path style="fill:#666666;" d="M11.169,65.692c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V65.692z"/>
+		<path style="fill:#666666;" d="M9.654,63.584l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,64.227,10.247,63.785,9.654,63.584z"/>
+		<polygon style="fill:#666666;" points="11.169,68.894 5.429,74.634 7.964,74.634 11.169,71.429 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,74.446H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,73.627,89.157,74.446,88.146,74.446z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,87.317h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,87.317z"/>
+		<path style="fill:#666666;" d="M0.008,78.211l2.054-2.054C0.966,76.237,0.089,77.114,0.008,78.211z"/>
+		<polygon style="fill:#666666;" points="0,79.096 0,81.631 5.484,76.148 2.948,76.148 		"/>
+		<polygon style="fill:#666666;" points="6.361,76.148 0,82.509 0,85.044 8.896,76.148 		"/>
+		<path style="fill:#666666;" d="M11.169,78.375c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V78.375z"/>
+		<path style="fill:#666666;" d="M9.654,76.267l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,76.91,10.247,76.468,9.654,76.267z"/>
+		<polygon style="fill:#666666;" points="11.169,81.577 5.429,87.317 7.964,87.317 11.169,84.112 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,87.129H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,86.31,89.157,87.129,88.146,87.129z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,100h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,100z"/>
+		<path style="fill:#666666;" d="M0.008,90.894l2.054-2.054C0.966,88.92,0.089,89.797,0.008,90.894z"/>
+		<polygon style="fill:#666666;" points="0,91.779 0,94.314 5.484,88.831 2.948,88.831 		"/>
+		<polygon style="fill:#666666;" points="6.361,88.831 0,95.192 0,97.727 8.896,88.831 		"/>
+		<path style="fill:#666666;" d="M11.169,91.058c0-0.068-0.004-0.134-0.01-0.2L2.027,99.99c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V91.058z"/>
+		<path style="fill:#666666;" d="M9.654,88.95l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,89.593,10.247,89.151,9.654,88.95z"/>
+		<polygon style="fill:#666666;" points="11.169,94.26 5.429,100 7.964,100 11.169,96.795 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,99.812H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,98.993,89.157,99.812,88.146,99.812z"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="5.637" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="18.37" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="31.104" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="43.837" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="56.57" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="69.304" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="82.037" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="94.77" r="3.875"/>
+</g>
+</svg>

+ 30 - 0
public/app/plugins/panel/pluginlist/module.html

@@ -0,0 +1,30 @@
+<div class="pluginlist">
+	<div class="pluginlist-section" ng-repeat="category in ctrl.viewModel">
+		<h6 class="pluginlist-section-header">
+			{{category.header}}
+		</h6>
+		<div class="pluginlist-item" ng-repeat="plugin in category.list">
+			<div class="pluginlist-link pluginlist-link-{{plugin.state}} pointer" ng-click="ctrl.gotoPlugin(plugin)">
+				<a href="plugins/{{plugin.id}}/edit">
+						<img ng-src="{{plugin.info.logos.small}}" class="pluginlist-image">
+						<span class="pluginlist-title">{{plugin.name}}</span>
+						<span class="pluginlist-version">v{{plugin.info.version}}</span>
+				</a>
+				<a class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="plugin.latestVersion">
+					Update available!
+				</a>
+				<span class="pluginlist-message pluginlist-message--enable" ng-show="!plugin.enabled && !plugin.hasUpdate">
+					Enable now
+				</span>
+				<span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
+					Up to date
+				</span>
+			</div>
+		</div>
+		<div class="pluginlist-item" ng-show="category.list.length === 0">
+			<a class="pluginlist-link pluginlist-link-{{plugin.state}}" href="http://grafana/net/plugins/">
+				<span class="pluginlist-none-installed">No additional panels installed. <span class="pluginlist-emphasis">Browse Grafana.net</span></span>
+			</a>
+		</div>
+	</div>
+</div>

+ 72 - 0
public/app/plugins/panel/pluginlist/module.ts

@@ -0,0 +1,72 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import _ from 'lodash';
+import config from 'app/core/config';
+import {PanelCtrl} from '../../../features/panel/panel_ctrl';
+
+// Set and populate defaults
+var panelDefaults = {
+};
+
+class PluginListCtrl extends PanelCtrl {
+  static templateUrl = 'module.html';
+
+  pluginList: any[];
+  viewModel: any;
+
+  /** @ngInject */
+  constructor($scope, $injector, private backendSrv, private $location) {
+    super($scope, $injector);
+    _.defaults(this.panel, panelDefaults);
+
+    this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
+    this.pluginList = [];
+    this.viewModel = [
+      {header: "Installed Apps", list: [], type: 'app'},
+      {header: "Installed Panels", list: [], type: 'panel'},
+      {header: "Installed Datasources", list: [], type: 'datasource'},
+    ];
+
+    this.update();
+  }
+
+  onInitEditMode() {
+    this.editorTabIndex = 1;
+    this.addEditorTab('Options', 'public/app/plugins/panel/pluginlist/editor.html');
+  }
+
+  gotoPlugin(plugin) {
+    this.$location.path(`plugins/${plugin.id}/edit`);
+  }
+
+  updateAvailable(plugin, $event) {
+    $event.stopPropagation();
+
+    var modalScope = this.$scope.$new(true);
+    modalScope.plugin = plugin;
+
+    this.publishAppEvent('show-modal', {
+      src: 'public/app/features/plugins/partials/update_instructions.html',
+      scope: modalScope
+    });
+  }
+
+  update() {
+    this.backendSrv.get('api/plugins', {embedded: 0, core: 0}).then(plugins => {
+      this.pluginList = plugins;
+      this.viewModel[0].list = _.filter(plugins, {type: 'app'});
+      this.viewModel[1].list = _.filter(plugins, {type: 'panel'});
+      this.viewModel[2].list = _.filter(plugins, {type: 'datasource'});
+
+      for (let plugin of this.pluginList) {
+        if (plugin.hasUpdate) {
+          plugin.state = 'has-update';
+        } else if (!plugin.enabled) {
+          plugin.state = 'not-enabled';
+        }
+      }
+    });
+  }
+}
+
+export {PluginListCtrl, PluginListCtrl as PanelCtrl}

+ 16 - 0
public/app/plugins/panel/pluginlist/plugin.json

@@ -0,0 +1,16 @@
+{
+  "type": "panel",
+  "name": "Plugin list",
+  "id": "pluginlist",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "http://grafana.org"
+},
+    "logos": {
+      "small": "img/icn-dashlist-panel.svg",
+      "large": "img/icn-dashlist-panel.svg"
+    }
+  }
+}

+ 30 - 24
public/dashboards/home.json

@@ -9,55 +9,61 @@
   "hideControls": true,
   "sharedCrosshair": false,
   "rows": [
-    {
+   {
       "collapse": false,
       "editable": true,
-      "height": "90px",
+      "height": "25px",
       "panels": [
         {
           "content": "<div class=\"text-center dashboard-header\">\n  <span>Home Dashboard</span>\n</div>",
           "editable": true,
           "id": 1,
+          "links": [],
           "mode": "html",
           "span": 12,
           "style": {},
           "title": "",
           "transparent": true,
-          "type": "text",
-          "links": []
+          "type": "text"
         }
       ],
       "title": "New row"
-    },
-    {
+   },
+   {
       "collapse": false,
       "editable": true,
       "height": "510px",
       "panels": [
         {
-          "id": 2,
-          "limit": 10,
-          "mode": "starred",
+          "id": 3,
+          "limit": 4,
+          "links": [],
           "query": "",
-          "span": 6,
+          "span": 7,
           "tags": [],
-          "title": "Starred dashboards",
-          "type": "dashlist"
+          "title": "",
+          "transparent": false,
+          "type": "dashlist",
+          "recent": true,
+          "search": false,
+          "starred": true,
+          "headings": true
         },
         {
-          "id": 3,
-          "limit": 10,
-          "mode": "recently viewed",
-          "query": "",
-          "span": 6,
-          "tags": [],
-          "title": "Recently viewed dashboards",
-          "type": "dashlist"
+          "editable": true,
+          "error": false,
+          "id": 4,
+          "isNew": true,
+          "links": [],
+          "span": 5,
+          "title": "",
+          "transparent": false,
+          "type": "pluginlist"
         }
       ],
       "title": "Row"
-    }
-  ],
+   }
+ ],
   "time": {
     "from": "now-6h",
     "to": "now"
@@ -95,7 +101,7 @@
   "annotations": {
     "list": []
   },
-  "schemaVersion": 9,
-  "version": 5,
+  "schemaVersion": 12,
+  "version": 2,
   "links": []
 }

+ 1 - 0
public/sass/_grafana.scss

@@ -42,6 +42,7 @@
 @import "components/panel_graph";
 @import "components/submenu";
 @import "components/panel_dashlist";
+@import "components/panel_pluginlist";
 @import "components/panel_singlestat";
 @import "components/panel_table";
 @import "components/panel_text";

+ 11 - 10
public/sass/_variables.dark.scss

@@ -70,6 +70,7 @@ $page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%
 $link-color:              darken($white,11%);
 $link-color-disabled:     darken($link-color,30%);
 $link-hover-color:        $white;
+$external-link-color:     $blue;
 
 // Typography
 // -------------------------
@@ -241,14 +242,6 @@ $successBackground:       $btn-success-bg;
 $infoText:                $blue-dark;
 $infoBackground:          $blue-dark;
 
-// Tooltips and popovers
-// -------------------------
-$tooltipColor:            $text-color;
-$tooltipBackground:       $dark-4;
-$tooltipArrowWidth:       5px;
-$tooltipArrowColor:       $tooltipBackground;
-$tooltipLinkColor:        $link-color;
-
 // popover
 $popover-bg:         $dark-4;
 $popover-color:      $text-color;
@@ -256,6 +249,16 @@ $popover-color:      $text-color;
 $popover-help-bg:         $btn-secondary-bg;
 $popover-help-color:      $text-color;
 
+
+// Tooltips and popovers
+// -------------------------
+$tooltipColor:            $text-color;
+$tooltipBackground:       $dark-5;
+$tooltipArrowWidth:       5px;
+$tooltipArrowColor:       $tooltipBackground;
+$tooltipLinkColor:        $link-color;
+$graph-tooltip-bg:        $dark-5;
+
 // images
 $checkboxImageUrl: '../img/checkbox.png';
 
@@ -263,5 +266,3 @@ $checkboxImageUrl: '../img/checkbox.png';
 $card-background: linear-gradient(135deg, #2f2f2f, #262626);
 $card-background-hover: linear-gradient(135deg, #343434, #262626);
 $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
-
-

+ 9 - 9
public/sass/_variables.light.scss

@@ -76,6 +76,7 @@ $page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%
 $link-color:             $gray-1;
 $link-color-disabled:    lighten($link-color, 30%);
 $link-hover-color:       darken($link-color, 20%);
+$external-link-color:    $blue;
 
 // Typography
 // -------------------------
@@ -267,6 +268,12 @@ $infoText:                $blue;
 $infoBackground:          $blue-dark;
 $infoBorder:              transparent;
 
+// popover
+$popover-bg:              $gray-5;
+$popover-color:           $text-color;
+
+$popover-help-bg:         $blue-dark;
+$popover-help-color:      $gray-6;
 
 // Tooltips and popovers
 // -------------------------
@@ -274,14 +281,8 @@ $tooltipColor:            $text-color;
 $tooltipBackground:       $gray-5;
 $tooltipArrowWidth:       5px;
 $tooltipArrowColor:       $tooltipBackground;
-$tooltipLinkColor:        $text-color;
-
-// popover
-$popover-bg:         $gray-5;
-$popover-color:      $text-color;
-
-$popover-help-bg:         $blue-dark;
-$popover-help-color:      $gray-6;
+$tooltipLinkColor:        $link-color;
+$graph-tooltip-bg:        $gray-5;
 
 // images
 $checkboxImageUrl: '../img/checkbox_white.png';
@@ -290,4 +291,3 @@ $checkboxImageUrl: '../img/checkbox_white.png';
 $card-background: linear-gradient(135deg, $gray-5, $gray-6);
 $card-background-hover: linear-gradient(135deg, $gray-6, $gray-7);
 $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1);
-

+ 10 - 1
public/sass/base/_code.scss

@@ -23,6 +23,16 @@ code {
   white-space: nowrap;
 }
 
+code.code--small {
+  font-size: $font-size-xs;
+  padding: 5px;
+  margin: 0 2px;
+}
+
+p.code--line {
+  line-height: 1.8;
+}
+
 // Blocks of code
 pre {
   display: block;
@@ -49,4 +59,3 @@ pre {
     border: 0;
   }
 }
-

+ 1 - 1
public/sass/base/_type.scss

@@ -114,7 +114,7 @@ hr {
 
 small,
 .small {
-  font-size: 85%;
+  font-size: $font-size-sm;
   font-weight: normal;
 }
 

+ 27 - 2
public/sass/components/_cards.scss

@@ -76,13 +76,20 @@
 }
 
 .card-item-header {
+  margin-bottom: $spacer;
+}
+
+.card-item-type {
   color: $text-color-weak;
   text-transform: uppercase;
-  margin-bottom: $spacer;
   font-size: $font-size-sm;
   font-weight: bold;
 }
 
+.card-item-notice {
+  font-size: $font-size-sm;
+}
+
 .card-item-name {
   color: $headings-color;
   overflow: hidden;
@@ -107,6 +114,16 @@
 
 .card-list-layout-grid {
 
+  .card-item-type {
+    display: inline-block;
+  }
+
+  .card-item-notice {
+    font-size: $font-size-sm;
+    display: inline-block;
+    margin-left: $spacer;
+  }
+
   .card-item-header-action {
     float: right;
   }
@@ -116,6 +133,10 @@
     padding: 0 1.5rem 1.5rem 0rem;
   }
 
+  .card-item-wrapper--clickable {
+    cursor: pointer;
+  }
+
   .card-item-figure {
     margin: 0 $spacer $spacer 0;
     height: 6rem;
@@ -157,6 +178,10 @@
     width: 100%;
   }
 
+  .card-item-wrapper--clickable {
+    cursor: pointer;
+  }
+
   .card-item {
     border-bottom: .2rem solid $page-bg;
     border-radius: 0;
@@ -165,6 +190,7 @@
 
   .card-item-header {
     float: right;
+    text-align: right;
   }
 
   .card-item-figure {
@@ -186,4 +212,3 @@
     margin-right: 0;
   }
 }
-

+ 8 - 0
public/sass/components/_panel_dashlist.scss

@@ -1,3 +1,11 @@
+.dashlist-section-header {
+  margin-bottom: $spacer;
+  color: $text-color-weak;
+}
+
+.dashlist-section {
+  margin-bottom: $spacer;
+}
 
 .dashlist-link {
   display: block;

+ 2 - 0
public/sass/components/_panel_graph.scss

@@ -234,6 +234,8 @@
 
 .graph-tooltip {
   white-space: nowrap;
+  font-size: $font-size-sm;
+  background-color: $graph-tooltip-bg;
 
   .graph-tooltip-time {
     text-align: center;

+ 75 - 0
public/sass/components/_panel_pluginlist.scss

@@ -0,0 +1,75 @@
+.pluginlist-section-header {
+  margin-bottom: $spacer;
+  color: $text-color-weak;
+}
+
+.pluginlist-section {
+  margin-bottom: $spacer;
+}
+
+.pluginlist-link {
+  display: block;
+  margin: 5px;
+  padding: 7px;
+  background-color: $tight-form-bg;
+
+  &:hover {
+    background-color: $tight-form-func-bg;
+  }
+}
+
+.pluginlist-icon {
+  vertical-align: sub;
+  font-size: $font-size-h1;
+  margin-right: $spacer / 2;
+}
+
+.pluginlist-image {
+  width: 20px;
+}
+
+.pluginlist-title {
+  margin-right: $spacer / 3;
+}
+
+.pluginlist-version {
+  font-size: $font-size-sm;
+  color: $text-color-weak;
+}
+
+.pluginlist-message {
+  float: right;
+  font-size: $font-size-sm;
+}
+
+.pluginlist-message--update {
+   &:hover {
+    border-bottom: 1px solid $text-color;
+  }
+}
+
+.pluginlist-message--enable{
+  color: $external-link-color;
+   &:hover {
+    border-bottom: 1px solid $external-link-color;
+  }
+}
+
+.pluginlist-message--no-update {
+  color: $text-color-weak;
+}
+
+.pluginlist-emphasis {
+  font-weight: 600;
+}
+
+.pluginlist-none-installed {
+  color: $text-color-weak;
+  font-size: $font-size-sm;
+}
+
+.pluginlist-inline-logo {
+  vertical-align: sub;
+  margin-right: $spacer / 3;
+  width: 16px;
+}

+ 1 - 1
public/sass/components/_tooltip.scss

@@ -2,7 +2,6 @@
 // Tooltips
 // --------------------------------------------------
 
-
 // Base class
 .tooltip {
   position: absolute;
@@ -37,6 +36,7 @@
   border-color: transparent;
   border-style: solid;
 }
+
 .tooltip {
   &.top .tooltip-arrow {
     bottom: 0;

+ 2 - 2
public/sass/pages/_dashboard.scss

@@ -278,11 +278,11 @@ div.flot-text {
 
 .dashboard-header {
   font-family: $headings-font-family;
-  font-size: $font-size-h2;
+  font-size: $font-size-h3;
   text-align: center;
   span {
     display: inline-block;
     @include brand-bottom-border();
-    padding: 1.2rem .5rem .4rem .5rem;
+    padding: 0.5rem .5rem .2rem .5rem;
   }
 }

+ 5 - 25
tsconfig.json

@@ -6,33 +6,13 @@
         "noImplicitAny": false,
         "target": "es5",
         "rootDir": "public/",
+        "sourceRoot": "public/",
         "module": "system",
         "noEmitOnError": true,
-        "emitDecoratorMetadata": true
+        "emitDecoratorMetadata": true,
+        "experimentalDecorators": true
     },
     "files": [
-        "public/app/app.ts",
-        "public/app/core/controllers/grafana_ctrl.ts",
-        "public/app/core/controllers/signup_ctrl.ts",
-        "public/app/core/core.ts",
-        "public/app/core/core_module.ts",
-        "public/app/core/directives/array_join.ts",
-        "public/app/core/directives/give_focus.ts",
-        "public/app/core/filters/filters.ts",
-        "public/app/core/routes/bundle_loader.ts",
-        "public/app/core/table_model.ts",
-        "public/app/core/time_series.ts",
-        "public/app/core/utils/datemath.ts",
-        "public/app/core/utils/flatten.ts",
-        "public/app/core/utils/rangeutil.ts",
-        "public/app/features/dashboard/timepicker/timepicker.ts",
-        "public/app/features/panel/panel_meta.ts",
-        "public/app/plugins/datasource/influxdb/influx_query.ts",
-        "public/app/plugins/datasource/influxdb/query_part.ts",
-        "public/app/plugins/panels/table/controller.ts",
-        "public/app/plugins/panels/table/editor.ts",
-        "public/app/plugins/panels/table/module.ts",
-        "public/app/plugins/panels/table/renderer.ts",
-        "public/app/plugins/panels/table/transformers.ts"
+        "public/app/**/*.ts"
     ]
-}
+}