mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into alerting
Conflicts: pkg/api/dashboard.go pkg/models/dashboards.go pkg/services/sqlstore/dashboard.go
This commit is contained in:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,3 +1,18 @@
|
|||||||
|
# 4.0-pre (unreleased)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
* **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674)
|
||||||
|
* **SingleStat**: Add seriename as option in singlestat panel, closes [#4740](https://github.com/grafana/grafana/issues/4740)
|
||||||
|
* **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)
|
||||||
|
|
||||||
|
# 3.1.0 stable (unreleased)
|
||||||
|
|
||||||
|
### Bugfixes & Enhancements,
|
||||||
|
* **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)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
@@ -11,8 +26,8 @@
|
|||||||
* **InfluxDB**: Add spread function, closes [#5211](https://github.com/grafana/grafana/issues/5211)
|
* **InfluxDB**: Add spread function, closes [#5211](https://github.com/grafana/grafana/issues/5211)
|
||||||
* **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282)
|
* **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282)
|
||||||
* **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590)
|
* **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590)
|
||||||
* **OpenTSDB**: Support nested template variables in tag_values function, closes [4398](https://github.com/grafana/grafana/issues/4398)
|
* **OpenTSDB**: Support nested template variables in tag_values function, closes [#4398](https://github.com/grafana/grafana/issues/4398)
|
||||||
* **Datasource**: Pending data source requests are cancelled before new ones are issues (Graphite & Prometheus), closes [5321](https://github.com/grafana/grafana/issues/5321)
|
* **Datasource**: Pending data source requests are cancelled before new ones are issues (Graphite & Prometheus), closes [#5321](https://github.com/grafana/grafana/issues/5321)
|
||||||
|
|
||||||
### Breaking changes
|
### Breaking changes
|
||||||
* **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log ouput.
|
* **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log ouput.
|
||||||
|
|||||||
@@ -178,6 +178,9 @@ login_hint = email or username
|
|||||||
# Default UI theme ("dark" or "light")
|
# Default UI theme ("dark" or "light")
|
||||||
default_theme = dark
|
default_theme = dark
|
||||||
|
|
||||||
|
# Allow users to sign in using username and password
|
||||||
|
allow_user_pass_login = true
|
||||||
|
|
||||||
#################################### Anonymous Auth ##########################
|
#################################### Anonymous Auth ##########################
|
||||||
[auth.anonymous]
|
[auth.anonymous]
|
||||||
# enable anonymous access
|
# enable anonymous access
|
||||||
|
|||||||
@@ -42,8 +42,9 @@
|
|||||||
# Prevents DNS rebinding attacks
|
# Prevents DNS rebinding attacks
|
||||||
;enforce_domain = false
|
;enforce_domain = false
|
||||||
|
|
||||||
# The full public facing url
|
# The full public facing url you use in browser, used for redirects and emails
|
||||||
;root_url = %(protocol)s://%(domain)s:%(http_port)s/
|
# If you use reverse proxy and sub path specify full url (with sub path)
|
||||||
|
;root_url = http://localhost:3000
|
||||||
|
|
||||||
# Log web requests
|
# Log web requests
|
||||||
;router_logging = false
|
;router_logging = false
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Name | Description
|
|||||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||||
Default | Default data source means that it will be pre-selected for new panels.
|
Default | Default data source means that it will be pre-selected for new panels.
|
||||||
Url | The http protocol, ip and port of you elasticsearch server.
|
Url | The http protocol, ip and port of you elasticsearch server.
|
||||||
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
|
Access | Proxy = access via Grafana backend, Direct = access directly from browser.
|
||||||
|
|
||||||
Proxy access means that the Grafana backend will proxy all requests from the browser, and send them on to the Data Source. This is useful because it can eliminate CORS (Cross Origin Site Resource) issues, as well as eliminate the need to disseminate authentication details to the Data Source to the browser.
|
Proxy access means that the Grafana backend will proxy all requests from the browser, and send them on to the Data Source. This is useful because it can eliminate CORS (Cross Origin Site Resource) issues, as well as eliminate the need to disseminate authentication details to the Data Source to the browser.
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Name | Description
|
|||||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||||
Default | Default data source means that it will be pre-selected for new panels.
|
Default | Default data source means that it will be pre-selected for new panels.
|
||||||
Url | The http protocol, ip and port of your graphite-web or graphite-api install.
|
Url | The http protocol, ip and port of your graphite-web or graphite-api install.
|
||||||
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
|
Access | Proxy = access via Grafana backend, Direct = access directly from browser.
|
||||||
|
|
||||||
|
|
||||||
Proxy access means that the Grafana backend will proxy all requests from the browser, and send them on to the Data Source. This is useful because it can eliminate CORS (Cross Origin Site Resource) issues, as well as eliminate the need to disseminate authentication details to the Data Source to the browser.
|
Proxy access means that the Grafana backend will proxy all requests from the browser, and send them on to the Data Source. This is useful because it can eliminate CORS (Cross Origin Site Resource) issues, as well as eliminate the need to disseminate authentication details to the Data Source to the browser.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Name | Description
|
|||||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||||
Default | Default data source means that it will be pre-selected for new panels.
|
Default | Default data source means that it will be pre-selected for new panels.
|
||||||
Url | The http protocol, ip and port of your kairosdb server (default port is usually 8080)
|
Url | The http protocol, ip and port of your kairosdb server (default port is usually 8080)
|
||||||
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
|
Access | Proxy = access via Grafana backend, Direct = access directly from browser.
|
||||||
|
|
||||||
## Query editor
|
## Query editor
|
||||||
Open a graph in edit mode by click the title.
|
Open a graph in edit mode by click the title.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Name | Description
|
|||||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||||
Default | Default data source means that it will be pre-selected for new panels.
|
Default | Default data source means that it will be pre-selected for new panels.
|
||||||
Url | The http protocol, ip and port of you opentsdb server (default port is usually 4242)
|
Url | The http protocol, ip and port of you opentsdb server (default port is usually 4242)
|
||||||
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
|
Access | Proxy = access via Grafana backend, Direct = access directly from browser.
|
||||||
Version | Version = opentsdb version, either <=2.1 or 2.2
|
Version | Version = opentsdb version, either <=2.1 or 2.2
|
||||||
Resolution | Metrics from opentsdb may have datapoints with either second or millisecond resolution.
|
Resolution | Metrics from opentsdb may have datapoints with either second or millisecond resolution.
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Name | Description
|
|||||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||||
Default | Default data source means that it will be pre-selected for new panels.
|
Default | Default data source means that it will be pre-selected for new panels.
|
||||||
Url | The http protocol, ip and port of you Prometheus server (default port is usually 9090)
|
Url | The http protocol, ip and port of you Prometheus server (default port is usually 9090)
|
||||||
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
|
Access | Proxy = access via Grafana backend, Direct = access directly from browser.
|
||||||
Basic Auth | Enable basic authentication to the Prometheus datasource.
|
Basic Auth | Enable basic authentication to the Prometheus datasource.
|
||||||
User | Name of your Prometheus user
|
User | Name of your Prometheus user
|
||||||
Password | Database user's password
|
Password | Database user's password
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ Beta .deb for Debian-based Linux | [3.1.0-beta1](https://grafanarel.s3.amazonaws
|
|||||||
|
|
||||||
Add the following line to your `/etc/apt/sources.list` file.
|
Add the following line to your `/etc/apt/sources.list` file.
|
||||||
|
|
||||||
deb https://packagecloud.io/grafana/stable/debian/ wheezy main
|
deb https://packagecloud.io/grafana/stable/debian/ jessie main
|
||||||
|
|
||||||
Use the above line even if you are on Ubuntu or another Debian version.
|
Use the above line even if you are on Ubuntu or another Debian version.
|
||||||
There is also a testing repository if you want beta or release
|
There is also a testing repository if you want beta or release
|
||||||
candidates.
|
candidates.
|
||||||
|
|
||||||
deb https://packagecloud.io/grafana/testing/debian/ wheezy main
|
deb https://packagecloud.io/grafana/testing/debian/ jessie main
|
||||||
|
|
||||||
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
|
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
|
||||||
allows you to install signed packages.
|
allows you to install signed packages.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"stable": "3.0.4",
|
"stable": "3.0.4",
|
||||||
"testing": "3.0.4"
|
"testing": "3.1.0-beta1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"company": "Coding Instinct AB"
|
"company": "Coding Instinct AB"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "3.2.0-pre1",
|
"version": "4.0.0-pre1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
@@ -53,11 +53,11 @@
|
|||||||
"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": {
|
||||||
"node": "0.4.x",
|
"node": "4.x",
|
||||||
"npm": "2.14.x"
|
"npm": "2.14.x"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -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
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -213,7 +213,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)
|
||||||
|
|||||||
@@ -32,8 +32,13 @@ func init() {
|
|||||||
"AWS/Billing": {"EstimatedCharges"},
|
"AWS/Billing": {"EstimatedCharges"},
|
||||||
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
||||||
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
||||||
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedItemCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
|
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
|
||||||
"AWS/ECS": {"CPUUtilization", "MemoryUtilization"},
|
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"},
|
||||||
|
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
|
||||||
|
"AWS/EC2Spot": {"AvailableInstancePoolsCount", "BidsSubmittedForCapacity", "EligibleInstancePoolCount", "FulfilledCapacity", "MaxPercentCapacityAllocation", "PendingCapacity", "PercentCapacityAllocation", "TargetCapacity", "TerminatingCapacity"},
|
||||||
|
"AWS/ECS": {"CPUReservation", "MemoryReservation", "CPUUtilization", "MemoryUtilization"},
|
||||||
|
"AWS/EFS": {"BurstCreditBalance", "ClientConnections", "DataReadIOBytes", "DataWriteIOBytes", "MetadataIOBytes", "TotalIOBytes", "PermittedThroughput", "PercentIOLimit"},
|
||||||
|
"AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"},
|
||||||
"AWS/ElastiCache": {
|
"AWS/ElastiCache": {
|
||||||
"CPUUtilization", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", "SwapUsage",
|
"CPUUtilization", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", "SwapUsage",
|
||||||
"BytesUsedForCacheItems", "BytesReadIntoMemcached", "BytesWrittenOutFromMemcached", "CasBadval", "CasHits", "CasMisses", "CmdFlush", "CmdGet", "CmdSet", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "Evictions", "GetHits", "GetMisses", "IncrHits", "IncrMisses", "Reclaimed",
|
"BytesUsedForCacheItems", "BytesReadIntoMemcached", "BytesWrittenOutFromMemcached", "CasBadval", "CasHits", "CasMisses", "CmdFlush", "CmdGet", "CmdSet", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "Evictions", "GetHits", "GetMisses", "IncrHits", "IncrMisses", "Reclaimed",
|
||||||
@@ -42,9 +47,6 @@ func init() {
|
|||||||
"BytesUsedForCache", "CacheHits", "CacheMisses", "CurrConnections", "Evictions", "HyperLogLogBasedCmds", "NewConnections", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress",
|
"BytesUsedForCache", "CacheHits", "CacheMisses", "CurrConnections", "Evictions", "HyperLogLogBasedCmds", "NewConnections", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress",
|
||||||
"CurrItems", "GetTypeCmds", "HashBasedCmds", "KeyBasedCmds", "ListBasedCmds", "SetBasedCmds", "SetTypeCmds", "SortedSetBasedCmds", "StringBasedCmds",
|
"CurrItems", "GetTypeCmds", "HashBasedCmds", "KeyBasedCmds", "ListBasedCmds", "SetBasedCmds", "SetTypeCmds", "SortedSetBasedCmds", "StringBasedCmds",
|
||||||
},
|
},
|
||||||
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"},
|
|
||||||
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
|
|
||||||
"AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"},
|
|
||||||
"AWS/ElasticBeanstalk": {
|
"AWS/ElasticBeanstalk": {
|
||||||
"EnvironmentHealth",
|
"EnvironmentHealth",
|
||||||
"ApplicationLatencyP10", "ApplicationLatencyP50", "ApplicationLatencyP75", "ApplicationLatencyP85", "ApplicationLatencyP90", "ApplicationLatencyP95", "ApplicationLatencyP99", "ApplicationLatencyP99.9",
|
"ApplicationLatencyP10", "ApplicationLatencyP50", "ApplicationLatencyP75", "ApplicationLatencyP85", "ApplicationLatencyP90", "ApplicationLatencyP95", "ApplicationLatencyP99", "ApplicationLatencyP99.9",
|
||||||
@@ -73,13 +75,13 @@ func init() {
|
|||||||
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||||
"AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "CPUCreditUsage", "CPUCreditBalance", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"},
|
"AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "CPUCreditUsage", "CPUCreditBalance", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"},
|
||||||
"AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
|
"AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
|
||||||
|
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
|
||||||
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
|
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
|
||||||
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
|
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
|
||||||
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
|
|
||||||
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
|
|
||||||
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
|
|
||||||
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
|
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
|
||||||
"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
|
"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
|
||||||
|
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
|
||||||
|
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
|
||||||
"AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
|
"AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
|
||||||
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
|
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
|
||||||
}
|
}
|
||||||
@@ -88,29 +90,31 @@ func init() {
|
|||||||
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
||||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||||
"AWS/CloudSearch": {},
|
"AWS/CloudSearch": {},
|
||||||
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation"},
|
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
|
||||||
"AWS/ECS": {"ClusterName", "ServiceName"},
|
|
||||||
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
|
|
||||||
"AWS/EBS": {"VolumeId"},
|
"AWS/EBS": {"VolumeId"},
|
||||||
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
|
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
|
||||||
|
"AWS/EC2Spot": {"AvailabilityZone", "FleetRequestId", "InstanceType"},
|
||||||
|
"AWS/ECS": {"ClusterName", "ServiceName"},
|
||||||
|
"AWS/EFS": {"FileSystemId"},
|
||||||
"AWS/ELB": {"LoadBalancerName", "AvailabilityZone"},
|
"AWS/ELB": {"LoadBalancerName", "AvailabilityZone"},
|
||||||
|
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
|
||||||
"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
|
"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
|
||||||
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
|
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
|
||||||
"AWS/ES": {"ClientId", "DomainName"},
|
"AWS/ES": {"ClientId", "DomainName"},
|
||||||
"AWS/Events": {"RuleName"},
|
"AWS/Events": {"RuleName"},
|
||||||
"AWS/Kinesis": {"StreamName", "ShardID"},
|
"AWS/Kinesis": {"StreamName", "ShardID"},
|
||||||
"AWS/Lambda": {"FunctionName"},
|
"AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"},
|
||||||
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
|
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
|
||||||
"AWS/ML": {"MLModelId", "RequestMode"},
|
"AWS/ML": {"MLModelId", "RequestMode"},
|
||||||
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
|
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
|
||||||
"AWS/Redshift": {"NodeID", "ClusterIdentifier"},
|
"AWS/Redshift": {"NodeID", "ClusterIdentifier"},
|
||||||
"AWS/RDS": {"DBInstanceIdentifier", "DatabaseClass", "EngineName"},
|
"AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DatabaseClass", "EngineName"},
|
||||||
"AWS/Route53": {"HealthCheckId"},
|
"AWS/Route53": {"HealthCheckId"},
|
||||||
|
"AWS/S3": {"BucketName", "StorageType"},
|
||||||
"AWS/SNS": {"Application", "Platform", "TopicName"},
|
"AWS/SNS": {"Application", "Platform", "TopicName"},
|
||||||
"AWS/SQS": {"QueueName"},
|
"AWS/SQS": {"QueueName"},
|
||||||
"AWS/S3": {"BucketName", "StorageType"},
|
|
||||||
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
|
|
||||||
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
||||||
|
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
|
||||||
"AWS/WAF": {"Rule", "WebACL"},
|
"AWS/WAF": {"Rule", "WebACL"},
|
||||||
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
|
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"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"
|
||||||
@@ -110,7 +111,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 {
|
||||||
@@ -123,31 +124,33 @@ 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 := "Dashboard is belongs to plugin " + pluginErr.PluginId + "."
|
||||||
|
// look up plugin name
|
||||||
|
if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist {
|
||||||
|
message = "Dashboard is 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.AlertingEnabled {
|
if setting.AlertingEnabled {
|
||||||
@@ -158,13 +161,12 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||||
c.JsonApiErr(500, "Failed to save alerts", err)
|
return ApiError(500, "Failed to save alerts", 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 {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type CurrentUser struct {
|
|||||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||||
GravatarUrl string `json:"gravatarUrl"`
|
GravatarUrl string `json:"gravatarUrl"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardMeta struct {
|
type DashboardMeta struct {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
@@ -21,6 +23,15 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
}
|
}
|
||||||
prefs := prefsQuery.Result
|
prefs := prefsQuery.Result
|
||||||
|
|
||||||
|
// Read locale from acccept-language
|
||||||
|
acceptLang := c.Req.Header.Get("Accept-Language")
|
||||||
|
locale := "en-US"
|
||||||
|
|
||||||
|
if len(acceptLang) > 0 {
|
||||||
|
parts := strings.Split(acceptLang, ",")
|
||||||
|
locale = parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
var data = dtos.IndexViewData{
|
var data = dtos.IndexViewData{
|
||||||
User: &dtos.CurrentUser{
|
User: &dtos.CurrentUser{
|
||||||
Id: c.UserId,
|
Id: c.UserId,
|
||||||
@@ -35,6 +46,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||||
LightTheme: prefs.Theme == "light",
|
LightTheme: prefs.Theme == "light",
|
||||||
Timezone: prefs.Timezone,
|
Timezone: prefs.Timezone,
|
||||||
|
Locale: locale,
|
||||||
},
|
},
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
AppUrl: setting.AppUrl,
|
AppUrl: setting.AppUrl,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func LoginView(c *middleware.Context) {
|
|||||||
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||||
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||||
viewData.Settings["loginHint"] = setting.LoginHint
|
viewData.Settings["loginHint"] = setting.LoginHint
|
||||||
|
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
|
||||||
|
|
||||||
if !tryLoginUsingRememberCookie(c) {
|
if !tryLoginUsingRememberCookie(c) {
|
||||||
c.HTML(200, VIEW_INDEX, viewData)
|
c.HTML(200, VIEW_INDEX, viewData)
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ type CommandLine interface {
|
|||||||
GlobalString(name string) string
|
GlobalString(name string) string
|
||||||
FlagNames() (names []string)
|
FlagNames() (names []string)
|
||||||
Generic(name string) interface{}
|
Generic(name string) interface{}
|
||||||
|
|
||||||
|
PluginDirectory() string
|
||||||
|
RepoDirectory() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type contextCommandLine struct {
|
type contextCommandLine struct {
|
||||||
@@ -33,3 +36,11 @@ func (c *contextCommandLine) ShowVersion() {
|
|||||||
func (c *contextCommandLine) Application() *cli.App {
|
func (c *contextCommandLine) Application() *cli.App {
|
||||||
return c.App
|
return c.App
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *contextCommandLine) PluginDirectory() string {
|
||||||
|
return c.GlobalString("pluginsDir")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *contextCommandLine) RepoDirectory() string {
|
||||||
|
return c.GlobalString("repo")
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,3 +93,11 @@ func (fcli *FakeCommandLine) Args() cli.Args {
|
|||||||
func (fcli *FakeCommandLine) ShowVersion() {
|
func (fcli *FakeCommandLine) ShowVersion() {
|
||||||
fcli.VersionShown = true
|
fcli.VersionShown = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fcli *FakeCommandLine) RepoDirectory() string {
|
||||||
|
return fcli.GlobalString("repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fcli *FakeCommandLine) PluginDirectory() string {
|
||||||
|
return fcli.GlobalString("pluginsDir")
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
|
|||||||
return errors.New("please specify plugin to install")
|
return errors.New("please specify plugin to install")
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginsDir := c.GlobalString("pluginsDir")
|
pluginsDir := c.PluginDirectory()
|
||||||
if pluginsDir == "" {
|
if pluginsDir == "" {
|
||||||
return errors.New("missing pluginsDir flag")
|
return errors.New("missing pluginsDir flag")
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func installCommand(c CommandLine) error {
|
func installCommand(c CommandLine) error {
|
||||||
pluginFolder := c.GlobalString("pluginsDir")
|
pluginFolder := c.PluginDirectory()
|
||||||
if err := validateInput(c, pluginFolder); err != nil {
|
if err := validateInput(c, pluginFolder); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -58,8 +58,8 @@ func installCommand(c CommandLine) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InstallPlugin(pluginName, version string, c CommandLine) error {
|
func InstallPlugin(pluginName, version string, c CommandLine) error {
|
||||||
plugin, err := s.GetPlugin(pluginName, c.GlobalString("repo"))
|
plugin, err := s.GetPlugin(pluginName, c.RepoDirectory())
|
||||||
pluginFolder := c.GlobalString("pluginsDir")
|
pluginFolder := c.PluginDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func listremoteCommand(c CommandLine) error {
|
func listremoteCommand(c CommandLine) error {
|
||||||
plugin, err := s.ListAllPlugins(c.GlobalString("repo"))
|
plugin, err := s.ListAllPlugins(c.RepoDirectory())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var validateLsCommand = func(pluginDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func lsCommand(c CommandLine) error {
|
func lsCommand(c CommandLine) error {
|
||||||
pluginDir := c.GlobalString("pluginsDir")
|
pluginDir := c.PluginDirectory()
|
||||||
if err := validateLsCommand(pluginDir); err != nil {
|
if err := validateLsCommand(pluginDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,32 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||||
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
|
var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
|
||||||
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
|
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
|
||||||
|
|
||||||
func removeCommand(c CommandLine) error {
|
func removeCommand(c CommandLine) error {
|
||||||
pluginPath := c.GlobalString("pluginsDir")
|
pluginPath := c.PluginDirectory()
|
||||||
localPlugins := getPluginss(pluginPath)
|
|
||||||
|
|
||||||
plugin := c.Args().First()
|
plugin := c.Args().First()
|
||||||
if plugin == "" {
|
if plugin == "" {
|
||||||
return errors.New("Missing plugin parameter")
|
return errors.New("Missing plugin parameter")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range localPlugins {
|
err := removePlugin(pluginPath, plugin)
|
||||||
if p.Id == c.Args().First() {
|
|
||||||
removePlugin(pluginPath, p.Id)
|
if err != nil {
|
||||||
return nil
|
if strings.Contains(err.Error(), "no such file or directory") {
|
||||||
}
|
return fmt.Errorf("Plugin does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Could not find plugin named %s", c.Args().First())
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func ShouldUpgrade(installed string, remote m.Plugin) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func upgradeAllCommand(c CommandLine) error {
|
func upgradeAllCommand(c CommandLine) error {
|
||||||
pluginsDir := c.GlobalString("pluginsDir")
|
pluginsDir := c.PluginDirectory()
|
||||||
|
|
||||||
localPlugins := s.GetLocalPlugins(pluginsDir)
|
localPlugins := s.GetLocalPlugins(pluginsDir)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func upgradeCommand(c CommandLine) error {
|
func upgradeCommand(c CommandLine) error {
|
||||||
pluginsDir := c.GlobalString("pluginsDir")
|
pluginsDir := c.PluginDirectory()
|
||||||
pluginName := c.Args().First()
|
pluginName := c.Args().First()
|
||||||
|
|
||||||
localPlugin, err := s.ReadPlugin(pluginsDir, pluginName)
|
localPlugin, err := s.ReadPlugin(pluginsDir, pluginName)
|
||||||
@@ -16,7 +16,7 @@ func upgradeCommand(c CommandLine) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err2 := s.GetPlugin(localPlugin.Id, c.GlobalString("repo"))
|
v, err2 := s.GetPlugin(localPlugin.Id, c.RepoDirectory())
|
||||||
|
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
return err2
|
return err2
|
||||||
|
|||||||
@@ -75,9 +75,16 @@ func GetLocalPlugins(pluginDir string) []m.InstalledPlugin {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveInstalledPlugin(pluginPath, id string) error {
|
func RemoveInstalledPlugin(pluginPath, pluginName string) error {
|
||||||
logger.Infof("Removing plugin: %v\n", id)
|
logger.Infof("Removing plugin: %v\n", pluginName)
|
||||||
return IoHelper.RemoveAll(path.Join(pluginPath, id))
|
pluginDir := path.Join(pluginPath, pluginName)
|
||||||
|
|
||||||
|
_, err := IoHelper.Stat(pluginDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return IoHelper.RemoveAll(pluginDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
|
func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *ldapUserInfo) error {
|
|||||||
|
|
||||||
func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
|
func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
|
||||||
if len(a.server.LdapGroups) == 0 {
|
if len(a.server.LdapGroups) == 0 {
|
||||||
|
log.Warn("Ldap: no group mappings defined")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -154,6 +165,12 @@ type GetDashboardsQuery struct {
|
|||||||
Result []*Dashboard
|
Result []*Dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetDashboardsByPluginIdQuery struct {
|
||||||
|
OrgId int64
|
||||||
|
PluginId string
|
||||||
|
Result []*Dashboard
|
||||||
|
}
|
||||||
|
|
||||||
type GetDashboardSlugByIdQuery struct {
|
type GetDashboardSlugByIdQuery struct {
|
||||||
Id int64
|
Id int64
|
||||||
Result string
|
Result string
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -261,6 +267,19 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
|
|||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type DashboardSlugDTO struct {
|
type DashboardSlugDTO struct {
|
||||||
Slug string
|
Slug string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ var (
|
|||||||
VerifyEmailEnabled bool
|
VerifyEmailEnabled bool
|
||||||
LoginHint string
|
LoginHint string
|
||||||
DefaultTheme string
|
DefaultTheme string
|
||||||
|
AllowUserPassLogin bool
|
||||||
|
|
||||||
// Http auth
|
// Http auth
|
||||||
AdminUser string
|
AdminUser string
|
||||||
@@ -286,19 +287,19 @@ func evalConfigValues() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSpecifedConfigFile(configFile string) {
|
func loadSpecifedConfigFile(configFile string) error {
|
||||||
if configFile == "" {
|
if configFile == "" {
|
||||||
configFile = filepath.Join(HomePath, "conf/custom.ini")
|
configFile = filepath.Join(HomePath, "conf/custom.ini")
|
||||||
// return without error if custom file does not exist
|
// return without error if custom file does not exist
|
||||||
if !pathExists(configFile) {
|
if !pathExists(configFile) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userConfig, err := ini.Load(configFile)
|
userConfig, err := ini.Load(configFile)
|
||||||
userConfig.BlockMode = false
|
userConfig.BlockMode = false
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(3, "Failed to parse %v, %v", configFile, err)
|
return fmt.Errorf("Failed to parse %v, %v", configFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, section := range userConfig.Sections() {
|
for _, section := range userConfig.Sections() {
|
||||||
@@ -320,6 +321,7 @@ func loadSpecifedConfigFile(configFile string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configFiles = append(configFiles, configFile)
|
configFiles = append(configFiles, configFile)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfiguration(args *CommandLineArgs) {
|
func loadConfiguration(args *CommandLineArgs) {
|
||||||
@@ -341,12 +343,12 @@ func loadConfiguration(args *CommandLineArgs) {
|
|||||||
// load default overrides
|
// load default overrides
|
||||||
applyCommandLineDefaultProperties(commandLineProps)
|
applyCommandLineDefaultProperties(commandLineProps)
|
||||||
|
|
||||||
// init logging before specific config so we can log errors from here on
|
|
||||||
DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
|
|
||||||
initLogging()
|
|
||||||
|
|
||||||
// load specified config file
|
// load specified config file
|
||||||
loadSpecifedConfigFile(args.Config)
|
err = loadSpecifedConfigFile(args.Config)
|
||||||
|
if err != nil {
|
||||||
|
initLogging()
|
||||||
|
log.Fatal(3, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// apply environment overrides
|
// apply environment overrides
|
||||||
applyEnvVariableOverrides()
|
applyEnvVariableOverrides()
|
||||||
@@ -488,6 +490,7 @@ func NewConfigContext(args *CommandLineArgs) error {
|
|||||||
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
|
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
|
||||||
LoginHint = users.Key("login_hint").String()
|
LoginHint = users.Key("login_hint").String()
|
||||||
DefaultTheme = users.Key("default_theme").String()
|
DefaultTheme = users.Key("default_theme").String()
|
||||||
|
AllowUserPassLogin = users.Key("allow_user_pass_login").MustBool(true)
|
||||||
|
|
||||||
// anonymous access
|
// anonymous access
|
||||||
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
|
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import $ from 'jquery';
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
import {coreModule} from './core/core';
|
import {coreModule} from './core/core';
|
||||||
|
|
||||||
export class GrafanaApp {
|
export class GrafanaApp {
|
||||||
@@ -41,10 +42,14 @@ export class GrafanaApp {
|
|||||||
var app = angular.module('grafana', []);
|
var app = angular.module('grafana', []);
|
||||||
app.constant('grafanaVersion', "@grafanaVersion@");
|
app.constant('grafanaVersion', "@grafanaVersion@");
|
||||||
|
|
||||||
|
moment.locale(config.bootData.user.locale);
|
||||||
|
|
||||||
app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => {
|
app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => {
|
||||||
|
|
||||||
if (config.buildInfo.env !== 'development') {
|
if (config.buildInfo.env !== 'development') {
|
||||||
$compileProvider.debugInfoEnabled(false);
|
$compileProvider.debugInfoEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$httpProvider.useApplyAsync(true);
|
$httpProvider.useApplyAsync(true);
|
||||||
|
|
||||||
this.registerFunctions.controller = $controllerProvider.register;
|
this.registerFunctions.controller = $controllerProvider.register;
|
||||||
|
|||||||
@@ -72,6 +72,10 @@
|
|||||||
Import
|
Import
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="pull-right small muted" target="_blank" href="https://grafana.net/dashboards?utm_source=grafana_search">
|
||||||
|
Explore ready made dashboards on Grafana.net
|
||||||
|
</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="login" class="sidemenu-item" target="_self">
|
<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>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export class SideMenuCtrl {
|
|||||||
mainLinks: any;
|
mainLinks: any;
|
||||||
orgMenu: any;
|
orgMenu: any;
|
||||||
appSubUrl: string;
|
appSubUrl: string;
|
||||||
|
loginUrl: string;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $scope, private $location, private contextSrv, private backendSrv, private $element) {
|
constructor(private $scope, private $location, private contextSrv, private backendSrv, private $element) {
|
||||||
@@ -22,13 +23,14 @@ 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.$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());
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl(url) {
|
getUrl(url) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ function (angular, coreModule, config) {
|
|||||||
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
||||||
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
||||||
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled;
|
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled;
|
||||||
|
$scope.allowUserPassLogin = config.allowUserPassLogin;
|
||||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||||
$scope.loginHint = config.loginHint;
|
$scope.loginHint = config.loginHint;
|
||||||
|
|
||||||
@@ -69,7 +70,12 @@ function (angular, coreModule, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backendSrv.post('/login', $scope.formModel).then(function(result) {
|
backendSrv.post('/login', $scope.formModel).then(function(result) {
|
||||||
if (result.redirectUrl) {
|
var params = $location.search();
|
||||||
|
|
||||||
|
if (params.redirect && params.redirect[0] === '/') {
|
||||||
|
window.location.href = config.appSubUrl + params.redirect;
|
||||||
|
}
|
||||||
|
else if (result.redirectUrl) {
|
||||||
window.location.href = result.redirectUrl;
|
window.location.href = result.redirectUrl;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = config.appSubUrl + '/';
|
window.location.href = config.appSubUrl + '/';
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ export class AlertSrv {
|
|||||||
}, this.$rootScope);
|
}, this.$rootScope);
|
||||||
|
|
||||||
appEvents.on('confirm-modal', this.showConfirmModal.bind(this));
|
appEvents.on('confirm-modal', this.showConfirmModal.bind(this));
|
||||||
|
|
||||||
this.$rootScope.onAppEvent('confirm-modal', (e, data) => {
|
|
||||||
this.showConfirmModal(data);
|
|
||||||
}, this.$rootScope);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set(title, text, severity, timeout) {
|
set(title, text, severity, timeout) {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export class BackendSrv {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$timeout(this.requestErrorHandler.bind(this), 50);
|
this.$timeout(this.requestErrorHandler.bind(this, err), 50);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -368,19 +368,43 @@ 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('£');
|
||||||
kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
|
kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
|
||||||
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
|
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
|
||||||
|
|
||||||
// Data
|
// Data (Binary)
|
||||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||||
kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B');
|
kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B');
|
||||||
kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1);
|
kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1);
|
||||||
kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2);
|
kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2);
|
||||||
kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3);
|
kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3);
|
||||||
|
|
||||||
|
// Data (Decimal)
|
||||||
|
kbn.valueFormats.decbits = kbn.formatBuilders.decimalSIPrefix('b');
|
||||||
|
kbn.valueFormats.decbytes = kbn.formatBuilders.decimalSIPrefix('B');
|
||||||
|
kbn.valueFormats.deckbytes = kbn.formatBuilders.decimalSIPrefix('B', 1);
|
||||||
|
kbn.valueFormats.decmbytes = kbn.formatBuilders.decimalSIPrefix('B', 2);
|
||||||
|
kbn.valueFormats.decgbytes = kbn.formatBuilders.decimalSIPrefix('B', 3);
|
||||||
|
|
||||||
// Data Rate
|
// Data Rate
|
||||||
kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps');
|
kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps');
|
||||||
kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps');
|
kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps');
|
||||||
@@ -397,6 +421,9 @@ function($, _, moment) {
|
|||||||
kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps');
|
kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps');
|
||||||
kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps');
|
kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps');
|
||||||
kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops');
|
kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops');
|
||||||
|
kbn.valueFormats.opm = kbn.formatBuilders.simpleCountUnit('opm');
|
||||||
|
kbn.valueFormats.rpm = kbn.formatBuilders.simpleCountUnit('rpm');
|
||||||
|
kbn.valueFormats.wpm = kbn.formatBuilders.simpleCountUnit('wpm');
|
||||||
|
|
||||||
// Energy
|
// Energy
|
||||||
kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
|
kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
|
||||||
@@ -607,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' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -634,13 +663,23 @@ function($, _, moment) {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'data',
|
text: 'data (IEC)',
|
||||||
submenu: [
|
submenu: [
|
||||||
{text: 'bits', value: 'bits' },
|
{text: 'bits', value: 'bits' },
|
||||||
{text: 'bytes', value: 'bytes' },
|
{text: 'bytes', value: 'bytes' },
|
||||||
{text: 'kilobytes', value: 'kbytes'},
|
{text: 'kibibytes', value: 'kbytes'},
|
||||||
{text: 'megabytes', value: 'mbytes'},
|
{text: 'mebibytes', value: 'mbytes'},
|
||||||
{text: 'gigabytes', value: 'gbytes'},
|
{text: 'gibibytes', value: 'gbytes'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'data (Metric)',
|
||||||
|
submenu: [
|
||||||
|
{text: 'bits', value: 'decbits' },
|
||||||
|
{text: 'bytes', value: 'decbytes' },
|
||||||
|
{text: 'kilobytes', value: 'deckbytes'},
|
||||||
|
{text: 'megabytes', value: 'decmbytes'},
|
||||||
|
{text: 'gigabytes', value: 'decgbytes'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -664,6 +703,9 @@ function($, _, moment) {
|
|||||||
{text: 'reads/sec (rps)', value: 'rps' },
|
{text: 'reads/sec (rps)', value: 'rps' },
|
||||||
{text: 'writes/sec (wps)', value: 'wps' },
|
{text: 'writes/sec (wps)', value: 'wps' },
|
||||||
{text: 'I/O ops/sec (iops)', value: 'iops'},
|
{text: 'I/O ops/sec (iops)', value: 'iops'},
|
||||||
|
{text: 'ops/min (opm)', value: 'opm' },
|
||||||
|
{text: 'reads/min (rpm)', value: 'rpm' },
|
||||||
|
{text: 'writes/min (wpm)', value: 'wpm' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export class DashboardCtrl {
|
|||||||
dynamicDashboardSrv,
|
dynamicDashboardSrv,
|
||||||
dashboardViewStateSrv,
|
dashboardViewStateSrv,
|
||||||
contextSrv,
|
contextSrv,
|
||||||
|
alertSrv,
|
||||||
$timeout) {
|
$timeout) {
|
||||||
|
|
||||||
$scope.editor = { index: 0 };
|
$scope.editor = { index: 0 };
|
||||||
@@ -29,6 +30,14 @@ export class DashboardCtrl {
|
|||||||
var resizeEventTimeout;
|
var resizeEventTimeout;
|
||||||
|
|
||||||
$scope.setupDashboard = function(data) {
|
$scope.setupDashboard = function(data) {
|
||||||
|
try {
|
||||||
|
$scope.setupDashboardInternal(data);
|
||||||
|
} catch (err) {
|
||||||
|
$scope.onInitFailed(err, 'Dashboard init failed', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setupDashboardInternal = function(data) {
|
||||||
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
|
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
|
||||||
dashboardSrv.setCurrent(dashboard);
|
dashboardSrv.setCurrent(dashboard);
|
||||||
|
|
||||||
@@ -37,9 +46,12 @@ export class DashboardCtrl {
|
|||||||
|
|
||||||
// template values service needs to initialize completely before
|
// template values service needs to initialize completely before
|
||||||
// the rest of the dashboard can load
|
// the rest of the dashboard can load
|
||||||
templateValuesSrv.init(dashboard).finally(function() {
|
templateValuesSrv.init(dashboard)
|
||||||
|
// template values failes are non fatal
|
||||||
|
.catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
|
||||||
|
// continue
|
||||||
|
.finally(function() {
|
||||||
dynamicDashboardSrv.init(dashboard);
|
dynamicDashboardSrv.init(dashboard);
|
||||||
|
|
||||||
unsavedChangesSrv.init(dashboard, $scope);
|
unsavedChangesSrv.init(dashboard, $scope);
|
||||||
|
|
||||||
$scope.dashboard = dashboard;
|
$scope.dashboard = dashboard;
|
||||||
@@ -52,13 +64,30 @@ export class DashboardCtrl {
|
|||||||
$scope.setWindowTitleAndTheme();
|
$scope.setWindowTitleAndTheme();
|
||||||
|
|
||||||
$scope.appEvent("dashboard-initialized", $scope.dashboard);
|
$scope.appEvent("dashboard-initialized", $scope.dashboard);
|
||||||
}).catch(function(err) {
|
})
|
||||||
if (err.data && err.data.message) { err.message = err.data.message; }
|
.catch($scope.onInitFailed.bind(this, 'Dashboard init failed', true));
|
||||||
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
|
};
|
||||||
});
|
|
||||||
|
$scope.onInitFailed = function(msg, fatal, err) {
|
||||||
|
console.log(msg, err);
|
||||||
|
|
||||||
|
if (err.data && err.data.message) {
|
||||||
|
err.message = err.data.message;
|
||||||
|
} else if (!err.message) {
|
||||||
|
err = {message: err.toString()};
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.appEvent("alert-error", [msg, err.message]);
|
||||||
|
|
||||||
|
// protect against recursive fallbacks
|
||||||
|
if (fatal && !$scope.loadedFallbackDashboard) {
|
||||||
|
$scope.loadedFallbackDashboard = true;
|
||||||
|
$scope.setupDashboard({dashboard: {title: 'Dashboard Init failed'}});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.templateVariableUpdated = function() {
|
$scope.templateVariableUpdated = function() {
|
||||||
|
console.log('dynamic update');
|
||||||
dynamicDashboardSrv.update($scope.dashboard);
|
dynamicDashboardSrv.update($scope.dashboard);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
<li ng-show="::dashboardMeta.canSave">
|
<li ng-show="::dashboardMeta.canSave">
|
||||||
<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
|
<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li ng-if="dashboard.snapshot.originalUrl">
|
||||||
|
<a ng-href="{{dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
|
||||||
|
</li>
|
||||||
<li ng-if="::showSettingsMenu" class="dropdown">
|
<li ng-if="::showSettingsMenu" class="dropdown">
|
||||||
<a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Manage dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
|
<a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Manage dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
|||||||
@@ -134,6 +134,21 @@ 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 overwritten next time you update the plugin. Use Save As to create custom version.',
|
||||||
|
yesText: "Save & Overwrite",
|
||||||
|
icon: "fa-warning",
|
||||||
|
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() {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ function (angular, _) {
|
|||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!external) {
|
||||||
|
$scope.dashboard.snapshot.originalUrl = $location.absUrl();
|
||||||
|
}
|
||||||
|
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.snapshot.external = external;
|
$scope.snapshot.external = external;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="openFromPicker">
|
<div ng-if="openFromPicker">
|
||||||
<datepicker ng-model="ctrl.absolute.fromJs" class="gf-timepicker-component" show-weeks="false" ng-change="ctrl.absoluteFromChanged()"></datepicker>
|
<datepicker ng-model="ctrl.absolute.fromJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteFromChanged()"></datepicker>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="openToPicker">
|
<div ng-if="openToPicker">
|
||||||
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="small">Refreshing every:</label>
|
<label class="small">Refreshing every:</label>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class TimePickerCtrl {
|
|||||||
refresh: any;
|
refresh: any;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isUtc: boolean;
|
isUtc: boolean;
|
||||||
|
firstDayOfWeek: number;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $scope, private $rootScope, private timeSrv) {
|
constructor(private $scope, private $rootScope, private timeSrv) {
|
||||||
@@ -43,6 +44,8 @@ export class TimePickerCtrl {
|
|||||||
|
|
||||||
_.defaults(this.panel, TimePickerCtrl.defaults);
|
_.defaults(this.panel, TimePickerCtrl.defaults);
|
||||||
|
|
||||||
|
this.firstDayOfWeek = moment.localeData().firstDayOfWeek();
|
||||||
|
|
||||||
var time = angular.copy(this.timeSrv.timeRange());
|
var time = angular.copy(this.timeSrv.timeRange());
|
||||||
var timeRaw = angular.copy(this.timeSrv.timeRange(false));
|
var timeRaw = angular.copy(this.timeSrv.timeRange(false));
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button class="tabbed-view-close-btn" ng-click="dismiss();dashboard.refresh();">
|
<button class="tabbed-view-close-btn" ng-click="dismiss();">
|
||||||
<i class="fa fa-remove"></i>
|
<i class="fa fa-remove"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
define([
|
define([
|
||||||
'angular',
|
'angular',
|
||||||
'lodash',
|
'lodash',
|
||||||
|
'jquery',
|
||||||
'app/core/utils/kbn',
|
'app/core/utils/kbn',
|
||||||
],
|
],
|
||||||
function (angular, _, kbn) {
|
function (angular, _, $, kbn) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var module = angular.module('grafana.services');
|
var module = angular.module('grafana.services');
|
||||||
@@ -27,7 +28,16 @@ function (angular, _, kbn) {
|
|||||||
.filter(function(variable) {
|
.filter(function(variable) {
|
||||||
return variable.refresh === 2;
|
return variable.refresh === 2;
|
||||||
}).map(function(variable) {
|
}).map(function(variable) {
|
||||||
return self.updateOptions(variable);
|
var previousOptions = variable.options.slice();
|
||||||
|
|
||||||
|
return self.updateOptions(variable).then(function () {
|
||||||
|
return self.variableUpdated(variable).then(function () {
|
||||||
|
// check if current options changed due to refresh
|
||||||
|
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
|
||||||
|
$rootScope.appEvent('template-variable-value-updated');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return $q.all(promises);
|
return $q.all(promises);
|
||||||
@@ -35,6 +45,7 @@ function (angular, _, kbn) {
|
|||||||
}, $rootScope);
|
}, $rootScope);
|
||||||
|
|
||||||
this.init = function(dashboard) {
|
this.init = function(dashboard) {
|
||||||
|
this.dashboard = dashboard;
|
||||||
this.variables = dashboard.templating.list;
|
this.variables = dashboard.templating.list;
|
||||||
templateSrv.init(this.variables);
|
templateSrv.init(this.variables);
|
||||||
|
|
||||||
@@ -145,7 +156,7 @@ function (angular, _, kbn) {
|
|||||||
|
|
||||||
this.variableUpdated = function(variable) {
|
this.variableUpdated = function(variable) {
|
||||||
templateSrv.updateTemplateData();
|
templateSrv.updateTemplateData();
|
||||||
return this.updateOptionsInChildVariables(variable);
|
return self.updateOptionsInChildVariables(variable);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateOptionsInChildVariables = function(updatedVariable) {
|
this.updateOptionsInChildVariables = function(updatedVariable) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form name="loginForm" class="login-form gf-form-group">
|
<form name="loginForm" class="login-form gf-form-group" ng-show="allowUserPassLogin">
|
||||||
<div class="gf-form" ng-if="loginMode">
|
<div class="gf-form" ng-if="loginMode">
|
||||||
<span class="gf-form-label width-7">User</span>
|
<span class="gf-form-label width-7">User</span>
|
||||||
<input type="text" name="username" class="gf-form-input max-width-14" required ng-model='formModel.user' placeholder={{loginHint}}>
|
<input type="text" name="username" class="gf-form-input max-width-14" required ng-model='formModel.user' placeholder={{loginHint}}>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div ng-if="loginMode">
|
<div ng-if="loginMode">
|
||||||
<div class="text-center login-divider" ng-if="oauthEnabled">
|
<div class="text-center login-divider" ng-show="oauthEnabled && allowUserPassLogin">
|
||||||
<div class="login-divider-line">
|
<div class="login-divider-line">
|
||||||
<span class="login-divider-text">
|
<span class="login-divider-text">
|
||||||
Or login with
|
Or login with
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<div class="login-oauth text-center" ng-if="oauthEnabled">
|
<div class="login-oauth text-center" ng-show="oauthEnabled">
|
||||||
<a class="btn btn-large btn-google" href="login/google" target="_self" ng-if="googleAuthEnabled">
|
<a class="btn btn-large btn-google" href="login/google" target="_self" ng-if="googleAuthEnabled">
|
||||||
<i class="fa fa-google"></i>
|
<i class="fa fa-google"></i>
|
||||||
with Google
|
with Google
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<div class="text-center password-recovery">
|
<div class="text-center password-recovery" ng-show="allowUserPassLogin">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="user/password/send-reset-email">
|
<a href="user/password/send-reset-email">
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
|
|||||||
@@ -205,13 +205,8 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
|||||||
};
|
};
|
||||||
|
|
||||||
function escapeForJson(value) {
|
function escapeForJson(value) {
|
||||||
return value
|
var luceneQuery = JSON.stringify(value);
|
||||||
.replace(/\s/g, '\\ ')
|
return luceneQuery.substr(1, luceneQuery.length - 2);
|
||||||
.replace(/\"/g, '\\"');
|
|
||||||
}
|
|
||||||
|
|
||||||
function luceneThenJsonFormat(value) {
|
|
||||||
return escapeForJson(templateSrv.luceneFormat(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getFields = function(query) {
|
this.getFields = function(query) {
|
||||||
@@ -256,12 +251,12 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
|||||||
var header = this.getQueryHeader('count', range.from, range.to);
|
var header = this.getQueryHeader('count', range.from, range.to);
|
||||||
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
|
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
|
||||||
|
|
||||||
esQuery = esQuery.replace("$lucene_query", escapeForJson(queryDef.query || '*'));
|
esQuery = esQuery.replace("$lucene_query", escapeForJson(queryDef.query));
|
||||||
esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
|
esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
|
||||||
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=count', esQuery).then(function(res) {
|
return this._post('_msearch?search_type=count', 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};
|
||||||
@@ -270,8 +265,9 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.metricFindQuery = function(query) {
|
this.metricFindQuery = function(query) {
|
||||||
query = templateSrv.replace(query, {}, luceneThenJsonFormat);
|
|
||||||
query = angular.fromJson(query);
|
query = angular.fromJson(query);
|
||||||
|
query.query = templateSrv.replace(query.query || '*', {}, 'lucene');
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return $q.when([]);
|
return $q.when([]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ export default class InfluxDatasource {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_seriesQuery(query) {
|
_seriesQuery(query) {
|
||||||
|
if (!query) { return this.$q.when({results: []}); }
|
||||||
|
|
||||||
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
|
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
|||||||
|
|
||||||
var query: any = {};
|
var query: any = {};
|
||||||
query.expr = templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr);
|
query.expr = templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr);
|
||||||
query.requestId = target.expr;
|
query.requestId = options.panelId + target.refId;
|
||||||
|
|
||||||
var interval = target.interval || options.interval;
|
var interval = target.interval || options.interval;
|
||||||
var intervalFactor = target.intervalFactor || 1;
|
var intervalFactor = target.intervalFactor || 1;
|
||||||
|
|||||||
@@ -272,8 +272,12 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholds) {
|
|||||||
function callPlot(incrementRenderCounter) {
|
function callPlot(incrementRenderCounter) {
|
||||||
try {
|
try {
|
||||||
$.plot(elem, sortedSeries, options);
|
$.plot(elem, sortedSeries, options);
|
||||||
|
delete ctrl.error;
|
||||||
|
delete ctrl.inspector;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('flotcharts error', e);
|
console.log('flotcharts error', e);
|
||||||
|
ctrl.error = e.message || "Render Error";
|
||||||
|
ctrl.inspector = {error: ctrl.error};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incrementRenderCounter) {
|
if (incrementRenderCounter) {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ define([
|
|||||||
$scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);
|
$scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);
|
||||||
$scope.addOverrideOption('Color', 'color', ['change']);
|
$scope.addOverrideOption('Color', 'color', ['change']);
|
||||||
$scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
|
$scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
|
||||||
$scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]);
|
$scope.addOverrideOption('Z-index', 'zindex', [-3,-2,-1,0,1,2,3]);
|
||||||
$scope.addOverrideOption('Transform', 'transform', ['negative-Y']);
|
$scope.addOverrideOption('Transform', 'transform', ['negative-Y']);
|
||||||
$scope.addOverrideOption('Legend', 'legend', [true, false]);
|
$scope.addOverrideOption('Legend', 'legend', [true, false]);
|
||||||
$scope.updateCurrentOverrides();
|
$scope.updateCurrentOverrides();
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
Value
|
Value
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<select class="input-small tight-form-input" ng-model="ctrl.panel.valueName" ng-options="f for f in ['min','max','avg', 'current', 'total']" ng-change="ctrl.render()"></select>
|
<select class="input-small tight-form-input"
|
||||||
|
ng-model="ctrl.panel.valueName"
|
||||||
|
ng-options="f for f in ctrl.valueNameOptions"
|
||||||
|
ng-change="ctrl.render()"></select>
|
||||||
</li>
|
</li>
|
||||||
<li class="tight-form-item">
|
<li class="tight-form-item">
|
||||||
Postfix
|
Postfix
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
fontSizes: any[];
|
fontSizes: any[];
|
||||||
unitFormats: any[];
|
unitFormats: any[];
|
||||||
invalidGaugeRange: boolean;
|
invalidGaugeRange: boolean;
|
||||||
|
panel: any;
|
||||||
|
events: any;
|
||||||
|
valueNameOptions: any[] = ['min','max','avg', 'current', 'total', 'name'];
|
||||||
|
|
||||||
// Set and populate defaults
|
// Set and populate defaults
|
||||||
panelDefaults = {
|
panelDefaults = {
|
||||||
@@ -186,9 +189,13 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
var lastPoint = _.last(this.series[0].datapoints);
|
var lastPoint = _.last(this.series[0].datapoints);
|
||||||
var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
|
var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
|
||||||
|
|
||||||
if (_.isString(lastValue)) {
|
if (this.panel.valueName === 'name') {
|
||||||
data.value = 0;
|
data.value = 0;
|
||||||
data.valueFormated = lastValue;
|
data.valueRounded = 0;
|
||||||
|
data.valueFormated = this.series[0].alias;
|
||||||
|
} else if (_.isString(lastValue)) {
|
||||||
|
data.value = 0;
|
||||||
|
data.valueFormated = _.escape(lastValue);
|
||||||
data.valueRounded = 0;
|
data.valueRounded = 0;
|
||||||
} else {
|
} else {
|
||||||
data.value = this.series[0].stats[this.panel.valueName];
|
data.value = this.series[0].stats[this.panel.valueName];
|
||||||
@@ -199,6 +206,13 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
|
data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
|
||||||
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
|
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add $__name variable for using in prefix or postfix
|
||||||
|
data.scopedVars = {
|
||||||
|
__name: {
|
||||||
|
value: this.series[0].label
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// check value to text mappings if its enabled
|
// check value to text mappings if its enabled
|
||||||
@@ -296,7 +310,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSpan(className, fontSize, value) {
|
function getSpan(className, fontSize, value) {
|
||||||
value = templateSrv.replace(value);
|
value = templateSrv.replace(value, data.scopedVars);
|
||||||
return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
|
return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
|
||||||
value + '</span>';
|
value + '</span>';
|
||||||
}
|
}
|
||||||
@@ -395,7 +409,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
value: {
|
value: {
|
||||||
color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null,
|
color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null,
|
||||||
formatter: function() { return getValueText(); },
|
formatter: function() { return getValueText(); },
|
||||||
font: { size: fontSize, family: 'Helvetica Neue", Helvetica, Arial, sans-serif' }
|
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' }
|
||||||
},
|
},
|
||||||
show: true
|
show: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,22 @@ describe('SingleStatCtrl', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
singleStatScenario('showing serie name instead of value', function(ctx) {
|
||||||
|
ctx.setup(function() {
|
||||||
|
ctx.datapoints = [[10,1], [20,2]];
|
||||||
|
ctx.ctrl.panel.valueName = 'name';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use series avg as default main value', function() {
|
||||||
|
expect(ctx.data.value).to.be(0);
|
||||||
|
expect(ctx.data.valueRounded).to.be(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set formated falue', function() {
|
||||||
|
expect(ctx.data.valueFormated).to.be('test.cpu1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(ctx) {
|
singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(ctx) {
|
||||||
ctx.setup(function() {
|
ctx.setup(function() {
|
||||||
ctx.datapoints = [[99.999,1], [99.99999,2]];
|
ctx.datapoints = [[99.999,1], [99.99999,2]];
|
||||||
|
|||||||
@@ -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,8 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportCsv() {
|
exportCsv() {
|
||||||
FileExport.exportTableDataToCsv(this.table);
|
var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize);
|
||||||
|
FileExport.exportTableDataToCsv(renderer.render_values());
|
||||||
}
|
}
|
||||||
|
|
||||||
link(scope, elem, attrs, ctrl) {
|
link(scope, elem, attrs, ctrl) {
|
||||||
@@ -159,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"';
|
||||||
@@ -141,4 +146,21 @@ export class TableRenderer {
|
|||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_values() {
|
||||||
|
let rows = [];
|
||||||
|
|
||||||
|
for (var y = 0; y < this.table.rows.length; y++) {
|
||||||
|
let row = this.table.rows[y];
|
||||||
|
let new_row = [];
|
||||||
|
for (var i = 0; i < this.table.columns.length; i++) {
|
||||||
|
new_row.push(this.formatColumnValue(i, row[i]));
|
||||||
|
}
|
||||||
|
rows.push(new_row);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
columns: this.table.columns,
|
||||||
|
rows: rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 +48,24 @@ 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);
|
||||||
expect(html).to.be('<td>2014-01-01T06:06:06+00:00</td>');
|
expect(html).to.be('<td>2014-01-01T06:06:06Z</td>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('number column with unit specified should ignore style unit', () => {
|
it('number column with unit specified should ignore style unit', () => {
|
||||||
@@ -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>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
// Media queries
|
// Media queries
|
||||||
// ---------------------
|
// ---------------------
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
div.panel {
|
div.panel {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -33,6 +34,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-down(xs) {
|
||||||
|
.page-dashboard .navbar-page-btn {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// form styles
|
// form styles
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
.page-dashboard .navbar-page-btn {
|
.page-dashboard .navbar-page-btn {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ $page-bg: $white;
|
|||||||
$body-color: $gray-1;
|
$body-color: $gray-1;
|
||||||
$text-color: $gray-1;
|
$text-color: $gray-1;
|
||||||
$text-color-strong: $white;
|
$text-color-strong: $white;
|
||||||
$text-color-weak: $gray-1;
|
$text-color-weak: $gray-2;
|
||||||
$text-color-faint: $gray-3;
|
$text-color-faint: $gray-3;
|
||||||
$text-color-emphasis: $dark-5;
|
$text-color-emphasis: $dark-5;
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ $gf-form-margin: 0.25rem;
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.gf-input-small {
|
||||||
|
padding: $input-padding-y/3 $input-padding-x/3;
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
}
|
||||||
|
|
||||||
// Customize the `:focus` state to imitate native WebKit styles.
|
// Customize the `:focus` state to imitate native WebKit styles.
|
||||||
@include form-control-focus();
|
@include form-control-focus();
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +101,7 @@
|
|||||||
|
|
||||||
.search-button-row {
|
.search-button-row {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
line-height: 2.5rem;
|
||||||
button, a {
|
button, a {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,13 @@ define([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('kbn deckbytes format when scaled decimals is null do not use it', function() {
|
||||||
|
it('should use specified decimals', function() {
|
||||||
|
var str = kbn.valueFormats['deckbytes'](10000000, 3, null);
|
||||||
|
expect(str).to.be('10.000 GB');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('kbn roundValue', function() {
|
describe('kbn roundValue', function() {
|
||||||
it('should should handle null value', function() {
|
it('should should handle null value', function() {
|
||||||
var str = kbn.roundValue(null, 2);
|
var str = kbn.roundValue(null, 2);
|
||||||
@@ -154,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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
9836
public/vendor/moment.js
vendored
9836
public/vendor/moment.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user