diff --git a/pkg/apis/peakq/v0alpha1/types.go b/pkg/apis/peakq/v0alpha1/types.go index 1e82dca4908..18cd54e1742 100644 --- a/pkg/apis/peakq/v0alpha1/types.go +++ b/pkg/apis/peakq/v0alpha1/types.go @@ -75,24 +75,35 @@ type VariableReplacement struct { Position *Position `json:"position,omitempty"` // 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! - Format string `json:"format,omitempty"` - - // Keep track of the values from previous iterations - History []ReplacementHistory `json:"history,omitempty"` + Format VariableFormat `json:"format,omitempty"` } -type ReplacementHistory struct { - // Who/what made the change - Source string `json:"source,omitempty"` +// Define how to format values in the template. +// See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options +// +enum +type VariableFormat string - // Value before replacement - Previous string `json:"previous"` +// Defines values for ItemType. +const ( + // Formats variables with multiple values as a comma-separated string. + FormatCSV VariableFormat = "csv" - // The value(s) that replaced the section - Replacement []string `json:"replacement"` -} + // Formats variables with multiple values as a comma-separated string. + FormatJSON VariableFormat = "json" + + // Formats single- and multi-valued variables into a comma-separated string + FormatDoubleQuote VariableFormat = "doublequote" + + // 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. diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go index 6079da06587..80d6da6e2e2 100644 --- a/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/peakq/v0alpha1/zz_generated.deepcopy.go @@ -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 } diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go b/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go index e4f973a7a87..0c3c52ebccf 100644 --- a/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/peakq/v0alpha1/zz_generated.openapi.go @@ -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"}, } } diff --git a/pkg/apis/peakq/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/peakq/v0alpha1/zz_generated.openapi_violation_exceptions.list index 9d032f4727f..91e16558329 100644 --- a/pkg/apis/peakq/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/peakq/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -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 diff --git a/pkg/registry/apis/peakq/format.go b/pkg/registry/apis/peakq/format.go new file mode 100644 index 00000000000..f88968d7f48 --- /dev/null +++ b/pkg/registry/apis/peakq/format.go @@ -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, ",") +} diff --git a/pkg/registry/apis/peakq/format_test.go b/pkg/registry/apis/peakq/format_test.go new file mode 100644 index 00000000000..930b5e3ddd3 --- /dev/null +++ b/pkg/registry/apis/peakq/format_test.go @@ -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) + } +} diff --git a/pkg/registry/apis/peakq/render.go b/pkg/registry/apis/peakq/render.go index bb3d20ed16f..5f084c2037f 100644 --- a/pkg/registry/apis/peakq/render.go +++ b/pkg/registry/apis/peakq/render.go @@ -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