mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Peakq: support multi-value template variables (#82036)
This commit is contained in:
parent
e77ec26897
commit
3464b6e581
@ -75,24 +75,35 @@ type VariableReplacement struct {
|
||||
Position *Position `json:"position,omitempty"`
|
||||
|
||||
// How values should be interpolated
|
||||
Format VariableFormat `json:"format,omitempty"`
|
||||
}
|
||||
|
||||
// Define how to format values in the template.
|
||||
// See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options
|
||||
// NOTE: the format parameter is not yet supported!
|
||||
Format string `json:"format,omitempty"`
|
||||
// +enum
|
||||
type VariableFormat string
|
||||
|
||||
// Keep track of the values from previous iterations
|
||||
History []ReplacementHistory `json:"history,omitempty"`
|
||||
}
|
||||
// Defines values for ItemType.
|
||||
const (
|
||||
// Formats variables with multiple values as a comma-separated string.
|
||||
FormatCSV VariableFormat = "csv"
|
||||
|
||||
type ReplacementHistory struct {
|
||||
// Who/what made the change
|
||||
Source string `json:"source,omitempty"`
|
||||
// Formats variables with multiple values as a comma-separated string.
|
||||
FormatJSON VariableFormat = "json"
|
||||
|
||||
// Value before replacement
|
||||
Previous string `json:"previous"`
|
||||
// Formats single- and multi-valued variables into a comma-separated string
|
||||
FormatDoubleQuote VariableFormat = "doublequote"
|
||||
|
||||
// The value(s) that replaced the section
|
||||
Replacement []string `json:"replacement"`
|
||||
}
|
||||
// Formats single- and multi-valued variables into a comma-separated string
|
||||
FormatSingleQuote VariableFormat = "singlequote"
|
||||
|
||||
// Formats variables with multiple values into a pipe-separated string.
|
||||
FormatPipe VariableFormat = "pipe"
|
||||
|
||||
// Formats variables with multiple values into comma-separated string.
|
||||
// This is the default behavior when no format is specified
|
||||
FormatRaw VariableFormat = "raw"
|
||||
)
|
||||
|
||||
// Position is where to do replacement in the targets
|
||||
// during render.
|
||||
|
@ -149,27 +149,6 @@ func (in *RenderedQuery) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ReplacementHistory) DeepCopyInto(out *ReplacementHistory) {
|
||||
*out = *in
|
||||
if in.Replacement != nil {
|
||||
in, out := &in.Replacement, &out.Replacement
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplacementHistory.
|
||||
func (in *ReplacementHistory) DeepCopy() *ReplacementHistory {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ReplacementHistory)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Target) DeepCopyInto(out *Target) {
|
||||
*out = *in
|
||||
@ -234,13 +213,6 @@ func (in *VariableReplacement) DeepCopyInto(out *VariableReplacement) {
|
||||
*out = new(Position)
|
||||
**out = **in
|
||||
}
|
||||
if in.History != nil {
|
||||
in, out := &in.History, &out.History
|
||||
*out = make([]ReplacementHistory, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateList": schema_pkg_apis_peakq_v0alpha1_QueryTemplateList(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec": schema_pkg_apis_peakq_v0alpha1_QueryTemplateSpec(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.RenderedQuery": schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory": schema_pkg_apis_peakq_v0alpha1_ReplacementHistory(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target": schema_pkg_apis_peakq_v0alpha1_Target(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.TemplateVariable": schema_pkg_apis_peakq_v0alpha1_TemplateVariable(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.VariableReplacement": schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref),
|
||||
@ -255,49 +254,6 @@ func schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref common.ReferenceCallback)
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_peakq_v0alpha1_ReplacementHistory(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"source": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Who/what made the change",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"previous": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Value before replacement",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"replacement": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The value(s) that replaced the section",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"previous", "replacement"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_peakq_v0alpha1_Target(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@ -421,23 +377,10 @@ func schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref common.ReferenceCall
|
||||
},
|
||||
"format": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "How values should be interpolated See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options NOTE: the format parameter is not yet supported!",
|
||||
Description: "How values should be interpolated See: NOTE: the format parameter is not yet supported!\n\nPossible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"history": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Keep track of the values from previous iterations",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Enum: []interface{}{"csv", "doublequote", "json", "pipe", "raw", "singlequote"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -445,6 +388,6 @@ func schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref common.ReferenceCall
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Position", "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory"},
|
||||
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Position"},
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,2 @@
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,ReplacementHistory,Replacement
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,VariableReplacement,History
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,QueryTemplateSpec,Variables
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,TemplateVariable,DefaultValues
|
||||
|
70
pkg/registry/apis/peakq/format.go
Normal file
70
pkg/registry/apis/peakq/format.go
Normal file
@ -0,0 +1,70 @@
|
||||
package peakq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1"
|
||||
)
|
||||
|
||||
func formatVariables(fmt peakq.VariableFormat, input []string) string {
|
||||
if len(input) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// MultiValued formats
|
||||
// nolint: exhaustive
|
||||
switch fmt {
|
||||
case peakq.FormatJSON:
|
||||
v, _ := json.Marshal(input)
|
||||
return string(v)
|
||||
|
||||
case peakq.FormatDoubleQuote:
|
||||
sb := bytes.NewBufferString("")
|
||||
for idx, val := range input {
|
||||
if idx > 0 {
|
||||
_, _ = sb.WriteRune(',')
|
||||
}
|
||||
_, _ = sb.WriteRune('"')
|
||||
_, _ = sb.WriteString(strings.ReplaceAll(val, `"`, `\"`))
|
||||
_, _ = sb.WriteRune('"')
|
||||
}
|
||||
return sb.String()
|
||||
|
||||
case peakq.FormatSingleQuote:
|
||||
sb := bytes.NewBufferString("")
|
||||
for idx, val := range input {
|
||||
if idx > 0 {
|
||||
_, _ = sb.WriteRune(',')
|
||||
}
|
||||
_, _ = sb.WriteRune('\'')
|
||||
_, _ = sb.WriteString(strings.ReplaceAll(val, `'`, `\'`))
|
||||
_, _ = sb.WriteRune('\'')
|
||||
}
|
||||
return sb.String()
|
||||
|
||||
case peakq.FormatCSV:
|
||||
sb := bytes.NewBufferString("")
|
||||
w := csv.NewWriter(sb)
|
||||
_ = w.Write(input)
|
||||
w.Flush()
|
||||
v := sb.Bytes()
|
||||
return string(v[:len(v)-1])
|
||||
}
|
||||
|
||||
// Single valued formats
|
||||
if len(input) == 1 {
|
||||
return input[0]
|
||||
}
|
||||
|
||||
// nolint: exhaustive
|
||||
switch fmt {
|
||||
case peakq.FormatPipe:
|
||||
return strings.Join(input, "|")
|
||||
}
|
||||
|
||||
// Raw output (joined with a comma)
|
||||
return strings.Join(input, ",")
|
||||
}
|
83
pkg/registry/apis/peakq/format_test.go
Normal file
83
pkg/registry/apis/peakq/format_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
package peakq
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1"
|
||||
)
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
// Invalid input
|
||||
require.Equal(t, "", formatVariables(peakq.FormatCSV, nil))
|
||||
require.Equal(t, "", formatVariables(peakq.FormatCSV, []string{}))
|
||||
|
||||
type check struct {
|
||||
name string
|
||||
input []string
|
||||
output map[peakq.VariableFormat]string
|
||||
}
|
||||
|
||||
tests := []check{
|
||||
{
|
||||
name: "three simple variables",
|
||||
input: []string{"a", "b", "c"},
|
||||
output: map[peakq.VariableFormat]string{
|
||||
peakq.FormatCSV: "a,b,c",
|
||||
peakq.FormatJSON: `["a","b","c"]`,
|
||||
peakq.FormatDoubleQuote: `"a","b","c"`,
|
||||
peakq.FormatSingleQuote: `'a','b','c'`,
|
||||
peakq.FormatPipe: `a|b|c`,
|
||||
peakq.FormatRaw: "a,b,c",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single value",
|
||||
input: []string{"a"},
|
||||
output: map[peakq.VariableFormat]string{
|
||||
peakq.FormatCSV: "a",
|
||||
peakq.FormatJSON: `["a"]`,
|
||||
peakq.FormatDoubleQuote: `"a"`,
|
||||
peakq.FormatSingleQuote: `'a'`,
|
||||
peakq.FormatPipe: "a",
|
||||
peakq.FormatRaw: "a",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "value with quote",
|
||||
input: []string{`hello "world"`},
|
||||
output: map[peakq.VariableFormat]string{
|
||||
peakq.FormatCSV: `"hello ""world"""`, // note the double quotes
|
||||
peakq.FormatJSON: `["hello \"world\""]`,
|
||||
peakq.FormatDoubleQuote: `"hello \"world\""`,
|
||||
peakq.FormatSingleQuote: `'hello "world"'`,
|
||||
peakq.FormatPipe: `hello "world"`,
|
||||
peakq.FormatRaw: `hello "world"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
// Make sure all keys are set in tests
|
||||
all := map[peakq.VariableFormat]bool{
|
||||
peakq.FormatRaw: true,
|
||||
peakq.FormatCSV: true,
|
||||
peakq.FormatJSON: true,
|
||||
peakq.FormatDoubleQuote: true,
|
||||
peakq.FormatSingleQuote: true,
|
||||
peakq.FormatPipe: true,
|
||||
}
|
||||
|
||||
// Check the default (no format) matches CSV
|
||||
require.Equal(t, test.output[peakq.FormatRaw],
|
||||
formatVariables("", test.input),
|
||||
"test %s default values are not raw", test.name)
|
||||
|
||||
// Check each input value
|
||||
for format, v := range test.output {
|
||||
require.Equal(t, v, formatVariables(format, test.input), "Test: %s (format:%s)", test.name, format)
|
||||
delete(all, format)
|
||||
}
|
||||
require.Empty(t, all, "test %s is missing cases for: %v", test.name, all)
|
||||
}
|
||||
}
|
@ -9,13 +9,13 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spyzhov/ajson"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1"
|
||||
"github.com/spyzhov/ajson"
|
||||
)
|
||||
|
||||
type renderREST struct {
|
||||
@ -107,6 +107,7 @@ func makeVarMapFromParams(v url.Values) (map[string][]string, error) {
|
||||
type replacement struct {
|
||||
*peakq.Position
|
||||
*peakq.TemplateVariable
|
||||
format peakq.VariableFormat
|
||||
}
|
||||
|
||||
func getReplacementMap(qt peakq.QueryTemplateSpec) map[int]map[string][]replacement {
|
||||
@ -127,6 +128,7 @@ func getReplacementMap(qt peakq.QueryTemplateSpec) map[int]map[string][]replacem
|
||||
replacement{
|
||||
Position: vReps[rI].Position,
|
||||
TemplateVariable: varMap[k],
|
||||
format: rep.Format,
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -178,13 +180,10 @@ func Render(qt peakq.QueryTemplateSpec, selectedValues map[string][]string) (*pe
|
||||
s = s[1 : len(s)-1]
|
||||
var offSet int64
|
||||
for _, r := range reps {
|
||||
value := []rune(formatVariables(r.format, selectedValues[r.Key]))
|
||||
if r.Position == nil {
|
||||
return nil, fmt.Errorf("nil position not support yet, will be full replacement")
|
||||
}
|
||||
if len(selectedValues[r.Key]) != 1 {
|
||||
return nil, fmt.Errorf("selected value missing, or more then one provided")
|
||||
}
|
||||
value := []rune(selectedValues[r.Key][0])
|
||||
s = append(s[:r.Start+offSet], append(value, s[r.End+offSet:]...)...)
|
||||
offSet += int64(len(value)) - (r.End - r.Start)
|
||||
}
|
||||
@ -199,7 +198,7 @@ func Render(qt peakq.QueryTemplateSpec, selectedValues map[string][]string) (*pe
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := v0alpha1.Unstructured{}
|
||||
u := common.Unstructured{}
|
||||
err = u.UnmarshalJSON(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
Loading…
Reference in New Issue
Block a user