浏览代码

Merge branch 'plugins-list-and-edit-view'

Torkel Ödegaard 9 年之前
父节点
当前提交
4fcca7c61b
共有 45 个文件被更改,包括 708 次插入508 次删除
  1. 3 3
      pkg/api/api.go
  2. 0 59
      pkg/api/app_settings.go
  3. 0 40
      pkg/api/dtos/apps.go
  4. 26 0
      pkg/api/dtos/plugins.go
  5. 1 1
      pkg/api/index.go
  6. 88 0
      pkg/api/plugin_setting.go
  7. 1 1
      pkg/api/pluginproxy/pluginproxy.go
  8. 13 13
      pkg/models/plugin_setting.go
  9. 4 0
      pkg/plugins/frontend_plugin.go
  10. 11 15
      pkg/plugins/queries.go
  11. 0 70
      pkg/services/sqlstore/app_settings.go
  12. 6 8
      pkg/services/sqlstore/migrations/plugin_setting.go
  13. 70 0
      pkg/services/sqlstore/plugin_setting.go
  14. 16 1
      public/app/core/components/sidemenu/sidemenu.ts
  15. 5 5
      public/app/core/directives/plugin_component.ts
  16. 12 12
      public/app/core/routes/routes.ts
  17. 4 1
      public/app/features/admin/partials/edit_org.html
  18. 4 1
      public/app/features/admin/partials/edit_user.html
  19. 4 1
      public/app/features/admin/partials/new_user.html
  20. 4 1
      public/app/features/admin/partials/orgs.html
  21. 4 1
      public/app/features/admin/partials/users.html
  22. 0 49
      public/app/features/apps/edit_ctrl.ts
  23. 0 17
      public/app/features/apps/list_ctrl.ts
  24. 0 108
      public/app/features/apps/partials/edit.html
  25. 0 47
      public/app/features/apps/partials/list.html
  26. 2 2
      public/app/features/datasources/partials/list.html
  27. 0 0
      public/app/features/plugins/all.ts
  28. 51 0
      public/app/features/plugins/edit_ctrl.ts
  29. 17 0
      public/app/features/plugins/list_ctrl.ts
  30. 0 0
      public/app/features/plugins/page_ctrl.ts
  31. 185 0
      public/app/features/plugins/partials/edit.html
  32. 42 0
      public/app/features/plugins/partials/list.html
  33. 0 0
      public/app/features/plugins/partials/page.html
  34. 3 2
      public/sass/_grafana.scss
  35. 4 2
      public/sass/_variables.dark.scss
  36. 2 0
      public/sass/_variables.light.scss
  37. 6 1
      public/sass/_variables.scss
  38. 1 1
      public/sass/base/_type.scss
  39. 1 6
      public/sass/components/_sidemenu.scss
  40. 1 1
      public/sass/components/_tabs.scss
  41. 14 0
      public/sass/layout/_lists.scss
  42. 39 13
      public/sass/layout/_page.scss
  43. 0 26
      public/sass/pages/_apps.scss
  44. 11 0
      public/sass/pages/_dashboard.scss
  45. 53 0
      public/sass/pages/_plugins.scss

+ 3 - 3
pkg/api/api.go

@@ -126,9 +126,9 @@ func Register(r *macaron.Macaron) {
 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
 
 			// apps
-			r.Get("/apps", wrap(GetOrgAppsList))
-			r.Get("/apps/:appId/settings", wrap(GetAppSettingsById))
-			r.Post("/apps/:appId/settings", bind(m.UpdateAppSettingsCmd{}), wrap(UpdateAppSettings))
+			r.Get("/plugins", wrap(GetPluginList))
+			r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
+			r.Post("/plugins/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
 		}, reqOrgAdmin)
 
 		// create new org

+ 0 - 59
pkg/api/app_settings.go

@@ -1,59 +0,0 @@
-package api
-
-import (
-	"github.com/grafana/grafana/pkg/api/dtos"
-	"github.com/grafana/grafana/pkg/bus"
-	"github.com/grafana/grafana/pkg/middleware"
-	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/plugins"
-)
-
-func GetOrgAppsList(c *middleware.Context) Response {
-	orgApps, err := plugins.GetOrgAppSettings(c.OrgId)
-
-	if err != nil {
-		return ApiError(500, "Failed to list of apps", err)
-	}
-
-	result := make([]*dtos.AppSettings, 0)
-	for _, app := range plugins.Apps {
-		orgApp := orgApps[app.Id]
-		result = append(result, dtos.NewAppSettingsDto(app, orgApp))
-	}
-
-	return Json(200, result)
-}
-
-func GetAppSettingsById(c *middleware.Context) Response {
-	appId := c.Params(":appId")
-
-	if pluginDef, exists := plugins.Apps[appId]; !exists {
-		return ApiError(404, "PluginId not found, no installed plugin with that id", nil)
-	} else {
-		orgApps, err := plugins.GetOrgAppSettings(c.OrgId)
-		if err != nil {
-			return ApiError(500, "Failed to get org app settings ", nil)
-		}
-		orgApp := orgApps[appId]
-
-		return Json(200, dtos.NewAppSettingsDto(pluginDef, orgApp))
-	}
-}
-
-func UpdateAppSettings(c *middleware.Context, cmd m.UpdateAppSettingsCmd) Response {
-	appId := c.Params(":appId")
-
-	cmd.OrgId = c.OrgId
-	cmd.AppId = appId
-
-	if _, ok := plugins.Apps[cmd.AppId]; !ok {
-		return ApiError(404, "App type not installed.", nil)
-	}
-
-	err := bus.Dispatch(&cmd)
-	if err != nil {
-		return ApiError(500, "Failed to update App Plugin", err)
-	}
-
-	return ApiSuccess("App updated")
-}

+ 0 - 40
pkg/api/dtos/apps.go

@@ -1,40 +0,0 @@
-package dtos
-
-import (
-	"github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/plugins"
-)
-
-type AppSettings struct {
-	Name     string                    `json:"name"`
-	AppId    string                    `json:"appId"`
-	Enabled  bool                      `json:"enabled"`
-	Pinned   bool                      `json:"pinned"`
-	Module   string                    `json:"module"`
-	BaseUrl  string                    `json:"baseUrl"`
-	Info     *plugins.PluginInfo       `json:"info"`
-	Pages    []*plugins.AppPluginPage  `json:"pages"`
-	Includes []*plugins.AppIncludeInfo `json:"includes"`
-	JsonData map[string]interface{}    `json:"jsonData"`
-}
-
-func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
-	dto := &AppSettings{
-		AppId:    def.Id,
-		Name:     def.Name,
-		Info:     &def.Info,
-		Module:   def.Module,
-		BaseUrl:  def.BaseUrl,
-		Pages:    def.Pages,
-		Includes: def.Includes,
-	}
-
-	if data != nil {
-		dto.Enabled = data.Enabled
-		dto.Pinned = data.Pinned
-		dto.Info = &def.Info
-		dto.JsonData = data.JsonData
-	}
-
-	return dto
-}

+ 26 - 0
pkg/api/dtos/plugins.go

