Merge remote-tracking branch 'origin' into ivana/es-precision-default-value

This commit is contained in:
Ivana Huckova
2023-08-28 15:33:21 +02:00
21 changed files with 795 additions and 195 deletions

View File

@@ -78,7 +78,7 @@
"@betterer/regexp": "5.4.0",
"@emotion/eslint-plugin": "11.11.0",
"@grafana/e2e": "workspace:*",
"@grafana/eslint-config": "6.0.0",
"@grafana/eslint-config": "6.0.1",
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules",
"@grafana/toolkit": "workspace:*",
"@grafana/tsconfig": "^1.3.0-rc1",

View File

@@ -180,6 +180,13 @@ export const Pages = {
Annotations: {
marker: 'data-testid annotation-marker',
},
Rows: {
Repeated: {
ConfigSection: {
warningMessage: 'data-testid Repeated rows warning message',
},
},
},
},
Dashboards: {
url: '/dashboards',

View File

@@ -39,6 +39,7 @@ func ProvideSecretsMigrator(
b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_refresh_token"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_token_type"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_id_token"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "secrets", columnName: "value"}, hasUpdatedColumn: true, encoding: base64.RawStdEncoding},
jsonSecret{tableName: "data_source"},
jsonSecret{tableName: "plugin_setting"},

View File

@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
@@ -74,7 +73,7 @@ func TestIntegrationPrometheus(t *testing.T) {
"datasource": map[string]interface{}{
"uid": uid,
},
"expr": "up",
"expr": "1",
"instantQuery": true,
})
buf1 := &bytes.Buffer{}
@@ -88,13 +87,9 @@ func TestIntegrationPrometheus(t *testing.T) {
// nolint:gosec
resp, err := http.Post(u, "application/json", buf1)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
_ = resp.Body.Close()
})
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotNil(t, outgoingRequest)
require.Equal(t, "/api/v1/query_range?q1=1&q2=2", outgoingRequest.URL.String())
@@ -124,13 +119,9 @@ func TestIntegrationPrometheus(t *testing.T) {
// nolint:gosec
resp, err := http.Post(u, "application/json", buf1)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
_ = resp.Body.Close()
})
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotNil(t, outgoingRequest)
require.Equal(t, "/api/v1/query_range", outgoingRequest.URL.Path)

View File

@@ -269,7 +269,7 @@ func addFiltersAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder
func addGeoHashGridAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder {
aggBuilder.GeoHashGrid(bucketAgg.ID, bucketAgg.Field, func(a *es.GeoHashGridAggregation, b es.AggBuilder) {
a.Precision = bucketAgg.Settings.Get("precision").MustInt(es.DefaultPrecision)
a.Precision = stringToIntWithDefaultValue(bucketAgg.Settings.Get("precision").MustString(), es.DefaultPrecision)
aggBuilder = b
})

View File

@@ -612,7 +612,7 @@ func TestExecuteElasticsearchDataQuery(t *testing.T) {
"id": "3",
"type": "geohash_grid",
"field": "@location",
"settings": { "precision": 3 }
"settings": { "precision": "6" }
}
],
"metrics": [{"type": "count", "id": "1" }]
@@ -625,6 +625,56 @@ func TestExecuteElasticsearchDataQuery(t *testing.T) {
require.Equal(t, firstLevel.Aggregation.Type, "geohash_grid")
ghGridAgg := firstLevel.Aggregation.Aggregation.(*es.GeoHashGridAggregation)
require.Equal(t, ghGridAgg.Field, "@location")
require.Equal(t, ghGridAgg.Precision, 6)
})
t.Run("With geo hash grid agg with invalid int precision", func(t *testing.T) {
c := newFakeClient()
_, err := executeElasticsearchDataQuery(c, `{
"bucketAggs": [
{
"id": "3",
"type": "geohash_grid",
"field": "@location",
"settings": { "precision": 7 }
}
],
"metrics": [{"type": "count", "id": "1" }]
}`, from, to)
require.NoError(t, err)
sr := c.multisearchRequests[0].Requests[0]
firstLevel := sr.Aggs[0]
require.Equal(t, firstLevel.Key, "3")
require.Equal(t, firstLevel.Aggregation.Type, "geohash_grid")
ghGridAgg := firstLevel.Aggregation.Aggregation.(*es.GeoHashGridAggregation)
require.Equal(t, ghGridAgg.Field, "@location")
// It should default to 3
require.Equal(t, ghGridAgg.Precision, 3)
})
t.Run("With geo hash grid agg with no precision", func(t *testing.T) {
c := newFakeClient()
_, err := executeElasticsearchDataQuery(c, `{
"bucketAggs": [
{
"id": "3",
"type": "geohash_grid",
"field": "@location",
"settings": {}
}
],
"metrics": [{"type": "count", "id": "1" }]
}`, from, to)
require.NoError(t, err)
sr := c.multisearchRequests[0].Requests[0]
firstLevel := sr.Aggs[0]
require.Equal(t, firstLevel.Key, "3")
require.Equal(t, firstLevel.Aggregation.Type, "geohash_grid")
ghGridAgg := firstLevel.Aggregation.Aggregation.(*es.GeoHashGridAggregation)
require.Equal(t, ghGridAgg.Field, "@location")
// It should default to 3
require.Equal(t, ghGridAgg.Precision, 3)
})

View File

