mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/develop' into gauge-value-mappings
This commit is contained in:
commit
c2b1f504a0
21
CHANGELOG.md
21
CHANGELOG.md
@ -1,5 +1,8 @@
|
|||||||
# 5.5.0 (unreleased)
|
# 5.5.0 (unreleased)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
|
||||||
|
|
||||||
### Minor
|
### Minor
|
||||||
|
|
||||||
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
|
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
|
||||||
@ -7,6 +10,24 @@
|
|||||||
* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
|
* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
|
||||||
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
|
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
|
||||||
|
|
||||||
|
# 5.4.1 (2018-12-10)
|
||||||
|
|
||||||
|
* **Stackdriver**: Fixes issue with data proxy and Authorization header [#14262](https://github.com/grafana/grafana/issues/14262)
|
||||||
|
* **Units**: fixedUnit for Flow:l/min and mL/min [#14294](https://github.com/grafana/grafana/issues/14294), thx [@flopp999](https://github.com/flopp999).
|
||||||
|
* **Logging**: Fix for issue where data proxy logged a secret when debug logging was enabled, now redacted. [#14319](https://github.com/grafana/grafana/issues/14319)
|
||||||
|
* **InfluxDB**: Add support for alerting on InfluxDB queries that use the cumulative_sum function. [#14314](https://github.com/grafana/grafana/pull/14314), thx [@nitti](https://github.com/nitti)
|
||||||
|
* **Plugins**: Panel plugins should no receive the panel-initialized event again as usual.
|
||||||
|
* **Embedded Graphs**: Iframe graph panels should now work as usual. [#14284](https://github.com/grafana/grafana/issues/14284)
|
||||||
|
* **Postgres**: Improve PostgreSQL Query Editor if using different Schemas, [#14313](
|
||||||
|
https://github.com/grafana/grafana/pull/14313)
|
||||||
|
* **Quotas**: Fixed for updating org & user quotas. [#14347](https://github.com/grafana/grafana/pull/14347), thx [#moznion](https://github.com/moznion)
|
||||||
|
* **Cloudwatch**: Add the AWS/SES Cloudwatch metrics of BounceRate and ComplaintRate to auto complete list. [#14401](https://github.com/grafana/grafana/pull/14401), thx [@sglajchEG](https://github.com/sglajchEG)
|
||||||
|
* **Dashboard Search**: Fixed filtering by tag issues.
|
||||||
|
* **Graph**: Fixed time region issues, [#14425](https://github.com/grafana/grafana/issues/14425), [#14280](https://github.com/grafana/grafana/issues/14280)
|
||||||
|
* **Graph**: Fixed issue with series color picker popover being placed outside window.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 5.4.0 (2018-12-03)
|
# 5.4.0 (2018-12-03)
|
||||||
|
|
||||||
* **Cloudwatch**: Fix invalid time range causes segmentation fault [#14150](https://github.com/grafana/grafana/issues/14150)
|
* **Cloudwatch**: Fix invalid time range causes segmentation fault [#14150](https://github.com/grafana/grafana/issues/14150)
|
||||||
|
@ -157,27 +157,29 @@ There are a couple of configuration options which need to be set up in Grafana U
|
|||||||
|
|
||||||
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling.
|
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling.
|
||||||
|
|
||||||
### All supported notifiers
|
### Google Hangouts Chat
|
||||||
|
|
||||||
Name | Type |Support images | Support reminders
|
Notifications can be sent by setting up an incoming webhook in Google Hangouts chat. Configuring such a webhook is described [here](https://developers.google.com/hangouts/chat/how-tos/webhooks).
|
||||||
-----|------------ | ------ | ------ |
|
|
||||||
Slack | `slack` | yes | yes
|
|
||||||
Pagerduty | `pagerduty` | yes | yes
|
|
||||||
Email | `email` | yes | yes
|
|
||||||
Webhook | `webhook` | link | yes
|
|
||||||
Kafka | `kafka` | no | yes
|
|
||||||
Hipchat | `hipchat` | yes | yes
|
|
||||||
VictorOps | `victorops` | yes | yes
|
|
||||||
Sensu | `sensu` | yes | yes
|
|
||||||
OpsGenie | `opsgenie` | yes | yes
|
|
||||||
Threema | `threema` | yes | yes
|
|
||||||
Pushover | `pushover` | no | yes
|
|
||||||
Telegram | `telegram` | no | yes
|
|
||||||
Line | `line` | no | yes
|
|
||||||
Microsoft Teams | `teams` | yes | yes
|
|
||||||
Prometheus Alertmanager | `prometheus-alertmanager` | no | no
|
|
||||||
|
|
||||||
|
### All supported notifier
|
||||||
|
|
||||||
|
Name | Type |Support images
|
||||||
|
-----|------------ | ------
|
||||||
|
Slack | `slack` | yes
|
||||||
|
Pagerduty | `pagerduty` | yes
|
||||||
|
Email | `email` | yes
|
||||||
|
Webhook | `webhook` | link
|
||||||
|
Kafka | `kafka` | no
|
||||||
|
Google Hangouts Chat | `googlechat` | yes
|
||||||
|
Hipchat | `hipchat` | yes
|
||||||
|
VictorOps | `victorops` | yes
|
||||||
|
Sensu | `sensu` | yes
|
||||||
|
OpsGenie | `opsgenie` | yes
|
||||||
|
Threema | `threema` | yes
|
||||||
|
Pushover | `pushover` | no
|
||||||
|
Telegram | `telegram` | no
|
||||||
|
Line | `line` | no
|
||||||
|
Prometheus Alertmanager | `prometheus-alertmanager` | no
|
||||||
|
|
||||||
# Enable images in notifications {#external-image-store}
|
# Enable images in notifications {#external-image-store}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#! /usr/bin/env bash
|
#! /usr/bin/env bash
|
||||||
version=5.0.2
|
version=5.4.1
|
||||||
|
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb
|
wget https://dl.grafana.com/oss/release/grafana_${version}_amd64.deb
|
||||||
|
|
||||||
package_cloud push grafana/stable/debian/jessie grafana_${version}_amd64.deb
|
package_cloud push grafana/stable/debian/jessie grafana_${version}_amd64.deb
|
||||||
package_cloud push grafana/stable/debian/wheezy grafana_${version}_amd64.deb
|
package_cloud push grafana/stable/debian/wheezy grafana_${version}_amd64.deb
|
||||||
@ -11,7 +11,7 @@ package_cloud push grafana/testing/debian/jessie grafana_${version}_amd64.deb
|
|||||||
package_cloud push grafana/testing/debian/wheezy grafana_${version}_amd64.deb --verbose
|
package_cloud push grafana/testing/debian/wheezy grafana_${version}_amd64.deb --verbose
|
||||||
package_cloud push grafana/testing/debian/stretch grafana_${version}_amd64.deb --verbose
|
package_cloud push grafana/testing/debian/stretch grafana_${version}_amd64.deb --verbose
|
||||||
|
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${version}-1.x86_64.rpm
|
wget https://dl.grafana.com/release/grafana-${version}-1.x86_64.rpm
|
||||||
|
|
||||||
package_cloud push grafana/testing/el/6 grafana-${version}-1.x86_64.rpm --verbose
|
package_cloud push grafana/testing/el/6 grafana-${version}-1.x86_64.rpm --verbose
|
||||||
package_cloud push grafana/testing/el/7 grafana-${version}-1.x86_64.rpm --verbose
|
package_cloud push grafana/testing/el/7 grafana-${version}-1.x86_64.rpm --verbose
|
||||||
|
@ -147,9 +147,6 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
|||||||
SubTitle: "Explore your data",
|
SubTitle: "Explore your data",
|
||||||
Icon: "fa fa-rocket",
|
Icon: "fa fa-rocket",
|
||||||
Url: setting.AppSubUrl + "/explore",
|
Url: setting.AppSubUrl + "/explore",
|
||||||
Children: []*dtos.NavLink{
|
|
||||||
{Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore"},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
215
pkg/services/alerting/notifiers/googlechat.go
Normal file
215
pkg/services/alerting/notifiers/googlechat.go
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||||
|
Type: "googlechat",
|
||||||
|
Name: "Google Hangouts Chat",
|
||||||
|
Description: "Sends notifications to Google Hangouts Chat via webhooks based on the official JSON message " +
|
||||||
|
"format (https://developers.google.com/hangouts/chat/reference/message-formats/).",
|
||||||
|
Factory: NewGoogleChatNotifier,
|
||||||
|
OptionsTemplate: `
|
||||||
|
<h3 class="page-heading">Google Hangouts Chat settings</h3>
|
||||||
|
<div class="gf-form max-width-30">
|
||||||
|
<span class="gf-form-label width-6">Url</span>
|
||||||
|
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Google Hangouts Chat incoming webhook url"></input>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGoogleChatNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||||
|
url := model.Settings.Get("url").MustString()
|
||||||
|
if url == "" {
|
||||||
|
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GoogleChatNotifier{
|
||||||
|
NotifierBase: NewNotifierBase(model),
|
||||||
|
Url: url,
|
||||||
|
log: log.New("alerting.notifier.googlechat"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoogleChatNotifier struct {
|
||||||
|
NotifierBase
|
||||||
|
Url string
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Structs used to build a custom Google Hangouts Chat message card.
|
||||||
|
See: https://developers.google.com/hangouts/chat/reference/message-formats/cards
|
||||||
|
*/
|
||||||
|
type outerStruct struct {
|
||||||
|
Cards []card `json:"cards"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type card struct {
|
||||||
|
Header header `json:"header"`
|
||||||
|
Sections []section `json:"sections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type header struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type section struct {
|
||||||
|
Widgets []widget `json:"widgets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// "generic" widget used to add different types of widgets (buttonWidget, textParagraphWidget, imageWidget)
|
||||||
|
type widget interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type buttonWidget struct {
|
||||||
|
Buttons []button `json:"buttons"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type textParagraphWidget struct {
|
||||||
|
Text text `json:"textParagraph"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type text struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageWidget struct {
|
||||||
|
Image image `json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type image struct {
|
||||||
|
ImageUrl string `json:"imageUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type button struct {
|
||||||
|
TextButton textButton `json:"textButton"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type textButton struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
OnClick onClick `json:"onClick"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type onClick struct {
|
||||||
|
OpenLink openLink `json:"openLink"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openLink struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||||
|
this.log.Info("Executing Google Chat notification")
|
||||||
|
|
||||||
|
headers := map[string]string{
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleUrl, err := evalContext.GetRuleUrl()
|
||||||
|
if err != nil {
|
||||||
|
this.log.Error("evalContext returned an invalid rule URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a text paragraph widget for the message
|
||||||
|
widgets := []widget{
|
||||||
|
textParagraphWidget{
|
||||||
|
Text: text{
|
||||||
|
Text: evalContext.Rule.Message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a text paragraph widget for the fields
|
||||||
|
var fields []textParagraphWidget
|
||||||
|
fieldLimitCount := 4
|
||||||
|
for index, evt := range evalContext.EvalMatches {
|
||||||
|
fields = append(fields,
|
||||||
|
textParagraphWidget{
|
||||||
|
Text: text{
|
||||||
|
Text: "<i>" + evt.Metric + ": " + fmt.Sprint(evt.Value) + "</i>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if index > fieldLimitCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widgets = append(widgets, fields)
|
||||||
|
|
||||||
|
// if an image exists, add it as an image widget
|
||||||
|
if evalContext.ImagePublicUrl != "" {
|
||||||
|
widgets = append(widgets, imageWidget{
|
||||||
|
Image: image{
|
||||||
|
ImageUrl: evalContext.ImagePublicUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.log.Info("Could not retrieve a public image URL.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a button widget (link to Grafana)
|
||||||
|
widgets = append(widgets, buttonWidget{
|
||||||
|
Buttons: []button{
|
||||||
|
{
|
||||||
|
TextButton: textButton{
|
||||||
|
Text: "OPEN IN GRAFANA",
|
||||||
|
OnClick: onClick{
|
||||||
|
OpenLink: openLink{
|
||||||
|
Url: ruleUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// add text paragraph widget for the build version and timestamp
|
||||||
|
widgets = append(widgets, textParagraphWidget{
|
||||||
|
Text: text{
|
||||||
|
Text: "Grafana v" + setting.BuildVersion + " | " + (time.Now()).Format(time.RFC822),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// nest the required structs
|
||||||
|
res1D := &outerStruct{
|
||||||
|
Cards: []card{
|
||||||
|
{
|
||||||
|
Header: header{
|
||||||
|
Title: evalContext.GetNotificationTitle(),
|
||||||
|
},
|
||||||
|
Sections: []section{
|
||||||
|
{
|
||||||
|
Widgets: widgets,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(res1D)
|
||||||
|
|
||||||
|
cmd := &m.SendWebhookSync{
|
||||||
|
Url: this.Url,
|
||||||
|
HttpMethod: "POST",
|
||||||
|
HttpHeader: headers,
|
||||||
|
Body: string(body),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||||
|
this.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", this.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
53
pkg/services/alerting/notifiers/googlechat_test.go
Normal file
53
pkg/services/alerting/notifiers/googlechat_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGoogleChatNotifier(t *testing.T) {
|
||||||
|
Convey("Google Hangouts Chat notifier tests", t, func() {
|
||||||
|
|
||||||
|
Convey("Parsing alert notification from settings", func() {
|
||||||
|
Convey("empty settings should return error", func() {
|
||||||
|
json := `{ }`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
model := &m.AlertNotification{
|
||||||
|
Name: "ops",
|
||||||
|
Type: "googlechat",
|
||||||
|
Settings: settingsJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewGoogleChatNotifier(model)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("from settings", func() {
|
||||||
|
json := `
|
||||||
|
{
|
||||||
|
"url": "http://google.com"
|
||||||
|
}`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
model := &m.AlertNotification{
|
||||||
|
Name: "ops",
|
||||||
|
Type: "googlechat",
|
||||||
|
Settings: settingsJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
not, err := NewGoogleChatNotifier(model)
|
||||||
|
webhookNotifier := not.(*GoogleChatNotifier)
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(webhookNotifier.Name, ShouldEqual, "ops")
|
||||||
|
So(webhookNotifier.Type, ShouldEqual, "googlechat")
|
||||||
|
So(webhookNotifier.Url, ShouldEqual, "http://google.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -101,7 +101,7 @@ func init() {
|
|||||||
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||||
"AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
|
"AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
|
||||||
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
|
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
|
||||||
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"},
|
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
|
||||||
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
|
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
|
||||||
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
|
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
|
||||||
"AWS/States": {"ExecutionTime", "ExecutionThrottled", "ExecutionsAborted", "ExecutionsFailed", "ExecutionsStarted", "ExecutionsSucceeded", "ExecutionsTimedOut", "ActivityRunTime", "ActivityScheduleTime", "ActivityTime", "ActivitiesFailed", "ActivitiesHeartbeatTimedOut", "ActivitiesScheduled", "ActivitiesScheduled", "ActivitiesSucceeded", "ActivitiesTimedOut", "LambdaFunctionRunTime", "LambdaFunctionScheduleTime", "LambdaFunctionTime", "LambdaFunctionsFailed", "LambdaFunctionsHeartbeatTimedOut", "LambdaFunctionsScheduled", "LambdaFunctionsStarted", "LambdaFunctionsSucceeded", "LambdaFunctionsTimedOut"},
|
"AWS/States": {"ExecutionTime", "ExecutionThrottled", "ExecutionsAborted", "ExecutionsFailed", "ExecutionsStarted", "ExecutionsSucceeded", "ExecutionsTimedOut", "ActivityRunTime", "ActivityScheduleTime", "ActivityTime", "ActivitiesFailed", "ActivitiesHeartbeatTimedOut", "ActivitiesScheduled", "ActivitiesScheduled", "ActivitiesSucceeded", "ActivitiesTimedOut", "LambdaFunctionRunTime", "LambdaFunctionScheduleTime", "LambdaFunctionTime", "LambdaFunctionsFailed", "LambdaFunctionsHeartbeatTimedOut", "LambdaFunctionsScheduled", "LambdaFunctionsStarted", "LambdaFunctionsSucceeded", "LambdaFunctionsTimedOut"},
|
||||||
|
@ -16,7 +16,7 @@ export function registerAngularDirectives() {
|
|||||||
react2AngularDirective('searchResult', SearchResult, []);
|
react2AngularDirective('searchResult', SearchResult, []);
|
||||||
react2AngularDirective('tagFilter', TagFilter, [
|
react2AngularDirective('tagFilter', TagFilter, [
|
||||||
'tags',
|
'tags',
|
||||||
['onSelect', { watchDepth: 'reference' }],
|
['onChange', { watchDepth: 'reference' }],
|
||||||
['tagOptions', { watchDepth: 'reference' }],
|
['tagOptions', { watchDepth: 'reference' }],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ interface Props {
|
|||||||
duration: number;
|
duration: number;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
in: boolean;
|
in: boolean;
|
||||||
|
unmountOnExit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FadeIn: SFC<Props> = props => {
|
export const FadeIn: SFC<Props> = props => {
|
||||||
@ -21,7 +22,7 @@ export const FadeIn: SFC<Props> = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition in={props.in} timeout={props.duration}>
|
<Transition in={props.in} timeout={props.duration} unmountOnExit={props.unmountOnExit || false}>
|
||||||
{state => (
|
{state => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
clearIndicator: () => ({}),
|
clearIndicator: () => ({}),
|
||||||
container: () => ({}),
|
container: () => ({}),
|
||||||
control: () => ({}),
|
control: () => ({}),
|
||||||
@ -11,7 +11,9 @@
|
|||||||
loadingIndicator: () => ({}),
|
loadingIndicator: () => ({}),
|
||||||
loadingMessage: () => ({}),
|
loadingMessage: () => ({}),
|
||||||
menu: () => ({}),
|
menu: () => ({}),
|
||||||
menuList: () => ({}),
|
menuList: ({ maxHeight }: { maxHeight: number }) => ({
|
||||||
|
maxHeight,
|
||||||
|
}),
|
||||||
multiValue: () => ({}),
|
multiValue: () => ({}),
|
||||||
multiValueLabel: () => ({}),
|
multiValueLabel: () => ({}),
|
||||||
multiValueRemove: () => ({}),
|
multiValueRemove: () => ({}),
|
||||||
|
@ -6,6 +6,7 @@ export interface Props {
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
labelClass?: string;
|
labelClass?: string;
|
||||||
switchClass?: string;
|
switchClass?: string;
|
||||||
|
transparent?: boolean;
|
||||||
onChange: (event) => any;
|
onChange: (event) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,11 +25,11 @@ export class Switch extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { labelClass = '', switchClass = '', label, checked } = this.props;
|
const { labelClass = '', switchClass = '', label, checked, transparent } = this.props;
|
||||||
|
|
||||||
const labelId = `check-${this.state.id}`;
|
const labelId = `check-${this.state.id}`;
|
||||||
const labelClassName = `gf-form-label ${labelClass} pointer`;
|
const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
|
||||||
const switchClassName = `gf-form-switch ${switchClass}`;
|
const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label htmlFor={labelId} className="gf-form-switch-container">
|
<label htmlFor={labelId} className="gf-form-switch-container">
|
||||||
|
@ -10,7 +10,7 @@ import ResetStyles from 'app/core/components/Picker/ResetStyles';
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
tagOptions: () => any;
|
tagOptions: () => any;
|
||||||
onSelect: (tag: string) => void;
|
onChange: (tags: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TagFilter extends React.Component<Props, any> {
|
export class TagFilter extends React.Component<Props, any> {
|
||||||
@ -18,12 +18,9 @@ export class TagFilter extends React.Component<Props, any> {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.searchTags = this.searchTags.bind(this);
|
|
||||||
this.onChange = this.onChange.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchTags(query) {
|
onLoadOptions = query => {
|
||||||
return this.props.tagOptions().then(options => {
|
return this.props.tagOptions().then(options => {
|
||||||
return options.map(option => ({
|
return options.map(option => ({
|
||||||
value: option.term,
|
value: option.term,
|
||||||
@ -31,18 +28,20 @@ export class TagFilter extends React.Component<Props, any> {
|
|||||||
count: option.count,
|
count: option.count,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onChange(newTags) {
|
onChange = (newTags: any[]) => {
|
||||||
this.props.onSelect(newTags);
|
this.props.onChange(newTags.map(tag => tag.value));
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const tags = this.props.tags.map(tag => ({ value: tag, label: tag, count: 0 }));
|
||||||
|
|
||||||
const selectOptions = {
|
const selectOptions = {
|
||||||
classNamePrefix: 'gf-form-select-box',
|
classNamePrefix: 'gf-form-select-box',
|
||||||
isMulti: true,
|
isMulti: true,
|
||||||
defaultOptions: true,
|
defaultOptions: true,
|
||||||
loadOptions: this.searchTags,
|
loadOptions: this.onLoadOptions,
|
||||||
onChange: this.onChange,
|
onChange: this.onChange,
|
||||||
className: 'gf-form-input gf-form-input--form-dropdown',
|
className: 'gf-form-input gf-form-input--form-dropdown',
|
||||||
placeholder: 'Tags',
|
placeholder: 'Tags',
|
||||||
@ -50,7 +49,7 @@ export class TagFilter extends React.Component<Props, any> {
|
|||||||
noOptionsMessage: () => 'No tags found',
|
noOptionsMessage: () => 'No tags found',
|
||||||
getOptionValue: i => i.value,
|
getOptionValue: i => i.value,
|
||||||
getOptionLabel: i => i.label,
|
getOptionLabel: i => i.label,
|
||||||
value: this.props.tags,
|
value: tags,
|
||||||
styles: ResetStyles,
|
styles: ResetStyles,
|
||||||
components: {
|
components: {
|
||||||
Option: TagOption,
|
Option: TagOption,
|
||||||
|
@ -1,48 +1,20 @@
|
|||||||
import React, { SFC, ReactNode, PureComponent, ReactElement } from 'react';
|
import React, { SFC, ReactNode, PureComponent } from 'react';
|
||||||
|
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||||
|
|
||||||
interface ToggleButtonGroupProps {
|
interface ToggleButtonGroupProps {
|
||||||
onChange: (value) => void;
|
|
||||||
value?: any;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
render: (props) => void;
|
children: JSX.Element[];
|
||||||
stackedButtons?: boolean;
|
transparent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ToggleButtonGroup extends PureComponent<ToggleButtonGroupProps> {
|
export default class ToggleButtonGroup extends PureComponent<ToggleButtonGroupProps> {
|
||||||
static defaultProps = {
|
|
||||||
stackedButtons: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
getValues() {
|
|
||||||
const { children } = this.props;
|
|
||||||
return React.Children.toArray(children).map((c: ReactElement<any>) => c.props.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
smallChildren() {
|
|
||||||
const { children } = this.props;
|
|
||||||
return React.Children.toArray(children).every((c: ReactElement<any>) => c.props.className.includes('small'));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggle(toggleValue) {
|
|
||||||
const { value, onChange } = this.props;
|
|
||||||
if (value && value === toggleValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onChange(toggleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { value, label, stackedButtons } = this.props;
|
const { children, label, transparent } = this.props;
|
||||||
const values = this.getValues();
|
|
||||||
const selectedValue = value || values[0];
|
|
||||||
const labelClassName = `gf-form-label ${this.smallChildren() ? 'small' : ''}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<div className={`toggle-button-group ${stackedButtons ? 'stacked' : ''}`}>
|
{label && <label className={`gf-form-label ${transparent ? 'gf-form-label--transparent' : ''}`}>{label}</label>}
|
||||||
{label && <label className={labelClassName}>{label}</label>}
|
<div className={`toggle-button-group ${transparent ? 'toggle-button-group--transparent' : ''}`}>{children}</div>
|
||||||
{this.props.render({ selectedValue, onChange: this.handleToggle.bind(this), stackedButtons: stackedButtons })}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -54,16 +26,16 @@ interface ToggleButtonProps {
|
|||||||
value: any;
|
value: any;
|
||||||
className?: string;
|
className?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
stackedButtons?: boolean;
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToggleButton: SFC<ToggleButtonProps> = ({
|
export const ToggleButton: SFC<ToggleButtonProps> = ({
|
||||||
children,
|
children,
|
||||||
selected,
|
selected,
|
||||||
className = '',
|
className = '',
|
||||||
value,
|
value = null,
|
||||||
|
tooltip,
|
||||||
onChange,
|
onChange,
|
||||||
stackedButtons,
|
|
||||||
}) => {
|
}) => {
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -72,10 +44,16 @@ export const ToggleButton: SFC<ToggleButtonProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnClassName = `btn ${className} ${selected ? 'active' : ''} ${stackedButtons ? 'stacked' : ''}`;
|
const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
|
||||||
return (
|
const button = (
|
||||||
<button className={btnClassName} onClick={handleChange}>
|
<button className={btnClassName} onClick={handleChange}>
|
||||||
<span>{children}</span>
|
<span>{children}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
return <Tooltip content={tooltip}>{button}</Tooltip>;
|
||||||
|
} else {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -44,7 +44,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
|||||||
const drop = new Drop({
|
const drop = new Drop({
|
||||||
target: this.pickerElem,
|
target: this.pickerElem,
|
||||||
content: dropContentElem,
|
content: dropContentElem,
|
||||||
position: 'top center',
|
position: 'bottom center',
|
||||||
classes: 'drop-popover',
|
classes: 'drop-popover',
|
||||||
openOn: 'hover',
|
openOn: 'hover',
|
||||||
hoverCloseDelay: 200,
|
hoverCloseDelay: 200,
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onSelect="ctrl.onTagSelect">
|
<tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onChange="ctrl.onTagFiltersChanged">
|
||||||
</tag-filter>
|
</tag-filter>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -25,8 +25,6 @@ export class SearchCtrl {
|
|||||||
appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
|
appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
|
||||||
|
|
||||||
this.initialFolderFilterTitle = 'All';
|
this.initialFolderFilterTitle = 'All';
|
||||||
this.getTags = this.getTags.bind(this);
|
|
||||||
this.onTagSelect = this.onTagSelect.bind(this);
|
|
||||||
this.isEditor = contextSrv.isEditor;
|
this.isEditor = contextSrv.isEditor;
|
||||||
this.hasEditPermissionInFolders = contextSrv.hasEditPermissionInFolders;
|
this.hasEditPermissionInFolders = contextSrv.hasEditPermissionInFolders;
|
||||||
}
|
}
|
||||||
@ -162,7 +160,7 @@ export class SearchCtrl {
|
|||||||
const localSearchId = this.currentSearchId;
|
const localSearchId = this.currentSearchId;
|
||||||
const query = {
|
const query = {
|
||||||
...this.query,
|
...this.query,
|
||||||
tag: this.query.tag.map(i => i.value),
|
tag: this.query.tag,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.searchSrv.search(query).then(results => {
|
return this.searchSrv.search(query).then(results => {
|
||||||
@ -195,14 +193,14 @@ export class SearchCtrl {
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTags() {
|
getTags = () => {
|
||||||
return this.searchSrv.getDashboardTags();
|
return this.searchSrv.getDashboardTags();
|
||||||
}
|
};
|
||||||
|
|
||||||
onTagSelect(newTags) {
|
onTagFiltersChanged = (tags: string[]) => {
|
||||||
this.query.tag = newTags;
|
this.query.tag = tags;
|
||||||
this.search();
|
this.search();
|
||||||
}
|
};
|
||||||
|
|
||||||
clearSearchFilter() {
|
clearSearchFilter() {
|
||||||
this.query.tag = [];
|
this.query.tag = [];
|
||||||
|
@ -15,7 +15,7 @@ const TopSectionItem: SFC<Props> = props => {
|
|||||||
{link.img && <img src={link.img} />}
|
{link.img && <img src={link.img} />}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{link.children && <SideMenuDropDown link={link} />}
|
<SideMenuDropDown link={link} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,5 +13,8 @@ exports[`Render should render component 1`] = `
|
|||||||
<i />
|
<i />
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
<SideMenuDropDown
|
||||||
|
link={Object {}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -88,6 +88,13 @@ export interface LogsStreamLabels {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LogsDedupDescription {
|
||||||
|
none = 'No de-duplication',
|
||||||
|
exact = 'De-duplication of successive lines that are identical, ignoring ISO datetimes.',
|
||||||
|
numbers = 'De-duplication of successive lines that are identical when ignoring numbers, e.g., IP addresses, latencies.',
|
||||||
|
signature = 'De-duplication of successive lines that have identical punctuation and whitespace.',
|
||||||
|
}
|
||||||
|
|
||||||
export enum LogsDedupStrategy {
|
export enum LogsDedupStrategy {
|
||||||
none = 'none',
|
none = 'none',
|
||||||
exact = 'exact',
|
exact = 'exact',
|
||||||
@ -242,32 +249,47 @@ export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSerie
|
|||||||
// Graph time series by log level
|
// Graph time series by log level
|
||||||
const seriesByLevel = {};
|
const seriesByLevel = {};
|
||||||
const bucketSize = intervalMs * 10;
|
const bucketSize = intervalMs * 10;
|
||||||
|
const seriesList = [];
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (!seriesByLevel[row.logLevel]) {
|
let series = seriesByLevel[row.logLevel];
|
||||||
seriesByLevel[row.logLevel] = { lastTs: null, datapoints: [], alias: row.logLevel };
|
|
||||||
|
if (!series) {
|
||||||
|
seriesByLevel[row.logLevel] = series = {
|
||||||
|
lastTs: null,
|
||||||
|
datapoints: [],
|
||||||
|
alias: row.logLevel,
|
||||||
|
color: LogLevelColor[row.logLevel],
|
||||||
|
};
|
||||||
|
|
||||||
|
seriesList.push(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
const levelSeries = seriesByLevel[row.logLevel];
|
// align time to bucket size
|
||||||
|
|
||||||
// Bucket to nearest minute
|
|
||||||
const time = Math.round(row.timeEpochMs / bucketSize) * bucketSize;
|
const time = Math.round(row.timeEpochMs / bucketSize) * bucketSize;
|
||||||
|
|
||||||
// Entry for time
|
// Entry for time
|
||||||
if (time === levelSeries.lastTs) {
|
if (time === series.lastTs) {
|
||||||
levelSeries.datapoints[levelSeries.datapoints.length - 1][0]++;
|
series.datapoints[series.datapoints.length - 1][0]++;
|
||||||
} else {
|
} else {
|
||||||
levelSeries.datapoints.push([1, time]);
|
series.datapoints.push([1, time]);
|
||||||
levelSeries.lastTs = time;
|
series.lastTs = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add zero to other levels to aid stacking so each level series has same number of points
|
||||||
|
for (const other of seriesList) {
|
||||||
|
if (other !== series && other.lastTs !== time) {
|
||||||
|
other.datapoints.push([0, time]);
|
||||||
|
other.lastTs = time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(seriesByLevel).reduce((acc, level) => {
|
return seriesList.map(series => {
|
||||||
if (seriesByLevel[level]) {
|
series.datapoints.sort((a, b) => {
|
||||||
const gs = new TimeSeries(seriesByLevel[level]);
|
return a[1] - b[1];
|
||||||
gs.setColor(LogLevelColor[level]);
|
});
|
||||||
acc.push(gs);
|
|
||||||
}
|
return new TimeSeries(series);
|
||||||
return acc;
|
});
|
||||||
}, []);
|
|
||||||
}
|
}
|
||||||
|
@ -1,97 +1,124 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import withKeyboardNavigation from './withKeyboardNavigation';
|
||||||
import { DataSourceSelectItem } from 'app/types';
|
import { DataSourceSelectItem } from 'app/types';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
onChangeDataSource: (ds: any) => void;
|
onChangeDataSource: (ds: any) => void;
|
||||||
datasources: DataSourceSelectItem[];
|
datasources: DataSourceSelectItem[];
|
||||||
|
selected?: number;
|
||||||
|
onKeyDown?: (evt: any, maxSelectedIndex: number, onEnterAction: () => void) => void;
|
||||||
|
onMouseEnter?: (select: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataSourcePicker extends PureComponent<Props, State> {
|
export const DataSourcePicker = withKeyboardNavigation(
|
||||||
searchInput: HTMLElement;
|
class DataSourcePicker extends PureComponent<Props, State> {
|
||||||
|
searchInput: HTMLElement;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataSources() {
|
getDataSources() {
|
||||||
const { searchQuery } = this.state;
|
const { searchQuery } = this.state;
|
||||||
const regex = new RegExp(searchQuery, 'i');
|
const regex = new RegExp(searchQuery, 'i');
|
||||||
const { datasources } = this.props;
|
const { datasources } = this.props;
|
||||||
|
|
||||||
const filtered = datasources.filter(item => {
|
const filtered = datasources.filter(item => {
|
||||||
return regex.test(item.name) || regex.test(item.meta.name);
|
return regex.test(item.name) || regex.test(item.meta.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDataSource = (ds: DataSourceSelectItem, index: number) => {
|
get maxSelectedIndex() {
|
||||||
const { onChangeDataSource } = this.props;
|
const filtered = this.getDataSources();
|
||||||
const onClick = () => onChangeDataSource(ds);
|
return filtered.length - 1;
|
||||||
const cssClass = classNames({
|
}
|
||||||
'ds-picker-list__item': true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
renderDataSource = (ds: DataSourceSelectItem, index: number) => {
|
||||||
<div key={index} className={cssClass} title={ds.name} onClick={onClick}>
|
const { onChangeDataSource, selected, onMouseEnter } = this.props;
|
||||||
<img className="ds-picker-list__img" src={ds.meta.info.logos.small} />
|
const onClick = () => onChangeDataSource(ds);
|
||||||
<div className="ds-picker-list__name">{ds.name}</div>
|
const isSelected = selected === index;
|
||||||
</div>
|
const cssClass = classNames({
|
||||||
);
|
'ds-picker-list__item': true,
|
||||||
};
|
'ds-picker-list__item--selected': isSelected,
|
||||||
|
});
|
||||||
componentDidMount() {
|
return (
|
||||||
setTimeout(() => {
|
<div
|
||||||
this.searchInput.focus();
|
key={index}
|
||||||
}, 300);
|
className={cssClass}
|
||||||
}
|
title={ds.name}
|
||||||
|
onClick={onClick}
|
||||||
onSearchQueryChange = evt => {
|
onMouseEnter={() => onMouseEnter(index)}
|
||||||
const value = evt.target.value;
|
>
|
||||||
this.setState(prevState => ({
|
<img className="ds-picker-list__img" src={ds.meta.info.logos.small} />
|
||||||
...prevState,
|
<div className="ds-picker-list__name">{ds.name}</div>
|
||||||
searchQuery: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
renderFilters() {
|
|
||||||
const { searchQuery } = this.state;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="gf-form--has-input-icon">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="gf-form-input width-13"
|
|
||||||
placeholder=""
|
|
||||||
ref={elem => (this.searchInput = elem)}
|
|
||||||
onChange={this.onSearchQueryChange}
|
|
||||||
value={searchQuery}
|
|
||||||
/>
|
|
||||||
<i className="gf-form-input-icon fa fa-search" />
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="cta-form__bar">
|
|
||||||
{this.renderFilters()}
|
|
||||||
<div className="gf-form--grow" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ds-picker-list">{this.getDataSources().map(this.renderDataSource)}</div>
|
);
|
||||||
</>
|
};
|
||||||
);
|
|
||||||
|
componentDidMount() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.searchInput.focus();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchQueryChange = evt => {
|
||||||
|
const value = evt.target.value;
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
searchQuery: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
renderFilters() {
|
||||||
|
const { searchQuery } = this.state;
|
||||||
|
const { onKeyDown } = this.props;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label className="gf-form--has-input-icon">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input width-13"
|
||||||
|
placeholder=""
|
||||||
|
ref={elem => (this.searchInput = elem)}
|
||||||
|
onChange={this.onSearchQueryChange}
|
||||||
|
value={searchQuery}
|
||||||
|
onKeyDown={evt => {
|
||||||
|
onKeyDown(evt, this.maxSelectedIndex, () => {
|
||||||
|
const { onChangeDataSource, selected } = this.props;
|
||||||
|
const ds = this.getDataSources()[selected];
|
||||||
|
onChangeDataSource(ds);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="cta-form__bar">
|
||||||
|
{this.renderFilters()}
|
||||||
|
<div className="gf-form--grow" />
|
||||||
|
</div>
|
||||||
|
<div className="ds-picker-list">{this.getDataSources().map(this.renderDataSource)}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
|
export default DataSourcePicker;
|
||||||
|
@ -117,7 +117,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="panel-editor__scroll">
|
<div className="panel-editor__scroll">
|
||||||
<CustomScrollbar autoHide={false}>
|
<CustomScrollbar autoHide={false}>
|
||||||
<FadeIn in={isOpen} duration={200}>
|
<FadeIn in={isOpen} duration={200} unmountOnExit={true}>
|
||||||
<div className="panel-editor__toolbar-view">{openView && this.renderOpenView(openView)}</div>
|
<div className="panel-editor__toolbar-view">{openView && this.renderOpenView(openView)}</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
<div className="panel-editor__content">
|
<div className="panel-editor__content">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { Switch } from 'app/core/components/Switch/Switch';
|
import { Switch } from 'app/core/components/Switch/Switch';
|
||||||
import { Input } from 'app/core/components/Form';
|
import { Input } from 'app/core/components/Form';
|
||||||
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
|
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
|
||||||
@ -64,12 +64,7 @@ export class TimeRangeOptions extends PureComponent<Props> {
|
|||||||
|
|
||||||
<div className="gf-form-group">
|
<div className="gf-form-group">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<span className="gf-form-label">
|
|
||||||
<i className="fa fa-clock-o" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="gf-form-label width-12">Override relative time</span>
|
<span className="gf-form-label width-12">Override relative time</span>
|
||||||
<span className="gf-form-label width-6">Last</span>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
className="gf-form-input max-width-8"
|
className="gf-form-input max-width-8"
|
||||||
@ -81,11 +76,7 @@ export class TimeRangeOptions extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<span className="gf-form-label">
|
|
||||||
<i className="fa fa-clock-o" />
|
|
||||||
</span>
|
|
||||||
<span className="gf-form-label width-12">Add time shift</span>
|
<span className="gf-form-label width-12">Add time shift</span>
|
||||||
<span className="gf-form-label width-6">Amount</span>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
className="gf-form-input max-width-8"
|
className="gf-form-input max-width-8"
|
||||||
@ -97,11 +88,6 @@ export class TimeRangeOptions extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
|
||||||
<span className="gf-form-label">
|
|
||||||
<i className="fa fa-clock-o" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Switch label="Hide time override info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
|
<Switch label="Hide time override info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { PanelPlugin } from 'app/types/plugins';
|
import { PanelPlugin } from 'app/types/plugins';
|
||||||
|
import VizTypePickerPlugin from './VizTypePickerPlugin';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
current: PanelPlugin;
|
current: PanelPlugin;
|
||||||
@ -12,6 +12,7 @@ interface Props {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
selected: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VizTypePicker extends PureComponent<Props, State> {
|
export class VizTypePicker extends PureComponent<Props, State> {
|
||||||
@ -23,9 +24,50 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
selected: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get maxSelectedIndex() {
|
||||||
|
const filteredPluginList = this.getFilteredPluginList();
|
||||||
|
return filteredPluginList.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
goRight = () => {
|
||||||
|
const nextIndex = this.state.selected >= this.maxSelectedIndex ? 0 : this.state.selected + 1;
|
||||||
|
this.setState({
|
||||||
|
selected: nextIndex,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
goLeft = () => {
|
||||||
|
const nextIndex = this.state.selected <= 0 ? this.maxSelectedIndex : this.state.selected - 1;
|
||||||
|
this.setState({
|
||||||
|
selected: nextIndex,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeyDown = evt => {
|
||||||
|
if (evt.key === 'ArrowDown') {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.goRight();
|
||||||
|
}
|
||||||
|
if (evt.key === 'ArrowUp') {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.goLeft();
|
||||||
|
}
|
||||||
|
if (evt.key === 'Enter') {
|
||||||
|
const filteredPluginList = this.getFilteredPluginList();
|
||||||
|
this.props.onTypeChanged(filteredPluginList[this.state.selected]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.searchInput.focus();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
getPanelPlugins(filter): PanelPlugin[] {
|
getPanelPlugins(filter): PanelPlugin[] {
|
||||||
const panels = _.chain(config.panels)
|
const panels = _.chain(config.panels)
|
||||||
.filter({ hideFromList: false })
|
.filter({ hideFromList: false })
|
||||||
@ -36,25 +78,28 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
return _.sortBy(panels, 'sort');
|
return _.sortBy(panels, 'sort');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderVizPlugin = (plugin: PanelPlugin, index: number) => {
|
onMouseEnter = (mouseEnterIndex: number) => {
|
||||||
const cssClass = classNames({
|
this.setState({
|
||||||
'viz-picker__item': true,
|
selected: mouseEnterIndex,
|
||||||
'viz-picker__item--selected': plugin.id === this.props.current.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
|
|
||||||
<div className="viz-picker__item-name">{plugin.name}</div>
|
|
||||||
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
renderVizPlugin = (plugin: PanelPlugin, index: number) => {
|
||||||
setTimeout(() => {
|
const isSelected = this.state.selected === index;
|
||||||
this.searchInput.focus();
|
const isCurrent = plugin.id === this.props.current.id;
|
||||||
}, 300);
|
return (
|
||||||
}
|
<VizTypePickerPlugin
|
||||||
|
key={plugin.id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isCurrent={isCurrent}
|
||||||
|
plugin={plugin}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
this.onMouseEnter(index);
|
||||||
|
}}
|
||||||
|
onClick={() => this.props.onTypeChanged(plugin)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
getFilteredPluginList = (): PanelPlugin[] => {
|
getFilteredPluginList = (): PanelPlugin[] => {
|
||||||
const { searchQuery } = this.state;
|
const { searchQuery } = this.state;
|
||||||
@ -73,6 +118,7 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
searchQuery: value,
|
searchQuery: value,
|
||||||
|
selected: 0,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,6 +132,7 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
placeholder=""
|
placeholder=""
|
||||||
ref={elem => (this.searchInput = elem)}
|
ref={elem => (this.searchInput = elem)}
|
||||||
onChange={this.onSearchQueryChange}
|
onChange={this.onSearchQueryChange}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
/>
|
/>
|
||||||
<i className="gf-form-input-icon fa fa-search" />
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
</label>
|
</label>
|
||||||
@ -102,7 +149,6 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
{this.renderFilters()}
|
{this.renderFilters()}
|
||||||
<div className="gf-form--grow" />
|
<div className="gf-form--grow" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="viz-picker">{filteredPluginList.map(this.renderVizPlugin)}</div>
|
<div className="viz-picker">{filteredPluginList.map(this.renderVizPlugin)}</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { PanelPlugin } from 'app/types/plugins';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isSelected: boolean;
|
||||||
|
isCurrent: boolean;
|
||||||
|
plugin: PanelPlugin;
|
||||||
|
onClick: () => void;
|
||||||
|
onMouseEnter: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VizTypePickerPlugin = React.memo(
|
||||||
|
({ isSelected, isCurrent, plugin, onClick, onMouseEnter }: Props) => {
|
||||||
|
const cssClass = classNames({
|
||||||
|
'viz-picker__item': true,
|
||||||
|
'viz-picker__item--selected': isSelected,
|
||||||
|
'viz-picker__item--current': isCurrent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssClass} onClick={onClick} title={plugin.name} onMouseEnter={onMouseEnter}>
|
||||||
|
<div className="viz-picker__item-name">{plugin.name}</div>
|
||||||
|
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prevProps, nextProps) => {
|
||||||
|
if (prevProps.isSelected === nextProps.isSelected && prevProps.isCurrent === nextProps.isCurrent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default VizTypePickerPlugin;
|
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Props } from './DataSourcePicker';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
selected: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withKeyboardNavigation = WrappedComponent => {
|
||||||
|
return class extends React.Component<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selected: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
goToNext = (maxSelectedIndex: number) => {
|
||||||
|
const nextIndex = this.state.selected >= maxSelectedIndex ? 0 : this.state.selected + 1;
|
||||||
|
this.setState({
|
||||||
|
selected: nextIndex,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
goToPrev = (maxSelectedIndex: number) => {
|
||||||
|
const nextIndex = this.state.selected <= 0 ? maxSelectedIndex : this.state.selected - 1;
|
||||||
|
this.setState({
|
||||||
|
selected: nextIndex,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeyDown = (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: any) => {
|
||||||
|
if (evt.key === 'ArrowDown') {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.goToNext(maxSelectedIndex);
|
||||||
|
}
|
||||||
|
if (evt.key === 'ArrowUp') {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.goToPrev(maxSelectedIndex);
|
||||||
|
}
|
||||||
|
if (evt.key === 'Enter' && onEnterAction) {
|
||||||
|
onEnterAction();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseEnter = (mouseEnterIndex: number) => {
|
||||||
|
this.setState({
|
||||||
|
selected: mouseEnterIndex,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<WrappedComponent
|
||||||
|
selected={this.state.selected}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withKeyboardNavigation;
|
@ -40,8 +40,8 @@ import Graph from './Graph';
|
|||||||
import Logs from './Logs';
|
import Logs from './Logs';
|
||||||
import Table from './Table';
|
import Table from './Table';
|
||||||
import ErrorBoundary from './ErrorBoundary';
|
import ErrorBoundary from './ErrorBoundary';
|
||||||
import TimePicker from './TimePicker';
|
|
||||||
import { Alert } from './Error';
|
import { Alert } from './Error';
|
||||||
|
import TimePicker, { parseTime } from './TimePicker';
|
||||||
|
|
||||||
interface ExploreProps {
|
interface ExploreProps {
|
||||||
datasourceSrv: DatasourceSrv;
|
datasourceSrv: DatasourceSrv;
|
||||||
@ -119,7 +119,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
} else {
|
} else {
|
||||||
const { datasource, queries, range } = props.urlState as ExploreUrlState;
|
const { datasource, queries, range } = props.urlState as ExploreUrlState;
|
||||||
initialQueries = ensureQueries(queries);
|
initialQueries = ensureQueries(queries);
|
||||||
const initialRange = range || { ...DEFAULT_RANGE };
|
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE };
|
||||||
// Millies step for helper bar charts
|
// Millies step for helper bar charts
|
||||||
const initialGraphInterval = 15 * 1000;
|
const initialGraphInterval = 15 * 1000;
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -687,7 +687,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState(state => {
|
this.setState(state => {
|
||||||
const { history, queryTransactions, scanning } = state;
|
const { history, queryTransactions } = state;
|
||||||
|
let { scanning } = state;
|
||||||
|
|
||||||
// Transaction might have been discarded
|
// Transaction might have been discarded
|
||||||
const transaction = queryTransactions.find(qt => qt.id === transactionId);
|
const transaction = queryTransactions.find(qt => qt.id === transactionId);
|
||||||
@ -724,15 +725,21 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||||
|
|
||||||
// Keep scanning for results if this was the last scanning transaction
|
// Keep scanning for results if this was the last scanning transaction
|
||||||
if (_.size(result) === 0 && scanning) {
|
if (scanning) {
|
||||||
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
|
if (_.size(result) === 0) {
|
||||||
if (!other) {
|
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
|
||||||
this.scanTimer = setTimeout(this.scanPreviousRange, 1000);
|
if (!other) {
|
||||||
|
this.scanTimer = setTimeout(this.scanPreviousRange, 1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We can stop scanning if we have a result
|
||||||
|
scanning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...results,
|
...results,
|
||||||
|
scanning,
|
||||||
history: nextHistory,
|
history: nextHistory,
|
||||||
queryTransactions: nextQueryTransactions,
|
queryTransactions: nextQueryTransactions,
|
||||||
};
|
};
|
||||||
@ -913,6 +920,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
onChange={this.onChangeDatasource}
|
onChange={this.onChangeDatasource}
|
||||||
options={exploreDatasources}
|
options={exploreDatasources}
|
||||||
styles={ResetStyles}
|
styles={ResetStyles}
|
||||||
|
maxMenuHeight={500}
|
||||||
placeholder="Select datasource"
|
placeholder="Select datasource"
|
||||||
loadingMessage={() => 'Loading datasources...'}
|
loadingMessage={() => 'Loading datasources...'}
|
||||||
noOptionsMessage={() => 'No datasources found'}
|
noOptionsMessage={() => 'No datasources found'}
|
||||||
|
@ -69,7 +69,7 @@ export class Stats extends PureComponent<{
|
|||||||
|
|
||||||
class Label extends PureComponent<
|
class Label extends PureComponent<
|
||||||
{
|
{
|
||||||
allRows?: LogRow[];
|
getRows?: () => LogRow[];
|
||||||
label: string;
|
label: string;
|
||||||
plain?: boolean;
|
plain?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
@ -98,13 +98,14 @@ class Label extends PureComponent<
|
|||||||
if (state.showStats) {
|
if (state.showStats) {
|
||||||
return { showStats: false, stats: null };
|
return { showStats: false, stats: null };
|
||||||
}
|
}
|
||||||
const stats = calculateLogsLabelStats(this.props.allRows, this.props.label);
|
const allRows = this.props.getRows();
|
||||||
|
const stats = calculateLogsLabelStats(allRows, this.props.label);
|
||||||
return { showStats: true, stats };
|
return { showStats: true, stats };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { allRows, label, plain, value } = this.props;
|
const { getRows, label, plain, value } = this.props;
|
||||||
const { showStats, stats } = this.state;
|
const { showStats, stats } = this.state;
|
||||||
const tooltip = `${label}: ${value}`;
|
const tooltip = `${label}: ${value}`;
|
||||||
return (
|
return (
|
||||||
@ -115,12 +116,12 @@ class Label extends PureComponent<
|
|||||||
{!plain && (
|
{!plain && (
|
||||||
<span title="Filter for label" onClick={this.onClickLabel} className="logs-label__icon fa fa-search-plus" />
|
<span title="Filter for label" onClick={this.onClickLabel} className="logs-label__icon fa fa-search-plus" />
|
||||||
)}
|
)}
|
||||||
{!plain && allRows && <span onClick={this.onClickStats} className="logs-label__icon fa fa-signal" />}
|
{!plain && getRows && <span onClick={this.onClickStats} className="logs-label__icon fa fa-signal" />}
|
||||||
{showStats && (
|
{showStats && (
|
||||||
<span className="logs-label__stats">
|
<span className="logs-label__stats">
|
||||||
<Stats
|
<Stats
|
||||||
stats={stats}
|
stats={stats}
|
||||||
rowCount={allRows.length}
|
rowCount={getRows().length}
|
||||||
label={label}
|
label={label}
|
||||||
value={value}
|
value={value}
|
||||||
onClickClose={this.onClickClose}
|
onClickClose={this.onClickClose}
|
||||||
@ -133,15 +134,15 @@ class Label extends PureComponent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class LogLabels extends PureComponent<{
|
export default class LogLabels extends PureComponent<{
|
||||||
allRows?: LogRow[];
|
getRows?: () => LogRow[];
|
||||||
labels: LogsStreamLabels;
|
labels: LogsStreamLabels;
|
||||||
plain?: boolean;
|
plain?: boolean;
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const { allRows, labels, onClickLabel, plain } = this.props;
|
const { getRows, labels, onClickLabel, plain } = this.props;
|
||||||
return Object.keys(labels).map(key => (
|
return Object.keys(labels).map(key => (
|
||||||
<Label key={key} allRows={allRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
|
<Label key={key} getRows={getRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import classnames from 'classnames';
|
|||||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||||
import { RawTimeRange } from 'app/types/series';
|
import { RawTimeRange } from 'app/types/series';
|
||||||
import {
|
import {
|
||||||
|
LogsDedupDescription,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
LogsModel,
|
LogsModel,
|
||||||
dedupLogRows,
|
dedupLogRows,
|
||||||
@ -56,13 +57,13 @@ const FieldHighlight = onClick => props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
allRows: LogRow[];
|
|
||||||
highlighterExpressions?: string[];
|
highlighterExpressions?: string[];
|
||||||
row: LogRow;
|
row: LogRow;
|
||||||
showDuplicates: boolean;
|
showDuplicates: boolean;
|
||||||
showLabels: boolean | null; // Tristate: null means auto
|
showLabels: boolean | null; // Tristate: null means auto
|
||||||
showLocalTime: boolean;
|
showLocalTime: boolean;
|
||||||
showUtc: boolean;
|
showUtc: boolean;
|
||||||
|
getRows: () => LogRow[];
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,11 +108,12 @@ class Row extends PureComponent<RowProps, RowState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onClickHighlight = (fieldText: string) => {
|
onClickHighlight = (fieldText: string) => {
|
||||||
const { allRows } = this.props;
|
const { getRows } = this.props;
|
||||||
const { parser } = this.state;
|
const { parser } = this.state;
|
||||||
|
|
||||||
const fieldMatch = fieldText.match(parser.fieldRegex);
|
const fieldMatch = fieldText.match(parser.fieldRegex);
|
||||||
if (fieldMatch) {
|
if (fieldMatch) {
|
||||||
|
const allRows = getRows();
|
||||||
// Build value-agnostic row matcher based on the field label
|
// Build value-agnostic row matcher based on the field label
|
||||||
const fieldLabel = fieldMatch[1];
|
const fieldLabel = fieldMatch[1];
|
||||||
const fieldValue = fieldMatch[2];
|
const fieldValue = fieldMatch[2];
|
||||||
@ -151,7 +153,7 @@ class Row extends PureComponent<RowProps, RowState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
allRows,
|
getRows,
|
||||||
highlighterExpressions,
|
highlighterExpressions,
|
||||||
onClickLabel,
|
onClickLabel,
|
||||||
row,
|
row,
|
||||||
@ -193,7 +195,7 @@ class Row extends PureComponent<RowProps, RowState> {
|
|||||||
)}
|
)}
|
||||||
{showLabels && (
|
{showLabels && (
|
||||||
<div className="logs-row__labels">
|
<div className="logs-row__labels">
|
||||||
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
|
<LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
|
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
|
||||||
@ -393,29 +395,11 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid options
|
|
||||||
// const cssColumnSizes = [];
|
|
||||||
// if (showDuplicates) {
|
|
||||||
// cssColumnSizes.push('max-content');
|
|
||||||
// }
|
|
||||||
// // Log-level indicator line
|
|
||||||
// cssColumnSizes.push('3px');
|
|
||||||
// if (showUtc) {
|
|
||||||
// cssColumnSizes.push('minmax(220px, max-content)');
|
|
||||||
// }
|
|
||||||
// if (showLocalTime) {
|
|
||||||
// cssColumnSizes.push('minmax(140px, max-content)');
|
|
||||||
// }
|
|
||||||
// if (showLabels) {
|
|
||||||
// cssColumnSizes.push('fit-content(20%)');
|
|
||||||
// }
|
|
||||||
// cssColumnSizes.push('1fr');
|
|
||||||
// const logEntriesStyle = {
|
|
||||||
// gridTemplateColumns: cssColumnSizes.join(' '),
|
|
||||||
// };
|
|
||||||
|
|
||||||
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
|
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
|
||||||
|
|
||||||
|
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
|
||||||
|
const getRows = () => processedRows;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-panel">
|
<div className="logs-panel">
|
||||||
<div className="logs-panel-graph">
|
<div className="logs-panel-graph">
|
||||||
@ -431,41 +415,37 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="logs-panel-options">
|
<div className="logs-panel-options">
|
||||||
<div className="logs-panel-controls">
|
<div className="logs-panel-controls">
|
||||||
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} />
|
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} transparent />
|
||||||
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} />
|
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} transparent />
|
||||||
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} />
|
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup label="Dedup" transparent={true}>
|
||||||
label="Dedup"
|
{Object.keys(LogsDedupStrategy).map((dedupType, i) => (
|
||||||
onChange={this.onChangeDedup}
|
<ToggleButton
|
||||||
value={dedup}
|
key={i}
|
||||||
render={({ selectedValue, onChange }) =>
|
value={dedupType}
|
||||||
Object.keys(LogsDedupStrategy).map((dedupType, i) => (
|
onChange={this.onChangeDedup}
|
||||||
<ToggleButton
|
selected={dedup === dedupType}
|
||||||
className="btn-small"
|
tooltip={LogsDedupDescription[dedupType]}
|
||||||
key={i}
|
>
|
||||||
value={dedupType}
|
{dedupType}
|
||||||
onChange={onChange}
|
</ToggleButton>
|
||||||
selected={selectedValue === dedupType}
|
))}
|
||||||
>
|
</ToggleButtonGroup>
|
||||||
{dedupType}
|
|
||||||
</ToggleButton>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{hasData &&
|
|
||||||
meta && (
|
|
||||||
<div className="logs-panel-meta">
|
|
||||||
{meta.map(item => (
|
|
||||||
<div className="logs-panel-meta__item" key={item.label}>
|
|
||||||
<span className="logs-panel-meta__label">{item.label}:</span>
|
|
||||||
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{hasData &&
|
||||||
|
meta && (
|
||||||
|
<div className="logs-panel-meta">
|
||||||
|
{meta.map(item => (
|
||||||
|
<div className="logs-panel-meta__item" key={item.label}>
|
||||||
|
<span className="logs-panel-meta__label">{item.label}:</span>
|
||||||
|
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="logs-rows">
|
<div className="logs-rows">
|
||||||
{hasData &&
|
{hasData &&
|
||||||
!deferLogs &&
|
!deferLogs &&
|
||||||
@ -473,7 +453,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
firstRows.map(row => (
|
firstRows.map(row => (
|
||||||
<Row
|
<Row
|
||||||
key={row.key + row.duplicates}
|
key={row.key + row.duplicates}
|
||||||
allRows={processedRows}
|
getRows={getRows}
|
||||||
highlighterExpressions={highlighterExpressions}
|
highlighterExpressions={highlighterExpressions}
|
||||||
row={row}
|
row={row}
|
||||||
showDuplicates={showDuplicates}
|
showDuplicates={showDuplicates}
|
||||||
@ -489,7 +469,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
lastRows.map(row => (
|
lastRows.map(row => (
|
||||||
<Row
|
<Row
|
||||||
key={row.key + row.duplicates}
|
key={row.key + row.duplicates}
|
||||||
allRows={processedRows}
|
getRows={getRows}
|
||||||
row={row}
|
row={row}
|
||||||
showDuplicates={showDuplicates}
|
showDuplicates={showDuplicates}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
|
@ -15,11 +15,14 @@ export const DEFAULT_RANGE = {
|
|||||||
* Return a human-editable string of either relative (inludes "now") or absolute local time (in the shape of DATE_FORMAT).
|
* Return a human-editable string of either relative (inludes "now") or absolute local time (in the shape of DATE_FORMAT).
|
||||||
* @param value Epoch or relative time
|
* @param value Epoch or relative time
|
||||||
*/
|
*/
|
||||||
export function parseTime(value: string, isUtc = false): string {
|
export function parseTime(value: string | moment.Moment, isUtc = false, ensureString = false): string | moment.Moment {
|
||||||
if (moment.isMoment(value)) {
|
if (moment.isMoment(value)) {
|
||||||
|
if (ensureString) {
|
||||||
|
return value.format(DATE_FORMAT);
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (value.indexOf('now') !== -1) {
|
if ((value as string).indexOf('now') !== -1) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
let time: any = value;
|
let time: any = value;
|
||||||
@ -50,6 +53,16 @@ interface TimePickerState {
|
|||||||
toRaw: string;
|
toRaw: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimePicker with dropdown menu for relative dates.
|
||||||
|
*
|
||||||
|
* Initialize with a range that is either based on relative time strings,
|
||||||
|
* or on Moment objects.
|
||||||
|
* Internally the component needs to keep a string representation in `fromRaw`
|
||||||
|
* and `toRaw` for the controlled inputs.
|
||||||
|
* When a time is picked, `onChangeTime` is called with the new range that
|
||||||
|
* is again based on relative time strings or Moment objects.
|
||||||
|
*/
|
||||||
export default class TimePicker extends PureComponent<TimePickerProps, TimePickerState> {
|
export default class TimePicker extends PureComponent<TimePickerProps, TimePickerState> {
|
||||||
dropdownEl: any;
|
dropdownEl: any;
|
||||||
|
|
||||||
@ -75,9 +88,9 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
|
|||||||
const from = props.range ? props.range.from : DEFAULT_RANGE.from;
|
const from = props.range ? props.range.from : DEFAULT_RANGE.from;
|
||||||
const to = props.range ? props.range.to : DEFAULT_RANGE.to;
|
const to = props.range ? props.range.to : DEFAULT_RANGE.to;
|
||||||
|
|
||||||
// Ensure internal format
|
// Ensure internal string format
|
||||||
const fromRaw = parseTime(from, props.isUtc);
|
const fromRaw = parseTime(from, props.isUtc, true);
|
||||||
const toRaw = parseTime(to, props.isUtc);
|
const toRaw = parseTime(to, props.isUtc, true);
|
||||||
const range = {
|
const range = {
|
||||||
from: fromRaw,
|
from: fromRaw,
|
||||||
to: toRaw,
|
to: toRaw,
|
||||||
|
@ -95,10 +95,17 @@ export class MetricsTabCtrl {
|
|||||||
target.datasource = config.defaultDatasource;
|
target.datasource = config.defaultDatasource;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.datasourceInstance && this.datasourceInstance.meta.mixed) {
|
} else if (this.datasourceInstance) {
|
||||||
_.each(this.panel.targets, target => {
|
// if switching from mixed
|
||||||
delete target.datasource;
|
if (this.datasourceInstance.meta.mixed) {
|
||||||
});
|
_.each(this.panel.targets, target => {
|
||||||
|
delete target.datasource;
|
||||||
|
});
|
||||||
|
} else if (this.datasourceInstance.meta.id !== datasource.meta.id) {
|
||||||
|
// we are changing data source type, clear queries
|
||||||
|
this.panel.targets = [{ refId: 'A' }];
|
||||||
|
this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.datasourceInstance = datasource;
|
this.datasourceInstance = datasource;
|
||||||
|
@ -12,7 +12,12 @@ function panelEditorTab(dynamicDirectiveSrv) {
|
|||||||
},
|
},
|
||||||
directive: scope => {
|
directive: scope => {
|
||||||
const pluginId = scope.ctrl.pluginId;
|
const pluginId = scope.ctrl.pluginId;
|
||||||
const tabName = scope.editorTab.title.toLowerCase().replace(' ', '-');
|
const tabName = scope.editorTab.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(' ', '-')
|
||||||
|
.replace('&', '')
|
||||||
|
.replace(' ', '')
|
||||||
|
.replace(' ', '-');
|
||||||
|
|
||||||
if (directiveCache[pluginId]) {
|
if (directiveCache[pluginId]) {
|
||||||
if (directiveCache[pluginId][tabName]) {
|
if (directiveCache[pluginId][tabName]) {
|
||||||
|
@ -38,8 +38,9 @@ export class CustomVariable implements Variable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateOptions() {
|
updateOptions() {
|
||||||
// extract options in comma separated string
|
// extract options in comma separated string (use backslash to escape wanted commas)
|
||||||
this.options = _.map(this.query.split(/[,]+/), text => {
|
this.options = _.map(this.query.match(/(?:\\,|[^,])+/g), text => {
|
||||||
|
text = text.replace('\\,', ',');
|
||||||
return { text: text.trim(), value: text.trim() };
|
return { text: text.trim(), value: text.trim() };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@
|
|||||||
<h5 class="section-heading">Custom Options</h5>
|
<h5 class="section-heading">Custom Options</h5>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-14">Values separated by comma</span>
|
<span class="gf-form-label width-14">Values separated by comma</span>
|
||||||
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"
|
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue, escaped\,value"
|
||||||
required></input>
|
required></input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -493,15 +493,17 @@ describe('VariableSrv', function(this: any) {
|
|||||||
scenario.setup(() => {
|
scenario.setup(() => {
|
||||||
scenario.variableModel = {
|
scenario.variableModel = {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
query: 'hej, hop, asd',
|
query: 'hej, hop, asd, escaped\\,var',
|
||||||
name: 'test',
|
name: 'test',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update options array', () => {
|
it('should update options array', () => {
|
||||||
expect(scenario.variable.options.length).toBe(3);
|
expect(scenario.variable.options.length).toBe(4);
|
||||||
expect(scenario.variable.options[0].text).toBe('hej');
|
expect(scenario.variable.options[0].text).toBe('hej');
|
||||||
expect(scenario.variable.options[1].value).toBe('hop');
|
expect(scenario.variable.options[1].value).toBe('hop');
|
||||||
|
expect(scenario.variable.options[2].value).toBe('asd');
|
||||||
|
expect(scenario.variable.options[3].value).toBe('escaped,var');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"annotations": false,
|
"annotations": false,
|
||||||
"logs": true,
|
"logs": true,
|
||||||
"explore": true,
|
"explore": true,
|
||||||
"tables": true,
|
"tables": false,
|
||||||
"info": {
|
"info": {
|
||||||
"description": "Loki Logging Data Source for Grafana",
|
"description": "Loki Logging Data Source for Grafana",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -138,6 +138,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
this.addEditorTab('Display options', 'public/app/plugins/panel/graph/tab_display.html');
|
this.addEditorTab('Display options', 'public/app/plugins/panel/graph/tab_display.html');
|
||||||
this.addEditorTab('Axes', axesEditorComponent);
|
this.addEditorTab('Axes', axesEditorComponent);
|
||||||
this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html');
|
this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html');
|
||||||
|
this.addEditorTab('Thresholds & Time Regions', 'public/app/plugins/panel/graph/tab_thresholds_time_regions.html');
|
||||||
this.subTabIndex = 0;
|
this.subTabIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +130,33 @@ describe('TimeRegionManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
plotOptionsScenario('for time from/to region', ctx => {
|
||||||
|
const regions = [{ from: '00:00', to: '05:00', fill: true, colorMode: 'red' }];
|
||||||
|
const from = moment('2018-12-01T00:00+01:00');
|
||||||
|
const to = moment('2018-12-03T23:59+01:00');
|
||||||
|
ctx.setup(regions, from, to);
|
||||||
|
|
||||||
|
it('should add 3 markings', () => {
|
||||||
|
expect(ctx.options.grid.markings.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add one fill between 00:00 and 05:00 each day', () => {
|
||||||
|
const markings = ctx.options.grid.markings;
|
||||||
|
|
||||||
|
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-01T01:00:00+01:00').format());
|
||||||
|
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-01T06:00:00+01:00').format());
|
||||||
|
expect(markings[0].color).toBe(colorModes.red.color.fill);
|
||||||
|
|
||||||
|
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-12-02T01:00:00+01:00').format());
|
||||||
|
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-12-02T06:00:00+01:00').format());
|
||||||
|
expect(markings[1].color).toBe(colorModes.red.color.fill);
|
||||||
|
|
||||||
|
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-12-03T01:00:00+01:00').format());
|
||||||
|
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-12-03T06:00:00+01:00').format());
|
||||||
|
expect(markings[2].color).toBe(colorModes.red.color.fill);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
plotOptionsScenario('for day of week from/to region', ctx => {
|
plotOptionsScenario('for day of week from/to region', ctx => {
|
||||||
const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
|
const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
|
||||||
const from = moment('2018-01-01T18:45:05+01:00');
|
const from = moment('2018-01-01T18:45:05+01:00');
|
||||||
@ -211,6 +238,42 @@ describe('TimeRegionManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
plotOptionsScenario('for day of week from/to time region', ctx => {
|
||||||
|
const regions = [{ fromDayOfWeek: 7, from: '23:00', toDayOfWeek: 1, to: '01:40', fill: true, colorMode: 'red' }];
|
||||||
|
const from = moment('2018-12-07T12:51:19+01:00');
|
||||||
|
const to = moment('2018-12-10T13:51:29+01:00');
|
||||||
|
ctx.setup(regions, from, to);
|
||||||
|
|
||||||
|
it('should add 1 marking', () => {
|
||||||
|
expect(ctx.options.grid.markings.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add one fill between sunday 23:00 and monday 01:40', () => {
|
||||||
|
const markings = ctx.options.grid.markings;
|
||||||
|
|
||||||
|
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-10T00:00:00+01:00').format());
|
||||||
|
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-10T02:40:00+01:00').format());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
plotOptionsScenario('for day of week from/to time region', ctx => {
|
||||||
|
const regions = [{ fromDayOfWeek: 6, from: '03:00', toDayOfWeek: 7, to: '02:00', fill: true, colorMode: 'red' }];
|
||||||
|
const from = moment('2018-12-07T12:51:19+01:00');
|
||||||
|
const to = moment('2018-12-10T13:51:29+01:00');
|
||||||
|
ctx.setup(regions, from, to);
|
||||||
|
|
||||||
|
it('should add 1 marking', () => {
|
||||||
|
expect(ctx.options.grid.markings.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add one fill between saturday 03:00 and sunday 02:00', () => {
|
||||||
|
const markings = ctx.options.grid.markings;
|
||||||
|
|
||||||
|
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-08T04:00:00+01:00').format());
|
||||||
|
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-09T03:00:00+01:00').format());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => {
|
plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => {
|
||||||
const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }];
|
const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }];
|
||||||
const from = moment('2018-03-17T06:00:00+01:00');
|
const from = moment('2018-03-17T06:00:00+01:00');
|
||||||
|
@ -1,28 +1,5 @@
|
|||||||
<div class="edit-tab-with-sidemenu">
|
|
||||||
<aside class="edit-sidemenu-aside">
|
|
||||||
<ul class="edit-sidemenu">
|
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
|
||||||
<a ng-click="ctrl.subTabIndex = 0">Draw options</a>
|
|
||||||
</li>
|
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
|
||||||
<a ng-click="ctrl.subTabIndex = 1">
|
|
||||||
Series overrides <span class="muted">({{ctrl.panel.seriesOverrides.length}})</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
|
||||||
<a ng-click="ctrl.subTabIndex = 2">
|
|
||||||
Thresholds <span class="muted">({{ctrl.panel.thresholds.length}})</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 3}">
|
|
||||||
<a ng-click="ctrl.subTabIndex = 3">
|
|
||||||
Time regions <span class="muted">({{ctrl.panel.timeRegions.length}})</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 0">
|
<div class="editor-row">
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Draw Modes</h5>
|
<h5 class="section-heading">Draw Modes</h5>
|
||||||
<gf-form-switch class="gf-form" label="Bars" label-class="width-5" checked="ctrl.panel.bars" on-change="ctrl.render()"></gf-form-switch>
|
<gf-form-switch class="gf-form" label="Bars" label-class="width-5" checked="ctrl.panel.bars" on-change="ctrl.render()"></gf-form-switch>
|
||||||
@ -89,9 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 1">
|
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
|
|
||||||
<div class="gf-form-inline" ng-repeat="override in ctrl.panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
|
<div class="gf-form-inline" ng-repeat="override in ctrl.panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label">alias or regex</label>
|
<label class="gf-form-label">alias or regex</label>
|
||||||
@ -110,35 +85,26 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($item, $subItem)">
|
<span class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($item, $subItem)">
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label">
|
<label class="gf-form-label">
|
||||||
<i class="fa fa-trash pointer" ng-click="ctrl.removeSeriesOverride(override)"></i>
|
<i class="fa fa-trash pointer" ng-click="ctrl.removeSeriesOverride(override)"></i>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form-button-row">
|
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.addSeriesOverride()">
|
||||||
|
<i class="fa fa-plus"></i> Add series override<tip>Regex match example: /server[0-3]/i </tip>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-inverse" ng-click="ctrl.addSeriesOverride()">
|
|
||||||
<i class="fa fa-plus"></i> Add override
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 2">
|
|
||||||
<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 3">
|
|
||||||
<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>
|
||||||
|
<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>
|
@ -1,5 +1,4 @@
|
|||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<h5>Thresholds</h5>
|
|
||||||
<p class="muted" ng-show="ctrl.disabled">
|
<p class="muted" ng-show="ctrl.disabled">
|
||||||
Visual thresholds options <strong>disabled.</strong>
|
Visual thresholds options <strong>disabled.</strong>
|
||||||
Visit the Alert tab update your thresholds. <br>
|
Visit the Alert tab update your thresholds. <br>
|
||||||
|
@ -87,6 +87,14 @@ export class TimeRegionManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timeRegion.from && !timeRegion.to) {
|
||||||
|
timeRegion.to = timeRegion.from;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeRegion.from && timeRegion.to) {
|
||||||
|
timeRegion.from = timeRegion.to;
|
||||||
|
}
|
||||||
|
|
||||||
hRange = {
|
hRange = {
|
||||||
from: this.parseTimeRange(timeRegion.from),
|
from: this.parseTimeRange(timeRegion.from),
|
||||||
to: this.parseTimeRange(timeRegion.to),
|
to: this.parseTimeRange(timeRegion.to),
|
||||||
@ -108,21 +116,13 @@ export class TimeRegionManager {
|
|||||||
hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek);
|
hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hRange.from.h && hRange.to.h) {
|
if (hRange.from.dayOfWeek && hRange.from.h === null && hRange.from.m === null) {
|
||||||
hRange.from = hRange.to;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hRange.from.h && !hRange.to.h) {
|
|
||||||
hRange.to = hRange.from;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) {
|
|
||||||
hRange.from.h = 0;
|
hRange.from.h = 0;
|
||||||
hRange.from.m = 0;
|
hRange.from.m = 0;
|
||||||
hRange.from.s = 0;
|
hRange.from.s = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) {
|
if (hRange.to.dayOfWeek && hRange.to.h === null && hRange.to.m === null) {
|
||||||
hRange.to.h = 23;
|
hRange.to.h = 23;
|
||||||
hRange.to.m = 59;
|
hRange.to.m = 59;
|
||||||
hRange.to.s = 59;
|
hRange.to.s = 59;
|
||||||
@ -169,8 +169,16 @@ export class TimeRegionManager {
|
|||||||
fromEnd.add(hRange.to.h - hRange.from.h, 'hours');
|
fromEnd.add(hRange.to.h - hRange.from.h, 'hours');
|
||||||
} else if (hRange.from.h + hRange.to.h < 23) {
|
} else if (hRange.from.h + hRange.to.h < 23) {
|
||||||
fromEnd.add(hRange.to.h, 'hours');
|
fromEnd.add(hRange.to.h, 'hours');
|
||||||
|
|
||||||
|
while (fromEnd.hour() !== hRange.to.h) {
|
||||||
|
fromEnd.add(-1, 'hours');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fromEnd.add(24 - hRange.from.h, 'hours');
|
fromEnd.add(24 - hRange.from.h, 'hours');
|
||||||
|
|
||||||
|
while (fromEnd.hour() !== hRange.to.h) {
|
||||||
|
fromEnd.add(1, 'hours');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fromEnd.set('minute', hRange.to.m);
|
fromEnd.set('minute', hRange.to.m);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<h5>Time regions <tip>All configured time regions refers to UTC time</tip></h5>
|
|
||||||
<div class="gf-form-inline" ng-repeat="timeRegion in ctrl.panel.timeRegions">
|
<div class="gf-form-inline" ng-repeat="timeRegion in ctrl.panel.timeRegions">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label">T{{$index+1}}</label>
|
<label class="gf-form-label">T{{$index+1}}</label>
|
||||||
@ -58,7 +57,7 @@
|
|||||||
|
|
||||||
<div class="gf-form-button-row">
|
<div class="gf-form-button-row">
|
||||||
<button class="btn btn-inverse" ng-click="ctrl.addTimeRegion()">
|
<button class="btn btn-inverse" ng-click="ctrl.addTimeRegion()">
|
||||||
<i class="fa fa-plus"></i> Add time region
|
<i class="fa fa-plus"></i> Add time region<tip>All configured time regions refers to UTC time</tip>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -107,7 +107,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDataReceived(dataList) {
|
onDataReceived(dataList) {
|
||||||
const data: any = {};
|
const data: any = {
|
||||||
|
scopedVars: _.extend({}, this.panel.scopedVars),
|
||||||
|
};
|
||||||
|
|
||||||
if (dataList.length > 0 && dataList[0].type === 'table') {
|
if (dataList.length > 0 && dataList[0].type === 'table') {
|
||||||
this.dataType = 'table';
|
this.dataType = 'table';
|
||||||
const tableData = dataList.map(this.tableHandler.bind(this));
|
const tableData = dataList.map(this.tableHandler.bind(this));
|
||||||
@ -117,6 +120,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
this.series = dataList.map(this.seriesHandler.bind(this));
|
this.series = dataList.map(this.seriesHandler.bind(this));
|
||||||
this.setValues(data);
|
this.setValues(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
@ -320,7 +324,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add $__name variable for using in prefix or postfix
|
// Add $__name variable for using in prefix or postfix
|
||||||
data.scopedVars = _.extend({}, this.panel.scopedVars);
|
|
||||||
data.scopedVars['__name'] = { value: this.series[0].label };
|
data.scopedVars['__name'] = { value: this.series[0].label };
|
||||||
}
|
}
|
||||||
this.setValueMapping(data);
|
this.setValueMapping(data);
|
||||||
|
@ -391,3 +391,8 @@ $panel-grid-placeholder-shadow: 0 0 4px $blue;
|
|||||||
|
|
||||||
// logs
|
// logs
|
||||||
$logs-color-unkown: $gray-2;
|
$logs-color-unkown: $gray-2;
|
||||||
|
|
||||||
|
// toggle-group
|
||||||
|
$button-toggle-group-btn-active-bg: linear-gradient(90deg, $orange, $red);
|
||||||
|
$button-toggle-group-btn-active-shadow: inset 0 0 4px $black;
|
||||||
|
$button-toggle-group-btn-seperator-border: 1px solid $page-bg;
|
||||||
|
@ -62,7 +62,7 @@ $critical: #ec2128;
|
|||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
||||||
$body-bg: $gray-7;
|
$body-bg: $gray-7;
|
||||||
$page-bg: $gray-6;
|
$page-bg: $gray-7;
|
||||||
$body-color: $gray-1;
|
$body-color: $gray-1;
|
||||||
$text-color: $gray-1;
|
$text-color: $gray-1;
|
||||||
$text-color-strong: $dark-2;
|
$text-color-strong: $dark-2;
|
||||||
@ -400,3 +400,8 @@ $panel-grid-placeholder-shadow: 0 0 4px $blue-light;
|
|||||||
|
|
||||||
// logs
|
// logs
|
||||||
$logs-color-unkown: $gray-5;
|
$logs-color-unkown: $gray-5;
|
||||||
|
|
||||||
|
// toggle-group
|
||||||
|
$button-toggle-group-btn-active-bg: $brand-primary;
|
||||||
|
$button-toggle-group-btn-active-shadow: inset 0 0 4px $white;
|
||||||
|
$button-toggle-group-btn-seperator-border: 1px solid $gray-6;
|
||||||
|
@ -87,7 +87,7 @@ body {
|
|||||||
// might still respond to pointer events.
|
// might still respond to pointer events.
|
||||||
//
|
//
|
||||||
// Credit: https://github.com/suitcss/base
|
// Credit: https://github.com/suitcss/base
|
||||||
[tabindex="-1"]:focus {
|
[tabindex='-1']:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@include tab-focus();
|
@include no-focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ img {
|
|||||||
// for traditionally non-focusable elements with role="button"
|
// for traditionally non-focusable elements with role="button"
|
||||||
// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
|
// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
|
||||||
|
|
||||||
[role="button"] {
|
[role='button'] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ img {
|
|||||||
a,
|
a,
|
||||||
area,
|
area,
|
||||||
button,
|
button,
|
||||||
[role="button"],
|
[role='button'],
|
||||||
input,
|
input,
|
||||||
label,
|
label,
|
||||||
select,
|
select,
|
||||||
@ -320,7 +320,7 @@ legend {
|
|||||||
// border: 0;
|
// border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="search"] {
|
input[type='search'] {
|
||||||
// This overrides the extra rounded corners on search inputs in iOS so that our
|
// This overrides the extra rounded corners on search inputs in iOS so that our
|
||||||
// `.form-control` class can properly style them. Note that this cannot simply
|
// `.form-control` class can properly style them. Note that this cannot simply
|
||||||
// be added to `.form-control` as it's not specific enough. For details, see
|
// be added to `.form-control` as it's not specific enough. For details, see
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
&.active {
|
&.active {
|
||||||
&:focus,
|
&:focus,
|
||||||
&.focus {
|
&.focus {
|
||||||
@include tab-focus();
|
@include no-focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,12 +50,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.react-grid-item {
|
.react-grid-item {
|
||||||
display: none;
|
|
||||||
transition-property: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
transition-property: none !important;
|
||||||
position: unset !important;
|
position: unset !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
transform: translate(0px, 0px) !important;
|
transform: translate(0px, 0px) !important;
|
||||||
|
@ -55,6 +55,11 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gf-form-select-box__menu-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
.tag-filter .gf-form-select-box__menu {
|
.tag-filter .gf-form-select-box__menu {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -117,9 +117,11 @@ $input-border: 1px solid $input-border-color;
|
|||||||
color: $critical;
|
color: $critical;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--small {
|
&--transparent {
|
||||||
padding: ($input-padding-y / 2) ($input-padding-x / 2);
|
background-color: transparent;
|
||||||
font-size: $font-size-xs;
|
border: 0;
|
||||||
|
text-align: right;
|
||||||
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
@ -157,21 +157,15 @@
|
|||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
transition: transform 1 ease;
|
transition: transform 1 ease;
|
||||||
|
|
||||||
&:hover {
|
&--current {
|
||||||
box-shadow: $panel-editor-viz-item-shadow-hover;
|
box-shadow: 0 0 6px $orange;
|
||||||
background: $panel-editor-viz-item-bg-hover;
|
border: 1px solid $orange;
|
||||||
border: $panel-editor-viz-item-border-hover;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
box-shadow: 0 0 6px $orange;
|
box-shadow: $panel-editor-viz-item-shadow-hover;
|
||||||
border: 1px solid $orange;
|
background: $panel-editor-viz-item-bg-hover;
|
||||||
|
border: $panel-editor-viz-item-border-hover;
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 0 6px $orange;
|
|
||||||
border: 1px solid $orange;
|
|
||||||
background: $panel-editor-viz-item-bg-hover-active;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,13 +257,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
|
||||||
&:hover {
|
&--selected {
|
||||||
background: $panel-editor-viz-item-bg-hover;
|
background: $panel-editor-viz-item-bg-hover;
|
||||||
border: $panel-editor-viz-item-border-hover;
|
border: $panel-editor-viz-item-border-hover;
|
||||||
box-shadow: $panel-editor-viz-item-shadow-hover;
|
box-shadow: $panel-editor-viz-item-shadow-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
&--active {
|
||||||
box-shadow: 0 0 6px $orange;
|
box-shadow: 0 0 6px $orange;
|
||||||
border: 1px solid $orange;
|
border: 1px solid $orange;
|
||||||
|
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
$column-horizontal-spacing: 10px;
|
$column-horizontal-spacing: 10px;
|
||||||
|
|
||||||
.logs-panel-controls {
|
.logs-panel-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: $page-bg;
|
background-color: $page-bg;
|
||||||
padding: $panel-padding;
|
padding: $panel-padding;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
margin: 2*$panel-margin 0;
|
margin: 2*$panel-margin 0 $panel-margin;
|
||||||
border: $panel-border;
|
border: $panel-border;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-panel-controls {
|
||||||
|
display: flex;
|
||||||
justify-items: flex-start;
|
justify-items: flex-start;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
@ -25,12 +31,14 @@ $column-horizontal-spacing: 10px;
|
|||||||
.logs-panel-meta {
|
.logs-panel-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: $text-color-weak;
|
color: $text-color-weak;
|
||||||
// Align first line with controls labels
|
margin-bottom: 10px;
|
||||||
margin-top: -2px;
|
min-width: 30%;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-panel-meta__item {
|
.logs-panel-meta__item {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-panel-meta__label {
|
.logs-panel-meta__label {
|
||||||
@ -131,7 +139,7 @@ $column-horizontal-spacing: 10px;
|
|||||||
&--warning,
|
&--warning,
|
||||||
&--warn {
|
&--warn {
|
||||||
&::after {
|
&::after {
|
||||||
background-color: $warn;
|
background-color: $yellow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,33 +27,39 @@ gf-form-switch[disabled] {
|
|||||||
border: 1px solid $input-border-color;
|
border: 1px solid $input-border-color;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-radius: $input-border-radius;
|
border-radius: $input-border-radius;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--transparent {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The slider */
|
/* The slider */
|
||||||
.gf-form-switch__slider {
|
.gf-form-switch__slider {
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 16px;
|
|
||||||
right: 14px;
|
|
||||||
bottom: 10px;
|
|
||||||
background: $switch-slider-off-bg;
|
background: $switch-slider-off-bg;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 29px;
|
width: 29px;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: '';
|
content: '';
|
||||||
height: 12px;
|
height: 12px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
left: 2px;
|
left: 1px;
|
||||||
bottom: 2px;
|
top: 2px;
|
||||||
background: $switch-slider-color;
|
background: $switch-slider-color;
|
||||||
transition: 0.4s;
|
transition: 0.4s;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
@ -1,28 +1,21 @@
|
|||||||
.toggle-button-group {
|
.toggle-button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.gf-form-label {
|
|
||||||
background-color: $input-label-bg;
|
|
||||||
&:first-child {
|
|
||||||
border-radius: $border-radius 0 0 $border-radius;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
&.small {
|
|
||||||
padding: ($input-padding-y / 2) ($input-padding-x / 2);
|
|
||||||
font-size: $font-size-xs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stacked {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
background-color: $typeahead-selected-bg;
|
@include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow);
|
||||||
|
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-weight: $font-weight-semi-bold;
|
||||||
|
font-size: $font-size-sm;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
color: $text-color;
|
border-right: $button-toggle-group-btn-seperator-border;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: $input-bg;
|
background: $button-toggle-group-btn-active-bg;
|
||||||
|
box-shadow: $button-toggle-group-btn-active-shadow;
|
||||||
|
border-right: 0;
|
||||||
|
color: $white;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
@ -37,9 +30,5 @@
|
|||||||
border-radius: 0 $border-radius $border-radius 0;
|
border-radius: 0 $border-radius $border-radius 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stacked {
|
|
||||||
border-radius: $border-radius;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@mixin clearfix() {
|
@mixin clearfix() {
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: '';
|
||||||
display: table;
|
display: table;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
@ -19,6 +19,10 @@
|
|||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin no-focus() {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
// Center-align a block level element
|
// Center-align a block level element
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
@mixin center-block() {
|
@mixin center-block() {
|
||||||
@ -265,20 +269,10 @@
|
|||||||
// Add an alphatransparency value to any background or border color (via Elyse Holladay)
|
// Add an alphatransparency value to any background or border color (via Elyse Holladay)
|
||||||
#translucent {
|
#translucent {
|
||||||
@mixin background($color: $white, $alpha: 1) {
|
@mixin background($color: $white, $alpha: 1) {
|
||||||
background-color: hsla(
|
background-color: hsla(hue($color), saturation($color), lightness($color), $alpha);
|
||||||
hue($color),
|
|
||||||
saturation($color),
|
|
||||||
lightness($color),
|
|
||||||
$alpha
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@mixin border($color: $white, $alpha: 1) {
|
@mixin border($color: $white, $alpha: 1) {
|
||||||
border-color: hsla(
|
border-color: hsla(hue($color), saturation($color), lightness($color), $alpha);
|
||||||
hue($color),
|
|
||||||
saturation($color),
|
|
||||||
lightness($color),
|
|
||||||
$alpha
|
|
||||||
);
|
|
||||||
@include background-clip(padding-box);
|
@include background-clip(padding-box);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,66 +288,37 @@
|
|||||||
// Gradients
|
// Gradients
|
||||||
@mixin gradient-horizontal($startColor: #555, $endColor: #333) {
|
@mixin gradient-horizontal($startColor: #555, $endColor: #333) {
|
||||||
background-color: $endColor;
|
background-color: $endColor;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(to right, $startColor, $endColor); // Standard, IE10
|
||||||
to right,
|
|
||||||
$startColor,
|
|
||||||
$endColor
|
|
||||||
); // Standard, IE10
|
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin gradient-vertical($startColor: #555, $endColor: #333) {
|
@mixin gradient-vertical($startColor: #555, $endColor: #333) {
|
||||||
background-color: mix($startColor, $endColor, 60%);
|
background-color: mix($startColor, $endColor, 60%);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(to bottom, $startColor, $endColor); // Standard, IE10
|
||||||
to bottom,
|
|
||||||
$startColor,
|
|
||||||
$endColor
|
|
||||||
); // Standard, IE10
|
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin gradient-directional($startColor: #555, $endColor: #333, $deg: 45deg) {
|
@mixin gradient-directional($startColor: #555, $endColor: #333, $deg: 45deg) {
|
||||||
background-color: $endColor;
|
background-color: $endColor;
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient($deg, $startColor, $endColor); // Standard, IE10
|
||||||
$deg,
|
|
||||||
$startColor,
|
|
||||||
$endColor
|
|
||||||
); // Standard, IE10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin gradient-horizontal-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) {
|
@mixin gradient-horizontal-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) {
|
||||||
background-color: mix($midColor, $endColor, 80%);
|
background-color: mix($midColor, $endColor, 80%);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(to right, $startColor, $midColor $colorStop, $endColor);
|
||||||
to right,
|
|
||||||
$startColor,
|
|
||||||
$midColor $colorStop,
|
|
||||||
$endColor
|
|
||||||
);
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin gradient-vertical-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) {
|
@mixin gradient-vertical-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) {
|
||||||
background-color: mix($midColor, $endColor, 80%);
|
background-color: mix($midColor, $endColor, 80%);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient($startColor, $midColor $colorStop, $endColor);
|
||||||
$startColor,
|
|
||||||
$midColor $colorStop,
|
|
||||||
$endColor
|
|
||||||
);
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin gradient-radial($innerColor: #555, $outerColor: #333) {
|
@mixin gradient-radial($innerColor: #555, $outerColor: #333) {
|
||||||
background-color: $outerColor;
|
background-color: $outerColor;
|
||||||
background-image: -webkit-gradient(
|
background-image: -webkit-gradient(radial, center center, 0, center center, 460, from($innerColor), to($outerColor));
|
||||||
radial,
|
|
||||||
center center,
|
|
||||||
0,
|
|
||||||
center center,
|
|
||||||
460,
|
|
||||||
from($innerColor),
|
|
||||||
to($outerColor)
|
|
||||||
);
|
|
||||||
background-image: -webkit-radial-gradient(circle, $innerColor, $outerColor);
|
background-image: -webkit-radial-gradient(circle, $innerColor, $outerColor);
|
||||||
background-image: -moz-radial-gradient(circle, $innerColor, $outerColor);
|
background-image: -moz-radial-gradient(circle, $innerColor, $outerColor);
|
||||||
background-image: -o-radial-gradient(circle, $innerColor, $outerColor);
|
background-image: -o-radial-gradient(circle, $innerColor, $outerColor);
|
||||||
@ -380,11 +345,7 @@
|
|||||||
|
|
||||||
@mixin left-brand-border-gradient() {
|
@mixin left-brand-border-gradient() {
|
||||||
border: none;
|
border: none;
|
||||||
border-image: linear-gradient(
|
border-image: linear-gradient(rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%);
|
||||||
rgba(255, 213, 0, 1) 0%,
|
|
||||||
rgba(255, 68, 0, 1) 99%,
|
|
||||||
rgba(255, 68, 0, 1) 100%
|
|
||||||
);
|
|
||||||
border-image-slice: 1;
|
border-image-slice: 1;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
|
@ -178,7 +178,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
1. This could be caused by your reverse proxy settings.<br /><br />
|
1. This could be caused by your reverse proxy settings.<br /><br />
|
||||||
2. If you host grafana under subpath make sure your grafana.ini root_path setting includes subpath<br /> <br />
|
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath<br /> <br />
|
||||||
3. If you have a local dev build make sure you build frontend using: npm run dev, npm run watch, or npm run
|
3. If you have a local dev build make sure you build frontend using: npm run dev, npm run watch, or npm run
|
||||||
build<br /> <br />
|
build<br /> <br />
|
||||||
4. Sometimes restarting grafana-server can help<br />
|
4. Sometimes restarting grafana-server can help<br />
|
||||||
|
Loading…
Reference in New Issue
Block a user