mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of https://github.com/grafana/grafana
This commit is contained in:
commit
2a501d6cdb
@ -6,10 +6,12 @@
|
|||||||
* **Localization**: Week start day now dependant on browser locale setting, closes [#3003](https://github.com/grafana/grafana/issues/3003)
|
* **Localization**: Week start day now dependant on browser locale setting, closes [#3003](https://github.com/grafana/grafana/issues/3003)
|
||||||
* **Templating**: Update panel repeats for variables that change on time refresh, closes [#5021](https://github.com/grafana/grafana/issues/5021)
|
* **Templating**: Update panel repeats for variables that change on time refresh, closes [#5021](https://github.com/grafana/grafana/issues/5021)
|
||||||
|
|
||||||
# 3.1.0 stable (unreleased)
|
# 3.1.0 stable (2016-07-12)
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes & Enhancements,
|
||||||
* **User Alert Notices**: Backend error alert popups did not show properly, fixes [#5435](https://github.com/grafana/grafana/issues/5435)
|
* **User Alert Notices**: Backend error alert popups did not show properly, fixes [#5435](https://github.com/grafana/grafana/issues/5435)
|
||||||
|
* **Table**: Added sanitize HTML option to allow links in table cells, fixes [#4596](https://github.com/grafana/grafana/issues/4596)
|
||||||
|
* **Apps**: App dashboards are automatically synced to DB at startup after plugin update, fixes [#5529](https://github.com/grafana/grafana/issues/5529)
|
||||||
|
|
||||||
# 3.1.0-beta1 (2016-06-23)
|
# 3.1.0-beta1 (2016-06-23)
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ Then you can override them using:
|
|||||||
|
|
||||||
## instance_name
|
## instance_name
|
||||||
Set the name of the grafana-server instance. Used in logging and internal metrics and in
|
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
|
environment variable `HOSTNAME`, if that is empty or does not exist Grafana will try to use
|
||||||
system calls to get the machine name.
|
system calls to get the machine name.
|
||||||
|
|
||||||
|
@ -10,20 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
|
|||||||
|
|
||||||
Description | Download
|
Description | Download
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
Stable .deb for Debian-based Linux | [3.0.4](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.4-1464167696_amd64.deb)
|
Stable .deb for Debian-based Linux | [3.1.0](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1468321182_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)
|
|
||||||
|
|
||||||
## Install Stable
|
## 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 apt-get install -y adduser libfontconfig
|
||||||
$ sudo dpkg -i grafana_3.0.4-1464167696_amd64.deb
|
$ sudo dpkg -i grafana_3.1.0-1468321182_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
|
|
||||||
|
|
||||||
## APT Repository
|
## APT Repository
|
||||||
|
|
||||||
|
@ -10,42 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
|
|||||||
|
|
||||||
Description | Download
|
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)
|
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)
|
||||||
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)
|
|
||||||
|
|
||||||
## Install Latest Stable
|
## Install Latest Stable
|
||||||
|
|
||||||
You can install Grafana using Yum directly.
|
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`.
|
Or install manually using `rpm`.
|
||||||
|
|
||||||
#### On CentOS / Fedora / Redhat:
|
#### On CentOS / Fedora / Redhat:
|
||||||
|
|
||||||
$ sudo yum install initscripts fontconfig
|
$ 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:
|
#### On OpenSuse:
|
||||||
|
|
||||||
$ sudo rpm -i --nodeps grafana-3.0.4-1464167696.x86_64.rpm
|
$ sudo rpm -i --nodeps grafana-3.1.0-1468321182.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
|
|
||||||
|
|
||||||
## Install via YUM Repository
|
## Install via YUM Repository
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
|
|||||||
|
|
||||||
Description | Download
|
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
|
## Configure
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"stable": "3.0.4",
|
"stable": "3.1.0",
|
||||||
"testing": "3.1.0-beta1"
|
"testing": "3.1.0"
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
"phantomjs-prebuilt": "^2.1.7",
|
"phantomjs-prebuilt": "^2.1.7",
|
||||||
"reflect-metadata": "0.1.2",
|
"reflect-metadata": "0.1.2",
|
||||||
"rxjs": "5.0.0-beta.4",
|
"rxjs": "5.0.0-beta.4",
|
||||||
"sass-lint": "^1.6.0",
|
"sass-lint": "^1.7.0",
|
||||||
"systemjs": "0.19.24"
|
"systemjs": "0.19.24"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -69,7 +69,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eventemitter3": "^1.2.0",
|
"eventemitter3": "^1.2.0",
|
||||||
"grunt-jscs": "~1.5.x",
|
"grunt-jscs": "~1.5.x",
|
||||||
"grunt-sass-lint": "^0.1.0",
|
"grunt-sass-lint": "^0.2.0",
|
||||||
"grunt-sync": "^0.4.1",
|
"grunt-sync": "^0.4.1",
|
||||||
"karma-sinon": "^1.0.3",
|
"karma-sinon": "^1.0.3",
|
||||||
"lodash": "^2.4.1",
|
"lodash": "^2.4.1",
|
||||||
|
@ -72,8 +72,6 @@ function isRunning() {
|
|||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
echo -n $"Starting $DESC: .... "
|
|
||||||
|
|
||||||
isRunning
|
isRunning
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "Already running."
|
echo "Already running."
|
||||||
@ -90,7 +88,7 @@ case "$1" in
|
|||||||
|
|
||||||
# Start Daemon
|
# Start Daemon
|
||||||
cd $GRAFANA_HOME
|
cd $GRAFANA_HOME
|
||||||
su -s /bin/sh -c "nohup ${DAEMON} ${DAEMON_OPTS} >> /dev/null 3>&1 &" $GRAFANA_USER 2> /dev/null
|
action $"Starting $DESC: ..." su -s /bin/sh -c "nohup ${DAEMON} ${DAEMON_OPTS} >> /dev/null 3>&1 &" $GRAFANA_USER 2> /dev/null
|
||||||
return=$?
|
return=$?
|
||||||
if [ $return -eq 0 ]
|
if [ $return -eq 0 ]
|
||||||
then
|
then
|
||||||
@ -114,26 +112,25 @@ case "$1" in
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "OK"
|
|
||||||
exit $return
|
exit $return
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
echo -n "Stopping $DESC ..."
|
echo -n "Stopping $DESC: ..."
|
||||||
|
|
||||||
if [ -f "$PID_FILE" ]; then
|
if [ -f "$PID_FILE" ]; then
|
||||||
killproc -p $PID_FILE -d 20 $NAME
|
killproc -p $PID_FILE -d 20 $NAME
|
||||||
if [ $? -eq 1 ]; then
|
if [ $? -eq 1 ]; then
|
||||||
echo -n "$DESC is not running but pid file exists, cleaning up"
|
echo "$DESC is not running but pid file exists, cleaning up"
|
||||||
elif [ $? -eq 3 ]; then
|
elif [ $? -eq 3 ]; then
|
||||||
PID="`cat $PID_FILE`"
|
PID="`cat $PID_FILE`"
|
||||||
echo -n "Failed to stop $DESC (pid $PID)"
|
echo "Failed to stop $DESC (pid $PID)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
echo "OK"
|
echo ""
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -n "(not running)"
|
echo "(not running)"
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
@ -211,7 +211,7 @@ func Register(r *macaron.Macaron) {
|
|||||||
// Dashboard
|
// Dashboard
|
||||||
r.Group("/dashboards", func() {
|
r.Group("/dashboards", func() {
|
||||||
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
|
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("/file/:file", GetDashboardFromJsonFile)
|
||||||
r.Get("/home", wrap(GetHomeDashboard))
|
r.Get("/home", wrap(GetHomeDashboard))
|
||||||
r.Get("/tags", GetDashboardTags)
|
r.Get("/tags", GetDashboardTags)
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
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/services/search"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@ -109,7 +110,7 @@ func DeleteDashboard(c *middleware.Context) {
|
|||||||
c.JSON(200, resp)
|
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
|
cmd.OrgId = c.OrgId
|
||||||
|
|
||||||
if !c.IsSignedIn {
|
if !c.IsSignedIn {
|
||||||
@ -122,35 +123,37 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
|
|||||||
if dash.Id == 0 {
|
if dash.Id == 0 {
|
||||||
limitReached, err := middleware.QuotaReached(c, "dashboard")
|
limitReached, err := middleware.QuotaReached(c, "dashboard")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JsonApiErr(500, "failed to get quota", err)
|
return ApiError(500, "failed to get quota", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if limitReached {
|
if limitReached {
|
||||||
c.JsonApiErr(403, "Quota reached", nil)
|
return ApiError(403, "Quota reached", nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bus.Dispatch(&cmd)
|
err := bus.Dispatch(&cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == m.ErrDashboardWithSameNameExists {
|
if err == m.ErrDashboardWithSameNameExists {
|
||||||
c.JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if err == m.ErrDashboardVersionMismatch {
|
if err == m.ErrDashboardVersionMismatch {
|
||||||
c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
|
return Json(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
|
||||||
return
|
}
|
||||||
|
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 {
|
if err == m.ErrDashboardNotFound {
|
||||||
c.JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
|
return Json(404, util.DynMap{"status": "not-found", "message": err.Error()})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.JsonApiErr(500, "Failed to save dashboard", err)
|
return ApiError(500, "Failed to save dashboard", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
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 {
|
func canEditDashboard(role m.RoleType) bool {
|
||||||
|
@ -18,7 +18,7 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range *dashboardQuery.Result {
|
for _, item := range dashboardQuery.Result {
|
||||||
result = append(result, m.PlaylistDashboardDto{
|
result = append(result, m.PlaylistDashboardDto{
|
||||||
Id: item.Id,
|
Id: item.Id,
|
||||||
Slug: item.Slug,
|
Slug: item.Slug,
|
||||||
|
@ -17,6 +17,14 @@ var (
|
|||||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
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 (
|
var (
|
||||||
DashTypeJson = "file"
|
DashTypeJson = "file"
|
||||||
DashTypeDB = "db"
|
DashTypeDB = "db"
|
||||||
@ -31,6 +39,7 @@ type Dashboard struct {
|
|||||||
OrgId int64
|
OrgId int64
|
||||||
GnetId int64
|
GnetId int64
|
||||||
Version int
|
Version int
|
||||||
|
PluginId string
|
||||||
|
|
||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
@ -95,6 +104,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
|||||||
|
|
||||||
dash.UpdatedBy = cmd.UserId
|
dash.UpdatedBy = cmd.UserId
|
||||||
dash.OrgId = cmd.OrgId
|
dash.OrgId = cmd.OrgId
|
||||||
|
dash.PluginId = cmd.PluginId
|
||||||
dash.UpdateSlug()
|
dash.UpdateSlug()
|
||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
@ -119,6 +129,7 @@ type SaveDashboardCommand struct {
|
|||||||
UserId int64 `json:"userId"`
|
UserId int64 `json:"userId"`
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Overwrite bool `json:"overwrite"`
|
Overwrite bool `json:"overwrite"`
|
||||||
|
PluginId string `json:"-"`
|
||||||
|
|
||||||
Result *Dashboard
|
Result *Dashboard
|
||||||
}
|
}
|
||||||
@ -151,7 +162,13 @@ type GetDashboardTagsQuery struct {
|
|||||||
|
|
||||||
type GetDashboardsQuery struct {
|
type GetDashboardsQuery struct {
|
||||||
DashboardIds []int64
|
DashboardIds []int64
|
||||||
Result *[]Dashboard
|
Result []*Dashboard
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetDashboardsByPluginIdQuery struct {
|
||||||
|
OrgId int64
|
||||||
|
PluginId string
|
||||||
|
Result []*Dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetDashboardSlugByIdQuery struct {
|
type GetDashboardSlugByIdQuery struct {
|
||||||
|
@ -20,6 +20,7 @@ type PluginSetting struct {
|
|||||||
Pinned bool
|
Pinned bool
|
||||||
JsonData map[string]interface{}
|
JsonData map[string]interface{}
|
||||||
SecureJsonData SecureJsonData
|
SecureJsonData SecureJsonData
|
||||||
|
PluginVersion string
|
||||||
|
|
||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
@ -44,11 +45,19 @@ type UpdatePluginSettingCmd struct {
|
|||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
JsonData map[string]interface{} `json:"jsonData"`
|
JsonData map[string]interface{} `json:"jsonData"`
|
||||||
SecureJsonData map[string]string `json:"secureJsonData"`
|
SecureJsonData map[string]string `json:"secureJsonData"`
|
||||||
|
PluginVersion string `json:"version"`
|
||||||
|
|
||||||
PluginId string `json:"-"`
|
PluginId string `json:"-"`
|
||||||
OrgId int64 `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 {
|
func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData {
|
||||||
encrypted := make(SecureJsonData)
|
encrypted := make(SecureJsonData)
|
||||||
for key, data := range cmd.SecureJsonData {
|
for key, data := range cmd.SecureJsonData {
|
||||||
@ -69,6 +78,7 @@ type PluginSettingInfoDTO struct {
|
|||||||
PluginId string
|
PluginId string
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Pinned bool
|
Pinned bool
|
||||||
|
PluginVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetPluginSettingByIdQuery struct {
|
type GetPluginSettingByIdQuery struct {
|
||||||
@ -76,3 +86,9 @@ type GetPluginSettingByIdQuery struct {
|
|||||||
OrgId int64
|
OrgId int64
|
||||||
Result *PluginSetting
|
Result *PluginSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PluginStateChangedEvent struct {
|
||||||
|
PluginId string
|
||||||
|
OrgId int64
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
@ -68,6 +68,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
|||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
UserId: cmd.UserId,
|
UserId: cmd.UserId,
|
||||||
Overwrite: cmd.Overwrite,
|
Overwrite: cmd.Overwrite,
|
||||||
|
PluginId: cmd.PluginId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&saveCmd); err != nil {
|
if err := bus.Dispatch(&saveCmd); err != nil {
|
||||||
|
@ -14,10 +14,12 @@ type PluginDashboardInfoDTO struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Imported bool `json:"imported"`
|
Imported bool `json:"imported"`
|
||||||
ImportedUri string `json:"importedUri"`
|
ImportedUri string `json:"importedUri"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
ImportedRevision int64 `json:"importedRevision"`
|
ImportedRevision int64 `json:"importedRevision"`
|
||||||
Revision int64 `json:"revision"`
|
Revision int64 `json:"revision"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
Removed bool `json:"removed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
|
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
|
||||||
@ -29,13 +31,52 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
|
|||||||
|
|
||||||
result := make([]*PluginDashboardInfoDTO, 0)
|
result := make([]*PluginDashboardInfoDTO, 0)
|
||||||
|
|
||||||
for _, include := range plugin.Includes {
|
// load current dashboards
|
||||||
if include.Type == PluginTypeDashboard {
|
query := m.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId}
|
||||||
if dashInfo, err := getDashboardImportStatus(orgId, plugin, include.Path); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
|
||||||
result = append(result, dashInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingMatches := make(map[int64]bool)
|
||||||
|
|
||||||
|
for _, include := range plugin.Includes {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
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"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
@ -31,6 +32,17 @@ func TestPluginDashboards(t *testing.T) {
|
|||||||
return m.ErrDashboardNotFound
|
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")
|
dashboards, err := GetPluginDashboards(1, "test-app")
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
@ -41,12 +53,12 @@ func TestPluginDashboards(t *testing.T) {
|
|||||||
|
|
||||||
Convey("should include installed version info", func() {
|
Convey("should include installed version info", func() {
|
||||||
So(dashboards[0].Title, ShouldEqual, "Nginx Connections")
|
So(dashboards[0].Title, ShouldEqual, "Nginx Connections")
|
||||||
//So(dashboards[0].Revision, ShouldEqual, "1.5")
|
So(dashboards[0].Revision, ShouldEqual, 25)
|
||||||
//So(dashboards[0].InstalledRevision, ShouldEqual, "1.1")
|
So(dashboards[0].ImportedRevision, ShouldEqual, 22)
|
||||||
//So(dashboards[0].InstalledUri, ShouldEqual, "db/nginx-connections")
|
So(dashboards[0].ImportedUri, ShouldEqual, "db/nginx-connections")
|
||||||
|
|
||||||
//So(dashboards[1].Revision, ShouldEqual, "2.0")
|
So(dashboards[1].Revision, ShouldEqual, 2)
|
||||||
//So(dashboards[1].InstalledRevision, ShouldEqual, "")
|
So(dashboards[1].ImportedRevision, ShouldEqual, 0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
139
pkg/plugins/dashboards_updater.go
Normal file
139
pkg/plugins/dashboards_updater.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
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 StartPluginUpdateChecker()
|
||||||
|
go updateAppDashboards()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ func init() {
|
|||||||
bus.AddHandler("sql", SearchDashboards)
|
bus.AddHandler("sql", SearchDashboards)
|
||||||
bus.AddHandler("sql", GetDashboardTags)
|
bus.AddHandler("sql", GetDashboardTags)
|
||||||
bus.AddHandler("sql", GetDashboardSlugById)
|
bus.AddHandler("sql", GetDashboardSlugById)
|
||||||
|
bus.AddHandler("sql", GetDashboardsByPluginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||||
@ -45,6 +46,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
|||||||
return m.ErrDashboardVersionMismatch
|
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)
|
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
|
return m.ErrCommandValidationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
var dashboards = make([]m.Dashboard, 0)
|
var dashboards = make([]*m.Dashboard, 0)
|
||||||
|
|
||||||
err := x.In("id", query.DashboardIds).Find(&dashboards)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -111,4 +111,13 @@ func addDashboardMigration(mg *Migrator) {
|
|||||||
mg.AddMigration("Add index for gnetId in dashboard", NewAddIndexMigration(dashboardV2, &Index{
|
mg.AddMigration("Add index for gnetId in dashboard", NewAddIndexMigration(dashboardV2, &Index{
|
||||||
Cols: []string{"gnet_id"}, Type: IndexType,
|
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 ------------------
|
//------- indexes ------------------
|
||||||
addTableIndicesMigrations(mg, "v1", pluginSettingTable)
|
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", GetPluginSettings)
|
||||||
bus.AddHandler("sql", GetPluginSettingById)
|
bus.AddHandler("sql", GetPluginSettingById)
|
||||||
bus.AddHandler("sql", UpdatePluginSetting)
|
bus.AddHandler("sql", UpdatePluginSetting)
|
||||||
|
bus.AddHandler("sql", UpdatePluginSettingVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPluginSettings(query *m.GetPluginSettingsQuery) error {
|
func GetPluginSettings(query *m.GetPluginSettingsQuery) error {
|
||||||
sql := `SELECT org_id, plugin_id, enabled, pinned
|
sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version
|
||||||
FROM plugin_setting
|
FROM plugin_setting `
|
||||||
WHERE org_id=?`
|
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)
|
query.Result = make([]*m.PluginSettingInfoDTO, 0)
|
||||||
return sess.Find(&query.Result)
|
return sess.Find(&query.Result)
|
||||||
}
|
}
|
||||||
@ -51,22 +57,52 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
|
|||||||
Enabled: cmd.Enabled,
|
Enabled: cmd.Enabled,
|
||||||
Pinned: cmd.Pinned,
|
Pinned: cmd.Pinned,
|
||||||
JsonData: cmd.JsonData,
|
JsonData: cmd.JsonData,
|
||||||
|
PluginVersion: cmd.PluginVersion,
|
||||||
SecureJsonData: cmd.GetEncryptedJsonData(),
|
SecureJsonData: cmd.GetEncryptedJsonData(),
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Updated: 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)
|
_, err = sess.Insert(&pluginSetting)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
for key, data := range cmd.SecureJsonData {
|
for key, data := range cmd.SecureJsonData {
|
||||||
pluginSetting.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
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.Updated = time.Now()
|
||||||
pluginSetting.Enabled = cmd.Enabled
|
pluginSetting.Enabled = cmd.Enabled
|
||||||
pluginSetting.JsonData = cmd.JsonData
|
pluginSetting.JsonData = cmd.JsonData
|
||||||
pluginSetting.Pinned = cmd.Pinned
|
pluginSetting.Pinned = cmd.Pinned
|
||||||
|
pluginSetting.PluginVersion = cmd.PluginVersion
|
||||||
|
|
||||||
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
|
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
|
||||||
return err
|
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
|
Import
|
||||||
</a>
|
</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 class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li ng-show="::!ctrl.isSignedIn">
|
<li ng-show="::!ctrl.isSignedIn">
|
||||||
<a href="{{ctrl.loginUrl}}" class="sidemenu-item">
|
<a href="{{ctrl.loginUrl}}" class="sidemenu-item" target="_self">
|
||||||
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span>
|
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span>
|
||||||
<span class="sidemenu-item-text">Sign in</span>
|
<span class="sidemenu-item-text">Sign in</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -23,13 +23,13 @@ export class SideMenuCtrl {
|
|||||||
|
|
||||||
this.mainLinks = config.bootData.mainNavLinks;
|
this.mainLinks = config.bootData.mainNavLinks;
|
||||||
this.openUserDropdown();
|
this.openUserDropdown();
|
||||||
this.loginUrl = '/login?redirect=' + encodeURIComponent(this.$location.path());
|
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
||||||
|
|
||||||
this.$scope.$on('$routeChangeSuccess', () => {
|
this.$scope.$on('$routeChangeSuccess', () => {
|
||||||
if (!this.contextSrv.pinned) {
|
if (!this.contextSrv.pinned) {
|
||||||
this.contextSrv.sidemenu = false;
|
this.contextSrv.sidemenu = false;
|
||||||
}
|
}
|
||||||
this.loginUrl = '/login?redirect=' + encodeURIComponent(this.$location.path());
|
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +73,8 @@ export class AlertSrv {
|
|||||||
scope.text = payload.text;
|
scope.text = payload.text;
|
||||||
scope.text2 = payload.text2;
|
scope.text2 = payload.text2;
|
||||||
scope.onConfirm = payload.onConfirm;
|
scope.onConfirm = payload.onConfirm;
|
||||||
|
scope.onAltAction = payload.onAltAction;
|
||||||
|
scope.altActionText = payload.altActionText;
|
||||||
scope.icon = payload.icon || "fa-check";
|
scope.icon = payload.icon || "fa-check";
|
||||||
scope.yesText = payload.yesText || "Yes";
|
scope.yesText = payload.yesText || "Yes";
|
||||||
scope.noText = payload.noText || "Cancel";
|
scope.noText = payload.noText || "Cancel";
|
||||||
|
@ -368,6 +368,23 @@ function($, _, moment) {
|
|||||||
return kbn.toFixed(100*size, decimals) + '%';
|
return kbn.toFixed(100*size, decimals) + '%';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Formats the value to hex. Uses float if specified decimals are not 0.
|
||||||
|
* There are two options, one with 0x, and one without */
|
||||||
|
|
||||||
|
kbn.valueFormats.hex = function(value, decimals) {
|
||||||
|
if (value == null) { return ""; }
|
||||||
|
return parseFloat(kbn.toFixed(value, decimals)).toString(16).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
kbn.valueFormats.hex0x = function(value, decimals) {
|
||||||
|
if (value == null) { return ""; }
|
||||||
|
var hexString = kbn.valueFormats.hex(value, decimals);
|
||||||
|
if (hexString.substring(0,1) === "-") {
|
||||||
|
return "-0x" + hexString.substring(1);
|
||||||
|
}
|
||||||
|
return "0x" + hexString;
|
||||||
|
};
|
||||||
|
|
||||||
// Currencies
|
// Currencies
|
||||||
kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
|
kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
|
||||||
kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
|
kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
|
||||||
@ -617,6 +634,8 @@ function($, _, moment) {
|
|||||||
{text: 'Humidity (%H)', value: 'humidity' },
|
{text: 'Humidity (%H)', value: 'humidity' },
|
||||||
{text: 'ppm', value: 'ppm' },
|
{text: 'ppm', value: 'ppm' },
|
||||||
{text: 'decibel', value: 'dB' },
|
{text: 'decibel', value: 'dB' },
|
||||||
|
{text: 'hexadecimal (0x)', value: 'hex0x' },
|
||||||
|
{text: 'hexadecimal', value: 'hex' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,7 @@ function (angular, $, _, moment) {
|
|||||||
|
|
||||||
this.id = data.id || null;
|
this.id = data.id || null;
|
||||||
this.title = data.title || 'No Title';
|
this.title = data.title || 'No Title';
|
||||||
|
this.autoUpdate = data.autoUpdate;
|
||||||
this.description = data.description;
|
this.description = data.description;
|
||||||
this.tags = data.tags || [];
|
this.tags = data.tags || [];
|
||||||
this.style = data.style || "dark";
|
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() {
|
$scope.deleteDashboard = function() {
|
||||||
|
@ -12,6 +12,8 @@ function (angular) {
|
|||||||
$scope.clone.id = null;
|
$scope.clone.id = null;
|
||||||
$scope.clone.editable = true;
|
$scope.clone.editable = true;
|
||||||
$scope.clone.title = $scope.clone.title + " Copy";
|
$scope.clone.title = $scope.clone.title + " Copy";
|
||||||
|
// remove auto update
|
||||||
|
delete $scope.clone.autoUpdate;
|
||||||
};
|
};
|
||||||
|
|
||||||
function saveDashboard(options) {
|
function saveDashboard(options) {
|
||||||
@ -37,8 +39,9 @@ function (angular) {
|
|||||||
err.isHandled = true;
|
err.isHandled = true;
|
||||||
|
|
||||||
$scope.appEvent('confirm-modal', {
|
$scope.appEvent('confirm-modal', {
|
||||||
title: 'Another dashboard with the same name exists',
|
title: 'Conflict',
|
||||||
text: "Would you still like to save this dashboard?",
|
text: 'Dashboard with the same name exists.',
|
||||||
|
text2: 'Would you still like to save this dashboard?',
|
||||||
yesText: "Save & Overwrite",
|
yesText: "Save & Overwrite",
|
||||||
icon: "fa-warning",
|
icon: "fa-warning",
|
||||||
onConfirm: function() {
|
onConfirm: function() {
|
||||||
|
@ -14,20 +14,19 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
v{{dash.revision}}
|
<span ng-if="dash.imported" bs-tooltip='"Imported revision:" + dash.importedRevision'>
|
||||||
<span ng-if="dash.installed">
|
Revision: {{dash.revision}}
|
||||||
(Imported v{{dash.importedRevision}})
|
|
||||||
<span>
|
<span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right">
|
<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
|
Import
|
||||||
</button>
|
</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
|
Update
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
|
<button class="btn btn-danger btn-small" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
|
||||||
Delete
|
<i class="fa fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>Plugins</h1>
|
<h1>Plugins</h1>
|
||||||
|
|
||||||
|
<!-- <a class="btn btn-inverse" href="https://grafana.net/plugins?utm_source=grafana_plugin_list" target="_blank"> -->
|
||||||
|
<!-- Explore plugins on Grafana.net -->
|
||||||
|
<!-- </a> -->
|
||||||
|
|
||||||
<div class="page-header-tabs">
|
<div class="page-header-tabs">
|
||||||
<ul class="gf-tabs">
|
<ul class="gf-tabs">
|
||||||
<li class="gf-tabs-item">
|
<li class="gf-tabs-item">
|
||||||
@ -23,6 +27,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -97,28 +97,7 @@ export class PluginEditCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
importDashboards() {
|
importDashboards() {
|
||||||
// move to dashboards tab
|
return Promise.resolve();
|
||||||
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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreUpdateHook(callback: () => any) {
|
setPreUpdateHook(callback: () => any) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body" ng-cloak>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-header-title">
|
<h2 class="modal-header-title">
|
||||||
<i class="fa {{icon}}"></i>
|
<i class="fa {{icon}}"></i>
|
||||||
@ -24,6 +24,7 @@
|
|||||||
<div class="confirm-modal-buttons">
|
<div class="confirm-modal-buttons">
|
||||||
<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
|
<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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
|||||||
esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
|
esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
|
||||||
esQuery = header + '\n' + esQuery + '\n';
|
esQuery = header + '\n' + esQuery + '\n';
|
||||||
|
|
||||||
return this._post('/_msearch?search_type=' + searchType, esQuery).then(function(res) {
|
return this._post('_msearch?search_type=' + searchType, esQuery).then(function(res) {
|
||||||
var buckets = res.responses[0].aggregations["1"].buckets;
|
var buckets = res.responses[0].aggregations["1"].buckets;
|
||||||
return _.map(buckets, function(bucket) {
|
return _.map(buckets, function(bucket) {
|
||||||
return {text: bucket.key, value: bucket.key};
|
return {text: bucket.key, value: bucket.key};
|
||||||
|
@ -228,7 +228,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nestedSeriesRegexReplacer(match, g1) {
|
function nestedSeriesRegexReplacer(match, g1) {
|
||||||
return targets[g1];
|
return targets[g1] || match;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < options.targets.length; i++) {
|
for (i = 0; i < options.targets.length; i++) {
|
||||||
|
@ -538,8 +538,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
elem.mouseleave(function() {
|
elem.mouseleave(function() {
|
||||||
if (panel.links.length === 0) { return;}
|
if (panel.links.length === 0) { return;}
|
||||||
|
$timeout(function() {
|
||||||
drilldownTooltip.detach();
|
drilldownTooltip.detach();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
elem.click(function(evt) {
|
elem.click(function(evt) {
|
||||||
if (!linkInfo) { return; }
|
if (!linkInfo) { return; }
|
||||||
|
@ -103,6 +103,11 @@
|
|||||||
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
|
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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 class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tight-form" ng-if="style.type === 'number'">
|
<div class="tight-form" ng-if="style.type === 'number'">
|
||||||
@ -152,7 +157,7 @@
|
|||||||
Decimals
|
Decimals
|
||||||
</li>
|
</li>
|
||||||
<li style="width: 105px">
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
@ -45,7 +45,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope, $injector, private annotationsSrv) {
|
constructor($scope, $injector, private annotationsSrv, private $sanitize) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
this.pageIndex = 0;
|
this.pageIndex = 0;
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportCsv() {
|
exportCsv() {
|
||||||
var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc());
|
var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize);
|
||||||
FileExport.exportTableDataToCsv(renderer.render_values());
|
FileExport.exportTableDataToCsv(renderer.render_values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendTableRows(tbodyElem) {
|
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.empty();
|
||||||
tbodyElem.html(renderer.render(ctrl.pageIndex));
|
tbodyElem.html(renderer.render(ctrl.pageIndex));
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export class TableRenderer {
|
|||||||
formaters: any[];
|
formaters: any[];
|
||||||
colorState: any;
|
colorState: any;
|
||||||
|
|
||||||
constructor(private panel, private table, private isUtc) {
|
constructor(private panel, private table, private isUtc, private sanitize) {
|
||||||
this.formaters = [];
|
this.formaters = [];
|
||||||
this.colorState = {};
|
this.colorState = {};
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@ export class TableRenderer {
|
|||||||
return _.first(style.colors);
|
return _.first(style.colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultCellFormater(v) {
|
defaultCellFormater(v, style) {
|
||||||
if (v === null || v === void 0 || v === undefined) {
|
if (v === null || v === void 0 || v === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -33,7 +33,11 @@ export class TableRenderer {
|
|||||||
v = v.join(', ');
|
v = v.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
return v;
|
if (style && style.sanitize) {
|
||||||
|
return this.sanitize(v);
|
||||||
|
} else {
|
||||||
|
return _.escape(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createColumnFormater(style, column) {
|
createColumnFormater(style, column) {
|
||||||
@ -61,7 +65,7 @@ export class TableRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_.isString(v)) {
|
if (_.isString(v)) {
|
||||||
return v;
|
return this.defaultCellFormater(v, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style.colorMode) {
|
if (style.colorMode) {
|
||||||
@ -72,7 +76,9 @@ export class TableRenderer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.defaultCellFormater;
|
return (value) => {
|
||||||
|
return this.defaultCellFormater(value, style);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
formatColumnValue(colIndex, value) {
|
formatColumnValue(colIndex, value) {
|
||||||
@ -96,7 +102,6 @@ export class TableRenderer {
|
|||||||
|
|
||||||
renderCell(columnIndex, value, addWidthHack = false) {
|
renderCell(columnIndex, value, addWidthHack = false) {
|
||||||
value = this.formatColumnValue(columnIndex, value);
|
value = this.formatColumnValue(columnIndex, value);
|
||||||
value = _.escape(value);
|
|
||||||
var style = '';
|
var style = '';
|
||||||
if (this.colorState.cell) {
|
if (this.colorState.cell) {
|
||||||
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
|
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
|
||||||
|
@ -13,6 +13,7 @@ describe('when rendering table', () => {
|
|||||||
{text: 'Undefined'},
|
{text: 'Undefined'},
|
||||||
{text: 'String'},
|
{text: 'String'},
|
||||||
{text: 'United', unit: 'bps'},
|
{text: 'United', unit: 'bps'},
|
||||||
|
{text: 'Sanitized'},
|
||||||
];
|
];
|
||||||
|
|
||||||
var panel = {
|
var panel = {
|
||||||
@ -47,11 +48,20 @@ describe('when rendering table', () => {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
unit: 'ms',
|
unit: 'ms',
|
||||||
decimals: 2,
|
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', () => {
|
it('time column should be formated', () => {
|
||||||
var html = renderer.renderCell(0, 1388556366666);
|
var html = renderer.renderCell(0, 1388556366666);
|
||||||
@ -107,6 +117,11 @@ describe('when rendering table', () => {
|
|||||||
var html = renderer.renderCell(3, undefined);
|
var html = renderer.renderCell(3, undefined);
|
||||||
expect(html).to.be('<td></td>');
|
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>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
1
public/img/grafana_net_logo.svg
Normal file
1
public/img/grafana_net_logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.confirm-modal-text2 {
|
.confirm-modal-text2 {
|
||||||
font-size: $font-size-h5;
|
font-size: $font-size-root;
|
||||||
padding-top: $spacer;
|
padding-top: $spacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,8 +101,24 @@
|
|||||||
|
|
||||||
.search-button-row {
|
.search-button-row {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
|
||||||
button, a {
|
button, a {
|
||||||
margin-right: 10px;
|
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 {
|
.get-more-plugins-link {
|
||||||
// padding: 1.2rem;
|
color: $text-muted;
|
||||||
// background: $panel-bg;
|
font-size: $font-size-sm;
|
||||||
// text-align: center;
|
padding-right: 7rem;
|
||||||
// img {
|
background: url(../img/grafana_net_logo.svg);
|
||||||
// max-width: 7rem;
|
background-size: 6.5rem 3rem;
|
||||||
// }
|
background-repeat: no-repeat;
|
||||||
// margin-right: 2rem;
|
background-position: right;
|
||||||
// }
|
position: relative;
|
||||||
//
|
top: 1.2rem;
|
||||||
// .app-edit-links {
|
&:hover {
|
||||||
// list-style: none;
|
color: $link-hover-color;
|
||||||
// margin: 0 0 0 2rem;
|
}
|
||||||
//
|
}
|
||||||
// li {
|
|
||||||
// background: $panel-bg;
|
@include media-breakpoint-down(sm) {
|
||||||
// margin-top: 4px;
|
.get-more-plugins-link {
|
||||||
// padding: 0.2rem 1rem;
|
display: none;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// .app-edit-description {
|
|
||||||
// font-style: italic;
|
|
||||||
// margin-bottom: 1.5rem;
|
|
||||||
// }
|
|
||||||
|
@ -161,4 +161,50 @@ define([
|
|||||||
expect(str).to.be('15ms');
|
expect(str).to.be('15ms');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hex', function() {
|
||||||
|
it('positive integer', function() {
|
||||||
|
var str = kbn.valueFormats.hex(100, 0);
|
||||||
|
expect(str).to.be('64');
|
||||||
|
});
|
||||||
|
it('negative integer', function() {
|
||||||
|
var str = kbn.valueFormats.hex(-100, 0);
|
||||||
|
expect(str).to.be('-64');
|
||||||
|
});
|
||||||
|
it('null', function() {
|
||||||
|
var str = kbn.valueFormats.hex(null, 0);
|
||||||
|
expect(str).to.be('');
|
||||||
|
});
|
||||||
|
it('positive float', function() {
|
||||||
|
var str = kbn.valueFormats.hex(50.52, 1);
|
||||||
|
expect(str).to.be('32.8');
|
||||||
|
});
|
||||||
|
it('negative float', function() {
|
||||||
|
var str = kbn.valueFormats.hex(-50.333, 2);
|
||||||
|
expect(str).to.be('-32.547AE147AE14');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hex 0x', function() {
|
||||||
|
it('positive integeter', function() {
|
||||||
|
var str = kbn.valueFormats.hex0x(7999,0);
|
||||||
|
expect(str).to.be('0x1F3F');
|
||||||
|
});
|
||||||
|
it('negative integer', function() {
|
||||||
|
var str = kbn.valueFormats.hex0x(-584,0);
|
||||||
|
expect(str).to.be('-0x248');
|
||||||
|
});
|
||||||
|
it('null', function() {
|
||||||
|
var str = kbn.valueFormats.hex0x(null, 0);
|
||||||
|
expect(str).to.be('');
|
||||||
|
});
|
||||||
|
it('positive float', function() {
|
||||||
|
var str = kbn.valueFormats.hex0x(74.443, 3);
|
||||||
|
expect(str).to.be('0x4A.716872B020C4');
|
||||||
|
});
|
||||||
|
it('negative float', function() {
|
||||||
|
var str = kbn.valueFormats.hex0x(-65.458, 1);
|
||||||
|
expect(str).to.be('-0x41.8');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
11
public/vendor/flot/jquery.flot.selection.js
vendored
11
public/vendor/flot/jquery.flot.selection.js
vendored
@ -162,8 +162,17 @@ The plugin allso adds the following methods to the plot object:
|
|||||||
if (!selection.show) return null;
|
if (!selection.show) return null;
|
||||||
|
|
||||||
var r = {}, c1 = selection.first, c2 = selection.second;
|
var r = {}, c1 = selection.first, c2 = selection.second;
|
||||||
$.each(plot.getAxes(), function (name, axis) {
|
var axes = plot.getAxes();
|
||||||
|
// look if no axis is used
|
||||||
|
var noAxisInUse = true;
|
||||||
|
$.each(axes, function (name, axis) {
|
||||||
if (axis.used) {
|
if (axis.used) {
|
||||||
|
anyUsed = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$.each(axes, function (name, axis) {
|
||||||
|
if (axis.used || noAxisInUse) {
|
||||||
var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
|
var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
|
||||||
r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
|
r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
|
|
||||||
"title": "Nginx Connections",
|
"title": "Nginx Connections",
|
||||||
"revision": "1.5",
|
"revision": 25,
|
||||||
"schemaVersion": 11,
|
"schemaVersion": 11,
|
||||||
"tags": ["tag1", "tag2"],
|
"tags": ["tag1", "tag2"],
|
||||||
"number_array": [1,2,3,10.33],
|
"number_array": [1,2,3,10.33],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"revision": "1.5",
|
"revision": 25,
|
||||||
"tags": ["tag1", "tag2"],
|
"tags": ["tag1", "tag2"],
|
||||||
"boolean_false": false,
|
"boolean_false": false,
|
||||||
"boolean_true": true,
|
"boolean_true": true,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "Nginx Memory",
|
"title": "Nginx Memory",
|
||||||
"revision": "2.0",
|
"revision": 2,
|
||||||
"schemaVersion": 11
|
"schemaVersion": 11
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user