@@ -2,7 +2,9 @@ package prometheus
import (
"context"
"io"
"net/http"
"strings"
"testing"
"time"
@@ -27,10 +29,19 @@ type healthCheckFailRoundTripper struct {
func (rt *healthCheckSuccessRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "200",
StatusCode: 200,
Header: nil,
Body: nil,
Status: "200",
StatusCode: 200,
Header: nil,
Body: io.NopCloser(strings.NewReader(`{
"status": "success",
"data": {
"resultType": "scalar",
"result": [
1692969348.331,
"2"
]
}
}`)),
ContentLength: 0,
Request: req,
}, nil

View File

@@ -0,0 +1,60 @@
// package jsonitere wraps json-iterator/go's Iterator methods with error returns
// so linting can catch unchecked errors.
// The underlying iterator's Error property is returned and not reset.
// See json-iterator/go for method documentation and additional methods that
// can be added to this library.
package jsonitere
import j "github.com/json-iterator/go"
type Iterator struct {
// named property instead of embedded so there is no
// confusion about which method or property is called
i *j.Iterator
}
func NewIterator(i *j.Iterator) *Iterator {
return &Iterator{i}
}
func (iter *Iterator) Read() (interface{}, error) {
return iter.i.Read(), iter.i.Error
}
func (iter *Iterator) ReadAny() (j.Any, error) {
return iter.i.ReadAny(), iter.i.Error
}
func (iter *Iterator) ReadArray() (bool, error) {
return iter.i.ReadArray(), iter.i.Error
}
func (iter *Iterator) ReadObject() (string, error) {
return iter.i.ReadObject(), iter.i.Error
}
func (iter *Iterator) ReadString() (string, error) {
return iter.i.ReadString(), iter.i.Error
}
func (iter *Iterator) WhatIsNext() (j.ValueType, error) {
return iter.i.WhatIsNext(), iter.i.Error
}
func (iter *Iterator) Skip() error {
iter.i.Skip()
return iter.i.Error
}
func (iter *Iterator) ReadVal(obj interface{}) error {
iter.i.ReadVal(obj)
return iter.i.Error
}
func (iter *Iterator) ReadFloat64() (float64, error) {
return iter.i.ReadFloat64(), iter.i.Error
}
func (iter *Iterator) ReadInt8() (int8, error) {
return iter.i.ReadInt8(), iter.i.Error
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/util/converter/jsonitere"
jsoniter "github.com/json-iterator/go"
)
@@ -20,40 +21,70 @@ type Options struct {
Dataplane bool
}
func rspErr(e error) backend.DataResponse {
return backend.DataResponse{Error: e}
}
// ReadPrometheusStyleResult will read results from a prometheus or loki server and return data frames
func ReadPrometheusStyleResult(iter *jsoniter.Iterator, opt Options) backend.DataResponse {
func ReadPrometheusStyleResult(jIter *jsoniter.Iterator, opt Options) backend.DataResponse {
iter := jsonitere.NewIterator(jIter)
var rsp backend.DataResponse
status := "unknown"
errorType := ""
err := ""
promErrString := ""
warnings := []data.Notice{}
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
l1Fields:
for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() {
if err != nil {
return rspErr(err)
}
switch l1Field {
case "status":
status = iter.ReadString()
if status, err = iter.ReadString(); err != nil {
return rspErr(err)
}
case "data":
rsp = readPrometheusData(iter, opt)
if rsp.Error != nil {
return rsp
}
case "error":
err = iter.ReadString()
if promErrString, err = iter.ReadString(); err != nil {
return rspErr(err)
}
case "errorType":
errorType = iter.ReadString()
if errorType, err = iter.ReadString(); err != nil {
return rspErr(err)
}
case "warnings":
warnings = readWarnings(iter)
if warnings, err = readWarnings(iter); err != nil {
return rspErr(err)
}
case "":
if err != nil {
return rspErr(err)
}
break l1Fields
default:
v := iter.Read()
v, err := iter.Read()
if err != nil {
rsp.Error = err
return rsp
}
logf("[ROOT] TODO, support key: %s / %v\n", l1Field, v)
}
}
if status == "error" {
return backend.DataResponse{
Error: fmt.Errorf("%s: %s", errorType, err),
Error: fmt.Errorf("%s: %s", errorType, promErrString),
}
}
@@ -69,27 +100,48 @@ func ReadPrometheusStyleResult(iter *jsoniter.Iterator, opt Options) backend.Dat
return rsp
}
func readWarnings(iter *jsoniter.Iterator) []data.Notice {
func readWarnings(iter *jsonitere.Iterator) ([]data.Notice, error) {
warnings := []data.Notice{}
if iter.WhatIsNext() != jsoniter.ArrayValue {
return warnings
next, err := iter.WhatIsNext()
if err != nil {
return nil, err
}
for iter.ReadArray() {
if iter.WhatIsNext() == jsoniter.StringValue {
if next != jsoniter.ArrayValue {
return warnings, nil
}
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
return nil, err
}
next, err := iter.WhatIsNext()
if err != nil {
return nil, err
}
if next == jsoniter.StringValue {
s, err := iter.ReadString()
if err != nil {
return nil, err
}
notice := data.Notice{
Severity: data.NoticeSeverityWarning,
Text: iter.ReadString(),
Text: s,
}
warnings = append(warnings, notice)
}
}
return warnings
return warnings, nil
}
func readPrometheusData(iter *jsoniter.Iterator, opt Options) backend.DataResponse {
t := iter.WhatIsNext()
func readPrometheusData(iter *jsonitere.Iterator, opt Options) backend.DataResponse {
var rsp backend.DataResponse
t, err := iter.WhatIsNext()
if err != nil {
return rspErr(err)
}
if t == jsoniter.ArrayValue {
return readArrayData(iter)
}
@@ -101,32 +153,54 @@ func readPrometheusData(iter *jsoniter.Iterator, opt Options) backend.DataRespon
}
resultType := ""
var rsp backend.DataResponse
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
l1Fields:
for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() {
if err != nil {
return rspErr(err)
}
switch l1Field {
case "resultType":
resultType = iter.ReadString()
resultType, err = iter.ReadString()
if err != nil {
return rspErr(err)
}
case "result":
switch resultType {
case "matrix", "vector":
rsp = readMatrixOrVectorMulti(iter, resultType, opt)
if rsp.Error != nil {
return rsp
}
case "streams":
rsp = readStream(iter)
if rsp.Error != nil {
return rsp
}
case "string":
rsp = readString(iter)
if rsp.Error != nil {
return rsp
}
case "scalar":
rsp = readScalar(iter)
if rsp.Error != nil {
return rsp
}
default:
iter.Skip()
if err = iter.Skip(); err != nil {
return rspErr(err)
}
rsp = backend.DataResponse{
Error: fmt.Errorf("unknown result type: %s", resultType),
}
}
case "stats":
v := iter.Read()
v, err := iter.Read()
if err != nil {
rspErr(err)
}
if len(rsp.Frames) > 0 {
meta := rsp.Frames[0].Meta
if meta == nil {
@@ -138,8 +212,17 @@ func readPrometheusData(iter *jsoniter.Iterator, opt Options) backend.DataRespon
}
}
case "":
if err != nil {
return rspErr(err)
}
break l1Fields
default:
v := iter.Read()
v, err := iter.Read()
if err != nil {
return rspErr(err)
}
logf("[data] TODO, support key: %s / %v\n", l1Field, v)
}
}
@@ -148,21 +231,38 @@ func readPrometheusData(iter *jsoniter.Iterator, opt Options) backend.DataRespon
}
// will return strings or exemplars
func readArrayData(iter *jsoniter.Iterator) backend.DataResponse {
func readArrayData(iter *jsonitere.Iterator) backend.DataResponse {
lookup := make(map[string]*data.Field)
var labelFrame *data.Frame
rsp := backend.DataResponse{}
stringField := data.NewFieldFromFieldType(data.FieldTypeString, 0)
stringField.Name = "Value"
for iter.ReadArray() {
switch iter.WhatIsNext() {
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
rspErr(err)
}
next, err := iter.WhatIsNext()
if err != nil {
return rspErr(err)
}
switch next {
case jsoniter.StringValue:
stringField.Append(iter.ReadString())
s, err := iter.ReadString()
if err != nil {
return rspErr(err)
}
stringField.Append(s)
// Either label or exemplars
case jsoniter.ObjectValue:
exemplar, labelPairs := readLabelsOrExemplars(iter)
exemplar, labelPairs, err := readLabelsOrExemplars(iter)
if err != nil {
rspErr(err)
}
if exemplar != nil {
rsp.Frames = append(rsp.Frames, exemplar)
} else if labelPairs != nil {
@@ -199,7 +299,10 @@ func readArrayData(iter *jsoniter.Iterator) backend.DataResponse {
default:
{
ext := iter.ReadAny()
ext, err := iter.ReadAny()
if err != nil {
rspErr(err)
}
v := fmt.Sprintf("%v", ext)
stringField.Append(v)
}
@@ -214,23 +317,38 @@ func readArrayData(iter *jsoniter.Iterator) backend.DataResponse {
}
// For consistent ordering read values to an array not a map
func readLabelsAsPairs(iter *jsoniter.Iterator) [][2]string {
func readLabelsAsPairs(iter *jsonitere.Iterator) ([][2]string, error) {
pairs := make([][2]string, 0, 10)
for k := iter.ReadObject(); k != ""; k = iter.ReadObject() {
pairs = append(pairs, [2]string{k, iter.ReadString()})
for k, err := iter.ReadObject(); k != ""; k, err = iter.ReadObject() {
if err != nil {
return nil, err
}
v, err := iter.ReadString()
if err != nil {
return nil, err
}
pairs = append(pairs, [2]string{k, v})
}
return pairs
return pairs, nil
}
func readLabelsOrExemplars(iter *jsoniter.Iterator) (*data.Frame, [][2]string) {
func readLabelsOrExemplars(iter *jsonitere.Iterator) (*data.Frame, [][2]string, error) {
pairs := make([][2]string, 0, 10)
labels := data.Labels{}
var frame *data.Frame
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
l1Fields:
for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() {
if err != nil {
return nil, nil, err
}
switch l1Field {
case "seriesLabels":
iter.ReadVal(&labels)
err = iter.ReadVal(&labels)
if err != nil {
return nil, nil, err
}
case "exemplars":
lookup := make(map[string]*data.Field)
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
@@ -242,21 +360,42 @@ func readLabelsOrExemplars(iter *jsoniter.Iterator) (*data.Frame, [][2]string) {
frame.Meta = &data.FrameMeta{
Custom: resultTypeToCustomMeta("exemplar"),
}
for iter.ReadArray() {
for l2Field := iter.ReadObject(); l2Field != ""; l2Field = iter.ReadObject() {
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
return nil, nil, err
}
for l2Field, err := iter.ReadObject(); l2Field != ""; l2Field, err = iter.ReadObject() {
if err != nil {
return nil, nil, err
}
switch l2Field {
// nolint:goconst
case "value":
v, _ := strconv.ParseFloat(iter.ReadString(), 64)
s, err := iter.ReadString()
if err != nil {
return nil, nil, err
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return nil, nil, err
}
valueField.Append(v)
case "timestamp":
ts := timeFromFloat(iter.ReadFloat64())
f, err := iter.ReadFloat64()
if err != nil {
return nil, nil, err
}
ts := timeFromFloat(f)
timeField.Append(ts)
case "labels":
max := 0
for _, pair := range readLabelsAsPairs(iter) {
pairs, err := readLabelsAsPairs(iter)
if err != nil {
return nil, nil, err
}
for _, pair := range pairs {
k := pair[0]
v := pair[1]
f, ok := lookup[k]
@@ -281,7 +420,10 @@ func readLabelsOrExemplars(iter *jsoniter.Iterator) (*data.Frame, [][2]string) {
}
default:
iter.Skip()
if err = iter.Skip(); err != nil {
return nil, nil, err
}
frame.AppendNotices(data.Notice{
Severity: data.NoticeSeverityError,
Text: fmt.Sprintf("unable to parse key: %s in response body", l2Field),
@@ -289,27 +431,54 @@ func readLabelsOrExemplars(iter *jsoniter.Iterator) (*data.Frame, [][2]string) {
}
}
}
case "":
if err != nil {
return nil, nil, err
}
break l1Fields
default:
v := fmt.Sprintf("%v", iter.Read())
iV, err := iter.Read()
if err != nil {
return nil, nil, err
}
v := fmt.Sprintf("%v", iV)
pairs = append(pairs, [2]string{l1Field, v})
}
}
return frame, pairs
return frame, pairs, nil
}
func readString(iter *jsoniter.Iterator) backend.DataResponse {
func readString(iter *jsonitere.Iterator) backend.DataResponse {
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
timeField.Name = data.TimeSeriesTimeFieldName
valueField := data.NewFieldFromFieldType(data.FieldTypeString, 0)
valueField.Name = data.TimeSeriesValueFieldName
valueField.Labels = data.Labels{}
iter.ReadArray()
t := iter.ReadFloat64()
iter.ReadArray()
v := iter.ReadString()
iter.ReadArray()
_, err := iter.ReadArray()
if err != nil {
return rspErr(err)
}
var t float64
if t, err = iter.ReadFloat64(); err != nil {
return rspErr(err)
}
if _, err = iter.ReadArray(); err != nil {
return rspErr(err)
}
var v string
if v, err = iter.ReadString(); err != nil {
return rspErr(err)
}
if _, err = iter.ReadArray(); err != nil {
return rspErr(err)
}
tt := timeFromFloat(t)
timeField.Append(tt)
@@ -326,7 +495,9 @@ func readString(iter *jsoniter.Iterator) backend.DataResponse {
}
}
func readScalar(iter *jsoniter.Iterator) backend.DataResponse {
func readScalar(iter *jsonitere.Iterator) backend.DataResponse {
rsp := backend.DataResponse{}
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
timeField.Name = data.TimeSeriesTimeFieldName
valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0)
@@ -334,10 +505,12 @@ func readScalar(iter *jsoniter.Iterator) backend.DataResponse {
valueField.Labels = data.Labels{}
t, v, err := readTimeValuePair(iter)
if err == nil {
timeField.Append(t)
valueField.Append(v)
if err != nil {
rsp.Error = err
return rsp
}
timeField.Append(t)
valueField.Append(v)
frame := data.NewFrame("", timeField, valueField)
frame.Meta = &data.FrameMeta{
@@ -350,10 +523,13 @@ func readScalar(iter *jsoniter.Iterator) backend.DataResponse {
}
}
func readMatrixOrVectorMulti(iter *jsoniter.Iterator, resultType string, opt Options) backend.DataResponse {
func readMatrixOrVectorMulti(iter *jsonitere.Iterator, resultType string, opt Options) backend.DataResponse {
rsp := backend.DataResponse{}
for iter.ReadArray() {
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
return rspErr(err)
}
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
timeField.Name = data.TimeSeriesTimeFieldName
valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0)
@@ -362,50 +538,64 @@ func readMatrixOrVectorMulti(iter *jsoniter.Iterator, resultType string, opt Opt
var histogram *histogramInfo
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
for l1Field, err := iter.ReadObject(); l1Field != ""; l1Field, err = iter.ReadObject() {
if err != nil {
return rspErr(err)
}
switch l1Field {
case "metric":
iter.ReadVal(&valueField.Labels)
if err = iter.ReadVal(&valueField.Labels); err != nil {
return rspErr(err)
}
case "value":
t, v, err := readTimeValuePair(iter)
if err == nil {
timeField.Append(t)
valueField.Append(v)
if err != nil {
return rspErr(err)
}
timeField.Append(t)
valueField.Append(v)
// nolint:goconst
case "values":
for iter.ReadArray() {
t, v, err := readTimeValuePair(iter)
if err == nil {
timeField.Append(t)
valueField.Append(v)
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
return rspErr(err)
}
t, v, err := readTimeValuePair(iter)
if err != nil {
return rspErr(err)
}
timeField.Append(t)
valueField.Append(v)
}
case "histogram":
if histogram == nil {
histogram = newHistogramInfo()
}
err := readHistogram(iter, histogram)
err = readHistogram(iter, histogram)
if err != nil {
rsp.Error = err
return rspErr(err)
}
case "histograms":
if histogram == nil {
histogram = newHistogramInfo()
}
for iter.ReadArray() {
err := readHistogram(iter, histogram)
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
rsp.Error = err
return rspErr(err)
}
if err = readHistogram(iter, histogram); err != nil {
return rspErr(err)
}
}
default:
iter.Skip()
if err = iter.Skip(); err != nil {
return rspErr(err)
}
logf("readMatrixOrVector: %s\n", l1Field)
}
}
@@ -439,12 +629,28 @@ func readMatrixOrVectorMulti(iter *jsoniter.Iterator, resultType string, opt Opt
return rsp
}
func readTimeValuePair(iter *jsoniter.Iterator) (time.Time, float64, error) {
iter.ReadArray()
t := iter.ReadFloat64()
iter.ReadArray()
v := iter.ReadString()
iter.ReadArray()
func readTimeValuePair(iter *jsonitere.Iterator) (time.Time, float64, error) {
if _, err := iter.ReadArray(); err != nil {
return time.Time{}, 0, err
}
t, err := iter.ReadFloat64()
if err != nil {
return time.Time{}, 0, err
}
if _, err = iter.ReadArray(); err != nil {
return time.Time{}, 0, err
}
var v string
if v, err = iter.ReadString(); err != nil {
return time.Time{}, 0, err
}
if _, err = iter.ReadArray(); err != nil {
return time.Time{}, 0, err
}
tt := timeFromFloat(t)
fv, err := strconv.ParseFloat(v, 64)
@@ -478,75 +684,123 @@ func newHistogramInfo() *histogramInfo {
// This will read a single sparse histogram
// [ time, { count, sum, buckets: [...] }]
func readHistogram(iter *jsoniter.Iterator, hist *histogramInfo) error {
func readHistogram(iter *jsonitere.Iterator, hist *histogramInfo) error {
// first element
iter.ReadArray()
t := timeFromFloat(iter.ReadFloat64())
if _, err := iter.ReadArray(); err != nil {
return err
}
var err error
f, err := iter.ReadFloat64()
if err != nil {
return err
}
t := timeFromFloat(f)
// next object element
iter.ReadArray()
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
if _, err := iter.ReadArray(); err != nil {
return err
}
for l1Field, err := iter.ReadObject(); l1Field != ""; l1Field, err = iter.ReadObject() {
if err != nil {
return err
}
switch l1Field {
case "count":
iter.Skip()
if err = iter.Skip(); err != nil {
return err
}
case "sum":
iter.Skip()
if err = iter.Skip(); err != nil {
return err
}
case "buckets":
for iter.ReadArray() {
hist.time.Append(t)
iter.ReadArray()
hist.yLayout.Append(iter.ReadInt8())
iter.ReadArray()
err = appendValueFromString(iter, hist.yMin)
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
return err
}
hist.time.Append(t)
if _, err := iter.ReadArray(); err != nil {
return err
}
v, err := iter.ReadInt8()
if err != nil {
return err
}
hist.yLayout.Append(v)
if _, err := iter.ReadArray(); err != nil {
return err
}
if err = appendValueFromString(iter, hist.yMin); err != nil {
return err
}
if _, err := iter.ReadArray(); err != nil {
return err
}
iter.ReadArray()
err = appendValueFromString(iter, hist.yMax)
if err != nil {
return err
}
iter.ReadArray()
if _, err := iter.ReadArray(); err != nil {
return err
}
err = appendValueFromString(iter, hist.count)
if err != nil {
return err
}
if iter.ReadArray() {
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
return err
}
return fmt.Errorf("expected close array")
}
}
default:
iter.Skip()
if err = iter.Skip(); err != nil {
return err
}
logf("[SKIP]readHistogram: %s\n", l1Field)
}
}
if iter.ReadArray() {
if more, err := iter.ReadArray(); more || err != nil {
if err != nil {
return err
}
return fmt.Errorf("expected to be done")
}
return nil
}
func appendValueFromString(iter *jsoniter.Iterator, field *data.Field) error {
v, err := strconv.ParseFloat(iter.ReadString(), 64)
if err != nil {
func appendValueFromString(iter *jsonitere.Iterator, field *data.Field) error {
var err error
var s string
if s, err = iter.ReadString(); err != nil {
return err
}
var v float64
if v, err = strconv.ParseFloat(s, 64); err != nil {
return err
}
field.Append(v)
return nil
}
func readStream(iter *jsoniter.Iterator) backend.DataResponse {
func readStream(iter *jsonitere.Iterator) backend.DataResponse {
rsp := backend.DataResponse{}
labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0)
@@ -568,34 +822,73 @@ func readStream(iter *jsoniter.Iterator) backend.DataResponse {
return backend.DataResponse{Error: err}
}
for iter.ReadArray() {
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
rspErr(err)
}
l1Fields:
for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() {
if err != nil {
return rspErr(err)
}
switch l1Field {
case "stream":
// we need to clear `labels`, because `iter.ReadVal`
// only appends to it
labels := data.Labels{}
iter.ReadVal(&labels)
labelJson, err = labelsToRawJson(labels)
if err != nil {
return backend.DataResponse{Error: err}
if err = iter.ReadVal(&labels); err != nil {
return rspErr(err)
}
if labelJson, err = labelsToRawJson(labels); err != nil {
return rspErr(err)
}
case "values":
for iter.ReadArray() {
iter.ReadArray()
ts := iter.ReadString()
iter.ReadArray()
line := iter.ReadString()
iter.ReadArray()
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
if err != nil {
rsp.Error = err
return rsp
}
t := timeFromLokiString(ts)
if _, err = iter.ReadArray(); err != nil {
return rspErr(err)
}
ts, err := iter.ReadString()
if err != nil {
return rspErr(err)
}
if _, err = iter.ReadArray(); err != nil {
return rspErr(err)
}
line, err := iter.ReadString()
if err != nil {
return rspErr(err)
}
if _, err = iter.ReadArray(); err != nil {
return rspErr(err)
}
t, err := timeFromLokiString(ts)
if err != nil {
return rspErr(err)
}
labelsField.Append(labelJson)
timeField.Append(t)
lineField.Append(line)
tsField.Append(ts)
}
case "":
if err != nil {
return rspErr(err)
}
break l1Fields
}
}
}
@@ -615,7 +908,7 @@ func timeFromFloat(fv float64) time.Time {
return time.UnixMilli(int64(fv * 1000.0)).UTC()
}
func timeFromLokiString(str string) time.Time {
func timeFromLokiString(str string) (time.Time, error) {
// normal time values look like: 1645030246277587968
// and are less than: math.MaxInt65=9223372036854775807
// This will do a fast path for any date before 2033
@@ -623,13 +916,17 @@ func timeFromLokiString(str string) time.Time {
if s < 19 || (s == 19 && str[0] == '1') {
ns, err := strconv.ParseInt(str, 10, 64)
if err == nil {
return time.Unix(0, ns).UTC()
return time.Unix(0, ns).UTC(), nil
}
}
if s < 10 {
return time.Time{}, fmt.Errorf("unexpected time format '%v' in response. response may have been truncated", str)
}
ss, _ := strconv.ParseInt(str[0:10], 10, 64)
ns, _ := strconv.ParseInt(str[10:], 10, 64)
return time.Unix(ss, ns).UTC()
return time.Unix(ss, ns).UTC(), nil
}
func labelsToRawJson(labels data.Labels) (json.RawMessage, error) {

View File

@@ -1,6 +1,7 @@
package converter
import (
"fmt"
"os"
"path"
"strings"
@@ -8,39 +9,63 @@ import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/grafana/grafana/pkg/infra/httpclient"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const update = true
const update = false
var files = []string{
"prom-labels",
"prom-matrix",
"prom-matrix-with-nans",
"prom-matrix-histogram-no-labels",
"prom-matrix-histogram-partitioned",
"prom-vector-histogram-no-labels",
"prom-vector",
"prom-string",
"prom-scalar",
"prom-series",
"prom-warnings",
"prom-error",
"prom-exemplars-a",
"prom-exemplars-b",
"loki-streams-a",
"loki-streams-b",
"loki-streams-c",
}
func TestReadPromFrames(t *testing.T) {
files := []string{
"prom-labels",
"prom-matrix",
"prom-matrix-with-nans",
"prom-matrix-histogram-no-labels",
"prom-matrix-histogram-partitioned",
"prom-vector-histogram-no-labels",
"prom-vector",
"prom-string",
"prom-scalar",
"prom-series",
"prom-warnings",
"prom-error",
"prom-exemplars-a",
"prom-exemplars-b",
"loki-streams-a",
"loki-streams-b",
"loki-streams-c",
}
for _, name := range files {
t.Run(name, runScenario(name, Options{}))
}
}
func TestReadLimited(t *testing.T) {
for _, name := range files {
p := path.Join("testdata", name+".json")
stat, err := os.Stat(p)
require.NoError(t, err)
size := stat.Size()
for i := int64(10); i < size-1; i += size / 10 {
t.Run(fmt.Sprintf("%v_%v", name, i), func(t *testing.T) {
//nolint:gosec
f, err := os.Open(p)
require.NoError(t, err)
mbr := httpclient.MaxBytesReader(f, i)
iter := jsoniter.Parse(jsoniter.ConfigDefault, mbr, 1024)
rsp := ReadPrometheusStyleResult(iter, Options{})
require.ErrorContains(t, rsp.Error, "response body too large")
})
}
}
}
// FIXME:
//
//lint:ignore U1000 Ignore used function for now
@@ -58,6 +83,7 @@ func runScenario(name string, opts Options) func(t *testing.T) {
require.Error(t, rsp.Error)
return
}
require.NoError(t, rsp.Error)
fname := name + "-frame"
experimental.CheckGoldenJSONResponse(t, "testdata", fname, &rsp, update)
@@ -70,12 +96,17 @@ func TestTimeConversions(t *testing.T) {
time.Date(2020, time.September, 14, 15, 22, 25, 479000000, time.UTC),
timeFromFloat(1600096945.479))
ti, err := timeFromLokiString("1645030246277587968")
require.NoError(t, err)
// Loki date parsing
assert.Equal(t,
time.Date(2022, time.February, 16, 16, 50, 46, 277587968, time.UTC),
timeFromLokiString("1645030246277587968"))
ti)
ti, err = timeFromLokiString("2000000000000000000")
require.NoError(t, err)
assert.Equal(t,
time.Date(2033, time.May, 18, 3, 33, 20, 0, time.UTC),
timeFromLokiString("2000000000000000000"))
ti)
}

View File

@@ -68,7 +68,7 @@ export class AppChromeService {
}
}
ignoreStateUpdate(newState: AppChromeState, current: AppChromeState) {
private ignoreStateUpdate(newState: AppChromeState, current: AppChromeState) {
if (isShallowEqual(newState, current)) {
return true;
}

View File

@@ -2,6 +2,8 @@ import { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/types';
import { PanelModel } from '../../state/PanelModel';
import { DashboardRow } from './DashboardRow';
@@ -17,6 +19,7 @@ describe('DashboardRow', () => {
canEdit: true,
},
events: { subscribe: jest.fn() },
getRowPanels: () => [],
};
panel = new PanelModel({ collapsed: false });
@@ -67,4 +70,28 @@ describe('DashboardRow', () => {
expect(screen.queryByRole('button', { name: 'Delete row' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Row options' })).not.toBeInTheDocument();
});
it('Should return warning message when row panel has a panel with dashboard ds set', async () => {
const panel = new PanelModel({
datasource: {
type: 'datasource',
uid: SHARED_DASHBOARD_QUERY,
},
});
const rowPanel = new PanelModel({ collapsed: true, panels: [panel] });
const dashboardRow = new DashboardRow({ panel: rowPanel, dashboard: dashboardMock });
expect(dashboardRow.getWarning()).toBeDefined();
});
it('Should not return warning message when row panel does not have a panel with dashboard ds set', async () => {
const panel = new PanelModel({
datasource: {
type: 'datasource',
uid: 'ds-uid',
},
});
const rowPanel = new PanelModel({ collapsed: true, panels: [panel] });
const dashboardRow = new DashboardRow({ panel: rowPanel, dashboard: dashboardMock });
expect(dashboardRow.getWarning()).not.toBeDefined();
});
});

View File

@@ -1,4 +1,5 @@
import classNames from 'classnames';
import { indexOf } from 'lodash';
import React from 'react';
import { Unsubscribable } from 'rxjs';
@@ -6,6 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { getTemplateSrv, RefreshEvent } from '@grafana/runtime';
import { Icon } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/types';
import { ShowConfirmModalEvent } from '../../../../types/events';
import { DashboardModel } from '../../state/DashboardModel';
@@ -38,6 +40,23 @@ export class DashboardRow extends React.Component<DashboardRowProps> {
this.props.dashboard.toggleRow(this.props.panel);
};
getWarning = () => {
const panels = !!this.props.panel.panels?.length
? this.props.panel.panels
: this.props.dashboard.getRowPanels(indexOf(this.props.dashboard.panels, this.props.panel));
const isAnyPanelUsingDashboardDS = panels.some((p) => p.datasource?.uid === SHARED_DASHBOARD_QUERY);
if (isAnyPanelUsingDashboardDS) {
return (
<p>
Panels in this row use the {SHARED_DASHBOARD_QUERY} data source. These panels will reference the panel in the
original row, not the ones in the repeated rows.
</p>
);
}
return undefined;
};
onUpdate = (title: string, repeat?: string | null) => {
this.props.panel.setProperty('title', title);
this.props.panel.setProperty('repeat', repeat ?? undefined);
@@ -94,6 +113,7 @@ export class DashboardRow extends React.Component<DashboardRowProps> {
title={this.props.panel.title}
repeat={this.props.panel.repeat}
onUpdate={this.onUpdate}
warning={this.getWarning()}
/>
<button type="button" className="pointer" onClick={this.onDelete} aria-label="Delete row">
<Icon name="trash-alt" />

View File

@@ -9,9 +9,10 @@ export interface RowOptionsButtonProps {
title: string;
repeat?: string | null;
onUpdate: OnRowOptionsUpdate;
warning?: React.ReactNode;
}
export const RowOptionsButton = ({ repeat, title, onUpdate }: RowOptionsButtonProps) => {
export const RowOptionsButton = ({ repeat, title, onUpdate, warning }: RowOptionsButtonProps) => {
const onUpdateChange = (hideModal: () => void) => (title: string, repeat?: string | null) => {
onUpdate(title, repeat);
hideModal();
@@ -26,7 +27,13 @@ export const RowOptionsButton = ({ repeat, title, onUpdate }: RowOptionsButtonPr
className="pointer"
aria-label="Row options"
onClick={() => {
showModal(RowOptionsModal, { title, repeat, onDismiss: hideModal, onUpdate: onUpdateChange(hideModal) });
showModal(RowOptionsModal, {
title,
repeat,
onDismiss: hideModal,
onUpdate: onUpdateChange(hideModal),
warning,
});
}}
>
<Icon name="cog" />

View File

@@ -0,0 +1,34 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { selectors } from '@grafana/e2e-selectors';
import { RowOptionsForm } from './RowOptionsForm';
jest.mock('../RepeatRowSelect/RepeatRowSelect', () => ({
RepeatRowSelect: () => <div />,
}));
describe('DashboardRow', () => {
it('Should show warning component when has warningMessage prop', () => {
render(
<TestProvider>
<RowOptionsForm repeat={'3'} title="" onCancel={jest.fn()} onUpdate={jest.fn()} warning="a warning message" />
</TestProvider>
);
expect(
screen.getByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage)
).toBeInTheDocument();
});
it('Should not show warning component when does not have warningMessage prop', () => {
render(
<TestProvider>
<RowOptionsForm repeat={'3'} title="" onCancel={jest.fn()} onUpdate={jest.fn()} />
</TestProvider>
);
expect(
screen.queryByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage)
).not.toBeInTheDocument();
});
});

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import { Button, Field, Form, Modal, Input } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { Button, Field, Form, Modal, Input, Alert } from '@grafana/ui';
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
@@ -11,9 +12,10 @@ export interface Props {
repeat?: string | null;
onUpdate: OnRowOptionsUpdate;
onCancel: () => void;
warning?: React.ReactNode;
}
export const RowOptionsForm = ({ repeat, title, onUpdate, onCancel }: Props) => {
export const RowOptionsForm = ({ repeat, title, warning, onUpdate, onCancel }: Props) => {
const [newRepeat, setNewRepeat] = useState<string | null | undefined>(repeat);
const onChangeRepeat = useCallback((name?: string | null) => setNewRepeat(name), [setNewRepeat]);
@@ -29,11 +31,20 @@ export const RowOptionsForm = ({ repeat, title, onUpdate, onCancel }: Props) =>
<Field label="Title">
<Input {...register('title')} type="text" />
</Field>
<Field label="Repeat for">
<RepeatRowSelect repeat={newRepeat} onChange={onChangeRepeat} />
</Field>
{warning && (
<Alert
data-testid={selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage}
severity="warning"
title=""
topSpacing={3}
bottomSpacing={0}
>
{warning}
</Alert>
)}
<Modal.ButtonRow>
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
Cancel

View File

@@ -8,15 +8,16 @@ import { OnRowOptionsUpdate, RowOptionsForm } from './RowOptionsForm';
export interface RowOptionsModalProps {
title: string;
repeat?: string | null;
warning?: React.ReactNode;
onDismiss: () => void;
onUpdate: OnRowOptionsUpdate;
}
export const RowOptionsModal = ({ repeat, title, onDismiss, onUpdate }: RowOptionsModalProps) => {
export const RowOptionsModal = ({ repeat, title, onDismiss, onUpdate, warning }: RowOptionsModalProps) => {
const styles = getStyles();
return (
<Modal isOpen={true} title="Row options" icon="copy" onDismiss={onDismiss} className={styles.modal}>
<RowOptionsForm repeat={repeat} title={title} onCancel={onDismiss} onUpdate={onUpdate} />
<RowOptionsForm repeat={repeat} title={title} onCancel={onDismiss} onUpdate={onUpdate} warning={warning} />
</Modal>
);
};

View File

@@ -461,7 +461,7 @@ describe('optionsPickerReducer', () => {
});
});
it('should toggle all values to false when $_all is selected', () => {
it('should toggle each individual value to true when $_all is selected and mark ALL as selected, not supporting empty values', () => {
const { initialState } = getVariableTestContext({
options: [
{ text: 'All', value: '$__all', selected: true },
@@ -479,14 +479,17 @@ describe('optionsPickerReducer', () => {
...initialState,
options: [
{ text: 'All', value: '$__all', selected: false },
{ text: 'A', value: 'A', selected: false },
{ text: 'B', value: 'B', selected: false },
{ text: 'A', value: 'A', selected: true },
{ text: 'B', value: 'B', selected: true },
],
selectedValues: [
{ text: 'A', value: 'A', selected: true },
{ text: 'B', value: 'B', selected: true },
],
selectedValues: [],
});
});
it('should toggle all values to false when a option is selected', () => {
it('should toggle to ALL value when one regular option is selected, as empty values are not accepted', () => {
const { initialState } = getVariableTestContext({
options: [
{ text: 'All', value: '$__all', selected: false },
@@ -503,11 +506,11 @@ describe('optionsPickerReducer', () => {
.thenStateShouldEqual({
...initialState,
options: [
{ text: 'All', value: '$__all', selected: false },
{ text: 'All', value: '$__all', selected: true },
{ text: 'A', value: 'A', selected: false },
{ text: 'B', value: 'B', selected: false },
],
selectedValues: [],
selectedValues: [{ text: 'All', value: '$__all', selected: true }],
});
});
});

View File

@@ -106,6 +106,15 @@ const updateAllSelection = (state: OptionsPickerState): OptionsPickerState => {
return state;
};
// Utility function to select all options except 'ALL_VARIABLE_VALUE'
const selectAllOptions = (options: VariableOption[]) =>
options
.filter((option) => option.value !== ALL_VARIABLE_VALUE)
.map((option) => ({
...option,
selected: true,
}));
const optionsPickerSlice = createSlice({
name: 'templating/optionsPicker',
initialState: initialOptionPickerState,
@@ -178,21 +187,51 @@ const optionsPickerSlice = createSlice({
highlightIndex: nextIndex,
};
},
/**
* Toggle the 'All' option or clear selections in the Options Picker dropdown.
* 1. If 'All' is configured but not selected, and some other options are selected, it deselects all other options and selects only 'All'.
* 2. If only 'All' is selected, it deselects 'All' and selects all other available options.
* 3. If some options are selected but 'All' is not configured in the variable,
* it clears all selections and defaults to the current behavior for scenarios where 'All' is not configured.
* 4. If no options are selected, it selects all available options.
*/
toggleAllOptions: (state, action: PayloadAction): OptionsPickerState => {
if (state.selectedValues.length > 0) {
// Check if 'All' option is configured by the user and if it's selected in the dropdown
const isAllSelected = state.selectedValues.find((option) => option.value === ALL_VARIABLE_VALUE);
const allOptionConfigured = state.options.find((option) => option.value === ALL_VARIABLE_VALUE);
// If 'All' option is not selected from the dropdown, but some options are, clear all options and select 'All'
if (state.selectedValues.length > 0 && !!allOptionConfigured && !isAllSelected) {
state.selectedValues = [];
state.selectedValues.push({
text: allOptionConfigured.text ?? 'All',
value: allOptionConfigured.value,
selected: true,
});
return applyStateChanges(state, updateOptions);
}
// If 'All' option is the only one selected in the dropdown, unselect "All" and select each one of the other options.
if (isAllSelected && state.selectedValues.length === 1) {
state.selectedValues = selectAllOptions(state.options);
return applyStateChanges(state, updateOptions);
}
// If some options are selected, but 'All' is not configured by the user, clear the selection and let the
// current behavior when "All" does not exist and user clear the selected items.
if (state.selectedValues.length > 0 && !allOptionConfigured) {
state.selectedValues = [];
return applyStateChanges(state, updateOptions);
}
state.selectedValues = state.options
.filter((option) => option.value !== ALL_VARIABLE_VALUE)
.map((option) => ({
...option,
selected: true,
}));
// If no options are selected and 'All' is not selected, select all options
state.selectedValues = selectAllOptions(state.options);
return applyStateChanges(state, updateOptions);
},
updateSearchQuery: (state, action: PayloadAction<string>): OptionsPickerState => {
state.queryValue = action.payload;
return state;

View File

@@ -78,6 +78,7 @@ class VariableOptions extends PureComponent<Props> {
styles.variableOption,
{
[styles.highlighted]: index === highlightIndex,
[styles.variableAllOption]: isAllOption,
},
styles.noStyledButton
)}
@@ -98,15 +99,15 @@ class VariableOptions extends PureComponent<Props> {
}
renderMultiToggle() {
const { multi, selectedValues, theme } = this.props;
const { multi, selectedValues, theme, values } = this.props;
const styles = getStyles(theme);
const isAllOptionConfigured = values.some((option) => option.value === ALL_VARIABLE_VALUE);
if (!multi) {
return null;
}
const tooltipContent = () => <Trans i18nKey="variable.picker.option-tooltip">Clear selections</Trans>;
return (
<Tooltip content={tooltipContent} placement={'top'}>
<button
@@ -114,7 +115,8 @@ class VariableOptions extends PureComponent<Props> {
clearButtonStyles(theme),
styles.variableOption,
styles.variableOptionColumnHeader,
styles.noStyledButton
styles.noStyledButton,
{ [styles.noPaddingBotton]: isAllOptionConfigured }
)}
role="checkbox"
aria-checked={selectedValues.length > 1 ? 'mixed' : 'false'}
@@ -201,6 +203,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
display: 'table',
width: '100%',
}),
variableAllOption: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
paddingBottom: theme.spacing(1),
}),
noPaddingBotton: css({
paddingBottom: 0,
}),
};
});

View File

@@ -3765,9 +3765,9 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/eslint-config@npm:6.0.0":
version: 6.0.0
resolution: "@grafana/eslint-config@npm:6.0.0"
"@grafana/eslint-config@npm:6.0.1":
version: 6.0.1
resolution: "@grafana/eslint-config@npm:6.0.1"
dependencies:
"@typescript-eslint/eslint-plugin": 5.59.9
"@typescript-eslint/parser": 5.59.9
@@ -3777,7 +3777,7 @@ __metadata:
eslint-plugin-react: 7.32.2
eslint-plugin-react-hooks: 4.6.0
typescript: 4.8.4
checksum: a22d21282ba88a3b27d7a68ca6cbd01fe18c39aa3227b23079a0303d3c4265937c8371f8a0ea87acb843c9cec9fc82c649e2a8f47698613af8259edc85fd027c
checksum: 6744cc4b48d7504f574798fc442e1c794f784bc408b435ed43cb1baef8d0ab83554c5ac2a6437af352ee5df241055050217d5a67e15491a6116555c92c349c31
languageName: node
linkType: hard
@@ -19249,7 +19249,7 @@ __metadata:
"@grafana/data": "workspace:*"
"@grafana/e2e": "workspace:*"
"@grafana/e2e-selectors": "workspace:*"
"@grafana/eslint-config": 6.0.0
"@grafana/eslint-config": 6.0.1
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules"
"@grafana/experimental": 1.6.1
"@grafana/faro-core": 1.1.2