Merge branch 'master' into alerting

Conflicts:
	pkg/api/dashboard.go
	pkg/models/dashboards.go
	pkg/services/sqlstore/dashboard.go
This commit is contained in:
Torkel Ödegaard
2016-07-11 18:28:07 +02:00
85 changed files with 9906 additions and 1054 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,4 +1,4 @@
{ {
"stable": "3.0.4", "stable": "3.0.4",
"testing": "3.0.4" "testing": "3.1.0-beta1"
} }

View File

@@ -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",

View File

@@ -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
;; ;;

View File

@@ -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)

View File

@@ -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"},
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)
}) })
}) })

View 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
}

View File

@@ -77,6 +77,8 @@ func Init() error {
} }
go StartPluginUpdateChecker() go StartPluginUpdateChecker()
go updateAppDashboards()
return nil return nil
} }

View File

@@ -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
} }

View File

@@ -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,
}))
} }

View File

@@ -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,
}))
} }

View File

@@ -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
})
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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 + '/';

View File

@@ -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) {

View File

@@ -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;
}); });
}; };

View File

@@ -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' },
] ]
}, },
{ {

View File

@@ -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";

View File

@@ -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);
}; };

View File

@@ -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">

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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));

View File

@@ -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}}
&nbsp;(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>

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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?

View File

@@ -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([]);
} }

View File

@@ -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'});
} }

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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]];

View File

@@ -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>

View File

@@ -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));
} }

View File

@@ -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,
};
}
} }

View File

@@ -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>');
});
}); });
}); });

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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],

View File

@@ -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,

View File

@@ -1,5 +1,5 @@
{ {
"title": "Nginx Memory", "title": "Nginx Memory",
"revision": "2.0", "revision": 2,
"schemaVersion": 11 "schemaVersion": 11
} }