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
commit d9096110f8
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)
### 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.

View File

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

View File

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

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

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

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {
if len(a.server.LdapGroups) == 0 {
log.Warn("Ldap: no group mappings defined")
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

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 updateAppDashboards()
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -44,6 +44,10 @@ function (angular, _) {
timestamp: new Date()
};
if (!external) {
$scope.dashboard.snapshot.originalUrl = $location.absUrl();
}
$scope.loading = true;
$scope.snapshot.external = external;

View File

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

View File

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

View File

@ -14,20 +14,19 @@
</span>
</td>
<td>
v{{dash.revision}}
<span ng-if="dash.installed">
&nbsp;(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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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) {
ctx.setup(function() {
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>
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -105,7 +105,7 @@
}
.confirm-modal-text2 {
font-size: $font-size-h5;
font-size: $font-size-root;
padding-top: $spacer;
}

View File

@ -101,6 +101,7 @@
.search-button-row {
padding-top: 20px;
line-height: 2.5rem;
button, a {
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() {
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

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
],
"title": "Nginx Connections",
"revision": "1.5",
"revision": 25,
"schemaVersion": 11,
"tags": ["tag1", "tag2"],
"number_array": [1,2,3,10.33],

View File

@ -1,5 +1,5 @@
{
"revision": "1.5",
"revision": 25,
"tags": ["tag1", "tag2"],
"boolean_false": false,
"boolean_true": true,

View File

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