Commit 4bb4d7f7 by Torkel Ödegaard

Merge branch 'v3.1.x'

parents 3f83bf6e 6c2a6100
......@@ -46,7 +46,7 @@ Then you can override them using:
## instance_name
Set the name of the grafana-server instance. Used in logging and internal metrics and in
clustering info. Defaults to: `${HOSTNAME}, which will be replaced with
clustering info. Defaults to: `${HOSTNAME}`, which will be replaced with
environment variable `HOSTNAME`, if that is empty or does not exist Grafana will try to use
system calls to get the machine name.
......
......@@ -10,20 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
Description | Download
------------ | -------------
Stable .deb for Debian-based Linux | [3.0.4](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.4-1464167696_amd64.deb)
Beta .deb for Debian-based Linux | [3.1.0-beta1](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1466666977beta1_amd64.deb)
Stable .deb for Debian-based Linux | [3.1.0](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1468321182_amd64.deb)
## Install Stable
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.4-1464167696_amd64.deb
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1468321182_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_3.0.4-1464167696_amd64.deb
## Install 3.1 beta
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1466666977beta1_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_3.1.0-1466666977beta1_amd64.deb
$ sudo dpkg -i grafana_3.1.0-1468321182_amd64.deb
## APT Repository
......
......@@ -10,42 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
Description | Download
------------ | -------------
Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.0.4 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.4-1464167696.x86_64.rpm)
Beta .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.0-beta1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm)
Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.0 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1468321182.x86_64.rpm)
## Install Latest Stable
You can install Grafana using Yum directly.
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.4-1464167696.x86_64.rpm
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1468321182.x86_64.rpm
Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-3.0.4-1464167696.x86_64.rpm
$ sudo rpm -Uvh grafana-3.1.0-1468321182.x86_64.rpm
#### On OpenSuse:
$ sudo rpm -i --nodeps grafana-3.0.4-1464167696.x86_64.rpm
## Install 3.1 Beta
You can install Grafana using Yum directly.
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm
Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm
#### On OpenSuse:
$ sudo rpm -i --nodeps https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm
$ sudo rpm -i --nodeps grafana-3.1.0-1468321182.x86_64.rpm
## Install via YUM Repository
......
......@@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
Description | Download
------------ | -------------
Stable Zip package for Windows | [grafana.3.0.4.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.0.4.windows-x64.zip)
Stable Zip package for Windows | [grafana.3.1.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.1.0.windows-x64.zip)
## Configure
......
......@@ -53,7 +53,7 @@
"phantomjs-prebuilt": "^2.1.7",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.4",
"sass-lint": "^1.6.0",
"sass-lint": "^1.7.0",
"systemjs": "0.19.24"
},
"engines": {
......@@ -69,7 +69,7 @@
"dependencies": {
"eventemitter3": "^1.2.0",
"grunt-jscs": "~1.5.x",
"grunt-sass-lint": "^0.1.0",
"grunt-sass-lint": "^0.2.0",
"grunt-sync": "^0.4.1",
"karma-sinon": "^1.0.3",
"lodash": "^2.4.1",
......
......@@ -211,7 +211,7 @@ func Register(r *macaron.Macaron) {
// Dashboard
r.Group("/dashboards", func() {
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard)
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
r.Get("/file/:file", GetDashboardFromJsonFile)
r.Get("/home", wrap(GetHomeDashboard))
r.Get("/tags", GetDashboardTags)
......
......@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
......@@ -109,7 +110,7 @@ func DeleteDashboard(c *middleware.Context) {
c.JSON(200, resp)
}
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
cmd.OrgId = c.OrgId
if !c.IsSignedIn {
......@@ -122,35 +123,37 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
if dash.Id == 0 {
limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil {
c.JsonApiErr(500, "failed to get quota", err)
return
return ApiError(500, "failed to get quota", err)
}
if limitReached {
c.JsonApiErr(403, "Quota reached", nil)
return
return ApiError(403, "Quota reached", nil)
}
}
err := bus.Dispatch(&cmd)
if err != nil {
if err == m.ErrDashboardWithSameNameExists {
c.JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()})
return
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
}
if err == m.ErrDashboardVersionMismatch {
c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
return
return Json(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
}
if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok {
message := "The dashboard belongs to plugin " + pluginErr.PluginId + "."
// look up plugin name
if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist {
message = "The dashboard belongs to plugin " + pluginDef.Name + "."
}
return Json(412, util.DynMap{"status": "plugin-dashboard", "message": message})
}
if err == m.ErrDashboardNotFound {
c.JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
return
return Json(404, util.DynMap{"status": "not-found", "message": err.Error()})
}
c.JsonApiErr(500, "Failed to save dashboard", err)
return
return ApiError(500, "Failed to save dashboard", err)
}
c.TimeRequest(metrics.M_Api_Dashboard_Save)
c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
}
func canEditDashboard(role m.RoleType) bool {
......
......@@ -18,7 +18,7 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
return result, err
}
for _, item := range *dashboardQuery.Result {
for _, item := range dashboardQuery.Result {
result = append(result, m.PlaylistDashboardDto{
Id: item.Id,
Slug: item.Slug,
......
......@@ -17,6 +17,14 @@ var (
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
)
type UpdatePluginDashboardError struct {
PluginId string
}
func (d UpdatePluginDashboardError) Error() string {
return "Dashboard belong to plugin"
}
var (
DashTypeJson = "file"
DashTypeDB = "db"
......@@ -31,6 +39,7 @@ type Dashboard struct {
OrgId int64
GnetId int64
Version int
PluginId string
Created time.Time
Updated time.Time
......@@ -95,6 +104,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash.UpdatedBy = cmd.UserId
dash.OrgId = cmd.OrgId
dash.PluginId = cmd.PluginId
dash.UpdateSlug()
return dash
}
......@@ -119,6 +129,7 @@ type SaveDashboardCommand struct {
UserId int64 `json:"userId"`
OrgId int64 `json:"-"`
Overwrite bool `json:"overwrite"`
PluginId string `json:"-"`
Result *Dashboard
}
......@@ -151,7 +162,13 @@ type GetDashboardTagsQuery struct {
type GetDashboardsQuery struct {
DashboardIds []int64
Result *[]Dashboard
Result []*Dashboard
}
type GetDashboardsByPluginIdQuery struct {
OrgId int64
PluginId string
Result []*Dashboard
}
type GetDashboardSlugByIdQuery struct {
......
......@@ -20,6 +20,7 @@ type PluginSetting struct {
Pinned bool
JsonData map[string]interface{}
SecureJsonData SecureJsonData
PluginVersion string
Created time.Time
Updated time.Time
......@@ -44,11 +45,19 @@ type UpdatePluginSettingCmd struct {
Pinned bool `json:"pinned"`
JsonData map[string]interface{} `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
PluginVersion string `json:"version"`
PluginId string `json:"-"`
OrgId int64 `json:"-"`
}
// specific command, will only update version
type UpdatePluginSettingVersionCmd struct {
PluginVersion string
PluginId string `json:"-"`
OrgId int64 `json:"-"`
}
func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData {
encrypted := make(SecureJsonData)
for key, data := range cmd.SecureJsonData {
......@@ -69,6 +78,7 @@ type PluginSettingInfoDTO struct {
PluginId string
Enabled bool
Pinned bool
PluginVersion string
}
type GetPluginSettingByIdQuery struct {
......@@ -76,3 +86,9 @@ type GetPluginSettingByIdQuery struct {
OrgId int64
Result *PluginSetting
}
type PluginStateChangedEvent struct {
PluginId string
OrgId int64
Enabled bool
}
......@@ -68,6 +68,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
OrgId: cmd.OrgId,
UserId: cmd.UserId,
Overwrite: cmd.Overwrite,
PluginId: cmd.PluginId,
}
if err := bus.Dispatch(&saveCmd); err != nil {
......
......@@ -14,10 +14,12 @@ type PluginDashboardInfoDTO struct {
Title string `json:"title"`
Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"`
Slug string `json:"slug"`
ImportedRevision int64 `json:"importedRevision"`
Revision int64 `json:"revision"`
Description string `json:"description"`
Path string `json:"path"`
Removed bool `json:"removed"`
}
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
......@@ -29,13 +31,52 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
result := make([]*PluginDashboardInfoDTO, 0)
// load current dashboards
query := m.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
existingMatches := make(map[int64]bool)
for _, include := range plugin.Includes {
if include.Type == PluginTypeDashboard {
if dashInfo, err := getDashboardImportStatus(orgId, plugin, include.Path); err != nil {
if include.Type != PluginTypeDashboard {
continue
}
res := &PluginDashboardInfoDTO{}
var dashboard *m.Dashboard
var err error
if dashboard, err = loadPluginDashboard(plugin.Id, include.Path); err != nil {
return nil, err
} else {
result = append(result, dashInfo)
}
res.Path = include.Path
res.PluginId = plugin.Id
res.Title = dashboard.Title
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
// find existing dashboard
for _, existingDash := range query.Result {
if existingDash.Slug == dashboard.Slug {
res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
existingMatches[existingDash.Id] = true
}
}
result = append(result, res)
}
// find deleted dashboards
for _, dash := range query.Result {
if _, exists := existingMatches[dash.Id]; !exists {
result = append(result, &PluginDashboardInfoDTO{
Slug: dash.Slug,
Removed: true,
})
}
}
......@@ -64,33 +105,3 @@ func loadPluginDashboard(pluginId, path string) (*m.Dashboard, error) {
return m.NewDashboardFromJson(data), nil
}
func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*PluginDashboardInfoDTO, error) {
res := &PluginDashboardInfoDTO{}
var dashboard *m.Dashboard
var err error
if dashboard, err = loadPluginDashboard(plugin.Id, path); err != nil {
return nil, err
}
res.Path = path
res.PluginId = plugin.Id
res.Title = dashboard.Title
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug}
if err := bus.Dispatch(&query); err != nil {
if err != m.ErrDashboardNotFound {
return nil, err
}
} else {
res.Imported = true
res.ImportedUri = "db/" + query.Result.Slug
res.ImportedRevision = query.Result.Data.Get("revision").MustInt64(1)
}
return res, nil
}
......@@ -4,6 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
......@@ -31,6 +32,17 @@ func TestPluginDashboards(t *testing.T) {
return m.ErrDashboardNotFound
})
bus.AddHandler("test", func(query *m.GetDashboardsByPluginIdQuery) error {
var data = simplejson.New()
data.Set("title", "Nginx Connections")
data.Set("revision", 22)
query.Result = []*m.Dashboard{
{Slug: "nginx-connections", Data: data},
}
return nil
})
dashboards, err := GetPluginDashboards(1, "test-app")
So(err, ShouldBeNil)
......@@ -41,12 +53,12 @@ func TestPluginDashboards(t *testing.T) {
Convey("should include installed version info", func() {
So(dashboards[0].Title, ShouldEqual, "Nginx Connections")
//So(dashboards[0].Revision, ShouldEqual, "1.5")
//So(dashboards[0].InstalledRevision, ShouldEqual, "1.1")
//So(dashboards[0].InstalledUri, ShouldEqual, "db/nginx-connections")
So(dashboards[0].Revision, ShouldEqual, 25)
So(dashboards[0].ImportedRevision, ShouldEqual, 22)
So(dashboards[0].ImportedUri, ShouldEqual, "db/nginx-connections")
//So(dashboards[1].Revision, ShouldEqual, "2.0")
//So(dashboards[1].InstalledRevision, ShouldEqual, "")
So(dashboards[1].Revision, ShouldEqual, 2)
So(dashboards[1].ImportedRevision, ShouldEqual, 0)
})
})
......
package plugins
import (
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddEventListener(handlePluginStateChanged)
}
func updateAppDashboards() {
time.Sleep(time.Second * 5)
plog.Debug("Looking for App Dashboard Updates")
query := m.GetPluginSettingsQuery{OrgId: 0}
if err := bus.Dispatch(&query); err != nil {
plog.Error("Failed to get all plugin settings", "error", err)
return
}
for _, pluginSetting := range query.Result {
// ignore disabled plugins
if !pluginSetting.Enabled {
continue
}
if pluginDef, exist := Plugins[pluginSetting.PluginId]; exist {
if pluginDef.Info.Version != pluginSetting.PluginVersion {
syncPluginDashboards(pluginDef, pluginSetting.OrgId)
}
}
}
}
func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) error {
if dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path); err != nil {
return err
} else {
plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
updateCmd := ImportDashboardCommand{
OrgId: orgId,
PluginId: pluginDashInfo.PluginId,
Overwrite: true,
Dashboard: dash.Data,
UserId: 0,
Path: pluginDashInfo.Path,
}
if err := bus.Dispatch(&updateCmd); err != nil {
return err
}
}
return nil
}
func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
plog.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.Id)
// Get plugin dashboards
dashboards, err := GetPluginDashboards(orgId, pluginDef.Id)
if err != nil {
plog.Error("Failed to load app dashboards", "error", err)
return
}
// Update dashboards with updated revisions
for _, dash := range dashboards {
// remove removed ones
if dash.Removed {
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug}
if err := bus.Dispatch(&deleteCmd); err != nil {
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
return
}
continue
}
// update updated ones
if dash.ImportedRevision != dash.Revision {
if err := autoUpdateAppDashboard(dash, orgId); err != nil {
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
return
}
}
}
// update version in plugin_setting table to mark that we have processed the update
query := m.GetPluginSettingByIdQuery{PluginId: pluginDef.Id, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
plog.Error("Failed to read plugin setting by id", "error", err)
return
}
appSetting := query.Result
cmd := m.UpdatePluginSettingVersionCmd{
OrgId: appSetting.OrgId,
PluginId: appSetting.PluginId,
PluginVersion: pluginDef.Info.Version,
}
if err := bus.Dispatch(&cmd); err != nil {
plog.Error("Failed to update plugin setting version", "error", err)
}
}
func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
plog.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
if event.Enabled {
syncPluginDashboards(Plugins[event.PluginId], event.OrgId)
} else {
query := m.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
if err := bus.Dispatch(&query); err != nil {
return err
} else {
for _, dash := range query.Result {
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug}
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
if err := bus.Dispatch(&deleteCmd); err != nil {
return err
}
}
}
}
return nil
}
......@@ -77,6 +77,8 @@ func Init() error {
}
go StartPluginUpdateChecker()
go updateAppDashboards()
return nil
}
......
......@@ -19,6 +19,7 @@ func init() {
bus.AddHandler("sql", SearchDashboards)
bus.AddHandler("sql", GetDashboardTags)
bus.AddHandler("sql", GetDashboardSlugById)
bus.AddHandler("sql", GetDashboardsByPluginId)
}
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
......@@ -45,6 +46,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
}
sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle)
......@@ -245,10 +251,23 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
return m.ErrCommandValidationFailed
}
var dashboards = make([]m.Dashboard, 0)
var dashboards = make([]*m.Dashboard, 0)
err := x.In("id", query.DashboardIds).Find(&dashboards)
query.Result = &dashboards
query.Result = dashboards
if err != nil {
return err
}
return nil
}
func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
var dashboards = make([]*m.Dashboard, 0)
err := x.Where("org_id=? AND plugin_id=?", query.OrgId, query.PluginId).Find(&dashboards)
query.Result = dashboards
if err != nil {
return err
......
......@@ -111,4 +111,13 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Add index for gnetId in dashboard", NewAddIndexMigration(dashboardV2, &Index{
Cols: []string{"gnet_id"}, Type: IndexType,
}))
// add column to store plugin_id
mg.AddMigration("Add column plugin_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 255,
}))
mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{
Cols: []string{"org_id", "plugin_id"}, Type: IndexType,
}))
}
......@@ -26,4 +26,10 @@ func addAppSettingsMigration(mg *Migrator) {
//------- indexes ------------------
addTableIndicesMigrations(mg, "v1", pluginSettingTable)
// add column to store installed version
mg.AddMigration("Add column plugin_version to plugin_settings", NewAddColumnMigration(pluginSettingTable, &Column{
Name: "plugin_version", Type: DB_NVarchar, Nullable: true, Length: 50,
}))
}
......@@ -13,14 +13,20 @@ func init() {
bus.AddHandler("sql", GetPluginSettings)
bus.AddHandler("sql", GetPluginSettingById)
bus.AddHandler("sql", UpdatePluginSetting)
bus.AddHandler("sql", UpdatePluginSettingVersion)
}
func GetPluginSettings(query *m.GetPluginSettingsQuery) error {
sql := `SELECT org_id, plugin_id, enabled, pinned
FROM plugin_setting
WHERE org_id=?`
sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version
FROM plugin_setting `
params := make([]interface{}, 0)
sess := x.Sql(sql, query.OrgId)
if query.OrgId != 0 {
sql += "WHERE org_id=?"
params = append(params, query.OrgId)
}
sess := x.Sql(sql, params...)
query.Result = make([]*m.PluginSettingInfoDTO, 0)
return sess.Find(&query.Result)
}
......@@ -51,22 +57,52 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
Enabled: cmd.Enabled,
Pinned: cmd.Pinned,
JsonData: cmd.JsonData,
PluginVersion: cmd.PluginVersion,
SecureJsonData: cmd.GetEncryptedJsonData(),
Created: time.Now(),
Updated: time.Now(),
}
// add state change event on commit success
sess.events = append(sess.events, &m.PluginStateChangedEvent{
PluginId: cmd.PluginId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
})
_, err = sess.Insert(&pluginSetting)
return err
} else {
for key, data := range cmd.SecureJsonData {
pluginSetting.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
// add state change event on commit success
if pluginSetting.Enabled != cmd.Enabled {
sess.events = append(sess.events, &m.PluginStateChangedEvent{
PluginId: cmd.PluginId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
})
}
pluginSetting.Updated = time.Now()
pluginSetting.Enabled = cmd.Enabled
pluginSetting.JsonData = cmd.JsonData
pluginSetting.Pinned = cmd.Pinned
pluginSetting.PluginVersion = cmd.PluginVersion
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
return err
}
})
}
func UpdatePluginSettingVersion(cmd *m.UpdatePluginSettingVersionCmd) error {
return inTransaction2(func(sess *session) error {
_, err := sess.Exec("UPDATE plugin_setting SET plugin_version=? WHERE org_id=? AND plugin_id=?", cmd.PluginVersion, cmd.OrgId, cmd.PluginId)
return err
})
}
......@@ -72,6 +72,10 @@
Import
</a>
<a class="pull-right search-button-row-explore-link" target="_blank" href="https://grafana.net/dashboards?utm_source=grafana_search">
Find dashboards on
</a>
<div class="clearfix"></div>
</div>
</div>
......@@ -73,6 +73,8 @@ export class AlertSrv {
scope.text = payload.text;
scope.text2 = payload.text2;
scope.onConfirm = payload.onConfirm;
scope.onAltAction = payload.onAltAction;
scope.altActionText = payload.altActionText;
scope.icon = payload.icon || "fa-check";
scope.yesText = payload.yesText || "Yes";
scope.noText = payload.noText || "Cancel";
......
......@@ -22,6 +22,7 @@ function (angular, $, _, moment) {
this.id = data.id || null;
this.title = data.title || 'No Title';
this.autoUpdate = data.autoUpdate;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || "dark";
......
......@@ -134,6 +134,25 @@ export class DashNavCtrl {
}
});
}
if (err.data && err.data.status === "plugin-dashboard") {
err.isHandled = true;
$scope.appEvent('confirm-modal', {
title: 'Plugin Dashboard',
text: err.data.message,
text2: 'Your changes will be lost when you update the plugin. Use Save As to create custom version.',
yesText: "Overwrite",
icon: "fa-warning",
altActionText: "Save As",
onAltAction: function() {
$scope.saveDashboardAs();
},
onConfirm: function() {
$scope.saveDashboard({overwrite: true});
}
});
}
};
$scope.deleteDashboard = function() {
......
......@@ -12,6 +12,8 @@ function (angular) {
$scope.clone.id = null;
$scope.clone.editable = true;
$scope.clone.title = $scope.clone.title + " Copy";
// remove auto update
delete $scope.clone.autoUpdate;
};
function saveDashboard(options) {
......@@ -37,8 +39,9 @@ function (angular) {
err.isHandled = true;
$scope.appEvent('confirm-modal', {
title: 'Another dashboard with the same name exists',
text: "Would you still like to save this dashboard?",
title: 'Conflict',
text: 'Dashboard with the same name exists.',
text2: 'Would you still like to save this dashboard?',
yesText: "Save & Overwrite",
icon: "fa-warning",
onConfirm: function() {
......
......@@ -14,20 +14,19 @@
</span>
</td>
<td>
v{{dash.revision}}
<span ng-if="dash.installed">
&nbsp;(Imported v{{dash.importedRevision}})
<span ng-if="dash.imported" bs-tooltip='"Imported revision:" + dash.importedRevision'>
Revision: {{dash.revision}}
<span>
</td>
<td style="text-align: right">
<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
<button class="btn btn-secondary btn-small" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
Import
</button>
<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.imported">
<button class="btn btn-secondary btn-small" ng-click="ctrl.import(dash, true)" ng-show="dash.imported">
Update
</button>
<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
Delete
<button class="btn btn-danger btn-small" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
......
......@@ -5,6 +5,10 @@
<div class="page-header">
<h1>Plugins</h1>
<!-- <a class="btn btn&#45;inverse" href="https://grafana.net/plugins?utm_source=grafana_plugin_list" target="_blank"> -->
<!-- Explore plugins on Grafana.net -->
<!-- </a> -->
<div class="page-header-tabs">
<ul class="gf-tabs">
<li class="gf-tabs-item">
......@@ -23,6 +27,10 @@
</a>
</li>
</ul>
<a class="get-more-plugins-link" href="https://grafana.net/plugins?utm_source=grafana_plugin_list" target="_blank">
Find plugins on
</a>
</div>
</div>
......
......@@ -97,28 +97,7 @@ export class PluginEditCtrl {
}
importDashboards() {
// move to dashboards tab
this.tabIndex = 2;
return new Promise((resolve) => {
if (!this.$scope.$$phase) {
this.$scope.$digest();
}
// let angular load dashboards tab
setTimeout(() => {
resolve();
}, 1000);
}).then(() => {
return new Promise((resolve, reject) => {
// send event to import list component
appEvents.emit('dashboard-list-import-all', {
resolve: resolve,
reject: reject
});
});
});
return Promise.resolve();
}
setPreUpdateHook(callback: () => any) {
......
<div class="modal-body">
<div class="modal-body" ng-cloak>
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa {{icon}}"></i>
......@@ -24,6 +24,7 @@
<div class="confirm-modal-buttons">
<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
<button type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();">{{yesText}}</button>
<button ng-show="onAltAction" type="button" class="btn btn-success" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
</div>
</div>
......
......@@ -103,6 +103,11 @@
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
</li>
</ul>
<ul class="tight-form-list" ng-if="style.type === 'string'">
<li class="tight-form-item">
<editor-checkbox text="Sanitize HTML" model="style.sanitize" change="editor.render()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="style.type === 'number'">
......@@ -152,7 +157,7 @@
Decimals
</li>
<li style="width: 105px">
<input type="number" class="input-mini tight-form-input" ng-model="style.decimals" ng-change="render()" ng-model-onblur>
<input type="number" class="input-mini tight-form-input" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
......
......@@ -45,7 +45,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
};
/** @ngInject */
constructor($scope, $injector, private annotationsSrv) {
constructor($scope, $injector, private annotationsSrv, private $sanitize) {
super($scope, $injector);
this.pageIndex = 0;
......@@ -160,7 +160,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
}
function appendTableRows(tbodyElem) {
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc());
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc(), ctrl.$sanitize);
tbodyElem.empty();
tbodyElem.html(renderer.render(ctrl.pageIndex));
}
......
......@@ -8,7 +8,7 @@ export class TableRenderer {
formaters: any[];
colorState: any;
constructor(private panel, private table, private isUtc) {
constructor(private panel, private table, private isUtc, private sanitize) {
this.formaters = [];
this.colorState = {};
}
......@@ -24,7 +24,7 @@ export class TableRenderer {
return _.first(style.colors);
}
defaultCellFormater(v) {
defaultCellFormater(v, style) {
if (v === null || v === void 0 || v === undefined) {
return '';
}
......@@ -33,7 +33,11 @@ export class TableRenderer {
v = v.join(', ');
}
return v;
if (style && style.sanitize) {
return this.sanitize(v);
} else {
return _.escape(v);
}
}
createColumnFormater(style, column) {
......@@ -61,7 +65,7 @@ export class TableRenderer {
}
if (_.isString(v)) {
return v;
return this.defaultCellFormater(v, style);
}
if (style.colorMode) {
......@@ -72,7 +76,9 @@ export class TableRenderer {
};
}
return this.defaultCellFormater;
return (value) => {
return this.defaultCellFormater(value, style);
};
}
formatColumnValue(colIndex, value) {
......@@ -96,7 +102,6 @@ export class TableRenderer {
renderCell(columnIndex, value, addWidthHack = false) {
value = this.formatColumnValue(columnIndex, value);
value = _.escape(value);
var style = '';
if (this.colorState.cell) {
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
......
......@@ -13,6 +13,7 @@ describe('when rendering table', () => {
{text: 'Undefined'},
{text: 'String'},
{text: 'United', unit: 'bps'},
{text: 'Sanitized'},
];
var panel = {
......@@ -47,11 +48,20 @@ describe('when rendering table', () => {
type: 'number',
unit: 'ms',
decimals: 2,
},
{
pattern: 'Sanitized',
type: 'string',
sanitize: true,
}
]
};
var renderer = new TableRenderer(panel, table, 'utc');
var sanitize = function(value) {
return 'sanitized';
};
var renderer = new TableRenderer(panel, table, 'utc', sanitize);
it('time column should be formated', () => {
var html = renderer.renderCell(0, 1388556366666);
......@@ -107,6 +117,11 @@ describe('when rendering table', () => {
var html = renderer.renderCell(3, undefined);
expect(html).to.be('<td></td>');
});
it('sanitized value should render as', () => {
var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>');
expect(html).to.be('<td>sanitized</td>');
});
});
});
......
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="81" viewBox="0 0 400 81"><path d="M136.223 42.913c-.463 11.743-9.719 20.884-21.231 20.884-12.149 0-21.174-9.835-21.174-21.694 0-11.975 9.777-21.81 21.694-21.81 5.38 0 10.645 2.314 15.099 6.479l-3.471 4.281c-3.413-2.951-7.521-4.975-11.628-4.975-8.735 0-15.909 7.173-15.909 16.024 0 8.967 6.768 15.909 15.388 15.909 7.752 0 13.826-5.669 15.041-12.959h-17.586v-5.149h23.777v3.01zm20.187-2.834h-3.24a6.48 6.48 0 0 0-6.479 6.479V63.45h-5.785V34.525h4.744v2.43c1.562-1.562 4.05-2.43 6.826-2.43h6.248l-2.314 5.554zm31.526 23.371h-4.917v-3.645c-3.804 3.717-9.803 5.496-15.874 2.83-4.506-1.979-7.854-6.084-8.745-10.924-1.723-9.355 5.472-17.649 14.669-17.649 3.876 0 7.347 1.562 9.892 4.107v-3.644h4.975V63.45zm-5.917-12.382c1.365-5.879-3.086-11.221-8.892-11.221-5.033 0-9.082 4.107-9.082 9.083 0 5.627 4.947 9.982 10.701 9.002 3.545-.604 6.46-3.361 7.273-6.864zm17.578-18.163v1.62h9.198v5.091h-9.198V63.45h-5.727V33.079c0-6.364 4.57-10.124 10.297-10.124h6.942l-2.314 5.438h-4.628c-2.546 0-4.57 2.024-4.57 4.512zm39.857 30.545h-4.917v-3.645c-3.804 3.717-9.803 5.496-15.874 2.83-4.506-1.979-7.854-6.084-8.745-10.924-1.723-9.355 5.472-17.649 14.669-17.649 3.876 0 7.347 1.562 9.892 4.107v-3.644h4.975V63.45zm-5.917-12.382c1.365-5.879-3.086-11.221-8.892-11.221-5.033 0-9.082 4.107-9.082 9.083 0 5.627 4.947 9.982 10.701 9.002 3.545-.604 6.459-3.361 7.273-6.864zm36.818-4.742V63.45h-5.785V46.326c0-3.587-2.951-6.48-6.48-6.48-3.644 0-6.537 2.893-6.537 6.48V63.45h-5.785V34.525h4.801v2.487a11.743 11.743 0 0 1 7.752-2.95c6.712 0 12.034 5.496 12.034 12.264zm34.014 17.124h-4.917v-3.645c-3.804 3.717-9.803 5.496-15.874 2.83-4.506-1.979-7.854-6.084-8.745-10.924-1.723-9.355 5.472-17.649 14.669-17.649 3.876 0 7.347 1.562 9.892 4.107v-3.644h4.975V63.45zm-5.917-12.382c1.365-5.879-3.086-11.221-8.892-11.221-5.033 0-9.082 4.107-9.082 9.083 0 5.627 4.947 9.982 10.701 9.002 3.545-.604 6.46-3.361 7.273-6.864z" fill="#e6e7e8"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="312.934" y1="89.824" x2="312.934" y2="23.688"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M315.624 60.907c0 1.43-1.27 2.793-3.09 2.581-.912-.106-1.738-.711-2.085-1.561-.788-1.934.834-3.739 2.456-3.739 1.62.001 2.719 1.215 2.719 2.719z" fill="url(#a)"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="333.545" y1="89.824" x2="333.545" y2="23.688"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M345.636 46.484V63.51h-4.05V46.407c0-3.823-2.554-7.316-6.294-8.108-5.224-1.105-9.788 2.897-9.788 7.856V63.51h-4.049V34.585h3.587v3.066c2.861-2.86 7.191-4.251 11.589-3.153 5.387 1.345 9.005 6.432 9.005 11.986z" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="365.193" y1="89.824" x2="365.193" y2="23.688"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M354.924 50.379c.868 5.669 5.323 9.487 10.587 9.487 4.049 0 7.462-2.256 9.545-4.512l2.487 3.066c-3.355 3.876-7.868 5.554-12.033 5.554-8.793 0-14.868-7.347-14.868-15.446 0-8.331 6.421-14.405 14.463-14.405 8.33 0 14.636 6.595 14.636 16.256h-24.817zm20.422-3.414c-.81-5.091-5.149-8.967-10.24-8.967-4.686 0-9.314 3.297-10.182 8.967h20.422z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="392.334" y1="89.824" x2="392.334" y2="23.688"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M398.843 63.511h-4.744c-5.38 0-9.43-3.992-9.43-9.43V25.387h4.05v9.199H400l-1.157 3.587h-10.124v15.909c0 2.959 2.421 5.38 5.38 5.38h4.744v4.049z" fill="url(#d)"/><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="38.25" y1="77.691" x2="38.25" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M36.916 72.962l-6.517 6.517a40.175 40.175 0 0 0 7.756 1.152l7.947-7.947a31.371 31.371 0 0 1-9.186.278z" fill="url(#e)"/><linearGradient id="f" gradientUnits="userSpaceOnUse" x1="28.542" y1="77.691" x2="28.542" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M28.02 70.598l-5.832 5.833a40.003 40.003 0 0 0 6.368 2.539l6.341-6.341a32.435 32.435 0 0 1-6.877-2.031z" fill="url(#f)"/><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="20.867" y1="77.691" x2="20.867" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M20.914 66.444l-5.608 5.608a40.166 40.166 0 0 0 5.341 3.566l5.781-5.781a33.48 33.48 0 0 1-5.514-3.393z" fill="url(#g)"/><linearGradient id="h" gradientUnits="userSpaceOnUse" x1="14.578" y1="77.691" x2="14.578" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M17.316 63.358a32.616 32.616 0 0 1-2.171-2.404l-5.574 5.574a40.37 40.37 0 0 0 4.446 4.462l5.569-5.569a33.777 33.777 0 0 1-2.27-2.063z" fill="url(#h)"/><linearGradient id="i" gradientUnits="userSpaceOnUse" x1="25.441" y1="77.691" x2="25.441" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M23.322 23.401c-3.03 3.03-5.048 6.702-6.09 10.594L33.65 17.577c-3.97 1.084-7.448 2.944-10.328 5.824z" fill="url(#i)"/><linearGradient id="j" gradientUnits="userSpaceOnUse" x1="44.531" y1="77.691" x2="44.531" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M58.977 28.382l-31.88 31.879a24.264 24.264 0 0 0 5.937 2.97l28.93-28.93a23.934 23.934 0 0 0-2.987-5.919z" fill="url(#j)"/><linearGradient id="k" gradientUnits="userSpaceOnUse" x1="30.273" y1="77.691" x2="30.273" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M34.762 41.337L21.329 54.77a24.154 24.154 0 0 0 2.116 2.459 25.933 25.933 0 0 0 2.308 2.024L39.216 45.79l-4.454-4.453z" fill="url(#k)"/><linearGradient id="l" gradientUnits="userSpaceOnUse" x1="19.821" y1="77.691" x2="19.821" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M17.193 17.272c3.561-3.561 8.031-6.117 13.111-7.609L39.642.325c-3.607.047-7.097.57-10.415 1.508L1.508 29.552A40.196 40.196 0 0 0 0 39.967l9.128-9.128a32.032 32.032 0 0 1 8.065-13.567z" fill="url(#l)"/><linearGradient id="m" gradientUnits="userSpaceOnUse" x1="9.554" y1="77.691" x2="9.554" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M10.831 54.008l-5.866 5.866a40.231 40.231 0 0 0 3.549 5.358l5.628-5.628a32.395 32.395 0 0 1-3.311-5.596z" fill="url(#m)"/><linearGradient id="n" gradientUnits="userSpaceOnUse" x1="5.875" y1="77.691" x2="5.875" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M8.205 45.374l-6.558 6.558a39.87 39.87 0 0 0 2.512 6.396l5.942-5.942c-.903-2.271-1.518-4.625-1.896-7.012z" fill="url(#n)"/><linearGradient id="o" gradientUnits="userSpaceOnUse" x1="41.198" y1="77.691" x2="41.198" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M20.337 53.41l44.57-44.57a40.296 40.296 0 0 0-5.358-3.549l-42.06 42.06a23.847 23.847 0 0 0 2.848 6.059z" fill="url(#o)"/><linearGradient id="p" gradientUnits="userSpaceOnUse" x1="4.198" y1="77.691" x2="4.198" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M8.364 33.956L.033 42.287c.117 2.675.499 5.28 1.114 7.794l6.768-6.768a33.036 33.036 0 0 1 .449-9.357z" fill="url(#p)"/><linearGradient id="q" gradientUnits="userSpaceOnUse" x1="41.589" y1="77.691" x2="41.589" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M40.052 8.141l.02 3.015 9.684-9.684A40.155 40.155 0 0 0 41.962.358l-8.539 8.539a40.655 40.655 0 0 1 6.629-.756z" fill="url(#q)"/><linearGradient id="r" gradientUnits="userSpaceOnUse" x1="75.867" y1="77.691" x2="75.867" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M71.428 47.357l8.877-8.877a40.175 40.175 0 0 0-1.152-7.756l-7.404 7.404a31.82 31.82 0 0 1-.321 9.229z" fill="url(#r)"/><linearGradient id="s" gradientUnits="userSpaceOnUse" x1="74.039" y1="77.691" x2="74.039" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M71.471 36.055l7.174-7.173a39.864 39.864 0 0 0-2.539-6.369l-6.672 6.672a32.636 32.636 0 0 1 2.037 6.87z" fill="url(#s)"/><linearGradient id="t" gradientUnits="userSpaceOnUse" x1="65.308" y1="77.691" x2="65.308" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M63.096 19.295c.425.46.8.946 1.195 1.422l6.374-6.374a40.545 40.545 0 0 0-4.462-4.446L59.95 16.15l3.146 3.145z" fill="url(#t)"/><linearGradient id="u" gradientUnits="userSpaceOnUse" x1="70.32" y1="77.691" x2="70.32" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M68.714 27.552l6.579-6.579a40.264 40.264 0 0 0-3.566-5.341l-6.38 6.38a33.41 33.41 0 0 1 3.367 5.54z" fill="url(#u)"/><linearGradient id="v" gradientUnits="userSpaceOnUse" x1="66.036" y1="77.691" x2="66.036" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M77.809 54.588L54.262 78.135a40.295 40.295 0 0 0 23.547-23.547z" fill="url(#v)"/><linearGradient id="w" gradientUnits="userSpaceOnUse" x1="37.212" y1="77.691" x2="37.212" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M40.107 16.495c-1.188.086-2.33.241-3.451.429L16.648 36.932a24.133 24.133 0 0 0 .355 8.553l41-41a39.927 39.927 0 0 0-6.396-2.512l-11.52 11.519.02 3.003z" fill="url(#w)"/><linearGradient id="x" gradientUnits="userSpaceOnUse" x1="49.038" y1="77.691" x2="49.038" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M62.504 36.114L34.855 63.763c2.759.696 5.587.878 8.363.545l19.849-19.849a23.081 23.081 0 0 0-.563-8.345z" fill="url(#x)"/><linearGradient id="y" gradientUnits="userSpaceOnUse" x1="54.332" y1="77.691" x2="54.332" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M62.558 47.32l-16.45 16.45a22.72 22.72 0 0 0 10.493-5.989c3.036-3.037 5.002-6.654 5.957-10.461z" fill="url(#y)"/><linearGradient id="z" gradientUnits="userSpaceOnUse" x1="60.406" y1="77.691" x2="60.406" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M80.348 40.789l-9.641 9.641a31.326 31.326 0 0 1-7.98 13.479c-3.981 3.981-8.644 6.641-13.555 8.055l-8.71 8.71a40.205 40.205 0 0 0 10.329-1.422l28.133-28.133a40.089 40.089 0 0 0 1.424-10.33z" fill="url(#z)"/><linearGradient id="A" gradientUnits="userSpaceOnUse" x1="14.191" y1="77.691" x2="14.191" y2="-.557"><stop offset="0" stop-color="#fff200"/><stop offset="1" stop-color="#f15a29"/></linearGradient><path d="M2.684 26.025L25.699 3.01A40.292 40.292 0 0 0 2.684 26.025z" fill="url(#A)"/></svg>
\ No newline at end of file
......@@ -105,7 +105,7 @@
}
.confirm-modal-text2 {
font-size: $font-size-h5;
font-size: $font-size-root;
padding-top: $spacer;
}
......
......@@ -101,8 +101,24 @@
.search-button-row {
padding-top: 20px;
button, a {
margin-right: 10px;
}
.search-button-row-explore-link {
color: $text-muted;
font-size: $font-size-sm;
padding-right: 7rem;
background: url(../img/grafana_net_logo.svg);
background-size: 6.5rem 3rem;
background-repeat: no-repeat;
background-position: right;
position: relative;
top: 0.8rem;
&:hover {
color: $link-hover-color;
}
}
}
......@@ -63,28 +63,23 @@
}
}
// .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;
// }
.get-more-plugins-link {
color: $text-muted;
font-size: $font-size-sm;
padding-right: 7rem;
background: url(../img/grafana_net_logo.svg);
background-size: 6.5rem 3rem;
background-repeat: no-repeat;
background-position: right;
position: relative;
top: 1.2rem;
&:hover {
color: $link-hover-color;
}
}
@include media-breakpoint-down(sm) {
.get-more-plugins-link {
display: none;
}
}
......@@ -8,7 +8,7 @@
],
"title": "Nginx Connections",
"revision": "1.5",
"revision": 25,
"schemaVersion": 11,
"tags": ["tag1", "tag2"],
"number_array": [1,2,3,10.33],
......
{
"revision": "1.5",
"revision": 25,
"tags": ["tag1", "tag2"],
"boolean_false": false,
"boolean_true": true,
......
{
"title": "Nginx Memory",
"revision": "2.0",
"revision": 2,
"schemaVersion": 11
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment