mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into alerting
Conflicts: pkg/api/dashboard.go pkg/models/dashboards.go pkg/services/sqlstore/dashboard.go
This commit is contained in:
commit
d9096110f8
19
CHANGELOG.md
19
CHANGELOG.md
@ -1,3 +1,18 @@
|
||||
# 4.0-pre (unreleased)
|
||||
|
||||
### Enhancements
|
||||
* **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674)
|
||||
* **SingleStat**: Add seriename as option in singlestat panel, closes [#4740](https://github.com/grafana/grafana/issues/4740)
|
||||
* **Localization**: Week start day now dependant on browser locale setting, closes [#3003](https://github.com/grafana/grafana/issues/3003)
|
||||
* **Templating**: Update panel repeats for variables that change on time refresh, closes [#5021](https://github.com/grafana/grafana/issues/5021)
|
||||
|
||||
# 3.1.0 stable (unreleased)
|
||||
|
||||
### Bugfixes & Enhancements,
|
||||
* **User Alert Notices**: Backend error alert popups did not show properly, fixes [#5435](https://github.com/grafana/grafana/issues/5435)
|
||||
* **Table**: Added sanitize HTML option to allow links in table cells, fixes [#4596](https://github.com/grafana/grafana/issues/4596)
|
||||
* **Apps**: App dashboards are automatically synced to DB at startup after plugin update, fixes [#5529](https://github.com/grafana/grafana/issues/5529)
|
||||
|
||||
# 3.1.0-beta1 (2016-06-23)
|
||||
|
||||
### Enhancements
|
||||
@ -11,8 +26,8 @@
|
||||
* **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)
|
||||
* **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)
|
||||
* **Datasource**: Pending data source requests are cancelled before new ones are issues (Graphite & Prometheus), closes [5321](https://github.com/grafana/grafana/issues/5321)
|
||||
* **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)
|
||||
|
||||
### 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.
|
||||
|
@ -178,6 +178,9 @@ login_hint = email or username
|
||||
# Default UI theme ("dark" or "light")
|
||||
default_theme = dark
|
||||
|
||||
# Allow users to sign in using username and password
|
||||
allow_user_pass_login = true
|
||||
|
||||
#################################### Anonymous Auth ##########################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
|
@ -42,8 +42,9 @@
|
||||
# Prevents DNS rebinding attacks
|
||||
;enforce_domain = false
|
||||
|
||||
# The full public facing url
|
||||
;root_url = %(protocol)s://%(domain)s:%(http_port)s/
|
||||
# The full public facing url you use in browser, used for redirects and emails
|
||||
# If you use reverse proxy and sub path specify full url (with sub path)
|
||||
;root_url = http://localhost:3000
|
||||
|
||||
# Log web requests
|
||||
;router_logging = false
|
||||
|
@ -26,7 +26,7 @@ Name | Description
|
||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||
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.
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
|
@ -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.
|
||||
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)
|
||||
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
|
||||
Access | Proxy = access via Grafana backend, Direct = access directly from browser.
|
||||
|
||||
## Query editor
|
||||
Open a graph in edit mode by click the title.
|
||||
|
@ -22,7 +22,7 @@ Name | Description
|
||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||
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)
|
||||
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
|
||||
Resolution | Metrics from opentsdb may have datapoints with either second or millisecond resolution.
|
||||
|
||||
|
@ -23,7 +23,7 @@ Name | Description
|
||||
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
|
||||
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)
|
||||
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.
|
||||
User | Name of your Prometheus user
|
||||
Password | Database user's password
|
||||
|
@ -46,7 +46,7 @@ Then you can override them using:
|
||||
|
||||
## instance_name
|
||||
Set the name of the grafana-server instance. Used in logging and internal metrics and in
|
||||
clustering info. Defaults to: `${HOSTNAME}, which will be replaced with
|
||||
clustering info. Defaults to: `${HOSTNAME}`, which will be replaced with
|
||||
environment variable `HOSTNAME`, if that is empty or does not exist Grafana will try to use
|
||||
system calls to get the machine name.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
There is also a testing repository if you want beta or release
|
||||
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
|
||||
allows you to install signed packages.
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "3.0.4",
|
||||
"testing": "3.0.4"
|
||||
"testing": "3.1.0-beta1"
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "3.2.0-pre1",
|
||||
"version": "4.0.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@ -53,11 +53,11 @@
|
||||
"phantomjs-prebuilt": "^2.1.7",
|
||||
"reflect-metadata": "0.1.2",
|
||||
"rxjs": "5.0.0-beta.4",
|
||||
"sass-lint": "^1.6.0",
|
||||
"sass-lint": "^1.7.0",
|
||||
"systemjs": "0.19.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": "0.4.x",
|
||||
"node": "4.x",
|
||||
"npm": "2.14.x"
|
||||
},
|
||||
"scripts": {
|
||||
@ -69,7 +69,7 @@
|
||||
"dependencies": {
|
||||
"eventemitter3": "^1.2.0",
|
||||
"grunt-jscs": "~1.5.x",
|
||||
"grunt-sass-lint": "^0.1.0",
|
||||
"grunt-sass-lint": "^0.2.0",
|
||||
"grunt-sync": "^0.4.1",
|
||||
"karma-sinon": "^1.0.3",
|
||||
"lodash": "^2.4.1",
|
||||
|
@ -72,8 +72,6 @@ function isRunning() {
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo -n $"Starting $DESC: .... "
|
||||
|
||||
isRunning
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Already running."
|
||||
@ -90,7 +88,7 @@ case "$1" in
|
||||
|
||||
# Start Daemon
|
||||
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=$?
|
||||
if [ $return -eq 0 ]
|
||||
then
|
||||
@ -114,26 +112,25 @@ case "$1" in
|
||||
done
|
||||
fi
|
||||
|
||||
echo "OK"
|
||||
exit $return
|
||||
;;
|
||||
stop)
|
||||
echo -n "Stopping $DESC ..."
|
||||
echo -n "Stopping $DESC: ..."
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
killproc -p $PID_FILE -d 20 $NAME
|
||||
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
|
||||
PID="`cat $PID_FILE`"
|
||||
echo -n "Failed to stop $DESC (pid $PID)"
|
||||
echo "Failed to stop $DESC (pid $PID)"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
echo "OK"
|
||||
echo ""
|
||||
exit 0
|
||||
else
|
||||
echo -n "(not running)"
|
||||
echo "(not running)"
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
|
@ -213,7 +213,7 @@ func Register(r *macaron.Macaron) {
|
||||
// Dashboard
|
||||
r.Group("/dashboards", func() {
|
||||
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
|
||||
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard)
|
||||
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
|
||||
r.Get("/file/:file", GetDashboardFromJsonFile)
|
||||
r.Get("/home", wrap(GetHomeDashboard))
|
||||
r.Get("/tags", GetDashboardTags)
|
||||
|
@ -32,8 +32,13 @@ func init() {
|
||||
"AWS/Billing": {"EstimatedCharges"},
|
||||
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
||||
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
||||
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedItemCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
|
||||
"AWS/ECS": {"CPUUtilization", "MemoryUtilization"},
|
||||
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
|
||||
"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": {
|
||||
"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",
|
||||
@ -42,9 +47,6 @@ func init() {
|
||||
"BytesUsedForCache", "CacheHits", "CacheMisses", "CurrConnections", "Evictions", "HyperLogLogBasedCmds", "NewConnections", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress",
|
||||
"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": {
|
||||
"EnvironmentHealth",
|
||||
"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/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/S3": {"BucketSizeBytes", "NumberOfObjects"},
|
||||
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
|
||||
"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",
|
||||
"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/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
|
||||
}
|
||||
@ -88,29 +90,31 @@ func init() {
|
||||
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||
"AWS/CloudSearch": {},
|
||||
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation"},
|
||||
"AWS/ECS": {"ClusterName", "ServiceName"},
|
||||
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
|
||||
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
|
||||
"AWS/EBS": {"VolumeId"},
|
||||
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
|
||||
"AWS/EC2Spot": {"AvailabilityZone", "FleetRequestId", "InstanceType"},
|
||||
"AWS/ECS": {"ClusterName", "ServiceName"},
|
||||
"AWS/EFS": {"FileSystemId"},
|
||||
"AWS/ELB": {"LoadBalancerName", "AvailabilityZone"},
|
||||
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
|
||||
"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
|
||||
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
|
||||
"AWS/ES": {"ClientId", "DomainName"},
|
||||
"AWS/Events": {"RuleName"},
|
||||
"AWS/Kinesis": {"StreamName", "ShardID"},
|
||||
"AWS/Lambda": {"FunctionName"},
|
||||
"AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"},
|
||||
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
|
||||
"AWS/ML": {"MLModelId", "RequestMode"},
|
||||
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
|
||||
"AWS/Redshift": {"NodeID", "ClusterIdentifier"},
|
||||
"AWS/RDS": {"DBInstanceIdentifier", "DatabaseClass", "EngineName"},
|
||||
"AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DatabaseClass", "EngineName"},
|
||||
"AWS/Route53": {"HealthCheckId"},
|
||||
"AWS/S3": {"BucketName", "StorageType"},
|
||||
"AWS/SNS": {"Application", "Platform", "TopicName"},
|
||||
"AWS/SQS": {"QueueName"},
|
||||
"AWS/S3": {"BucketName", "StorageType"},
|
||||
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
|
||||
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
||||
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
|
||||
"AWS/WAF": {"Rule", "WebACL"},
|
||||
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -110,7 +111,7 @@ func DeleteDashboard(c *middleware.Context) {
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
|
||||
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if !c.IsSignedIn {
|
||||
@ -123,31 +124,33 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
|
||||
if dash.Id == 0 {
|
||||
limitReached, err := middleware.QuotaReached(c, "dashboard")
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "failed to get quota", err)
|
||||
return
|
||||
return ApiError(500, "failed to get quota", err)
|
||||
}
|
||||
if limitReached {
|
||||
c.JsonApiErr(403, "Quota reached", nil)
|
||||
return
|
||||
return ApiError(403, "Quota reached", nil)
|
||||
}
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil {
|
||||
if err == m.ErrDashboardWithSameNameExists {
|
||||
c.JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||
return
|
||||
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||
}
|
||||
if err == m.ErrDashboardVersionMismatch {
|
||||
c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
|
||||
return
|
||||
return Json(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
|
||||
}
|
||||
if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok {
|
||||
message := "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 {
|
||||
c.JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
|
||||
return
|
||||
return Json(404, util.DynMap{"status": "not-found", "message": err.Error()})
|
||||
}
|
||||
c.JsonApiErr(500, "Failed to save dashboard", err)
|
||||
return
|
||||
return ApiError(500, "Failed to save dashboard", err)
|
||||
}
|
||||
|
||||
if setting.AlertingEnabled {
|
||||
@ -158,13 +161,12 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
c.JsonApiErr(500, "Failed to save alerts", err)
|
||||
return
|
||||
return ApiError(500, "Failed to save alerts", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||
c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
|
||||
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
|
||||
}
|
||||
|
||||
func canEditDashboard(role m.RoleType) bool {
|
||||
|
@ -34,6 +34,7 @@ type CurrentUser struct {
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||
GravatarUrl string `json:"gravatarUrl"`
|
||||
Timezone string `json:"timezone"`
|
||||
Locale string `json:"locale"`
|
||||
}
|
||||
|
||||
type DashboardMeta struct {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
@ -21,6 +23,15 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
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{
|
||||
User: &dtos.CurrentUser{
|
||||
Id: c.UserId,
|
||||
@ -35,6 +46,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
LightTheme: prefs.Theme == "light",
|
||||
Timezone: prefs.Timezone,
|
||||
Locale: locale,
|
||||
},
|
||||
Settings: settings,
|
||||
AppUrl: setting.AppUrl,
|
||||
|
@ -29,6 +29,7 @@ func LoginView(c *middleware.Context) {
|
||||
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||
viewData.Settings["loginHint"] = setting.LoginHint
|
||||
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
|
||||
|
||||
if !tryLoginUsingRememberCookie(c) {
|
||||
c.HTML(200, VIEW_INDEX, viewData)
|
||||
|
@ -16,6 +16,9 @@ type CommandLine interface {
|
||||
GlobalString(name string) string
|
||||
FlagNames() (names []string)
|
||||
Generic(name string) interface{}
|
||||
|
||||
PluginDirectory() string
|
||||
RepoDirectory() string
|
||||
}
|
||||
|
||||
type contextCommandLine struct {
|
||||
@ -33,3 +36,11 @@ func (c *contextCommandLine) ShowVersion() {
|
||||
func (c *contextCommandLine) Application() *cli.App {
|
||||
return c.App
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) PluginDirectory() string {
|
||||
return c.GlobalString("pluginsDir")
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) RepoDirectory() string {
|
||||
return c.GlobalString("repo")
|
||||
}
|
||||
|
@ -93,3 +93,11 @@ func (fcli *FakeCommandLine) Args() cli.Args {
|
||||
func (fcli *FakeCommandLine) ShowVersion() {
|
||||
fcli.VersionShown = true
|
||||
}
|
||||
|
||||
func (fcli *FakeCommandLine) RepoDirectory() string {
|
||||
return fcli.GlobalString("repo")
|
||||
}
|
||||
|
||||
func (fcli *FakeCommandLine) PluginDirectory() string {
|
||||
return fcli.GlobalString("pluginsDir")
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
|
||||
return errors.New("please specify plugin to install")
|
||||
}
|
||||
|
||||
pluginsDir := c.GlobalString("pluginsDir")
|
||||
pluginsDir := c.PluginDirectory()
|
||||
if pluginsDir == "" {
|
||||
return errors.New("missing pluginsDir flag")
|
||||
}
|
||||
@ -46,7 +46,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
|
||||
}
|
||||
|
||||
func installCommand(c CommandLine) error {
|
||||
pluginFolder := c.GlobalString("pluginsDir")
|
||||
pluginFolder := c.PluginDirectory()
|
||||
if err := validateInput(c, pluginFolder); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -58,8 +58,8 @@ func installCommand(c CommandLine) error {
|
||||
}
|
||||
|
||||
func InstallPlugin(pluginName, version string, c CommandLine) error {
|
||||
plugin, err := s.GetPlugin(pluginName, c.GlobalString("repo"))
|
||||
pluginFolder := c.GlobalString("pluginsDir")
|
||||
plugin, err := s.GetPlugin(pluginName, c.RepoDirectory())
|
||||
pluginFolder := c.PluginDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func listremoteCommand(c CommandLine) error {
|
||||
plugin, err := s.ListAllPlugins(c.GlobalString("repo"))
|
||||
plugin, err := s.ListAllPlugins(c.RepoDirectory())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -32,7 +32,7 @@ var validateLsCommand = func(pluginDir string) error {
|
||||
}
|
||||
|
||||
func lsCommand(c CommandLine) error {
|
||||
pluginDir := c.GlobalString("pluginsDir")
|
||||
pluginDir := c.PluginDirectory()
|
||||
if err := validateLsCommand(pluginDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -2,30 +2,32 @@ package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"fmt"
|
||||
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
|
||||
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
|
||||
|
||||
func removeCommand(c CommandLine) error {
|
||||
pluginPath := c.GlobalString("pluginsDir")
|
||||
localPlugins := getPluginss(pluginPath)
|
||||
pluginPath := c.PluginDirectory()
|
||||
|
||||
plugin := c.Args().First()
|
||||
if plugin == "" {
|
||||
return errors.New("Missing plugin parameter")
|
||||
}
|
||||
|
||||
for _, p := range localPlugins {
|
||||
if p.Id == c.Args().First() {
|
||||
removePlugin(pluginPath, p.Id)
|
||||
return nil
|
||||
err := removePlugin(pluginPath, plugin)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such file or directory") {
|
||||
return fmt.Errorf("Plugin does not exist")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("Could not find plugin named %s", c.Args().First())
|
||||
return nil
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ func ShouldUpgrade(installed string, remote m.Plugin) bool {
|
||||
}
|
||||
|
||||
func upgradeAllCommand(c CommandLine) error {
|
||||
pluginsDir := c.GlobalString("pluginsDir")
|
||||
pluginsDir := c.PluginDirectory()
|
||||
|
||||
localPlugins := s.GetLocalPlugins(pluginsDir)
|
||||
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func upgradeCommand(c CommandLine) error {
|
||||
pluginsDir := c.GlobalString("pluginsDir")
|
||||
pluginsDir := c.PluginDirectory()
|
||||
pluginName := c.Args().First()
|
||||
|
||||
localPlugin, err := s.ReadPlugin(pluginsDir, pluginName)
|
||||
@ -16,7 +16,7 @@ func upgradeCommand(c CommandLine) error {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err2 := s.GetPlugin(localPlugin.Id, c.GlobalString("repo"))
|
||||
v, err2 := s.GetPlugin(localPlugin.Id, c.RepoDirectory())
|
||||
|
||||
if err2 != nil {
|
||||
return err2
|
||||
|
@ -75,9 +75,16 @@ func GetLocalPlugins(pluginDir string) []m.InstalledPlugin {
|
||||
return result
|
||||
}
|
||||
|
||||
func RemoveInstalledPlugin(pluginPath, id string) error {
|
||||
logger.Infof("Removing plugin: %v\n", id)
|
||||
return IoHelper.RemoveAll(path.Join(pluginPath, id))
|
||||
func RemoveInstalledPlugin(pluginPath, pluginName string) error {
|
||||
logger.Infof("Removing plugin: %v\n", pluginName)
|
||||
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) {
|
||||
|
@ -164,6 +164,7 @@ func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *ldapUserInfo) error {
|
||||
|
||||
func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
|
||||
if len(a.server.LdapGroups) == 0 {
|
||||
log.Warn("Ldap: no group mappings defined")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,14 @@ var (
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
)
|
||||
|
||||
type UpdatePluginDashboardError struct {
|
||||
PluginId string
|
||||
}
|
||||
|
||||
func (d UpdatePluginDashboardError) Error() string {
|
||||
return "Dashboard belong to plugin"
|
||||
}
|
||||
|
||||
var (
|
||||
DashTypeJson = "file"
|
||||
DashTypeDB = "db"
|
||||
@ -26,11 +34,12 @@ var (
|
||||
|
||||
// Dashboard model
|
||||
type Dashboard struct {
|
||||
Id int64
|
||||
Slug string
|
||||
OrgId int64
|
||||
GnetId int64
|
||||
Version int
|
||||
Id int64
|
||||
Slug string
|
||||
OrgId int64
|
||||
GnetId int64
|
||||
Version int
|
||||
PluginId string
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
@ -95,6 +104,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
||||
|
||||
dash.UpdatedBy = cmd.UserId
|
||||
dash.OrgId = cmd.OrgId
|
||||
dash.PluginId = cmd.PluginId
|
||||
dash.UpdateSlug()
|
||||
return dash
|
||||
}
|
||||
@ -119,6 +129,7 @@ type SaveDashboardCommand struct {
|
||||
UserId int64 `json:"userId"`
|
||||
OrgId int64 `json:"-"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
PluginId string `json:"-"`
|
||||
|
||||
Result *Dashboard
|
||||
}
|
||||
@ -154,6 +165,12 @@ type GetDashboardsQuery struct {
|
||||
Result []*Dashboard
|
||||
}
|
||||
|
||||
type GetDashboardsByPluginIdQuery struct {
|
||||
OrgId int64
|
||||
PluginId string
|
||||
Result []*Dashboard
|
||||
}
|
||||
|
||||
type GetDashboardSlugByIdQuery struct {
|
||||
Id int64
|
||||
Result string
|
||||
|
@ -20,6 +20,7 @@ type PluginSetting struct {
|
||||
Pinned bool
|
||||
JsonData map[string]interface{}
|
||||
SecureJsonData SecureJsonData
|
||||
PluginVersion string
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
@ -44,11 +45,19 @@ type UpdatePluginSettingCmd struct {
|
||||
Pinned bool `json:"pinned"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
SecureJsonData map[string]string `json:"secureJsonData"`
|
||||
PluginVersion string `json:"version"`
|
||||
|
||||
PluginId string `json:"-"`
|
||||
OrgId int64 `json:"-"`
|
||||
}
|
||||
|
||||
// specific command, will only update version
|
||||
type UpdatePluginSettingVersionCmd struct {
|
||||
PluginVersion string
|
||||
PluginId string `json:"-"`
|
||||
OrgId int64 `json:"-"`
|
||||
}
|
||||
|
||||
func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData {
|
||||
encrypted := make(SecureJsonData)
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
@ -65,10 +74,11 @@ type GetPluginSettingsQuery struct {
|
||||
}
|
||||
|
||||
type PluginSettingInfoDTO struct {
|
||||
OrgId int64
|
||||
PluginId string
|
||||
Enabled bool
|
||||
Pinned bool
|
||||
OrgId int64
|
||||
PluginId string
|
||||
Enabled bool
|
||||
Pinned bool
|
||||
PluginVersion string
|
||||
}
|
||||
|
||||
type GetPluginSettingByIdQuery struct {
|
||||
@ -76,3 +86,9 @@ type GetPluginSettingByIdQuery struct {
|
||||
OrgId int64
|
||||
Result *PluginSetting
|
||||
}
|
||||
|
||||
type PluginStateChangedEvent struct {
|
||||
PluginId string
|
||||
OrgId int64
|
||||
Enabled bool
|
||||
}
|
||||
|
@ -68,6 +68,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
||||
OrgId: cmd.OrgId,
|
||||
UserId: cmd.UserId,
|
||||
Overwrite: cmd.Overwrite,
|
||||
PluginId: cmd.PluginId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveCmd); err != nil {
|
||||
|
@ -14,10 +14,12 @@ type PluginDashboardInfoDTO struct {
|
||||
Title string `json:"title"`
|
||||
Imported bool `json:"imported"`
|
||||
ImportedUri string `json:"importedUri"`
|
||||
Slug string `json:"slug"`
|
||||
ImportedRevision int64 `json:"importedRevision"`
|
||||
Revision int64 `json:"revision"`
|
||||
Description string `json:"description"`
|
||||
Path string `json:"path"`
|
||||
Removed bool `json:"removed"`
|
||||
}
|
||||
|
||||
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
|
||||
@ -29,14 +31,53 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
|
||||
|
||||
result := make([]*PluginDashboardInfoDTO, 0)
|
||||
|
||||
// load current dashboards
|
||||
query := m.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingMatches := make(map[int64]bool)
|
||||
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Type == PluginTypeDashboard {
|
||||
if dashInfo, err := getDashboardImportStatus(orgId, plugin, include.Path); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
result = append(result, dashInfo)
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@ -64,33 +105,3 @@ func loadPluginDashboard(pluginId, path string) (*m.Dashboard, error) {
|
||||
|
||||
return m.NewDashboardFromJson(data), nil
|
||||
}
|
||||
|
||||
func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*PluginDashboardInfoDTO, error) {
|
||||
res := &PluginDashboardInfoDTO{}
|
||||
|
||||
var dashboard *m.Dashboard
|
||||
var err error
|
||||
|
||||
if dashboard, err = loadPluginDashboard(plugin.Id, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Path = path
|
||||
res.PluginId = plugin.Id
|
||||
res.Title = dashboard.Title
|
||||
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||
|
||||
query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err != m.ErrDashboardNotFound {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
res.Imported = true
|
||||
res.ImportedUri = "db/" + query.Result.Slug
|
||||
res.ImportedRevision = query.Result.Data.Get("revision").MustInt64(1)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@ -31,6 +32,17 @@ func TestPluginDashboards(t *testing.T) {
|
||||
return m.ErrDashboardNotFound
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardsByPluginIdQuery) error {
|
||||
var data = simplejson.New()
|
||||
data.Set("title", "Nginx Connections")
|
||||
data.Set("revision", 22)
|
||||
|
||||
query.Result = []*m.Dashboard{
|
||||
{Slug: "nginx-connections", Data: data},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
dashboards, err := GetPluginDashboards(1, "test-app")
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
@ -41,12 +53,12 @@ func TestPluginDashboards(t *testing.T) {
|
||||
|
||||
Convey("should include installed version info", func() {
|
||||
So(dashboards[0].Title, ShouldEqual, "Nginx Connections")
|
||||
//So(dashboards[0].Revision, ShouldEqual, "1.5")
|
||||
//So(dashboards[0].InstalledRevision, ShouldEqual, "1.1")
|
||||
//So(dashboards[0].InstalledUri, ShouldEqual, "db/nginx-connections")
|
||||
So(dashboards[0].Revision, ShouldEqual, 25)
|
||||
So(dashboards[0].ImportedRevision, ShouldEqual, 22)
|
||||
So(dashboards[0].ImportedUri, ShouldEqual, "db/nginx-connections")
|
||||
|
||||
//So(dashboards[1].Revision, ShouldEqual, "2.0")
|
||||
//So(dashboards[1].InstalledRevision, ShouldEqual, "")
|
||||
So(dashboards[1].Revision, ShouldEqual, 2)
|
||||
So(dashboards[1].ImportedRevision, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
|
139
pkg/plugins/dashboards_updater.go
Normal file
139
pkg/plugins/dashboards_updater.go
Normal file
@ -0,0 +1,139 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddEventListener(handlePluginStateChanged)
|
||||
}
|
||||
|
||||
func updateAppDashboards() {
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
plog.Debug("Looking for App Dashboard Updates")
|
||||
|
||||
query := m.GetPluginSettingsQuery{OrgId: 0}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
plog.Error("Failed to get all plugin settings", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pluginSetting := range query.Result {
|
||||
// ignore disabled plugins
|
||||
if !pluginSetting.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginDef, exist := Plugins[pluginSetting.PluginId]; exist {
|
||||
if pluginDef.Info.Version != pluginSetting.PluginVersion {
|
||||
syncPluginDashboards(pluginDef, pluginSetting.OrgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) error {
|
||||
if dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path); err != nil {
|
||||
return err
|
||||
} else {
|
||||
plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
updateCmd := ImportDashboardCommand{
|
||||
OrgId: orgId,
|
||||
PluginId: pluginDashInfo.PluginId,
|
||||
Overwrite: true,
|
||||
Dashboard: dash.Data,
|
||||
UserId: 0,
|
||||
Path: pluginDashInfo.Path,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&updateCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
|
||||
plog.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.Id)
|
||||
|
||||
// Get plugin dashboards
|
||||
dashboards, err := GetPluginDashboards(orgId, pluginDef.Id)
|
||||
|
||||
if err != nil {
|
||||
plog.Error("Failed to load app dashboards", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update dashboards with updated revisions
|
||||
for _, dash := range dashboards {
|
||||
// remove removed ones
|
||||
if dash.Removed {
|
||||
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
|
||||
|
||||
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug}
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// update updated ones
|
||||
if dash.ImportedRevision != dash.Revision {
|
||||
if err := autoUpdateAppDashboard(dash, orgId); err != nil {
|
||||
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update version in plugin_setting table to mark that we have processed the update
|
||||
query := m.GetPluginSettingByIdQuery{PluginId: pluginDef.Id, OrgId: orgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
plog.Error("Failed to read plugin setting by id", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
appSetting := query.Result
|
||||
cmd := m.UpdatePluginSettingVersionCmd{
|
||||
OrgId: appSetting.OrgId,
|
||||
PluginId: appSetting.PluginId,
|
||||
PluginVersion: pluginDef.Info.Version,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
plog.Error("Failed to update plugin setting version", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
|
||||
plog.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
|
||||
|
||||
if event.Enabled {
|
||||
syncPluginDashboards(Plugins[event.PluginId], event.OrgId)
|
||||
} else {
|
||||
query := m.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return err
|
||||
} else {
|
||||
for _, dash := range query.Result {
|
||||
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug}
|
||||
|
||||
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -77,6 +77,8 @@ func Init() error {
|
||||
}
|
||||
|
||||
go StartPluginUpdateChecker()
|
||||
go updateAppDashboards()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ func init() {
|
||||
bus.AddHandler("sql", SearchDashboards)
|
||||
bus.AddHandler("sql", GetDashboardTags)
|
||||
bus.AddHandler("sql", GetDashboardSlugById)
|
||||
bus.AddHandler("sql", GetDashboardsByPluginId)
|
||||
}
|
||||
|
||||
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
@ -45,6 +46,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
return m.ErrDashboardVersionMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
}
|
||||
|
||||
sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle)
|
||||
@ -261,6 +267,19 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
|
||||
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 {
|
||||
Slug string
|
||||
}
|
||||
|
@ -111,4 +111,13 @@ func addDashboardMigration(mg *Migrator) {
|
||||
mg.AddMigration("Add index for gnetId in dashboard", NewAddIndexMigration(dashboardV2, &Index{
|
||||
Cols: []string{"gnet_id"}, Type: IndexType,
|
||||
}))
|
||||
|
||||
// add column to store plugin_id
|
||||
mg.AddMigration("Add column plugin_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
||||
Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 255,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{
|
||||
Cols: []string{"org_id", "plugin_id"}, Type: IndexType,
|
||||
}))
|
||||
}
|
||||
|
@ -26,4 +26,10 @@ func addAppSettingsMigration(mg *Migrator) {
|
||||
|
||||
//------- indexes ------------------
|
||||
addTableIndicesMigrations(mg, "v1", pluginSettingTable)
|
||||
|
||||
// add column to store installed version
|
||||
mg.AddMigration("Add column plugin_version to plugin_settings", NewAddColumnMigration(pluginSettingTable, &Column{
|
||||
Name: "plugin_version", Type: DB_NVarchar, Nullable: true, Length: 50,
|
||||
}))
|
||||
|
||||
}
|
||||
|
@ -13,14 +13,20 @@ func init() {
|
||||
bus.AddHandler("sql", GetPluginSettings)
|
||||
bus.AddHandler("sql", GetPluginSettingById)
|
||||
bus.AddHandler("sql", UpdatePluginSetting)
|
||||
bus.AddHandler("sql", UpdatePluginSettingVersion)
|
||||
}
|
||||
|
||||
func GetPluginSettings(query *m.GetPluginSettingsQuery) error {
|
||||
sql := `SELECT org_id, plugin_id, enabled, pinned
|
||||
FROM plugin_setting
|
||||
WHERE org_id=?`
|
||||
sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version
|
||||
FROM plugin_setting `
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sess := x.Sql(sql, query.OrgId)
|
||||
if query.OrgId != 0 {
|
||||
sql += "WHERE org_id=?"
|
||||
params = append(params, query.OrgId)
|
||||
}
|
||||
|
||||
sess := x.Sql(sql, params...)
|
||||
query.Result = make([]*m.PluginSettingInfoDTO, 0)
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
@ -51,22 +57,52 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
|
||||
Enabled: cmd.Enabled,
|
||||
Pinned: cmd.Pinned,
|
||||
JsonData: cmd.JsonData,
|
||||
PluginVersion: cmd.PluginVersion,
|
||||
SecureJsonData: cmd.GetEncryptedJsonData(),
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// add state change event on commit success
|
||||
sess.events = append(sess.events, &m.PluginStateChangedEvent{
|
||||
PluginId: cmd.PluginId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
})
|
||||
|
||||
_, err = sess.Insert(&pluginSetting)
|
||||
return err
|
||||
} else {
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
pluginSetting.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
||||
}
|
||||
|
||||
// add state change event on commit success
|
||||
if pluginSetting.Enabled != cmd.Enabled {
|
||||
sess.events = append(sess.events, &m.PluginStateChangedEvent{
|
||||
PluginId: cmd.PluginId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
pluginSetting.Updated = time.Now()
|
||||
pluginSetting.Enabled = cmd.Enabled
|
||||
pluginSetting.JsonData = cmd.JsonData
|
||||
pluginSetting.Pinned = cmd.Pinned
|
||||
pluginSetting.PluginVersion = cmd.PluginVersion
|
||||
|
||||
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UpdatePluginSettingVersion(cmd *m.UpdatePluginSettingVersionCmd) error {
|
||||
return inTransaction2(func(sess *session) error {
|
||||
|
||||
_, err := sess.Exec("UPDATE plugin_setting SET plugin_version=? WHERE org_id=? AND plugin_id=?", cmd.PluginVersion, cmd.OrgId, cmd.PluginId)
|
||||
return err
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ var (
|
||||
VerifyEmailEnabled bool
|
||||
LoginHint string
|
||||
DefaultTheme string
|
||||
AllowUserPassLogin bool
|
||||
|
||||
// Http auth
|
||||
AdminUser string
|
||||
@ -286,19 +287,19 @@ func evalConfigValues() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadSpecifedConfigFile(configFile string) {
|
||||
func loadSpecifedConfigFile(configFile string) error {
|
||||
if configFile == "" {
|
||||
configFile = filepath.Join(HomePath, "conf/custom.ini")
|
||||
// return without error if custom file does not exist
|
||||
if !pathExists(configFile) {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
userConfig, err := ini.Load(configFile)
|
||||
userConfig.BlockMode = false
|
||||
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() {
|
||||
@ -320,6 +321,7 @@ func loadSpecifedConfigFile(configFile string) {
|
||||
}
|
||||
|
||||
configFiles = append(configFiles, configFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadConfiguration(args *CommandLineArgs) {
|
||||
@ -341,12 +343,12 @@ func loadConfiguration(args *CommandLineArgs) {
|
||||
// load default overrides
|
||||
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
|
||||
loadSpecifedConfigFile(args.Config)
|
||||
err = loadSpecifedConfigFile(args.Config)
|
||||
if err != nil {
|
||||
initLogging()
|
||||
log.Fatal(3, err.Error())
|
||||
}
|
||||
|
||||
// apply environment overrides
|
||||
applyEnvVariableOverrides()
|
||||
@ -488,6 +490,7 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
|
||||
LoginHint = users.Key("login_hint").String()
|
||||
DefaultTheme = users.Key("default_theme").String()
|
||||
AllowUserPassLogin = users.Key("allow_user_pass_login").MustBool(true)
|
||||
|
||||
// anonymous access
|
||||
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
|
||||
|
@ -14,6 +14,7 @@ import $ from 'jquery';
|
||||
import angular from 'angular';
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {coreModule} from './core/core';
|
||||
|
||||
export class GrafanaApp {
|
||||
@ -41,10 +42,14 @@ export class GrafanaApp {
|
||||
var app = angular.module('grafana', []);
|
||||
app.constant('grafanaVersion', "@grafanaVersion@");
|
||||
|
||||
moment.locale(config.bootData.user.locale);
|
||||
|
||||
app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => {
|
||||
|
||||
if (config.buildInfo.env !== 'development') {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
}
|
||||
|
||||
$httpProvider.useApplyAsync(true);
|
||||
|
||||
this.registerFunctions.controller = $controllerProvider.register;
|
||||
|
@ -72,6 +72,10 @@
|
||||
Import
|
||||
</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>
|
||||
</div>
|
||||
|
@ -45,7 +45,7 @@
|
||||
</li>
|
||||
|
||||
<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="sidemenu-item-text">Sign in</span>
|
||||
</a>
|
||||
|
@ -12,6 +12,7 @@ export class SideMenuCtrl {
|
||||
mainLinks: any;
|
||||
orgMenu: any;
|
||||
appSubUrl: string;
|
||||
loginUrl: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $location, private contextSrv, private backendSrv, private $element) {
|
||||
@ -22,13 +23,14 @@ export class SideMenuCtrl {
|
||||
|
||||
this.mainLinks = config.bootData.mainNavLinks;
|
||||
this.openUserDropdown();
|
||||
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
||||
|
||||
this.$scope.$on('$routeChangeSuccess', () => {
|
||||
if (!this.contextSrv.pinned) {
|
||||
this.contextSrv.sidemenu = false;
|
||||
}
|
||||
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
getUrl(url) {
|
||||
|
@ -18,6 +18,7 @@ function (angular, coreModule, config) {
|
||||
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
||||
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
||||
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled;
|
||||
$scope.allowUserPassLogin = config.allowUserPassLogin;
|
||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||
$scope.loginHint = config.loginHint;
|
||||
|
||||
@ -69,7 +70,12 @@ function (angular, coreModule, config) {
|
||||
}
|
||||
|
||||
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;
|
||||
} else {
|
||||
window.location.href = config.appSubUrl + '/';
|
||||
|
@ -28,10 +28,6 @@ export class AlertSrv {
|
||||
}, this.$rootScope);
|
||||
|
||||
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) {
|
||||
|
@ -87,7 +87,7 @@ export class BackendSrv {
|
||||
});
|
||||
}
|
||||
|
||||
this.$timeout(this.requestErrorHandler.bind(this), 50);
|
||||
this.$timeout(this.requestErrorHandler.bind(this, err), 50);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
@ -368,19 +368,43 @@ function($, _, moment) {
|
||||
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
|
||||
kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
|
||||
kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
|
||||
kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
|
||||
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
|
||||
|
||||
// Data
|
||||
// Data (Binary)
|
||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||
kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B');
|
||||
kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1);
|
||||
kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2);
|
||||
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
|
||||
kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps');
|
||||
kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps');
|
||||
@ -397,6 +421,9 @@ function($, _, moment) {
|
||||
kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps');
|
||||
kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps');
|
||||
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
|
||||
kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
|
||||
@ -607,6 +634,8 @@ function($, _, moment) {
|
||||
{text: 'Humidity (%H)', value: 'humidity' },
|
||||
{text: 'ppm', value: 'ppm' },
|
||||
{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: [
|
||||
{text: 'bits', value: 'bits' },
|
||||
{text: 'bytes', value: 'bytes' },
|
||||
{text: 'kilobytes', value: 'kbytes'},
|
||||
{text: 'megabytes', value: 'mbytes'},
|
||||
{text: 'gigabytes', value: 'gbytes'},
|
||||
{text: 'kibibytes', value: 'kbytes'},
|
||||
{text: 'mebibytes', value: 'mbytes'},
|
||||
{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: 'writes/sec (wps)', value: 'wps' },
|
||||
{text: 'I/O ops/sec (iops)', value: 'iops'},
|
||||
{text: 'ops/min (opm)', value: 'opm' },
|
||||
{text: 'reads/min (rpm)', value: 'rpm' },
|
||||
{text: 'writes/min (wpm)', value: 'wpm' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -22,6 +22,7 @@ function (angular, $, _, moment) {
|
||||
|
||||
this.id = data.id || null;
|
||||
this.title = data.title || 'No Title';
|
||||
this.autoUpdate = data.autoUpdate;
|
||||
this.description = data.description;
|
||||
this.tags = data.tags || [];
|
||||
this.style = data.style || "dark";
|
||||
|
@ -21,6 +21,7 @@ export class DashboardCtrl {
|
||||
dynamicDashboardSrv,
|
||||
dashboardViewStateSrv,
|
||||
contextSrv,
|
||||
alertSrv,
|
||||
$timeout) {
|
||||
|
||||
$scope.editor = { index: 0 };
|
||||
@ -29,6 +30,14 @@ export class DashboardCtrl {
|
||||
var resizeEventTimeout;
|
||||
|
||||
$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);
|
||||
dashboardSrv.setCurrent(dashboard);
|
||||
|
||||
@ -37,9 +46,12 @@ export class DashboardCtrl {
|
||||
|
||||
// template values service needs to initialize completely before
|
||||
// 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);
|
||||
|
||||
unsavedChangesSrv.init(dashboard, $scope);
|
||||
|
||||
$scope.dashboard = dashboard;
|
||||
@ -52,13 +64,30 @@ export class DashboardCtrl {
|
||||
$scope.setWindowTitleAndTheme();
|
||||
|
||||
$scope.appEvent("dashboard-initialized", $scope.dashboard);
|
||||
}).catch(function(err) {
|
||||
if (err.data && err.data.message) { err.message = err.data.message; }
|
||||
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
|
||||
});
|
||||
})
|
||||
.catch($scope.onInitFailed.bind(this, 'Dashboard init failed', true));
|
||||
};
|
||||
|
||||
$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() {
|
||||
console.log('dynamic update');
|
||||
dynamicDashboardSrv.update($scope.dashboard);
|
||||
};
|
||||
|
||||
|
@ -46,6 +46,9 @@
|
||||
<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>
|
||||
</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">
|
||||
<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">
|
||||
|
@ -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() {
|
||||
|
@ -12,6 +12,8 @@ function (angular) {
|
||||
$scope.clone.id = null;
|
||||
$scope.clone.editable = true;
|
||||
$scope.clone.title = $scope.clone.title + " Copy";
|
||||
// remove auto update
|
||||
delete $scope.clone.autoUpdate;
|
||||
};
|
||||
|
||||
function saveDashboard(options) {
|
||||
@ -37,8 +39,9 @@ function (angular) {
|
||||
err.isHandled = true;
|
||||
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Another dashboard with the same name exists',
|
||||
text: "Would you still like to save this dashboard?",
|
||||
title: 'Conflict',
|
||||
text: 'Dashboard with the same name exists.',
|
||||
text2: 'Would you still like to save this dashboard?',
|
||||
yesText: "Save & Overwrite",
|
||||
icon: "fa-warning",
|
||||
onConfirm: function() {
|
||||
|
@ -44,6 +44,10 @@ function (angular, _) {
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
if (!external) {
|
||||
$scope.dashboard.snapshot.originalUrl = $location.absUrl();
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.snapshot.external = external;
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<label class="small">Refreshing every:</label>
|
||||
|
@ -24,6 +24,7 @@ export class TimePickerCtrl {
|
||||
refresh: any;
|
||||
isOpen: boolean;
|
||||
isUtc: boolean;
|
||||
firstDayOfWeek: number;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $rootScope, private timeSrv) {
|
||||
@ -43,6 +44,8 @@ export class TimePickerCtrl {
|
||||
|
||||
_.defaults(this.panel, TimePickerCtrl.defaults);
|
||||
|
||||
this.firstDayOfWeek = moment.localeData().firstDayOfWeek();
|
||||
|
||||
var time = angular.copy(this.timeSrv.timeRange());
|
||||
var timeRaw = angular.copy(this.timeSrv.timeRange(false));
|
||||
|
||||
|
@ -14,20 +14,19 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
v{{dash.revision}}
|
||||
<span ng-if="dash.installed">
|
||||
(Imported v{{dash.importedRevision}})
|
||||
<span>
|
||||
<span ng-if="dash.imported" bs-tooltip='"Imported revision:" + dash.importedRevision'>
|
||||
Revision: {{dash.revision}}
|
||||
<span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
|
||||
<button class="btn btn-secondary btn-small" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
|
||||
Import
|
||||
</button>
|
||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.imported">
|
||||
<button class="btn btn-secondary btn-small" ng-click="ctrl.import(dash, true)" ng-show="dash.imported">
|
||||
Update
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
|
||||
Delete
|
||||
<button class="btn btn-danger btn-small" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -5,6 +5,10 @@
|
||||
<div class="page-header">
|
||||
<h1>Plugins</h1>
|
||||
|
||||
<a class="btn btn-inverse" href="https://grafana.net/plugins?utm_source=grafana_plugin_list" target="_blank">
|
||||
Explore plugins on Grafana.net
|
||||
</a>
|
||||
|
||||
<div class="page-header-tabs">
|
||||
<ul class="gf-tabs">
|
||||
<li class="gf-tabs-item">
|
||||
|
@ -97,28 +97,7 @@ export class PluginEditCtrl {
|
||||
}
|
||||
|
||||
importDashboards() {
|
||||
// move to dashboards tab
|
||||
this.tabIndex = 2;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!this.$scope.$$phase) {
|
||||
this.$scope.$digest();
|
||||
}
|
||||
|
||||
// let angular load dashboards tab
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
|
||||
}).then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// send event to import list component
|
||||
appEvents.emit('dashboard-list-import-all', {
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
});
|
||||
});
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
setPreUpdateHook(callback: () => any) {
|
||||
|
@ -20,7 +20,7 @@
|
||||
</li>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,9 +1,10 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'jquery',
|
||||
'app/core/utils/kbn',
|
||||
],
|
||||
function (angular, _, kbn) {
|
||||
function (angular, _, $, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
@ -27,7 +28,16 @@ function (angular, _, kbn) {
|
||||
.filter(function(variable) {
|
||||
return variable.refresh === 2;
|
||||
}).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);
|
||||
@ -35,6 +45,7 @@ function (angular, _, kbn) {
|
||||
}, $rootScope);
|
||||
|
||||
this.init = function(dashboard) {
|
||||
this.dashboard = dashboard;
|
||||
this.variables = dashboard.templating.list;
|
||||
templateSrv.init(this.variables);
|
||||
|
||||
@ -145,7 +156,7 @@ function (angular, _, kbn) {
|
||||
|
||||
this.variableUpdated = function(variable) {
|
||||
templateSrv.updateTemplateData();
|
||||
return this.updateOptionsInChildVariables(variable);
|
||||
return self.updateOptionsInChildVariables(variable);
|
||||
};
|
||||
|
||||
this.updateOptionsInChildVariables = function(updatedVariable) {
|
||||
|
@ -17,7 +17,7 @@
|
||||
</button>
|
||||
</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">
|
||||
<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}}>
|
||||
@ -40,7 +40,7 @@
|
||||
</form>
|
||||
|
||||
<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">
|
||||
<span class="login-divider-text">
|
||||
Or login with
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
<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">
|
||||
<i class="fa fa-google"></i>
|
||||
with Google
|
||||
@ -64,7 +64,7 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="text-center password-recovery">
|
||||
<div class="text-center password-recovery" ng-show="allowUserPassLogin">
|
||||
<div class="text-center">
|
||||
<a href="user/password/send-reset-email">
|
||||
Forgot your password?
|
||||
|
@ -205,13 +205,8 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
};
|
||||
|
||||
function escapeForJson(value) {
|
||||
return value
|
||||
.replace(/\s/g, '\\ ')
|
||||
.replace(/\"/g, '\\"');
|
||||
}
|
||||
|
||||
function luceneThenJsonFormat(value) {
|
||||
return escapeForJson(templateSrv.luceneFormat(value));
|
||||
var luceneQuery = JSON.stringify(value);
|
||||
return luceneQuery.substr(1, luceneQuery.length - 2);
|
||||
}
|
||||
|
||||
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 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(/\$timeTo/g, range.to.valueOf());
|
||||
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;
|
||||
return _.map(buckets, function(bucket) {
|
||||
return {text: bucket.key, value: bucket.key};
|
||||
@ -270,8 +265,9 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
};
|
||||
|
||||
this.metricFindQuery = function(query) {
|
||||
query = templateSrv.replace(query, {}, luceneThenJsonFormat);
|
||||
query = angular.fromJson(query);
|
||||
query.query = templateSrv.replace(query.query || '*', {}, 'lucene');
|
||||
|
||||
if (!query) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
@ -134,6 +134,8 @@ export default class InfluxDatasource {
|
||||
};
|
||||
|
||||
_seriesQuery(query) {
|
||||
if (!query) { return this.$q.when({results: []}); }
|
||||
|
||||
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
|
||||
var query: any = {};
|
||||
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 intervalFactor = target.intervalFactor || 1;
|
||||
|
@ -272,8 +272,12 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholds) {
|
||||
function callPlot(incrementRenderCounter) {
|
||||
try {
|
||||
$.plot(elem, sortedSeries, options);
|
||||
delete ctrl.error;
|
||||
delete ctrl.inspector;
|
||||
} catch (e) {
|
||||
console.log('flotcharts error', e);
|
||||
ctrl.error = e.message || "Render Error";
|
||||
ctrl.inspector = {error: ctrl.error};
|
||||
}
|
||||
|
||||
if (incrementRenderCounter) {
|
||||
|
@ -105,7 +105,7 @@ define([
|
||||
$scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);
|
||||
$scope.addOverrideOption('Color', 'color', ['change']);
|
||||
$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('Legend', 'legend', [true, false]);
|
||||
$scope.updateCurrentOverrides();
|
||||
|
@ -16,7 +16,10 @@
|
||||
Value
|
||||
</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 class="tight-form-item">
|
||||
Postfix
|
||||
|
@ -19,6 +19,9 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
fontSizes: any[];
|
||||
unitFormats: any[];
|
||||
invalidGaugeRange: boolean;
|
||||
panel: any;
|
||||
events: any;
|
||||
valueNameOptions: any[] = ['min','max','avg', 'current', 'total', 'name'];
|
||||
|
||||
// Set and populate defaults
|
||||
panelDefaults = {
|
||||
@ -186,9 +189,13 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
var lastPoint = _.last(this.series[0].datapoints);
|
||||
var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
|
||||
|
||||
if (_.isString(lastValue)) {
|
||||
if (this.panel.valueName === 'name') {
|
||||
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;
|
||||
} else {
|
||||
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.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
|
||||
@ -296,7 +310,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
function getSpan(className, fontSize, value) {
|
||||
value = templateSrv.replace(value);
|
||||
value = templateSrv.replace(value, data.scopedVars);
|
||||
return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
|
||||
value + '</span>';
|
||||
}
|
||||
@ -395,7 +409,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
value: {
|
||||
color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null,
|
||||
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
|
||||
}
|
||||
|
@ -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) {
|
||||
ctx.setup(function() {
|
||||
ctx.datapoints = [[99.999,1], [99.99999,2]];
|
||||
|
@ -103,6 +103,11 @@
|
||||
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="tight-form-list" ng-if="style.type === 'string'">
|
||||
<li class="tight-form-item">
|
||||
<editor-checkbox text="Sanitize HTML" model="style.sanitize" change="editor.render()"></editor-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" ng-if="style.type === 'number'">
|
||||
@ -152,7 +157,7 @@
|
||||
Decimals
|
||||
</li>
|
||||
<li style="width: 105px">
|
||||
<input type="number" class="input-mini tight-form-input" ng-model="style.decimals" ng-change="render()" ng-model-onblur>
|
||||
<input type="number" class="input-mini tight-form-input" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
|
@ -45,7 +45,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, private annotationsSrv) {
|
||||
constructor($scope, $injector, private annotationsSrv, private $sanitize) {
|
||||
super($scope, $injector);
|
||||
this.pageIndex = 0;
|
||||
|
||||
@ -139,7 +139,8 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -159,7 +160,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
function appendTableRows(tbodyElem) {
|
||||
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc());
|
||||
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc(), ctrl.$sanitize);
|
||||
tbodyElem.empty();
|
||||
tbodyElem.html(renderer.render(ctrl.pageIndex));
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export class TableRenderer {
|
||||
formaters: any[];
|
||||
colorState: any;
|
||||
|
||||
constructor(private panel, private table, private isUtc) {
|
||||
constructor(private panel, private table, private isUtc, private sanitize) {
|
||||
this.formaters = [];
|
||||
this.colorState = {};
|
||||
}
|
||||
@ -24,7 +24,7 @@ export class TableRenderer {
|
||||
return _.first(style.colors);
|
||||
}
|
||||
|
||||
defaultCellFormater(v) {
|
||||
defaultCellFormater(v, style) {
|
||||
if (v === null || v === void 0 || v === undefined) {
|
||||
return '';
|
||||
}
|
||||
@ -33,7 +33,11 @@ export class TableRenderer {
|
||||
v = v.join(', ');
|
||||
}
|
||||
|
||||
return v;
|
||||
if (style && style.sanitize) {
|
||||
return this.sanitize(v);
|
||||
} else {
|
||||
return _.escape(v);
|
||||
}
|
||||
}
|
||||
|
||||
createColumnFormater(style, column) {
|
||||
@ -61,7 +65,7 @@ export class TableRenderer {
|
||||
}
|
||||
|
||||
if (_.isString(v)) {
|
||||
return v;
|
||||
return this.defaultCellFormater(v, style);
|
||||
}
|
||||
|
||||
if (style.colorMode) {
|
||||
@ -72,7 +76,9 @@ export class TableRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
return this.defaultCellFormater;
|
||||
return (value) => {
|
||||
return this.defaultCellFormater(value, style);
|
||||
};
|
||||
}
|
||||
|
||||
formatColumnValue(colIndex, value) {
|
||||
@ -96,7 +102,6 @@ export class TableRenderer {
|
||||
|
||||
renderCell(columnIndex, value, addWidthHack = false) {
|
||||
value = this.formatColumnValue(columnIndex, value);
|
||||
value = _.escape(value);
|
||||
var style = '';
|
||||
if (this.colorState.cell) {
|
||||
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
|
||||
@ -141,4 +146,21 @@ export class TableRenderer {
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
render_values() {
|
||||
let rows = [];
|
||||
|
||||
for (var y = 0; y < this.table.rows.length; y++) {
|
||||
let row = this.table.rows[y];
|
||||
let new_row = [];
|
||||
for (var i = 0; i < this.table.columns.length; i++) {
|
||||
new_row.push(this.formatColumnValue(i, row[i]));
|
||||
}
|
||||
rows.push(new_row);
|
||||
}
|
||||
return {
|
||||
columns: this.table.columns,
|
||||
rows: rows,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ describe('when rendering table', () => {
|
||||
{text: 'Undefined'},
|
||||
{text: 'String'},
|
||||
{text: 'United', unit: 'bps'},
|
||||
{text: 'Sanitized'},
|
||||
];
|
||||
|
||||
var panel = {
|
||||
@ -47,15 +48,24 @@ describe('when rendering table', () => {
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 2,
|
||||
},
|
||||
{
|
||||
pattern: 'Sanitized',
|
||||
type: 'string',
|
||||
sanitize: true,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var renderer = new TableRenderer(panel, table, 'utc');
|
||||
var sanitize = function(value) {
|
||||
return 'sanitized';
|
||||
};
|
||||
|
||||
var renderer = new TableRenderer(panel, table, 'utc', sanitize);
|
||||
|
||||
it('time column should be formated', () => {
|
||||
var html = renderer.renderCell(0, 1388556366666);
|
||||
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', () => {
|
||||
@ -107,6 +117,11 @@ describe('when rendering table', () => {
|
||||
var html = renderer.renderCell(3, undefined);
|
||||
expect(html).to.be('<td></td>');
|
||||
});
|
||||
|
||||
it('sanitized value should render as', () => {
|
||||
var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>');
|
||||
expect(html).to.be('<td>sanitized</td>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
// Media queries
|
||||
// ---------------------
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
div.panel {
|
||||
width: 100% !important;
|
||||
@ -33,6 +34,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.page-dashboard .navbar-page-btn {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
// form styles
|
||||
@include media-breakpoint-up(md) {
|
||||
.page-dashboard .navbar-page-btn {
|
||||
|
@ -60,7 +60,7 @@ $page-bg: $white;
|
||||
$body-color: $gray-1;
|
||||
$text-color: $gray-1;
|
||||
$text-color-strong: $white;
|
||||
$text-color-weak: $gray-1;
|
||||
$text-color-weak: $gray-2;
|
||||
$text-color-faint: $gray-3;
|
||||
$text-color-emphasis: $dark-5;
|
||||
|
||||
|
@ -102,6 +102,11 @@ $gf-form-margin: 0.25rem;
|
||||
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.
|
||||
@include form-control-focus();
|
||||
|
||||
|
@ -105,7 +105,7 @@
|
||||
}
|
||||
|
||||
.confirm-modal-text2 {
|
||||
font-size: $font-size-h5;
|
||||
font-size: $font-size-root;
|
||||
padding-top: $spacer;
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,7 @@
|
||||
|
||||
.search-button-row {
|
||||
padding-top: 20px;
|
||||
line-height: 2.5rem;
|
||||
button, a {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
@ -110,6 +110,13 @@ define([
|
||||
});
|
||||
});
|
||||
|
||||
describe('kbn deckbytes format when scaled decimals is null do not use it', function() {
|
||||
it('should use specified decimals', function() {
|
||||
var str = kbn.valueFormats['deckbytes'](10000000, 3, null);
|
||||
expect(str).to.be('10.000 GB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('kbn roundValue', function() {
|
||||
it('should should handle null value', function() {
|
||||
var str = kbn.roundValue(null, 2);
|
||||
@ -154,4 +161,50 @@ define([
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
9906
public/vendor/moment.js
vendored
9906
public/vendor/moment.js
vendored
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
||||
],
|
||||
|
||||
"title": "Nginx Connections",
|
||||
"revision": "1.5",
|
||||
"revision": 25,
|
||||
"schemaVersion": 11,
|
||||
"tags": ["tag1", "tag2"],
|
||||
"number_array": [1,2,3,10.33],
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"revision": "1.5",
|
||||
"revision": 25,
|
||||
"tags": ["tag1", "tag2"],
|
||||
"boolean_false": false,
|
||||
"boolean_true": true,
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Nginx Memory",
|
||||
"revision": "2.0",
|
||||
"revision": 2,
|
||||
"schemaVersion": 11
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user