@@ -0,0 +1,26 @@
+package dtos
+
+import "github.com/grafana/grafana/pkg/plugins"
+
+type PluginSetting struct {
+	Name     string                    `json:"name"`
+	Type     string                    `json:"type"`
+	PluginId string                    `json:"pluginId"`
+	Enabled  bool                      `json:"enabled"`
+	Pinned   bool                      `json:"pinned"`
+	Module   string                    `json:"module"`
+	BaseUrl  string                    `json:"baseUrl"`
+	Info     *plugins.PluginInfo       `json:"info"`
+	Pages    []*plugins.AppPluginPage  `json:"pages"`
+	Includes []*plugins.AppIncludeInfo `json:"includes"`
+	JsonData map[string]interface{}    `json:"jsonData"`
+}
+
+type PluginListItem struct {
+	Name     string              `json:"name"`
+	Type     string              `json:"type"`
+	PluginId string              `json:"pluginId"`
+	Enabled  bool                `json:"enabled"`
+	Pinned   bool                `json:"pinned"`
+	Info     *plugins.PluginInfo `json:"info"`
+}

+ 1 - 1
pkg/api/index.go

@@ -72,7 +72,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
 			Text: "Plugins",
 			Icon: "icon-gf icon-gf-apps",
-			Url:  setting.AppSubUrl + "/apps",
+			Url:  setting.AppSubUrl + "/plugins",
 		})
 	}
 

+ 88 - 0
pkg/api/plugin_setting.go

@@ -0,0 +1,88 @@
+package api
+
+import (
+	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+)
+
+func GetPluginList(c *middleware.Context) Response {
+	pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
+
+	if err != nil {
+		return ApiError(500, "Failed to get list of plugins", err)
+	}
+
+	result := make([]*dtos.PluginListItem, 0)
+	for _, pluginDef := range plugins.Plugins {
+		listItem := &dtos.PluginListItem{
+			PluginId: pluginDef.Id,
+			Name:     pluginDef.Name,
+			Type:     pluginDef.Type,
+			Info:     &pluginDef.Info,
+		}
+
+		if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
+			listItem.Enabled = pluginSetting.Enabled
+			listItem.Pinned = pluginSetting.Pinned
+		}
+
+		result = append(result, listItem)
+	}
+
+	return Json(200, result)
+}
+
+func GetPluginSettingById(c *middleware.Context) Response {
+	pluginId := c.Params(":pluginId")
+
+	if def, exists := plugins.Plugins[pluginId]; !exists {
+		return ApiError(404, "Plugin not found, no installed plugin with that id", nil)
+	} else {
+		dto := &dtos.PluginSetting{
+			Type:     def.Type,
+			PluginId: def.Id,
+			Name:     def.Name,
+			Info:     &def.Info,
+		}
+
+		if app, exists := plugins.Apps[pluginId]; exists {
+			dto.Pages = app.Pages
+			dto.Includes = app.Includes
+			dto.BaseUrl = app.BaseUrl
+			dto.Module = app.Module
+		}
+
+		query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId}
+		if err := bus.Dispatch(&query); err != nil {
+			if err != m.ErrPluginSettingNotFound {
+				return ApiError(500, "Failed to get login settings", nil)
+			}
+		} else {
+			dto.Enabled = query.Result.Enabled
+			dto.Pinned = query.Result.Pinned
+			dto.JsonData = query.Result.JsonData
+		}
+
+		return Json(200, dto)
+	}
+}
+
+func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Response {
+	pluginId := c.Params(":pluginId")
+
+	cmd.OrgId = c.OrgId
+	cmd.PluginId = pluginId
+
+	if _, ok := plugins.Apps[cmd.PluginId]; !ok {
+		return ApiError(404, "Plugin not installed.", nil)
+	}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to update plugin setting", err)
+	}
+
+	return ApiSuccess("Plugin settings updated")
+}

+ 1 - 1
pkg/api/pluginproxy/pluginproxy.go

@@ -26,7 +26,7 @@ type templateData struct {
 func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.Header, error) {
 	result := http.Header{}
 
-	query := m.GetAppSettingByAppIdQuery{OrgId: orgId, AppId: appId}
+	query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appId}
 
 	if err := bus.Dispatch(&query); err != nil {
 		return nil, err

+ 13 - 13
pkg/models/app_settings.go → pkg/models/plugin_setting.go

@@ -9,12 +9,12 @@ import (
 )
 
 var (
-	ErrAppSettingNotFound = errors.New("AppSetting not found")
+	ErrPluginSettingNotFound = errors.New("Plugin setting not found")
 )
 
-type AppSettings struct {
+type PluginSetting struct {
 	Id             int64
-	AppId          string
+	PluginId       string
 	OrgId          int64
 	Enabled        bool
 	Pinned         bool
@@ -39,17 +39,17 @@ func (s SecureJsonData) Decrypt() map[string]string {
 // COMMANDS
 
 // Also acts as api DTO
-type UpdateAppSettingsCmd struct {
+type UpdatePluginSettingCmd struct {
 	Enabled        bool                   `json:"enabled"`
 	Pinned         bool                   `json:"pinned"`
 	JsonData       map[string]interface{} `json:"jsonData"`
 	SecureJsonData map[string]string      `json:"secureJsonData"`
 
-	AppId string `json:"-"`
-	OrgId int64  `json:"-"`
+	PluginId string `json:"-"`
+	OrgId    int64  `json:"-"`
 }
 
-func (cmd *UpdateAppSettingsCmd) GetEncryptedJsonData() SecureJsonData {
+func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData {
 	encrypted := make(SecureJsonData)
 	for key, data := range cmd.SecureJsonData {
 		encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey)
@@ -59,13 +59,13 @@ func (cmd *UpdateAppSettingsCmd) GetEncryptedJsonData() SecureJsonData {
 
 // ---------------------
 // QUERIES
-type GetAppSettingsQuery struct {
+type GetPluginSettingsQuery struct {
 	OrgId  int64
-	Result []*AppSettings
+	Result []*PluginSetting
 }
 
-type GetAppSettingByAppIdQuery struct {
-	AppId  string
-	OrgId  int64
-	Result *AppSettings
+type GetPluginSettingByIdQuery struct {
+	PluginId string
+	OrgId    int64
+	Result   *PluginSetting
 }

+ 4 - 0
pkg/plugins/frontend_plugin.go

@@ -56,6 +56,10 @@ func (fp *FrontendPluginBase) handleModuleDefaults() {
 }
 
 func evalRelativePluginUrlPath(pathStr string, pluginId string) string {
+	if pathStr == "" {
+		return ""
+	}
+
 	u, _ := url.Parse(pathStr)
 	if u.IsAbs() {
 		return pathStr

+ 11 - 15
pkg/plugins/queries.go

@@ -5,44 +5,40 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 )
 
-func GetOrgAppSettings(orgId int64) (map[string]*m.AppSettings, error) {
-	query := m.GetAppSettingsQuery{OrgId: orgId}
+func GetPluginSettings(orgId int64) (map[string]*m.PluginSetting, error) {
+	query := m.GetPluginSettingsQuery{OrgId: orgId}
 
 	if err := bus.Dispatch(&query); err != nil {
 		return nil, err
 	}
 
-	orgAppsMap := make(map[string]*m.AppSettings)
-	for _, orgApp := range query.Result {
-		orgAppsMap[orgApp.AppId] = orgApp
+	pluginMap := make(map[string]*m.PluginSetting)
+	for _, plug := range query.Result {
+		pluginMap[plug.PluginId] = plug
 	}
 
-	return orgAppsMap, nil
+	return pluginMap, nil
 }
 
 func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
 	enabledPlugins := NewEnabledPlugins()
-	orgApps, err := GetOrgAppSettings(orgId)
+	orgPlugins, err := GetPluginSettings(orgId)
 	if err != nil {
 		return nil, err
 	}
 
 	enabledApps := make(map[string]bool)
 
-	for appId, installedApp := range Apps {
-		var app AppPlugin
-		app = *installedApp
+	for pluginId, app := range Apps {
 
-		// check if the app is stored in the DB for this org and if so, use the
-		// state stored there.
-		if b, ok := orgApps[appId]; ok {
+		if b, ok := orgPlugins[pluginId]; ok {
 			app.Enabled = b.Enabled
 			app.Pinned = b.Pinned
 		}
 
 		if app.Enabled {
-			enabledApps[app.Id] = true
-			enabledPlugins.Apps = append(enabledPlugins.Apps, &app)
+			enabledApps[pluginId] = true
+			enabledPlugins.Apps = append(enabledPlugins.Apps, app)
 		}
 	}
 

+ 0 - 70
pkg/services/sqlstore/app_settings.go

@@ -1,70 +0,0 @@
-package sqlstore
-
-import (
-	"time"
-
-	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/setting"
-	"github.com/grafana/grafana/pkg/util"
-)
-
-func init() {
-	bus.AddHandler("sql", GetAppSettings)
-	bus.AddHandler("sql", GetAppSettingByAppId)
-	bus.AddHandler("sql", UpdateAppSettings)
-}
-
-func GetAppSettings(query *m.GetAppSettingsQuery) error {
-	sess := x.Where("org_id=?", query.OrgId)
-
-	query.Result = make([]*m.AppSettings, 0)
-	return sess.Find(&query.Result)
-}
-
-func GetAppSettingByAppId(query *m.GetAppSettingByAppIdQuery) error {
-	appSetting := m.AppSettings{OrgId: query.OrgId, AppId: query.AppId}
-	has, err := x.Get(&appSetting)
-	if err != nil {
-		return err
-	} else if has == false {
-		return m.ErrAppSettingNotFound
-	}
-	query.Result = &appSetting
-	return nil
-}
-
-func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
-	return inTransaction2(func(sess *session) error {
-		var app m.AppSettings
-
-		exists, err := sess.Where("org_id=? and app_id=?", cmd.OrgId, cmd.AppId).Get(&app)
-		sess.UseBool("enabled")
-		sess.UseBool("pinned")
-		if !exists {
-			app = m.AppSettings{
-				AppId:          cmd.AppId,
-				OrgId:          cmd.OrgId,
-				Enabled:        cmd.Enabled,
-				Pinned:         cmd.Pinned,
-				JsonData:       cmd.JsonData,
-				SecureJsonData: cmd.GetEncryptedJsonData(),
-				Created:        time.Now(),
-				Updated:        time.Now(),
-			}
-			_, err = sess.Insert(&app)
-			return err
-		} else {
-			for key, data := range cmd.SecureJsonData {
-				app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
-			}
-			app.SecureJsonData = cmd.GetEncryptedJsonData()
-			app.Updated = time.Now()
-			app.Enabled = cmd.Enabled
-			app.JsonData = cmd.JsonData
-			app.Pinned = cmd.Pinned
-			_, err = sess.Id(app.Id).Update(&app)
-			return err
-		}
-	})
-}

+ 6 - 8
pkg/services/sqlstore/migrations/app_settings.go → pkg/services/sqlstore/migrations/plugin_setting.go

@@ -4,12 +4,12 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 
 func addAppSettingsMigration(mg *Migrator) {
 
-	appSettingsV2 := Table{
-		Name: "app_settings",
+	pluginSettingTable := Table{
+		Name: "plugin_setting",
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "org_id", Type: DB_BigInt, Nullable: true},
-			{Name: "app_id", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "plugin_id", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "enabled", Type: DB_Bool, Nullable: false},
 			{Name: "pinned", Type: DB_Bool, Nullable: false},
 			{Name: "json_data", Type: DB_Text, Nullable: true},
@@ -18,14 +18,12 @@ func addAppSettingsMigration(mg *Migrator) {
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 		},
 		Indices: []*Index{
-			{Cols: []string{"org_id", "app_id"}, Type: UniqueIndex},
+			{Cols: []string{"org_id", "plugin_id"}, Type: UniqueIndex},
 		},
 	}
 
-	mg.AddMigration("Drop old table app_settings v1", NewDropTableMigration("app_settings"))
-
-	mg.AddMigration("create app_settings table v2", NewAddTableMigration(appSettingsV2))
+	mg.AddMigration("create plugin_setting table", NewAddTableMigration(pluginSettingTable))
 
 	//-------  indexes ------------------
-	addTableIndicesMigrations(mg, "v3", appSettingsV2)
+	addTableIndicesMigrations(mg, "v1", pluginSettingTable)
 }

+ 70 - 0
pkg/services/sqlstore/plugin_setting.go

@@ -0,0 +1,70 @@
+package sqlstore
+
+import (
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+func init() {
+	bus.AddHandler("sql", GetPluginSettings)
+	bus.AddHandler("sql", GetPluginSettingById)
+	bus.AddHandler("sql", UpdatePluginSetting)
+}
+
+func GetPluginSettings(query *m.GetPluginSettingsQuery) error {
+	sess := x.Where("org_id=?", query.OrgId)
+
+	query.Result = make([]*m.PluginSetting, 0)
+	return sess.Find(&query.Result)
+}
+
+func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
+	pluginSetting := m.PluginSetting{OrgId: query.OrgId, PluginId: query.PluginId}
+	has, err := x.Get(&pluginSetting)
+	if err != nil {
+		return err
+	} else if has == false {
+		return m.ErrPluginSettingNotFound
+	}
+	query.Result = &pluginSetting
+	return nil
+}
+
+func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
+	return inTransaction2(func(sess *session) error {
+		var pluginSetting m.PluginSetting
+
+		exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting)
+		sess.UseBool("enabled")
+		sess.UseBool("pinned")
+		if !exists {
+			pluginSetting = m.PluginSetting{
+				PluginId:       cmd.PluginId,
+				OrgId:          cmd.OrgId,
+				Enabled:        cmd.Enabled,
+				Pinned:         cmd.Pinned,
+				JsonData:       cmd.JsonData,
+				SecureJsonData: cmd.GetEncryptedJsonData(),
+				Created:        time.Now(),
+				Updated:        time.Now(),
+			}
+			_, err = sess.Insert(&pluginSetting)
+			return err
+		} else {
+			for key, data := range cmd.SecureJsonData {
+				pluginSetting.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
+			}
+			pluginSetting.SecureJsonData = cmd.GetEncryptedJsonData()
+			pluginSetting.Updated = time.Now()
+			pluginSetting.Enabled = cmd.Enabled
+			pluginSetting.JsonData = cmd.JsonData
+			pluginSetting.Pinned = cmd.Pinned
+			_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
+			return err
+		}
+	})
+}

+ 16 - 1
public/app/core/components/sidemenu/sidemenu.ts

@@ -38,7 +38,6 @@ export class SideMenuCtrl {
  openUserDropdown() {
    this.orgMenu = [
      {section: 'You', cssClass: 'dropdown-menu-title'},
-     {text: 'Preferences', url: this.getUrl('/profile')},
      {text: 'Profile', url: this.getUrl('/profile')},
    ];
 
@@ -100,6 +99,22 @@ export function sideMenuDirective() {
     bindToController: true,
     controllerAs: 'ctrl',
     scope: {},
+    link: function(scope, elem) {
+      // hack to hide dropdown menu
+      elem.on('click.dropdown', '.dropdown-menu a', function(evt) {
+        var menu = $(evt.target).parents('.dropdown-menu');
+        var parent = menu.parent();
+        menu.detach();
+
+        setTimeout(function() {
+          parent.append(menu);
+        }, 100);
+      });
+
+      scope.$on("$destory", function() {
+        elem.off('click.dropdown');
+      });
+    }
   };
 }
 

+ 5 - 5
public/app/core/directives/plugin_component.ts

@@ -160,13 +160,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
       }
       // AppConfigCtrl
       case 'app-config-ctrl': {
-        let appModel = scope.ctrl.appModel;
-        return System.import(appModel.module).then(function(appModule) {
+        let model = scope.ctrl.model;
+        return System.import(model.module).then(function(appModule) {
           return {
-            baseUrl: appModel.baseUrl,
-            name: 'app-config-' + appModel.appId,
+            baseUrl: model.baseUrl,
+            name: 'app-config-' + model.pluginId,
             bindings: {appModel: "=", appEditCtrl: "="},
-            attrs: {"app-model": "ctrl.appModel", "app-edit-ctrl": "ctrl"},
+            attrs: {"app-model": "ctrl.model", "app-edit-ctrl": "ctrl"},
             Component: appModule.ConfigCtrl,
           };
         });

+ 12 - 12
public/app/core/routes/routes.ts

@@ -11,7 +11,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
   $locationProvider.html5Mode(true);
 
   var loadOrgBundle = new BundleLoader('app/features/org/all');
-  var loadAppsBundle = new BundleLoader('app/features/apps/all');
+  var loadPluginsBundle = new BundleLoader('app/features/plugins/all');
   var loadAdminBundle = new BundleLoader('app/features/admin/admin');
 
   $routeProvider
@@ -165,23 +165,23 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     controller : 'SnapshotsCtrl',
     controllerAs: 'ctrl',
   })
-  .when('/apps', {
-    templateUrl: 'public/app/features/apps/partials/list.html',
-    controller: 'AppListCtrl',
+  .when('/plugins', {
+    templateUrl: 'public/app/features/plugins/partials/list.html',
+    controller: 'PluginListCtrl',
     controllerAs: 'ctrl',
-    resolve: loadAppsBundle,
+    resolve: loadPluginsBundle,
   })
-  .when('/apps/:appId/edit', {
-    templateUrl: 'public/app/features/apps/partials/edit.html',
-    controller: 'AppEditCtrl',
+  .when('/plugins/:pluginId/edit', {
+    templateUrl: 'public/app/features/plugins/partials/edit.html',
+    controller: 'PluginEditCtrl',
     controllerAs: 'ctrl',
-    resolve: loadAppsBundle,
+    resolve: loadPluginsBundle,
   })
-  .when('/apps/:appId/page/:slug', {
-    templateUrl: 'public/app/features/apps/partials/page.html',
+  .when('/plugins/:pluginId/page/:slug', {
+    templateUrl: 'public/app/features/plugins/partials/page.html',
     controller: 'AppPageCtrl',
     controllerAs: 'ctrl',
-    resolve: loadAppsBundle,
+    resolve: loadPluginsBundle,
   })
   .when('/global-alerts', {
     templateUrl: 'public/app/features/dashboard/partials/globalAlerts.html',

+ 4 - 1
public/app/features/admin/partials/edit_org.html

@@ -1,5 +1,8 @@
 <navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<nav-button title="Orgs" title-url="admin/orgs" icon="icon-gf icon-gf-users"></nav-button>
+	<a href="admin/orgs" class="navbar-page-btn">
+		<i class="icon-gf icon-gf-users"></i>
+		Orgs
+	</a>
 </navbar>
 
 <div class="page-container">

+ 4 - 1
public/app/features/admin/partials/edit_user.html

@@ -1,5 +1,8 @@
 <navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<nav-button title="Users" title-url="admin/users" icon="icon-gf icon-gf-users"></nav-button>
+	<a href="admin/users" class="navbar-page-btn">
+		<i class="icon-gf icon-gf-users"></i>
+		Users
+	</a>
 </navbar>
 
 <div class="page-container">

+ 4 - 1
public/app/features/admin/partials/new_user.html

@@ -1,5 +1,8 @@
 <navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<nav-button title="Users" title-url="admin/users" icon="icon-gf icon-gf-users"></nav-button>
+	<a href="admin/users" class="navbar-page-btn">
+		<i class="icon-gf icon-gf-users"></i>
+		Users
+	</a>
 </navbar>
 
 <div class="page-container">

+ 4 - 1
public/app/features/admin/partials/orgs.html

@@ -1,5 +1,8 @@
 <navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<nav-button title="Orgs" title-url="admin/orgs" icon="icon-gf icon-gf-users"></nav-button>
+	<a href="admin/orgs" class="navbar-page-btn">
+		<i class="icon-gf icon-gf-users"></i>
+		Orgs
+	</a>
 </navbar>
 
 <div class="page-container">

+ 4 - 1
public/app/features/admin/partials/users.html

@@ -1,5 +1,8 @@
 <navbar icon="fa fa-fw fa-cogs" title="Admin" title-url="admin">
-	<nav-button title="Users" title-url="admin/users" icon="icon-gf icon-gf-users"></nav-button>
+	<a href="admin/users" class="navbar-page-btn">
+		<i class="icon-gf icon-gf-users"></i>
+		Users
+	</a>
 </navbar>
 
 <div class="page-container">

+ 0 - 49
public/app/features/apps/edit_ctrl.ts

@@ -1,49 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import angular from 'angular';
-import _ from 'lodash';
-
-export class AppEditCtrl {
-  appModel: any;
-  appId: any;
-  includedPanels: any;
-  includedDatasources: any;
-
-  /** @ngInject */
-  constructor(private backendSrv: any, private $routeParams: any) {
-    this.appModel = {};
-    this.appId = $routeParams.appId;
-
-    this.backendSrv.get(`/api/org/apps/${this.appId}/settings`).then(result => {
-      this.appModel = result;
-      this.includedPanels = _.where(result.includes, {type: 'panel'});
-      this.includedDatasources = _.where(result.includes, {type: 'datasource'});
-    });
-  }
-
-  update() {
-    var updateCmd = _.extend({
-      appId: this.appModel.appId,
-      orgId: this.appModel.orgId,
-      enabled: this.appModel.enabled,
-      pinned: this.appModel.pinned,
-      jsonData: this.appModel.jsonData,
-      secureJsonData: this.appModel.secureJsonData,
-    }, {});
-
-    this.backendSrv.post(`/api/org/apps/${this.appId}/settings`, updateCmd).then(function() {
-      window.location.href = window.location.href;
-    });
-  }
-
-  toggleEnabled() {
-    this.update();
-  }
-
-  togglePinned() {
-    this.update();
-  }
-}
-
-angular.module('grafana.controllers').controller('AppEditCtrl', AppEditCtrl);
-

+ 0 - 17
public/app/features/apps/list_ctrl.ts

@@ -1,17 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import angular from 'angular';
-
-export class AppListCtrl {
-  apps: any[];
-
-  /** @ngInject */
-  constructor(private backendSrv: any) {
-
-    this.backendSrv.get('api/org/apps').then(apps => {
-      this.apps = apps;
-    });
-  }
-}
-
-angular.module('grafana.controllers').controller('AppListCtrl', AppListCtrl);

+ 0 - 108
public/app/features/apps/partials/edit.html

@@ -1,108 +0,0 @@
-<navbar title="Plugins" title-url="Plugins" icon="icon-gf icon-gf-apps">
-</navbar>
-
-<div class="page-container">
-	<div class="flex-container">
-		<div class="flex-column app-edit-logo-box">
-			<img src="{{ctrl.appModel.info.logos.large}}">
-		</div>
-		<div class="flex-column">
-			<h1>
-				{{ctrl.appModel.name}}
-			</h1>
-			<div class="app-edit-description">
-				{{ctrl.appModel.info.description}}<br>
-				<span style="small">
-					Version: {{ctrl.appModel.info.version}} &nbsp; &nbsp; Updated: {{ctrl.appModel.info.updated}}
-				</span>
-			</div>
-
-			<div class="form-inline">
-				<editor-checkbox text="Enabled" model="ctrl.appModel.enabled" change="ctrl.toggleEnabled()"></editor-checkbox>
-				&nbsp; &nbsp; &nbsp;
-				<editor-checkbox text="Pinned" model="ctrl.appModel.pinned" change="ctrl.togglePinned()"></editor-checkbox>
-			</div>
-		</div>
-		<div class="flex-column">
-			<ul class="app-edit-links">
-				<li>
-					By <a href="{{ctrl.appModel.info.author.url}}" class="external-link" target="_blank">{{ctrl.appModel.info.author.name}}</a>
-				</li>
-				<li ng-repeat="link in ctrl.appModel.info.links">
-					<a href="{{link.url}}" class="external-link" target="_blank">{{link.name}}</a>
-				</li>
-			</ul>
-		</div>
-	</div>
-
-	<section class="simple-box">
-		<h3 class="simple-box-header">Included with app:</h3>
-		<div class="flex-container">
-			<div class="simple-box-body simple-box-column">
-				<div class="simple-box-column-header">
-					<i class="fa fa-th-large"></i>
-					Dashboards
-				</div>
-				<ul>
-					<li><em class="small">None</em></li>
-				</ul>
-			</div>
-			<div class="simple-box-body simple-box-column">
-				<div class="simple-box-column-header">
-					<i class="fa fa-line-chart"></i>
-					Panels
-				</div>
-				<ul>
-					<li ng-show="!ctrl.includedPanels.length"><em class="small">None</em></li>
-					<li ng-repeat="panel in ctrl.includedPanels">
-						{{panel.name}}
-					</li>
-				</ul>
-			</div>
-			<div class="simple-box-body simple-box-column">
-				<div class="simple-box-column-header">
-					<i class="fa fa-database"></i>
-					Datasources
-				</div>
-				<ul>
-					<li ng-show="!ctrl.includedDatasources.length"><em class="small">None</em></li>
-					<li ng-repeat="ds in ctrl.includedDatasources">
-						{{ds.name}}
-					</li>
-				</ul>
-			</div>
-			<div class="simple-box-body simple-box-column">
-				<div class="simple-box-column-header">
-					<i class="fa fa-files-o"></i>
-					Pages
-				</div>
-				<ul>
-					<li ng-repeat="page in ctrl.appModel.pages">
-						<a href="apps/{{ctrl.appId}}/page/{{page.slug}}" class="external-link">{{page.name}}</a>
-					</li>
-				</ul>
-			</div>
-
-		</div>
-	</section>
-
-	<section class="simple-box">
-		<h3 class="simple-box-header">Dependencies:</h3>
-		<div class="simple-box-body">
-			Grafana 2.6.x
-		</div>
-	</section>
-
-	<section class="simple-box">
-		<h3 class="simple-box-header">Configuration:</h3>
-		<div class="simple-box-body">
-			<div ng-if="ctrl.appModel.appId">
-				<plugin-component type="app-config-ctrl"></plugin-component>
-				<div class="clearfix"></div>
-				<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
-			</div>
-		</div>
-	</section>
-
-
-</div>

+ 0 - 47
public/app/features/apps/partials/list.html

@@ -1,47 +0,0 @@
-<navbar title="Plugins" icon="icon-gf icon-gf-apps" title-url="apps">
-</navbar>
-
-<div class="page-container">
-  <div class="page-header">
-    <h1>Plugins</h1>
-    </div>
-	<div ng-if="!ctrl.apps">
-		<em>No apps defined</em>
-	</div>
-
-	<ul class="filter-list">
-      <li ng-repeat="app in ctrl.apps">
-        <ul class="filter-list-card">
-					<li class="filter-list-card-image">
-						<img ng-src="{{app.info.logos.small}}">
-					</li>
-          <li>
-            <div class="filter-list-card-controls">
-              <div class="filter-list-card-config">
-								<a href="apps/{{app.appId}}/edit">
-									<i class="fa fa-cog"></i>
-								</a>
-              </div>
-            </div>
-						<span class="filter-list-card-title">
-							<a href="apps/{{app.appId}}/edit">
-								{{app.name}}
-							</a>
-							&nbsp; &nbsp;
-							<span class="label label-info" ng-if="app.enabled">
-								Enabled
-							</span>
-							&nbsp;
-							<span class="label label-info" ng-if="app.pinned">
-								Pinned
-							</span>
-
-						</span>
-            <span class="filter-list-card-status">
-              <span class="filter-list-card-state">Dashboards: 1</span>
-            </span>
-          </li>
-        </ul>
-      </li>
-	</ul>
-</div>

+ 2 - 2
public/app/features/datasources/partials/list.html

@@ -22,8 +22,8 @@
 		<table class="filter-table" ng-if="ctrl.datasources.length > 0">
 			<thead>
 				<tr>
-					<th><strong>Name</strong></th>
-					<th><strong>Url</strong></th>
+					<th><strong>name</strong></th>
+					<th><strong>url</strong></th>
 					<th style="width: 60px;"></th>
 					<th style="width: 85px;"></th>
 					<th style="width: 44px;"></th>

+ 0 - 0
public/app/features/apps/all.ts → public/app/features/plugins/all.ts


+ 51 - 0
public/app/features/plugins/edit_ctrl.ts

@@ -0,0 +1,51 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+export class PluginEditCtrl {
+  model: any;
+  pluginId: any;
+  includedPanels: any;
+  includedDatasources: any;
+  tabIndex: number;
+
+  /** @ngInject */
+  constructor(private backendSrv: any, private $routeParams: any) {
+    this.model = {};
+    this.pluginId = $routeParams.pluginId;
+    this.tabIndex = 0;
+
+    this.backendSrv.get(`/api/org/plugins/${this.pluginId}/settings`).then(result => {
+      this.model = result;
+      this.includedPanels = _.where(result.includes, {type: 'panel'});
+      this.includedDatasources = _.where(result.includes, {type: 'datasource'});
+    });
+  }
+
+  update() {
+    var updateCmd = _.extend({
+      pluginId: this.model.pluginId,
+      orgId: this.model.orgId,
+      enabled: this.model.enabled,
+      pinned: this.model.pinned,
+      jsonData: this.model.jsonData,
+      secureJsonData: this.model.secureJsonData,
+    }, {});
+
+    this.backendSrv.post(`/api/org/plugins/${this.pluginId}/settings`, updateCmd).then(function() {
+      window.location.href = window.location.href;
+    });
+  }
+
+  toggleEnabled() {
+    this.update();
+  }
+
+  togglePinned() {
+    this.update();
+  }
+}
+
+angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl);
+

+ 17 - 0
public/app/features/plugins/list_ctrl.ts

@@ -0,0 +1,17 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+
+export class PluginListCtrl {
+  plugins: any[];
+
+  /** @ngInject */
+  constructor(private backendSrv: any) {
+
+    this.backendSrv.get('api/org/plugins').then(plugins => {
+      this.plugins = plugins;
+    });
+  }
+}
+
+angular.module('grafana.controllers').controller('PluginListCtrl', PluginListCtrl);

+ 0 - 0
public/app/features/apps/page_ctrl.ts → public/app/features/plugins/page_ctrl.ts


+ 185 - 0
public/app/features/plugins/partials/edit.html

@@ -0,0 +1,185 @@
+<navbar title="Plugins" title-url="plugins" icon="icon-gf icon-gf-apps">
+	<a href="plugins/apps" class="navbar-page-btn">
+		<i class="fa fa-chevron-right"></i>
+		Apps
+	</a>
+</navbar>
+
+<div class="page-container">
+  <div class="plugin-header">
+		<span ng-show="ctrl.model.info.logos.large" class="plugin-header-logo">
+			<img src="{{ctrl.model.info.logos.large}}">
+		</span>
+		<div class="plugin-header-info-block">
+			<h1 class="plugin-header-name">{{ctrl.model.name}}</h1>
+			<div class="plugin-header-author">By {{ctrl.model.info.author.name}}</div>
+			<div class="plugin-header-stamps">
+				<span class="plugin-header-stamps-type">
+					<i class="icon-gf icon-gf-apps"></i> {{ctrl.model.type}}
+				</span>
+			</div>
+		</div>
+	</div>
+
+	<ul class="nav nav-tabs nav-tabs-alt">
+		<li ng-repeat="tab in ::['Overview', 'Details', 'Config']" ng-class="{active: ctrl.tabIndex === $index}">
+			<a ng-click="ctrl.tabIndex= $index">
+				{{::tab}}
+			</a>
+		</li>
+	</ul>
+
+	<div class="page-body">
+		<div class="tab-content page-content-with-sidebar" ng-if="ctrl.tabIndex === 0">
+			README.md
+		</div>
+
+		<div class="tab-content page-content-with-sidebar" ng-if="ctrl.tabIndex === 1">
+			Details
+		</div>
+
+		<div class="tab-content page-content-with-sidebar" ng-if="ctrl.tabIndex === 2">
+			<div class="gf-form-inline">
+				<div class="gf-form">
+					<editor-checkbox text="Enabled" model="ctrl.model.enabled" change="ctrl.toggleEnabled()"></editor-checkbox>
+				</div>
+				<div class="gf-form">
+					<editor-checkbox text="Pinned" model="ctrl.model.pinned" change="ctrl.togglePinned()"></editor-checkbox>
+				</div>
+			</div>
+
+			<div ng-if="ctrl.model.pluginId">
+				<plugin-component type="app-config-ctrl"></plugin-component>
+				<div class="clearfix"></div>
+				<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
+			</div>
+
+		</div>
+
+		<aside class="page-sidebar">
+			<section class="page-sidebar-section">
+				<h4>Version</h4>
+				<span>1.0.1</span>
+			</section>
+			<section class="page-sidebar-section" ng-show="ctrl.model.type === 'app'">
+				<h5>Includes</h4>
+				<ul class="ui-list">
+					<li ng-show="!ctrl.includedPanels.length"><em>None</em></li>
+					<li ng-repeat="panel in ctrl.includedPanels">
+						{{panel.name}}
+					</li>
+					<li ng-repeat="ds in ctrl.includedDatasources">
+						{{ds.name}}
+					</li>
+					<li ng-repeat="page in ctrl.model.pages">
+						<a href="apps/{{ctrl.appId}}/page/{{page.slug}}" class="external-link">{{page.name}}</a>
+					</li>
+				</ul>
+			</section>
+			<section class="page-sidebar-section">
+				<h5>Dependencies</h4>
+				<span>TODO</span>
+			</section>
+			<section class="page-sidebar-section">
+				<h5>Links</h4>
+				<ul class="ui-list">
+					<li ng-repeat="link in ctrl.model.info.links">
+						<a href="{{link.url}}" class="external-link" target="_blank">{{link.name}}</a>
+					</li>
+				</ul>
+			</section>
+		</aside>
+	</div>
+</div>
+</div>
+
+<!-- 	<div class="app&#45;edit&#45;description"> -->
+	<!-- 		{{ctrl.model.info.description}}<br> -->
+	<!-- 		<span style="small"> -->
+		<!-- 			Version: {{ctrl.model.info.version}} &#38;nbsp; &#38;nbsp; Updated: {{ctrl.model.info.updated}} -->
+		<!-- 		</span> -->
+	<!-- 	</div> -->
+<!--  -->
+<!-- </div> -->
+		<!-- <div class="flex&#45;column"> -->
+			<!-- 	<ul class="app&#45;edit&#45;links"> -->
+				<!-- 		<li> -->
+					<!-- 			By <a href="{{ctrl.model.info.author.url}}" class="external&#45;link" target="_blank">{{ctrl.model.info.author.name}}</a> -->
+					<!-- 		</li> -->
+				<!-- 		<li ng&#45;repeat="link in ctrl.model.info.links"> -->
+					<!-- 			<a href="{{link.url}}" class="external&#45;link" target="_blank">{{link.name}}</a> -->
+					<!-- 		</li> -->
+				<!-- 	</ul> -->
+			<!-- </div> -->
+
+		<!-- <section class="simple&#45;box"> -->
+			<!-- 	<h3 class="simple&#45;box&#45;header">Included with app:</h3> -->
+			<!-- 	<div class="flex&#45;container"> -->
+				<!-- 		<div class="simple&#45;box&#45;body simple&#45;box&#45;column"> -->
+					<!-- 			<div class="simple&#45;box&#45;column&#45;header"> -->
+						<!-- 				<i class="fa fa&#45;th&#45;large"></i> -->
+						<!-- 				Dashboards -->
+						<!-- 			</div> -->
+					<!-- 			<ul> -->
+						<!-- 				<li><em class="small">None</em></li> -->
+						<!-- 			</ul> -->
+					<!-- 		</div> -->
+				<!-- 		<div class="simple&#45;box&#45;body simple&#45;box&#45;column"> -->
+					<!-- 			<div class="simple&#45;box&#45;column&#45;header"> -->
+						<!-- 				<i class="fa fa&#45;line&#45;chart"></i> -->
+						<!-- 				Panels -->
+						<!-- 			</div> -->
+					<!-- 			<ul> -->
+						<!-- 				<li ng&#45;show="!ctrl.includedPanels.length"><em class="small">None</em></li> -->
+						<!-- 				<li ng&#45;repeat="panel in ctrl.includedPanels"> -->
+							<!-- 					{{panel.name}} -->
+							<!-- 				</li> -->
+						<!-- 			</ul> -->
+					<!-- 		</div> -->
+				<!-- 		<div class="simple&#45;box&#45;body simple&#45;box&#45;column"> -->
+					<!-- 			<div class="simple&#45;box&#45;column&#45;header"> -->
+						<!-- 				<i class="fa fa&#45;database"></i> -->
+						<!-- 				Datasources -->
+						<!-- 			</div> -->
+					<!-- 			<ul> -->
+						<!-- 				<li ng&#45;show="!ctrl.includedDatasources.length"><em class="small">None</em></li> -->
+						<!-- 				<li ng&#45;repeat="ds in ctrl.includedDatasources"> -->
+							<!-- 					{{ds.name}} -->
+							<!-- 				</li> -->
+						<!-- 			</ul> -->
+					<!-- 		</div> -->
+				<!-- 		<div class="simple&#45;box&#45;body simple&#45;box&#45;column"> -->
+					<!-- 			<div class="simple&#45;box&#45;column&#45;header"> -->
+						<!-- 				<i class="fa fa&#45;files&#45;o"></i> -->
+						<!-- 				Pages -->
+						<!-- 			</div> -->
+					<!-- 			<ul> -->
+						<!-- 				<li ng&#45;repeat="page in ctrl.model.pages"> -->
+							<!-- 					<a href="apps/{{ctrl.appId}}/page/{{page.slug}}" class="external&#45;link">{{page.name}}</a> -->
+							<!-- 				</li> -->
+						<!-- 			</ul> -->
+					<!-- 		</div> -->
+				<!--  -->
+				<!-- 	</div> -->
+			<!-- </section> -->
+		<!--  -->
+		<!-- <section class="simple&#45;box"> -->
+			<!-- 	<h3 class="simple&#45;box&#45;header">Dependencies:</h3> -->
+			<!-- 	<div class="simple&#45;box&#45;body"> -->
+				<!-- 		Grafana 2.6.x -->
+				<!-- 	</div> -->
+			<!-- </section> -->
+		<!--  -->
+		<!-- <section class="simple&#45;box"> -->
+			<!-- 	<h3 class="simple&#45;box&#45;header">Configuration:</h3> -->
+			<!-- 	<div class="simple&#45;box&#45;body"> -->
+				<!-- 		<div ng&#45;if="ctrl.model.appId"> -->
+					<!-- 			<plugin&#45;component type="app&#45;config&#45;ctrl"></plugin&#45;component> -->
+					<!-- 			<div class="clearfix"></div> -->
+					<!-- 			<button type="submit" class="btn btn&#45;success" ng&#45;click="ctrl.update()">Save</button> -->
+					<!-- 		</div> -->
+				<!-- 	</div> -->
+			<!-- </section> -->
+		<!--  -->
+		<!--  -->
+		<!-- </div> -->

+ 42 - 0
public/app/features/plugins/partials/list.html

@@ -0,0 +1,42 @@
+<navbar title="Plugins" icon="icon-gf icon-gf-apps" title-url="plugins">
+</navbar>
+
+<div class="page-container">
+  <div class="page-header">
+    <h1>Plugins</h1>
+	</div>
+
+	<table class="filter-table">
+		<thead>
+			<tr>
+				<th><strong>Name</strong></th>
+				<th><strong>Type</strong></th>
+				<th style="width: 60px;"></th>
+				<th style="width: 80px;"></th>
+			</tr>
+		</thead>
+		<tbody>
+			<tr ng-repeat="plugin in ctrl.plugins">
+				<td>
+					<a href="plugins/{{plugin.pluginId}}/edit">
+						{{plugin.name}}
+					</a>
+				</td>
+				<td>
+					{{plugin.type}}
+				</td>
+				<td>
+					<span class="label label-info" ng-if="plugin.enabled">Enabled</span>
+					<span class="label label-info" ng-if="plugin.pinned">Pinned</span>
+				</td>
+				<td class="text-right">
+					<a href="plugins/{{plugin.pluginId}}/edit" class="btn btn-inverse btn-small">
+							<i class="fa fa-edit"></i>
+							Edit
+						</a>
+					</td>
+				</tr>
+			</tbody>
+		</table>
+	</div>
+</div>

+ 0 - 0
public/app/features/apps/partials/page.html → public/app/features/plugins/partials/page.html


+ 3 - 2
public/sass/_grafana.scss

@@ -26,6 +26,7 @@
 @import "utils/widths";
 
 // LAYOUTS
+@import "layout/lists";
 @import "layout/page";
 
 // COMPONENTS
@@ -44,7 +45,6 @@
 @import "components/tagsinput";
 @import "components/tables_lists";
 @import "components/search";
-@import "components/dashboard";
 @import "components/tightform";
 @import "components/gf-form";
 @import "components/sidemenu";
@@ -69,10 +69,11 @@
 
 // PAGES
 @import "pages/login";
+@import "pages/dashboard";
 @import "pages/playlist";
 @import "pages/admin";
 @import "pages/alerting";
-@import "pages/apps";
+@import "pages/plugins";
 @import "pages/signup";
 @import "pages/styleguide";
 

+ 4 - 2
public/sass/_variables.dark.scss

@@ -50,8 +50,10 @@ $critical:              #ed2e18;
 // -------------------------
 $body-bg:  			   rgb(20,20,20);
 $page-bg:  			   $dark-2;
-$body-color:   		   $gray-4;
-$text-color:   	       $gray-4;
+$body-color:   		 $gray-4;
+$text-color:   	   $gray-4;
+$text-color-strong: $white;
+$text-color-weak: $gray-2;
 
 // gradients
 $brand-gradient: linear-gradient(to right, rgba(255,213,0,0.7) 0%, rgba(255,68,0,0.7) 99%, rgba(255,68,0,0.7) 100%);

+ 2 - 0
public/sass/_variables.light.scss

@@ -58,6 +58,8 @@ $body-bg:  		  $white;
 $page-bg:  		  $white;
 $body-color:    $gray-1;
 $text-color:    $gray-1;
+$text-color-strong: $white;
+$text-color-weak: $gray-1;
 
 // gradients
 $brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 0%, rgba(255,68,0,1.0) 99%, rgba(255,68,0,1.0) 100%);

+ 6 - 1
public/sass/_variables.scss

@@ -93,7 +93,7 @@ $font-size-sm:   .875rem !default;
 $font-size-xs:   .75rem !default;
 
 $line-height-base: 1.5 !default;
-$font-weight-semi-bold:       600;
+$font-weight-semi-bold: 600;
 
 $font-size-h1: 2.0rem !default;
 $font-size-h2: 1.75rem !default;
@@ -141,6 +141,11 @@ $border-radius-sm:       0.1rem !default;
 $caret-width:            .3em !default;
 $caret-width-lg:         $caret-width !default;
 
+// Page
+
+$page-sidebar-width: 10rem;
+$page-sidebar-margin: 5rem;
+
 // Links
 // -------------------------
 $link-decoration:         none !default;

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

@@ -65,7 +65,7 @@ h1, h2, h3, h4, h5, h6,
   color: $headings-color;
 }
 
-h1, .h1 { font-size: $font-size-h1; }
+h1, .h1 { font-size: $font-size-h1; font-style: italic; }
 h2, .h2 { font-size: $font-size-h2; }
 h3, .h3 { font-size: $font-size-h3; }
 h4, .h4 { font-size: $font-size-h4; }

+ 1 - 6
public/sass/components/_sidemenu.scss

@@ -73,7 +73,7 @@
         // again by the mouse getting outside the hover space
         left: $side-menu-width - 0.2rem;
         background-color: rgba($side-menu-bg,$side-menu-opacity);
-        @include animation('dropdown-anim 100ms ease-in-out 100ms forwards');
+        @include animation('dropdown-anim 150ms ease-in-out 100ms forwards');
         z-index: -9999;
       }
     }
@@ -82,11 +82,6 @@
 
 @include keyframes(dropdown-anim) {
   0% {
-    display: none;
-    opacity: 0;
-  }
-  1% {
-    display: block;
     opacity: 0;
     transform: translate3d(-5%,0,0);
   }

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

@@ -13,7 +13,7 @@
     @include border-radius(3px);
 	  border: 1px solid $divider-border-color;
 	  background-color: transparent;
-	  border-bottom: 1px solid $panel-bg;
+	  border-bottom: 1px solid $page-bg;
 	  color: $link-color;
 	}
 

+ 14 - 0
public/sass/layout/_lists.scss

@@ -0,0 +1,14 @@
+
+.ui-list {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+
+  > li {
+    margin-bottom: 0.3125rem;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}

+ 39 - 13
public/sass/layout/_page.scss

@@ -3,21 +3,10 @@
   height: 100%;
 }
 
-.dashboard-container {
-  padding: $dashboard-padding;
-  width: 100%;
-}
-
 .main-view {
   height: 100%;
 }
 
-.page-dashboard {
-  .main-view {
-    background-image: none;
-  }
-}
-
 .page-container {
   background-color: $page-bg;
   padding: ($spacer * 2) ($spacer * 4);
@@ -27,8 +16,29 @@
   background-image: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%)
 }
 
+.page-body {
+  @include media-breakpoint-up(md) {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+  }
+}
+
+.page-content-with-sidebar {
+  width: calc(100% - #{$page-sidebar-width + $page-sidebar-margin}); // sidebar width + margin
+}
+
+.page-sidebar {
+  @include media-breakpoint-up(md) {
+    width: $page-sidebar-width;
+    margin-left: $page-sidebar-margin;
+  }
+}
+
 .page-header {
   padding: $spacer 0 $spacer/2 0;
+  margin-bottom: 2rem;
+
   display: flex;
   justify-content: flex-end;
   align-items: flex-end;
@@ -36,13 +46,12 @@
   @include brand-bottom-border();
 
   h1 {
-    font-style: italic;
     flex-grow: 1;
   }
+
   button, a {
     margin-left: $spacer;
   }
-  margin-bottom: 2rem;
 }
 
 .page-heading {
@@ -71,3 +80,20 @@
   }
 }
 
+.page-sidebar {
+  color: $text-color-weak;
+  h4 {
+    font-size: $font-size-base;
+    font-weight: $font-weight-semi-bold;
+    color: $text-color-strong;
+  }
+  h5 {
+    font-size: $font-size-base;
+    color: $text-color-weak;
+    font-weight: $font-weight-semi-bold;
+  }
+}
+
+.page-sidebar-section {
+  margin-bottom: $spacer*2;
+}

+ 0 - 26
public/sass/pages/_apps.scss

@@ -1,26 +0,0 @@
-
-.app-edit-logo-box {
-  padding: 1.2rem;
-  background: $panel-bg;
-  text-align: center;
-  img {
-    max-width: 7rem;
-  }
-  margin-right: 2rem;
-}
-
-.app-edit-links {
-  list-style: none;
-  margin: 0 0 0 2rem;
-
-  li {
-    background: $panel-bg;
-    margin-top: 4px;
-    padding: 0.2rem 1rem;
-  }
-}
-
-.app-edit-description {
-  font-style: italic;
-  margin-bottom: 1.5rem;
-}

+ 11 - 0
public/sass/components/_dashboard.scss → public/sass/pages/_dashboard.scss

@@ -1,3 +1,14 @@
+.dashboard-container {
+  padding: $dashboard-padding;
+  width: 100%;
+}
+
+.page-dashboard {
+  .main-view {
+    background-image: none;
+  }
+}
+
 .template-variable {
   color: $variable;
 }

+ 53 - 0
public/sass/pages/_plugins.scss

@@ -0,0 +1,53 @@
+.plugin-header {
+  @include clearfix();
+
+  padding: $spacer 0 $spacer/2 0;
+  margin-bottom: 2rem;
+}
+
+.plugin-header-logo {
+  float: left;
+  width: 7rem;
+  img {
+    width: 7rem;
+  }
+  margin-right: $spacer;
+}
+
+.plugin-header-info-block {
+  padding-top: $spacer;
+}
+
+.plugin-header-author {
+}
+
+.plugin-header-stamps-type {
+  color: $link-color-disabled;
+  text-transform: uppercase;
+}
+
+// .app-edit-logo-box {
+//   padding: 1.2rem;
+//   background: $panel-bg;
+//   text-align: center;
+//   img {
+//     max-width: 7rem;
+//   }
+//   margin-right: 2rem;
+// }
+//
+// .app-edit-links {
+//   list-style: none;
+//   margin: 0 0 0 2rem;
+//
+//   li {
+//     background: $panel-bg;
+//     margin-top: 4px;
+//     padding: 0.2rem 1rem;
+//   }
+// }
+//
+// .app-edit-description {
+//   font-style: italic;
+//   margin-bottom: 1.5rem;
+// }