{!!addonBefore &&
{addonBefore}
}
@@ -130,7 +132,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }:
css({
label: 'input-wrapper',
display: 'flex',
- width: width ? theme.spacing(width) : '100%', // Not used in Input, as this causes performance issues with auto sizing
+ width: width ? theme.spacing(width) : '100%',
height: theme.spacing(theme.components.height.md),
borderRadius: theme.shape.radius.default,
'&:hover': {
diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx
index 5da049a3d0c..c677d402dc9 100644
--- a/packages/grafana-ui/src/components/Slider/Slider.tsx
+++ b/packages/grafana-ui/src/components/Slider/Slider.tsx
@@ -109,6 +109,7 @@ export const Slider = ({
{
- const dark = useDarkMode();
+ // Default to system theme for pages that don't have associated stories
+ // Currently this is only the case for the docs `Intro` page
+ let themeId = 'system';
+ if (context.componentStories().length > 0) {
+ const story = context.storyById();
+ const { globals } = context.getStoryContext(story);
+ themeId = globals.theme;
+ }
+ const theme = getThemeById(themeId);
return (
-
+
{children}
diff --git a/packages/grafana-ui/src/utils/storybook/withTheme.tsx b/packages/grafana-ui/src/utils/storybook/withTheme.tsx
index d865769c5ab..aa1f761f85f 100644
--- a/packages/grafana-ui/src/utils/storybook/withTheme.tsx
+++ b/packages/grafana-ui/src/utils/storybook/withTheme.tsx
@@ -1,17 +1,17 @@
import { Decorator } from '@storybook/react';
import * as React from 'react';
-import { useDarkMode } from 'storybook-dark-mode';
-import { createTheme, GrafanaTheme2, ThemeContext } from '@grafana/data';
+import { getThemeById, GrafanaTheme2, ThemeContext } from '@grafana/data';
import { GlobalStyles } from '../../themes/GlobalStyles/GlobalStyles';
type SassThemeChangeHandler = (theme: GrafanaTheme2) => void;
-const ThemeableStory = ({
- children,
- handleSassThemeChange,
-}: React.PropsWithChildren<{ handleSassThemeChange: SassThemeChangeHandler }>) => {
- const theme = createTheme({ colors: { mode: useDarkMode() ? 'dark' : 'light' } });
+interface ThemeableStoryProps {
+ themeId: string;
+ handleSassThemeChange: SassThemeChangeHandler;
+}
+const ThemeableStory = ({ children, handleSassThemeChange, themeId }: React.PropsWithChildren) => {
+ const theme = getThemeById(themeId);
handleSassThemeChange(theme);
@@ -38,4 +38,8 @@ const ThemeableStory = ({
export const withTheme =
(handleSassThemeChange: SassThemeChangeHandler): Decorator =>
// eslint-disable-next-line react/display-name
- (story) => {story()};
+ (story, context) => (
+
+ {story()}
+
+ );
diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go
index 977f3307ca1..83e11eb91cf 100644
--- a/pkg/api/annotations_test.go
+++ b/pkg/api/annotations_test.go
@@ -403,7 +403,7 @@ func TestAPI_Annotations(t *testing.T) {
hs.folderService = folderService
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, folderService))
- hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderDB, dashService, folderService))
+ hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashService, folderService))
})
var body io.Reader
if tt.body != "" {
diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go
index e71ec51d376..627cace5d61 100644
--- a/pkg/apimachinery/identity/context.go
+++ b/pkg/apimachinery/identity/context.go
@@ -32,7 +32,7 @@ func checkNilRequester(r Requester) bool {
const serviceName = "service"
-// WithServiceIdentity sets creates an identity representing the service itself in provided org and store it in context.
+// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context.
// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with
// static permissions so it can be used in legacy code paths.
func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) {
@@ -53,6 +53,17 @@ func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Req
return WithRequester(ctx, r), r
}
+// WithServiceIdentityContext sets an identity representing the service itself in context.
+func WithServiceIdentityContext(ctx context.Context, orgID int64) context.Context {
+ ctx, _ = WithServiceIdentity(ctx, orgID)
+ return ctx
+}
+
+// WithServiceIdentityFN calls provided closure with an context contaning the identity of the service.
+func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error)) (T, error) {
+ return fn(WithServiceIdentityContext(ctx, orgID))
+}
+
func getWildcardPermissions(actions ...string) map[string][]string {
permissions := make(map[string][]string, len(actions))
for _, a := range actions {
diff --git a/pkg/apis/dashboard/v0alpha1/register.go b/pkg/apis/dashboard/v0alpha1/register.go
index 9423f9ef195..e163aa01257 100644
--- a/pkg/apis/dashboard/v0alpha1/register.go
+++ b/pkg/apis/dashboard/v0alpha1/register.go
@@ -14,6 +14,7 @@ import (
const (
GROUP = "dashboard.grafana.app"
VERSION = "v0alpha1"
+ RESOURCE = "dashboards"
APIVERSION = GROUP + "/" + VERSION
)
diff --git a/pkg/expr/convert_to_long.go b/pkg/expr/convert_to_long.go
new file mode 100644
index 00000000000..3ab3339b8b0
--- /dev/null
+++ b/pkg/expr/convert_to_long.go
@@ -0,0 +1,311 @@
+package expr
+
+import (
+ "fmt"
+ "sort"
+ "time"
+
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+func ConvertToLong(frames data.Frames) (data.Frames, error) {
+ if len(frames) == 0 {
+ // general empty case for now
+ return frames, nil
+ }
+ // Four Conversion Possible Cases
+ // 1. NumericMulti -> NumericLong
+ // 2. NumericWide -> NumericLong
+ // 3. TimeSeriesMulti -> TimeSeriesLong
+ // 4. TimeSeriesWide -> TimeSeriesLong
+
+ // Detect if input type is declared
+ // First Check Frame Meta Type
+
+ var inputType data.FrameType
+ if frames[0].Meta != nil && frames[0].Meta.Type != "" {
+ inputType = frames[0].Meta.Type
+ }
+
+ // TODO: Add some guessing of Type if not declared
+ if inputType == "" {
+ return frames, fmt.Errorf("no input dataframe type set")
+ }
+
+ if !supportedToLongConversion(inputType) {
+ return frames, fmt.Errorf("unsupported input dataframe type %s for SQL expression", inputType)
+ }
+
+ toLong := getToLongConversionFunc(inputType)
+ if toLong == nil {
+ return frames, fmt.Errorf("could not get conversion function for input type %s", inputType)
+ }
+
+ return toLong(frames)
+}
+
+func convertNumericMultiToNumericLong(frames data.Frames) (data.Frames, error) {
+ // Apart from metadata, NumericMulti is basically NumericWide, except one frame per thing
+ // so we collapse into wide and call the wide conversion
+ wide := convertNumericMultiToNumericWide(frames)
+ return convertNumericWideToNumericLong(wide)
+}
+
+func convertNumericMultiToNumericWide(frames data.Frames) data.Frames {
+ newFrame := data.NewFrame("")
+ for _, frame := range frames {
+ for _, field := range frame.Fields {
+ if !field.Type().Numeric() {
+ continue
+ }
+ newField := data.NewFieldFromFieldType(field.Type(), field.Len())
+ newField.Name = field.Name
+ newField.Labels = field.Labels.Copy()
+ if field.Len() == 1 {
+ newField.Set(0, field.CopyAt(0))
+ }
+ newFrame.Fields = append(newFrame.Fields, newField)
+ }
+ }
+ return data.Frames{newFrame}
+}
+
+func convertNumericWideToNumericLong(frames data.Frames) (data.Frames, error) {
+ // Wide should only be one frame
+ if len(frames) != 1 {
+ return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames))
+ }
+ inputFrame := frames[0]
+
+ // The Frame should have no more than one row
+ if inputFrame.Rows() > 1 {
+ return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows())
+ }
+
+ // Gather:
+ // - unique numeric Field Names, and
+ // - unique Label Keys (from Numeric Fields only)
+ // each one maps to a field in the output long Frame.
+ uniqueNames := make([]string, 0)
+ uniqueKeys := make([]string, 0)
+
+ uniqueNamesMap := make(map[string]data.FieldType)
+ uniqueKeysMap := make(map[string]struct{})
+
+ prints := make(map[string]int)
+
+ registerPrint := func(labels data.Labels) {
+ fp := labels.Fingerprint().String()
+ if _, ok := prints[fp]; !ok {
+ prints[fp] = len(prints)
+ }
+ }
+
+ for _, field := range inputFrame.Fields {
+ if field.Type().Numeric() {
+ if _, ok := uniqueNamesMap[field.Name]; !ok {
+ uniqueNames = append(uniqueNames, field.Name)
+ uniqueNamesMap[field.Name] = field.Type()
+ }
+
+ if field.Labels != nil {
+ registerPrint(field.Labels)
+ for key := range field.Labels {
+ if _, ok := uniqueKeysMap[key]; !ok {
+ uniqueKeys = append(uniqueKeys, key)
+ }
+ uniqueKeysMap[key] = struct{}{}
+ }
+ }
+ }
+ }
+
+ // Create new fields for output Long frame
+ fields := make([]*data.Field, 0, len(uniqueNames)+len(uniqueKeys))
+
+ // Create the Numeric Fields, tracking the index of each field by name
+ // Note: May want to use FloatAt and and prepopulate with NaN so missing
+ // combinations of value can be NA instead of the zero value of 0.
+ var nameIndexMap = make(map[string]int, len(uniqueNames))
+ for i, name := range uniqueNames {
+ field := data.NewFieldFromFieldType(uniqueNamesMap[name], len(prints))
+ field.Name = name
+ fields = append(fields, field)
+ nameIndexMap[name] = i
+ }
+
+ // Create the String fields, tracking the index of each field by key
+ var keyIndexMap = make(map[string]int, len(uniqueKeys))
+ for i, k := range uniqueKeys {
+ fields = append(fields, data.NewField(k, nil, make([]string, len(prints))))
+ keyIndexMap[k] = len(nameIndexMap) + i
+ }
+
+ longFrame := data.NewFrame("", fields...)
+
+ if inputFrame.Rows() == 0 {
+ return data.Frames{longFrame}, nil
+ }
+
+ // Add Rows to the fields
+ for _, field := range inputFrame.Fields {
+ if !field.Type().Numeric() {
+ continue
+ }
+ fieldIdx := prints[field.Labels.Fingerprint().String()]
+ longFrame.Fields[nameIndexMap[field.Name]].Set(fieldIdx, field.CopyAt(0))
+ for key, value := range field.Labels {
+ longFrame.Fields[keyIndexMap[key]].Set(fieldIdx, value)
+ }
+ }
+
+ return data.Frames{longFrame}, nil
+}
+
+func convertTimeSeriesMultiToTimeSeriesLong(frames data.Frames) (data.Frames, error) {
+ // Collect all time values and ensure no duplicates
+ timeSet := make(map[time.Time]struct{})
+ labelKeys := make(map[string]struct{}) // Collect all unique label keys
+ numericFields := make(map[string]struct{}) // Collect unique numeric field names
+
+ for _, frame := range frames {
+ for _, field := range frame.Fields {
+ if field.Type() == data.FieldTypeTime {
+ for i := 0; i < field.Len(); i++ {
+ t := field.At(i).(time.Time)
+ timeSet[t] = struct{}{}
+ }
+ } else if field.Type().Numeric() {
+ numericFields[field.Name] = struct{}{}
+ if field.Labels != nil {
+ for key := range field.Labels {
+ labelKeys[key] = struct{}{}
+ }
+ }
+ }
+ }
+ }
+
+ // Create a sorted slice of unique time values
+ times := make([]time.Time, 0, len(timeSet))
+ for t := range timeSet {
+ times = append(times, t)
+ }
+ sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
+
+ // Create output fields: Time, one numeric field per unique numeric name, and label fields
+ timeField := data.NewField("Time", nil, times)
+ outputNumericFields := make(map[string]*data.Field)
+ for name := range numericFields {
+ outputNumericFields[name] = data.NewField(name, nil, make([]float64, len(times)))
+ }
+ outputLabelFields := make(map[string]*data.Field)
+ for key := range labelKeys {
+ outputLabelFields[key] = data.NewField(key, nil, make([]string, len(times)))
+ }
+
+ // Map time to index for quick lookup
+ timeIndexMap := make(map[time.Time]int, len(times))
+ for i, t := range times {
+ timeIndexMap[t] = i
+ }
+
+ // Populate output fields
+ for _, frame := range frames {
+ var timeField *data.Field
+ for _, field := range frame.Fields {
+ if field.Type() == data.FieldTypeTime {
+ timeField = field
+ break
+ }
+ }
+
+ if timeField == nil {
+ return nil, fmt.Errorf("no time field found in frame")
+ }
+
+ for _, field := range frame.Fields {
+ if field.Type().Numeric() {
+ for i := 0; i < field.Len(); i++ {
+ t := timeField.At(i).(time.Time)
+ val, err := field.FloatAt(i)
+ if err != nil {
+ val = 0 // Default value for missing data
+ }
+ idx := timeIndexMap[t]
+ if outputField, exists := outputNumericFields[field.Name]; exists {
+ outputField.Set(idx, val)
+ }
+
+ // Add labels for the numeric field
+ for key, value := range field.Labels {
+ if outputField, exists := outputLabelFields[key]; exists {
+ outputField.Set(idx, value)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Build the output frame
+ outputFields := []*data.Field{timeField}
+ for _, field := range outputNumericFields {
+ outputFields = append(outputFields, field)
+ }
+ for _, field := range outputLabelFields {
+ outputFields = append(outputFields, field)
+ }
+ outputFrame := data.NewFrame("time_series_long", outputFields...)
+
+ // Set metadata
+ if outputFrame.Meta == nil {
+ outputFrame.Meta = &data.FrameMeta{}
+ }
+ outputFrame.Meta.Type = data.FrameTypeTimeSeriesLong
+
+ return data.Frames{outputFrame}, nil
+}
+
+func convertTimeSeriesWideToTimeSeriesLong(frames data.Frames) (data.Frames, error) {
+ // Wide should only be one frame
+ if len(frames) != 1 {
+ return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames))
+ }
+ inputFrame := frames[0]
+ longFrame, err := data.WideToLong(inputFrame)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert wide time series to long timeseries for sql expression: %w", err)
+ }
+ return data.Frames{longFrame}, nil
+}
+
+func getToLongConversionFunc(inputType data.FrameType) func(data.Frames) (data.Frames, error) {
+ switch inputType {
+ case data.FrameTypeNumericMulti:
+ return convertNumericMultiToNumericLong
+ case data.FrameTypeNumericWide:
+ return convertNumericWideToNumericLong
+ case data.FrameTypeTimeSeriesMulti:
+ return convertTimeSeriesMultiToTimeSeriesLong
+ case data.FrameTypeTimeSeriesWide:
+ return convertTimeSeriesWideToTimeSeriesLong
+ default:
+ return convertErr
+ }
+}
+
+func convertErr(_ data.Frames) (data.Frames, error) {
+ return nil, fmt.Errorf("unsupported input type for SQL expression")
+}
+
+func supportedToLongConversion(inputType data.FrameType) bool {
+ switch inputType {
+ case data.FrameTypeNumericMulti, data.FrameTypeNumericWide:
+ return true
+ case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide:
+ return true
+ default:
+ return false
+ }
+}
diff --git a/pkg/expr/convert_to_long_test.go b/pkg/expr/convert_to_long_test.go
new file mode 100644
index 00000000000..291fdb62f17
--- /dev/null
+++ b/pkg/expr/convert_to_long_test.go
@@ -0,0 +1,48 @@
+package expr
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/stretchr/testify/require"
+)
+
+func TestConvertNumericMultiToLong(t *testing.T) {
+ input := data.Frames{
+ data.NewFrame("test",
+ data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5})),
+ data.NewFrame("test",
+ data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}),
+ ),
+ }
+ expectedFrame := data.NewFrame("",
+ data.NewField("Value", nil, []int64{5, 7}),
+ data.NewField("city", nil, []string{"MIA", "LGA"}),
+ )
+ output, err := convertNumericMultiToNumericLong(input)
+ require.NoError(t, err)
+
+ if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" {
+ require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
+ }
+}
+
+func TestConvertNumericWideToLong(t *testing.T) {
+ input := data.Frames{
+ data.NewFrame("test",
+ data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5}),
+ data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}),
+ ),
+ }
+ expectedFrame := data.NewFrame("",
+ data.NewField("Value", nil, []int64{5, 7}),
+ data.NewField("city", nil, []string{"MIA", "LGA"}),
+ )
+ output, err := convertNumericWideToNumericLong(input)
+ require.NoError(t, err)
+
+ if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" {
+ require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
+ }
+}
diff --git a/pkg/expr/converter.go b/pkg/expr/converter.go
index 1eae19a972d..0e8cc461d47 100644
--- a/pkg/expr/converter.go
+++ b/pkg/expr/converter.go
@@ -23,7 +23,6 @@ type ResultConverter struct {
func (c *ResultConverter) Convert(ctx context.Context,
datasourceType string,
frames data.Frames,
- allowLongFrames bool,
) (string, mathexp.Results, error) {
if len(frames) == 0 {
return "no-data", mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil
@@ -80,7 +79,7 @@ func (c *ResultConverter) Convert(ctx context.Context,
continue
}
- if schema.Type != data.TimeSeriesTypeWide && !allowLongFrames {
+ if schema.Type != data.TimeSeriesTypeWide {
return "", mathexp.Results{}, fmt.Errorf("%w but got type %s (input refid)", ErrSeriesMustBeWide, schema.Type)
}
filtered = append(filtered, frame)
diff --git a/pkg/expr/converter_test.go b/pkg/expr/converter_test.go
index c2a8a445247..1509294e3de 100644
--- a/pkg/expr/converter_test.go
+++ b/pkg/expr/converter_test.go
@@ -40,7 +40,7 @@ func TestConvertDataFramesToResults(t *testing.T) {
for _, dtype := range supported {
t.Run(dtype, func(t *testing.T) {
- resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames)
+ resultType, res, err := converter.Convert(context.Background(), dtype, frames)
require.NoError(t, err)
assert.Equal(t, "single frame series", resultType)
require.Len(t, res.Values, 2)
@@ -68,7 +68,7 @@ func TestConvertDataFramesToResults(t *testing.T) {
for _, dtype := range supported {
t.Run(dtype, func(t *testing.T) {
- resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames)
+ resultType, res, err := converter.Convert(context.Background(), dtype, frames)
require.NoError(t, err)
assert.Equal(t, "multi frame series", resultType)
require.Len(t, res.Values, 2)
@@ -101,7 +101,7 @@ func TestConvertDataFramesToResults(t *testing.T) {
for _, dtype := range supported {
t.Run(dtype, func(t *testing.T) {
- resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames)
+ resultType, res, err := converter.Convert(context.Background(), dtype, frames)
require.NoError(t, err)
assert.Equal(t, "multi frame series", resultType)
require.Len(t, res.Values, 2)
diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go
index 369ff116a94..6632a6b74c1 100644
--- a/pkg/expr/graph.go
+++ b/pkg/expr/graph.go
@@ -77,8 +77,6 @@ func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (m
executeDSNodesGrouped(c, now, vars, s, dsNodes)
}
- s.allowLongFrames = hasSqlExpression(*dp)
-
for _, node := range *dp {
if groupByDSFlag && node.NodeType() == TypeDatasourceNode {
continue // already executed via executeDSNodesGrouped
@@ -321,12 +319,26 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
neededNode, ok := registry[neededVar]
if !ok {
_, ok := cmdNode.Command.(*SQLCommand)
+ // If the SSE is a SQL expression, and the node can't be found, it might be a CTE table name
+ // CTEs are calculated during the evaluation of the SQL, so we won't have a node for them
+ // So we `continue` in order to support CTE functionality
+ // TODO: remove CTE table names from the list of table names during parsing of the SQL
if ok {
continue
}
return fmt.Errorf("unable to find dependent node '%v'", neededVar)
}
+ // If the input is SQL, conversion is handled differently
+ if _, ok := cmdNode.Command.(*SQLCommand); ok {
+ if dsNode, ok := neededNode.(*DSNode); ok {
+ dsNode.isInputToSQLExpr = true
+ } else {
+ // Only allow data source nodes as SQL expression inputs for now
+ return fmt.Errorf("only data source queries may be inputs to a sql expression, %v is the input for %v", neededVar, cmdNode.RefID())
+ }
+ }
+
if neededNode.ID() == cmdNode.ID() {
return fmt.Errorf("expression '%v' cannot reference itself. Must be query or another expression", neededVar)
}
@@ -343,6 +355,13 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
}
}
+ if neededNode.NodeType() == TypeCMDNode {
+ if neededNode.(*CMDNode).CMDType == TypeSQL {
+ // Do not allow SQL expressions to be inputs for other expressions for now
+ return fmt.Errorf("sql expressions can not be the input for other expressions, but %v in the input for %v", neededVar, cmdNode.RefID())
+ }
+ }
+
edge := dp.NewEdge(neededNode, cmdNode)
dp.SetEdge(edge)
@@ -370,37 +389,3 @@ func GetCommandsFromPipeline[T Command](pipeline DataPipeline) []T {
}
return results
}
-
-func hasSqlExpression(dp DataPipeline) bool {
- for _, node := range dp {
- if node.NodeType() == TypeCMDNode {
- cmdNode := node.(*CMDNode)
- _, ok := cmdNode.Command.(*SQLCommand)
- if ok {
- return true
- }
- }
- }
- return false
-}
-
-// func graphHasSqlExpresssion(dp *simple.DirectedGraph) bool {
-// node := dp.Nodes()
-// for node.Next() {
-// if cmdNode, ok := node.Node().(*CMDNode); ok {
-// // res[dpNode.RefID()] = dpNode
-// _, ok := cmdNode.Command.(*SQLCommand)
-// if ok {
-// return true
-// }
-// }
-// // if node.NodeType() == TypeCMDNode {
-// // cmdNode := node.(*CMDNode)
-// // _, ok := cmdNode.Command.(*SQLCommand)
-// // if ok {
-// // return true
-// // }
-// // }
-// }
-// return false
-// }
diff --git a/pkg/expr/mathexp/types.go b/pkg/expr/mathexp/types.go
index 8317f97c5bd..fd79061af82 100644
--- a/pkg/expr/mathexp/types.go
+++ b/pkg/expr/mathexp/types.go
@@ -250,7 +250,7 @@ func NewNoData() NoData {
return NoData{data.NewFrame("no data")}
}
-// TableData is an untyped no data response.
+// TableData is a single table data frame with no labels on any fields.
type TableData struct{ Frame *data.Frame }
// Type returns the Value type and allows it to fulfill the Value interface.
diff --git a/pkg/expr/ml.go b/pkg/expr/ml.go
index e7bde5487f2..4c0dba76b89 100644
--- a/pkg/expr/ml.go
+++ b/pkg/expr/ml.go
@@ -130,7 +130,7 @@ func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s *
}
// process the response the same way DSNode does. Use plugin ID as data source type. Semantically, they are the same.
- responseType, result, err = s.converter.Convert(ctx, mlPluginID, dataFrames, s.allowLongFrames)
+ responseType, result, err = s.converter.Convert(ctx, mlPluginID, dataFrames)
return result, err
}
diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go
index 80de28e9883..1159ef13d04 100644
--- a/pkg/expr/nodes.go
+++ b/pkg/expr/nodes.go
@@ -112,6 +112,12 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er
return nil, fmt.Errorf("invalid command type in expression '%v': %w", rn.RefID, err)
}
+ if commandType == TypeSQL {
+ if !toggles.IsEnabledGlobally(featuremgmt.FlagSqlExpressions) {
+ return nil, fmt.Errorf("sql expressions are disabled")
+ }
+ }
+
node := &CMDNode{
baseNode: baseNode{
id: rn.idx,
@@ -185,6 +191,8 @@ type DSNode struct {
intervalMS int64
maxDP int64
request Request
+
+ isInputToSQLExpr bool
}
func (dn *DSNode) String() string {
@@ -333,7 +341,7 @@ func executeDSNodesGrouped(ctx context.Context, now time.Time, vars mathexp.Vars
}
var result mathexp.Results
- responseType, result, err := s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames)
+ responseType, result, err := s.converter.Convert(ctx, dn.datasource.Type, dataFrames)
if err != nil {
result.Error = makeConversionError(dn.RefID(), err)
}
@@ -401,7 +409,44 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s
}
var result mathexp.Results
- responseType, result, err = s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames)
+ // If the datasource node is an input to a SQL expression,
+ // the data must be in the Long format
+ if dn.isInputToSQLExpr {
+ var needsConversion bool
+ // Convert it if Multi:
+ if len(dataFrames) > 1 {
+ needsConversion = true
+ }
+
+ // Convert it if Wide (has labels):
+ if len(dataFrames) == 1 {
+ for _, field := range dataFrames[0].Fields {
+ if len(field.Labels) > 0 {
+ needsConversion = true
+ break
+ }
+ }
+ }
+
+ if needsConversion {
+ convertedFrames, err := ConvertToLong(dataFrames)
+ if err != nil {
+ return result, fmt.Errorf("failed to convert data frames to long format for sql: %w", err)
+ }
+ result.Values = mathexp.Values{
+ mathexp.TableData{Frame: convertedFrames[0]},
+ }
+ return result, nil
+ }
+
+ // Otherwise it is already Long format; return as is
+ result.Values = mathexp.Values{
+ mathexp.TableData{Frame: dataFrames[0]},
+ }
+ return result, nil
+ }
+
+ responseType, result, err = s.converter.Convert(ctx, dn.datasource.Type, dataFrames)
if err != nil {
err = makeConversionError(dn.refID, err)
}
diff --git a/pkg/expr/service.go b/pkg/expr/service.go
index 1f78491d9e3..1b04ce41c3f 100644
--- a/pkg/expr/service.go
+++ b/pkg/expr/service.go
@@ -64,9 +64,8 @@ type Service struct {
pluginsClient backend.CallResourceHandler
- tracer tracing.Tracer
- metrics *metrics
- allowLongFrames bool
+ tracer tracing.Tracer
+ metrics *metrics
}
type pluginContextProvider interface {
diff --git a/pkg/expr/service_sql_test.go b/pkg/expr/service_sql_test.go
new file mode 100644
index 00000000000..5730ca82fd0
--- /dev/null
+++ b/pkg/expr/service_sql_test.go
@@ -0,0 +1,104 @@
+package expr
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/grafana/grafana-plugin-sdk-go/backend"
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/grafana/grafana/pkg/services/datasources"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSQLService(t *testing.T) {
+ inputFrame := data.NewFrame("",
+ data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
+ data.NewField("value", nil, []*float64{fp(2)}),
+ )
+
+ resp := map[string]backend.DataResponse{
+ "A": {Frames: data.Frames{inputFrame}},
+ }
+
+ newABSQLQueries := func(q string) []Query {
+ q, err := jsonEscape(q)
+ require.NoError(t, err)
+ return []Query{
+ {
+ RefID: "A",
+ DataSource: &datasources.DataSource{
+ OrgID: 1,
+ UID: "test",
+ Type: "test",
+ },
+ JSON: json.RawMessage(`{ "datasource": { "uid": "1" }, "intervalMs": 1000, "maxDataPoints": 1000 }`),
+ TimeRange: AbsoluteTimeRange{
+ From: time.Time{},
+ To: time.Time{},
+ },
+ },
+ {
+ RefID: "B",
+ DataSource: dataSourceModel(),
+ JSON: json.RawMessage(fmt.Sprintf(`{ "datasource": { "uid": "__expr__", "type": "__expr__"}, "type": "sql", "expression": "%s" }`, q)),
+ TimeRange: AbsoluteTimeRange{
+ From: time.Time{},
+ To: time.Time{},
+ },
+ },
+ }
+ }
+ t.Run("no feature flag no queries for you", func(t *testing.T) {
+ s, req := newMockQueryService(resp, newABSQLQueries(""))
+
+ _, err := s.BuildPipeline(req)
+ require.Error(t, err, "should not be able to build pipeline without feature flag")
+ })
+
+ t.Run("with feature flag basic select works", func(t *testing.T) {
+ s, req := newMockQueryService(resp, newABSQLQueries("SELECT * FROM A"))
+ s.features = featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions)
+ pl, err := s.BuildPipeline(req)
+ require.NoError(t, err)
+
+ res, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
+ require.NoError(t, err)
+
+ inputFrame.RefID = "B"
+ inputFrame.Name = "B"
+ if diff := cmp.Diff(res.Responses["B"].Frames[0], inputFrame, data.FrameTestCompareOptions()...); diff != "" {
+ require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
+ }
+ })
+
+ t.Run("load_file is blocked", func(t *testing.T) {
+ s, req := newMockQueryService(resp,
+ newABSQLQueries(`SELECT CAST(load_file('/etc/topSecretz') AS CHAR(10000) CHARACTER SET utf8)`),
+ )
+
+ s.features = featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions)
+
+ pl, err := s.BuildPipeline(req)
+ require.NoError(t, err)
+
+ rsp, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
+ require.NoError(t, err)
+
+ require.Error(t, rsp.Responses["B"].Error, "should return invalid sql error")
+ require.ErrorContains(t, rsp.Responses["B"].Error, "blocked function load_file")
+ })
+}
+
+func jsonEscape(input string) (string, error) {
+ escaped, err := json.Marshal(input)
+ if err != nil {
+ return "", err
+ }
+ // json.Marshal returns the escaped string with quotes, so we need to trim them
+ return string(escaped[1 : len(escaped)-1]), nil
+}
diff --git a/pkg/expr/service_test.go b/pkg/expr/service_test.go
index 35378b289e8..2fe6f6e1e9c 100644
--- a/pkg/expr/service_test.go
+++ b/pkg/expr/service_test.go
@@ -29,32 +29,11 @@ import (
func TestService(t *testing.T) {
dsDF := data.NewFrame("test",
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
- data.NewField("value", data.Labels{"test": "label"}, []*float64{fp(2)}))
+ data.NewField("value", data.Labels{"test": "label"}, []*float64{fp(2)}),
+ )
- me := &mockEndpoint{
- Responses: map[string]backend.DataResponse{
- "A": {Frames: data.Frames{dsDF}},
- },
- }
-
- pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{
- PluginList: []pluginstore.Plugin{
- {JSONData: plugins.JSONData{ID: "test"}},
- },
- }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider())
-
- features := featuremgmt.WithFeatures()
- s := Service{
- cfg: setting.NewCfg(),
- dataService: me,
- pCtxProvider: pCtxProvider,
- features: features,
- tracer: tracing.InitializeTracerForTest(),
- metrics: newMetrics(nil),
- converter: &ResultConverter{
- Features: features,
- Tracer: tracing.InitializeTracerForTest(),
- },
+ resp := map[string]backend.DataResponse{
+ "A": {Frames: data.Frames{dsDF}},
}
queries := []Query{
@@ -78,7 +57,7 @@ func TestService(t *testing.T) {
},
}
- req := &Request{Queries: queries, User: &user.SignedInUser{}}
+ s, req := newMockQueryService(resp, queries)
pl, err := s.BuildPipeline(req)
require.NoError(t, err)
@@ -121,26 +100,9 @@ func TestService(t *testing.T) {
}
func TestDSQueryError(t *testing.T) {
- me := &mockEndpoint{
- Responses: map[string]backend.DataResponse{
- "A": {Error: fmt.Errorf("womp womp")},
- "B": {Frames: data.Frames{}},
- },
- }
-
- pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{
- PluginList: []pluginstore.Plugin{
- {JSONData: plugins.JSONData{ID: "test"}},
- },
- }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider())
-
- s := Service{
- cfg: setting.NewCfg(),
- dataService: me,
- pCtxProvider: pCtxProvider,
- features: featuremgmt.WithFeatures(),
- tracer: tracing.InitializeTracerForTest(),
- metrics: newMetrics(nil),
+ resp := map[string]backend.DataResponse{
+ "A": {Error: fmt.Errorf("womp womp")},
+ "B": {Frames: data.Frames{}},
}
queries := []Query{
@@ -169,19 +131,19 @@ func TestDSQueryError(t *testing.T) {
},
}
- req := &Request{Queries: queries, User: &user.SignedInUser{}}
+ s, req := newMockQueryService(resp, queries)
pl, err := s.BuildPipeline(req)
require.NoError(t, err)
- resp, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
+ res, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
require.NoError(t, err)
var utilErr errutil.Error
- require.ErrorContains(t, resp.Responses["A"].Error, "womp womp")
- require.ErrorAs(t, resp.Responses["B"].Error, &utilErr)
+ require.ErrorContains(t, res.Responses["A"].Error, "womp womp")
+ require.ErrorAs(t, res.Responses["B"].Error, &utilErr)
require.ErrorIs(t, utilErr, DependencyError)
- require.Equal(t, fp(42), resp.Responses["C"].Frames[0].Fields[0].At(0))
+ require.Equal(t, fp(42), res.Responses["C"].Frames[0].Fields[0].At(0))
}
func fp(f float64) *float64 {
@@ -204,3 +166,28 @@ func dataSourceModel() *datasources.DataSource {
d, _ := DataSourceModelFromNodeType(TypeCMDNode)
return d
}
+
+func newMockQueryService(responses map[string]backend.DataResponse, queries []Query) (*Service, *Request) {
+ me := &mockEndpoint{
+ Responses: responses,
+ }
+ pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{
+ PluginList: []pluginstore.Plugin{
+ {JSONData: plugins.JSONData{ID: "test"}},
+ },
+ }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider())
+
+ features := featuremgmt.WithFeatures()
+ return &Service{
+ cfg: setting.NewCfg(),
+ dataService: me,
+ pCtxProvider: pCtxProvider,
+ features: featuremgmt.WithFeatures(),
+ tracer: tracing.InitializeTracerForTest(),
+ metrics: newMetrics(nil),
+ converter: &ResultConverter{
+ Features: features,
+ Tracer: tracing.InitializeTracerForTest(),
+ },
+ }, &Request{Queries: queries, User: &user.SignedInUser{}}
+}
diff --git a/pkg/expr/sql/db.go b/pkg/expr/sql/db.go
index 90e23342c4b..f1af425ad6d 100644
--- a/pkg/expr/sql/db.go
+++ b/pkg/expr/sql/db.go
@@ -1,22 +1,61 @@
+//go:build !arm
+
package sql
import (
- "errors"
+ "context"
+ sqle "github.com/dolthub/go-mysql-server"
+ mysql "github.com/dolthub/go-mysql-server/sql"
+
+ "github.com/dolthub/go-mysql-server/sql/analyzer"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
-type DB struct {
-}
+// DB is a database that can execute SQL queries against a set of Frames.
+type DB struct{}
-func (db *DB) RunCommands(commands []string) (string, error) {
- return "", errors.New("not implemented")
-}
+// QueryFrames runs the sql query query against a database created from frames, and returns the frame.
+// The RefID of each frame becomes a table in the database.
+// It is expected that there is only one frame per RefID.
+// The name becomes the name and RefID of the returned frame.
+func (db *DB) QueryFrames(ctx context.Context, name string, query string, frames []*data.Frame) (*data.Frame, error) {
+ // We are parsing twice due to TablesList, but don't care fow now. We can save the parsed query and reuse it later if we want.
+ if allow, err := AllowQuery(query); err != nil || !allow {
+ if err != nil {
+ return nil, err
+ }
+ return nil, err
+ }
-func (db *DB) QueryFramesInto(name string, query string, frames []*data.Frame, f *data.Frame) error {
- return errors.New("not implemented")
-}
+ pro := NewFramesDBProvider(frames)
+ session := mysql.NewBaseSession()
+ mCtx := mysql.NewContext(ctx, mysql.WithSession(session))
-func NewInMemoryDB() *DB {
- return &DB{}
+ // Select the database in the context
+ mCtx.SetCurrentDatabase(dbName)
+
+ // Empty dir does not disable secure_file_priv
+ //ctx.SetSessionVariable(ctx, "secure_file_priv", "")
+
+ // TODO: Check if it's wise to reuse the existing provider, rather than creating a new one
+ a := analyzer.NewDefault(pro)
+
+ engine := sqle.New(a, &sqle.Config{
+ IsReadOnly: true,
+ })
+
+ schema, iter, _, err := engine.Query(mCtx, query)
+ if err != nil {
+ return nil, err
+ }
+
+ f, err := convertToDataFrame(mCtx, iter, schema)
+ if err != nil {
+ return nil, err
+ }
+ f.Name = name
+ f.RefID = name
+
+ return f, nil
}
diff --git a/pkg/expr/sql/db_test.go b/pkg/expr/sql/db_test.go
new file mode 100644
index 00000000000..258507121c2
--- /dev/null
+++ b/pkg/expr/sql/db_test.go
@@ -0,0 +1,187 @@
+//go:build !arm
+
+package sql
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/stretchr/testify/require"
+)
+
+func TestQueryFrames(t *testing.T) {
+ db := DB{}
+
+ tests := []struct {
+ name string
+ query string
+ input_frames []*data.Frame
+ expected *data.Frame
+ }{
+ {
+ name: "valid query with no input frames, one row one column",
+ query: `SELECT '1' AS 'n';`,
+ input_frames: []*data.Frame{},
+ expected: data.NewFrame(
+ "sqlExpressionRefId",
+ data.NewField("n", nil, []string{"1"}),
+ ),
+ },
+ {
+ name: "valid query with no input frames, one row two columns",
+ query: `SELECT 'sam' AS 'name', 40 AS 'age';`,
+ input_frames: []*data.Frame{},
+ expected: data.NewFrame(
+ "sqlExpressionRefId",
+ data.NewField("name", nil, []string{"sam"}),
+ data.NewField("age", nil, []int8{40}),
+ ),
+ },
+ {
+ // TODO: Also ORDER BY to ensure the order is preserved
+ name: "query all rows from single input frame",
+ query: `SELECT * FROM inputFrameRefId LIMIT 1;`,
+ input_frames: []*data.Frame{
+ setRefID(data.NewFrame(
+ "",
+ //nolint:misspell
+ data.NewField("OSS Projects with Typos", nil, []string{"Garfana", "Pormetheus"}),
+ ), "inputFrameRefId"),
+ },
+ expected: data.NewFrame(
+ "sqlExpressionRefId",
+ data.NewField("OSS Projects with Typos", nil, []string{"Garfana"}),
+ ),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ frame, err := db.QueryFrames(context.Background(), "sqlExpressionRefId", tt.query, tt.input_frames)
+ require.NoError(t, err)
+ require.NotNil(t, frame.Fields)
+
+ require.Equal(t, tt.expected.Name, frame.RefID)
+ require.Equal(t, len(tt.expected.Fields), len(frame.Fields))
+ for i := range tt.expected.Fields {
+ require.Equal(t, tt.expected.Fields[i].Name, frame.Fields[i].Name)
+ require.Equal(t, tt.expected.Fields[i].At(0), frame.Fields[i].At(0))
+ }
+ })
+ }
+}
+
+func TestQueryFramesInOut(t *testing.T) {
+ frameA := &data.Frame{
+ RefID: "a",
+ Name: "a",
+ Fields: []*data.Field{
+ data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
+ data.NewField("time_nullable", nil, []*time.Time{p(time.Now()), nil}),
+
+ data.NewField("string", nil, []string{"cat", "dog"}),
+ data.NewField("null_nullable", nil, []*string{p("cat"), nil}),
+
+ data.NewField("float64", nil, []float64{1, 3}),
+ data.NewField("float64_nullable", nil, []*float64{p(2.0), nil}),
+
+ data.NewField("int64", nil, []int64{1, 3}),
+ data.NewField("int64_nullable", nil, []*int64{p(int64(2)), nil}),
+
+ data.NewField("bool", nil, []bool{true, false}),
+ data.NewField("bool_nullable", nil, []*bool{p(true), nil}),
+ },
+ }
+
+ db := DB{}
+ qry := `SELECT * from a`
+
+ resultFrame, err := db.QueryFrames(context.Background(), "a", qry, []*data.Frame{frameA})
+ require.NoError(t, err)
+
+ if diff := cmp.Diff(frameA, resultFrame, data.FrameTestCompareOptions()...); diff != "" {
+ require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
+ }
+}
+
+func TestQueryFramesNumericSelect(t *testing.T) {
+ expectedFrame := &data.Frame{
+ RefID: "a",
+ Name: "a",
+ Fields: []*data.Field{
+ data.NewField("decimal", nil, []float64{2.35}),
+ data.NewField("tinySigned", nil, []int8{-128}),
+ data.NewField("smallSigned", nil, []int16{-32768}),
+ data.NewField("mediumSigned", nil, []int32{-8388608}),
+ data.NewField("intSigned", nil, []int32{-2147483648}),
+ data.NewField("bigSigned", nil, []int64{-9223372036854775808}),
+ data.NewField("tinyUnsigned", nil, []uint8{255}),
+ data.NewField("smallUnsigned", nil, []uint16{65535}),
+ data.NewField("mediumUnsigned", nil, []int32{16777215}),
+ data.NewField("intUnsigned", nil, []uint32{4294967295}),
+ data.NewField("bigUnsigned", nil, []uint64{18446744073709551615}),
+ },
+ }
+
+ db := DB{}
+ qry := `SELECT 2.35 AS 'decimal',
+ -128 AS 'tinySigned',
+ -32768 AS 'smallSigned',
+ -8388608 AS 'mediumSigned',
+ -2147483648 AS 'intSigned',
+ -9223372036854775808 AS 'bigSigned',
+ 255 AS 'tinyUnsigned',
+ 65535 AS 'smallUnsigned',
+ 16777215 AS 'mediumUnsigned',
+ 4294967295 AS 'intUnsigned',
+ 18446744073709551615 AS 'bigUnsigned'`
+
+ resultFrame, err := db.QueryFrames(context.Background(), "a", qry, []*data.Frame{})
+ require.NoError(t, err)
+
+ if diff := cmp.Diff(expectedFrame, resultFrame, data.FrameTestCompareOptions()...); diff != "" {
+ require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
+ }
+}
+
+func TestQueryFramesDateTimeSelect(t *testing.T) {
+ t.Skip("need a fix in go-mysql-server, and then handle the datetime strings (or figure out why strings and not time.Time)")
+ expectedFrame := &data.Frame{
+ RefID: "a",
+ Name: "a",
+ Fields: []*data.Field{
+ data.NewField("ts", nil, []time.Time{}),
+ },
+ }
+
+ db := DB{}
+
+ // It doesn't like the T in the time string
+ qry := `SELECT str_to_date('2025-02-03T03:00:00','%Y-%m-%dT%H:%i:%s') as ts`
+
+ // This comes back as a string, which needs to be dealt with?
+ //qry := `SELECT str_to_date('2025-02-03-03:00:00','%Y-%m-%d-%H:%i:%s') as ts`
+
+ // This is a datetime(6), need to deal with that as well
+ //qry := `SELECT current_timestamp() as ts`
+
+ f, err := db.QueryFrames(context.Background(), "b", qry, []*data.Frame{})
+ require.NoError(t, err)
+
+ if diff := cmp.Diff(expectedFrame, f, data.FrameTestCompareOptions()...); diff != "" {
+ require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
+ }
+}
+
+// p is a utility for pointers from constants
+func p[T any](v T) *T {
+ return &v
+}
+
+func setRefID(f *data.Frame, refID string) *data.Frame {
+ f.RefID = refID
+ return f
+}
diff --git a/pkg/expr/sql/dummy_arm.go b/pkg/expr/sql/dummy_arm.go
new file mode 100644
index 00000000000..95d20159e08
--- /dev/null
+++ b/pkg/expr/sql/dummy_arm.go
@@ -0,0 +1,18 @@
+//go:build arm
+
+package sql
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+type DB struct{}
+
+// Stub out the QueryFrames method for ARM builds
+// See github.com/dolthub/go-mysql-server/issues/2837
+func (db *DB) QueryFrames(_ context.Context, _, _ string, _ []*data.Frame) (*data.Frame, error) {
+ return nil, fmt.Errorf("sql expressions not supported in arm")
+}
diff --git a/pkg/expr/sql/frame_db.go b/pkg/expr/sql/frame_db.go
new file mode 100644
index 00000000000..26317594746
--- /dev/null
+++ b/pkg/expr/sql/frame_db.go
@@ -0,0 +1,65 @@
+//go:build !arm
+
+package sql
+
+import (
+ mysql "github.com/dolthub/go-mysql-server/sql"
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+var dbName = "frames"
+
+// FramesDBProvider is a go-mysql-server DatabaseProvider that provides access to a set of Frames.
+type FramesDBProvider struct {
+ db mysql.Database
+}
+
+func (p *FramesDBProvider) Database(_ *mysql.Context, _ string) (mysql.Database, error) {
+ return p.db, nil
+}
+
+func (p *FramesDBProvider) HasDatabase(_ *mysql.Context, _ string) bool {
+ return true
+}
+
+func (p *FramesDBProvider) AllDatabases(_ *mysql.Context) []mysql.Database {
+ return []mysql.Database{p.db}
+}
+
+// NewFramesDBProvider creates a new FramesDBProvider with the given set of Frames.
+func NewFramesDBProvider(frames data.Frames) mysql.DatabaseProvider {
+ fMap := make(map[string]mysql.Table, len(frames))
+ for _, frame := range frames {
+ fMap[frame.RefID] = &FrameTable{Frame: frame}
+ }
+ return &FramesDBProvider{
+ db: &framesDB{
+ frames: fMap,
+ },
+ }
+}
+
+// framesDB is a go-mysql-server Database that provides access to a set of Frames.
+type framesDB struct {
+ frames map[string]mysql.Table
+}
+
+func (db *framesDB) GetTableInsensitive(_ *mysql.Context, tblName string) (mysql.Table, bool, error) {
+ tbl, ok := mysql.GetTableInsensitive(tblName, db.frames)
+ if !ok {
+ return nil, false, nil
+ }
+ return tbl, ok, nil
+}
+
+func (db *framesDB) GetTableNames(_ *mysql.Context) ([]string, error) {
+ s := make([]string, 0, len(db.frames))
+ for k := range db.frames {
+ s = append(s, k)
+ }
+ return s, nil
+}
+
+func (db *framesDB) Name() string {
+ return dbName
+}
diff --git a/pkg/expr/sql/frame_db_conv.go b/pkg/expr/sql/frame_db_conv.go
new file mode 100644
index 00000000000..ce0edfc2341
--- /dev/null
+++ b/pkg/expr/sql/frame_db_conv.go
@@ -0,0 +1,474 @@
+//go:build !arm
+
+package sql
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "time"
+
+ mysql "github.com/dolthub/go-mysql-server/sql"
+ "github.com/dolthub/go-mysql-server/sql/types"
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/shopspring/decimal"
+)
+
+// TODO: Should this accept a row limit and converters, like sqlutil.FrameFromRows?
+func convertToDataFrame(ctx *mysql.Context, iter mysql.RowIter, schema mysql.Schema) (*data.Frame, error) {
+ f := &data.Frame{}
+ // Create fields based on the schema
+ for _, col := range schema {
+ fT, err := MySQLColToFieldType(col)
+ if err != nil {
+ return nil, err
+ }
+
+ field := data.NewFieldFromFieldType(fT, 0)
+ field.Name = col.Name
+ f.Fields = append(f.Fields, field)
+ }
+
+ // Iterate through the rows and append data to fields
+ for {
+ row, err := iter.Next(ctx)
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("error reading row: %v", err)
+ }
+
+ for i, val := range row {
+ v, err := fieldValFromRowVal(f.Fields[i].Type(), val)
+ if err != nil {
+ return nil, fmt.Errorf("unexpected type for column %s: %w", schema[i].Name, err)
+ }
+ f.Fields[i].Append(v)
+ }
+ }
+
+ return f, nil
+}
+
+// MySQLColToFieldType converts a MySQL column to a data.FieldType
+func MySQLColToFieldType(col *mysql.Column) (data.FieldType, error) {
+ var fT data.FieldType
+
+ switch col.Type {
+ case types.Int8:
+ fT = data.FieldTypeInt8
+ case types.Uint8:
+ fT = data.FieldTypeUint8
+ case types.Int16:
+ fT = data.FieldTypeInt16
+ case types.Uint16:
+ fT = data.FieldTypeUint16
+ case types.Int32:
+ fT = data.FieldTypeInt32
+ case types.Uint32:
+ fT = data.FieldTypeUint32
+ case types.Int64:
+ fT = data.FieldTypeInt64
+ case types.Uint64:
+ fT = data.FieldTypeUint64
+ case types.Float64:
+ fT = data.FieldTypeFloat64
+ // StringType represents all string types, including VARCHAR and BLOB.
+ case types.Text, types.LongText:
+ fT = data.FieldTypeString
+ case types.Timestamp:
+ fT = data.FieldTypeTime
+ case types.Datetime:
+ fT = data.FieldTypeTime
+ case types.Boolean:
+ fT = data.FieldTypeBool
+ default:
+ if types.IsDecimal(col.Type) {
+ fT = data.FieldTypeFloat64
+ } else {
+ return fT, fmt.Errorf("unsupported type for column %s of type %v", col.Name, col.Type)
+ }
+ }
+
+ if col.Nullable {
+ fT = fT.NullableType()
+ }
+
+ return fT, nil
+}
+
+// Helper function to convert data.FieldType to types.Type
+func convertDataType(fieldType data.FieldType) mysql.Type {
+ switch fieldType {
+ case data.FieldTypeInt8, data.FieldTypeNullableInt8:
+ return types.Int8
+ case data.FieldTypeUint8, data.FieldTypeNullableUint8:
+ return types.Uint8
+ case data.FieldTypeInt16, data.FieldTypeNullableInt16:
+ return types.Int16
+ case data.FieldTypeUint16, data.FieldTypeNullableUint16:
+ return types.Uint16
+ case data.FieldTypeInt32, data.FieldTypeNullableInt32:
+ return types.Int32
+ case data.FieldTypeUint32, data.FieldTypeNullableUint32:
+ return types.Uint32
+ case data.FieldTypeInt64, data.FieldTypeNullableInt64:
+ return types.Int64
+ case data.FieldTypeUint64, data.FieldTypeNullableUint64:
+ return types.Uint64
+ case data.FieldTypeFloat32, data.FieldTypeNullableFloat32:
+ return types.Float32
+ case data.FieldTypeFloat64, data.FieldTypeNullableFloat64:
+ return types.Float64
+ case data.FieldTypeString, data.FieldTypeNullableString:
+ return types.Text
+ case data.FieldTypeBool, data.FieldTypeNullableBool:
+ return types.Boolean
+ case data.FieldTypeTime, data.FieldTypeNullableTime:
+ return types.Timestamp
+ default:
+ fmt.Printf("------- Unsupported field type: %v", fieldType)
+ return types.JSON
+ }
+}
+
+// fieldValFromRowVal converts a go-mysql-server row value to a data.field value
+//
+//nolint:gocyclo
+func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{}, error) {
+ // the input val may be nil, it also may not be a pointer even if the fieldtype is a nullable pointer type
+ if val == nil {
+ return nil, nil
+ }
+
+ switch fieldType {
+ // ----------------------------
+ // Int8 / Nullable Int8
+ // ----------------------------
+ case data.FieldTypeInt8:
+ v, ok := val.(int8)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableInt8:
+ vP, ok := val.(*int8)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(int8)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8 or *int8", val, val)
+
+ // ----------------------------
+ // Uint8 / Nullable Uint8
+ // ----------------------------
+ case data.FieldTypeUint8:
+ v, ok := val.(uint8)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableUint8:
+ vP, ok := val.(*uint8)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(uint8)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8 or *uint8", val, val)
+
+ // ----------------------------
+ // Int16 / Nullable Int16
+ // ----------------------------
+ case data.FieldTypeInt16:
+ v, ok := val.(int16)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableInt16:
+ vP, ok := val.(*int16)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(int16)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16 or *int16", val, val)
+
+ // ----------------------------
+ // Uint16 / Nullable Uint16
+ // ----------------------------
+ case data.FieldTypeUint16:
+ v, ok := val.(uint16)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableUint16:
+ vP, ok := val.(*uint16)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(uint16)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16 or *uint16", val, val)
+
+ // ----------------------------
+ // Int32 / Nullable Int32
+ // ----------------------------
+ case data.FieldTypeInt32:
+ v, ok := val.(int32)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableInt32:
+ vP, ok := val.(*int32)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(int32)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32 or *int32", val, val)
+
+ // ----------------------------
+ // Uint32 / Nullable Uint32
+ // ----------------------------
+ case data.FieldTypeUint32:
+ v, ok := val.(uint32)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableUint32:
+ vP, ok := val.(*uint32)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(uint32)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32 or *uint32", val, val)
+
+ // ----------------------------
+ // Int64 / Nullable Int64
+ // ----------------------------
+ case data.FieldTypeInt64:
+ v, ok := val.(int64)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableInt64:
+ vP, ok := val.(*int64)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(int64)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64 or *int64", val, val)
+
+ // ----------------------------
+ // Uint64 / Nullable Uint64
+ // ----------------------------
+ case data.FieldTypeUint64:
+ v, ok := val.(uint64)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableUint64:
+ vP, ok := val.(*uint64)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(uint64)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64 or *uint64", val, val)
+
+ // ----------------------------
+ // Float64 / Nullable Float64
+ // ----------------------------
+ case data.FieldTypeFloat64:
+ // Accept float64 or decimal.Decimal, convert decimal.Decimal -> float64
+ if v, ok := val.(float64); ok {
+ return v, nil
+ }
+ if d, ok := val.(decimal.Decimal); ok {
+ return d.InexactFloat64(), nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64 or decimal.Decimal", val, val)
+
+ case data.FieldTypeNullableFloat64:
+ // Possibly already *float64
+ if vP, ok := val.(*float64); ok {
+ return vP, nil
+ }
+ // Possibly float64
+ if v, ok := val.(float64); ok {
+ return &v, nil
+ }
+ // Possibly decimal.Decimal
+ if d, ok := val.(decimal.Decimal); ok {
+ f := d.InexactFloat64()
+ return &f, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64, *float64, or decimal.Decimal", val, val)
+
+ // ----------------------------
+ // Time / Nullable Time
+ // ----------------------------
+ case data.FieldTypeTime:
+ v, ok := val.(time.Time)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableTime:
+ vP, ok := val.(*time.Time)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(time.Time)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time or *time.Time", val, val)
+
+ // ----------------------------
+ // String / Nullable String
+ // ----------------------------
+ case data.FieldTypeString:
+ v, ok := val.(string)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableString:
+ vP, ok := val.(*string)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(string)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string or *string", val, val)
+
+ // ----------------------------
+ // Bool / Nullable Bool
+ // ----------------------------
+ case data.FieldTypeBool:
+ v, ok := val.(bool)
+ if !ok {
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool", val, val)
+ }
+ return v, nil
+
+ case data.FieldTypeNullableBool:
+ vP, ok := val.(*bool)
+ if ok {
+ return vP, nil
+ }
+ v, ok := val.(bool)
+ if ok {
+ return &v, nil
+ }
+ return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool or *bool", val, val)
+
+ // ----------------------------
+ // Fallback / Unsupported
+ // ----------------------------
+ default:
+ return nil, fmt.Errorf("unsupported field type %s for val %v", fieldType, val)
+ }
+}
+
+// Is the field nilAt the index. Can panic if out of range.
+// TODO: Maybe this should be a method on data.Field?
+func nilAt(field data.Field, at int) bool {
+ if !field.Nullable() {
+ return false
+ }
+
+ switch field.Type() {
+ case data.FieldTypeNullableInt8:
+ v := field.At(at).(*int8)
+ return v == nil
+
+ case data.FieldTypeNullableUint8:
+ v := field.At(at).(*uint8)
+ return v == nil
+
+ case data.FieldTypeNullableInt16:
+ v := field.At(at).(*int16)
+ return v == nil
+
+ case data.FieldTypeNullableUint16:
+ v := field.At(at).(*uint16)
+ return v == nil
+
+ case data.FieldTypeNullableInt32:
+ v := field.At(at).(*int32)
+ return v == nil
+
+ case data.FieldTypeNullableUint32:
+ v := field.At(at).(*uint32)
+ return v == nil
+
+ case data.FieldTypeNullableInt64:
+ v := field.At(at).(*int64)
+ return v == nil
+
+ case data.FieldTypeNullableUint64:
+ v := field.At(at).(*uint64)
+ return v == nil
+
+ case data.FieldTypeNullableFloat64:
+ v := field.At(at).(*float64)
+ return v == nil
+
+ case data.FieldTypeNullableString:
+ v := field.At(at).(*string)
+ return v == nil
+
+ case data.FieldTypeNullableTime:
+ v := field.At(at).(*time.Time)
+ return v == nil
+
+ case data.FieldTypeNullableBool:
+ v := field.At(at).(*bool)
+ return v == nil
+
+ default:
+ // Either it's not a nullable type or it's unsupported
+ return false
+ }
+}
diff --git a/pkg/expr/sql/frame_table.go b/pkg/expr/sql/frame_table.go
new file mode 100644
index 00000000000..a511a3606fa
--- /dev/null
+++ b/pkg/expr/sql/frame_table.go
@@ -0,0 +1,126 @@
+//go:build !arm
+
+package sql
+
+import (
+ "io"
+ "strings"
+
+ mysql "github.com/dolthub/go-mysql-server/sql"
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+// FrameTable fulfills the mysql.Table interface for a data.Frame.
+type FrameTable struct {
+ Frame *data.Frame
+ schema mysql.Schema
+}
+
+// Name implements the sql.Nameable interface
+func (ft *FrameTable) Name() string {
+ return ft.Frame.RefID
+}
+
+// String implements the fmt.Stringer interface
+func (ft *FrameTable) String() string {
+ return ft.Name()
+}
+
+func schemaFromFrame(frame *data.Frame) mysql.Schema {
+ schema := make(mysql.Schema, len(frame.Fields))
+
+ for i, field := range frame.Fields {
+ schema[i] = &mysql.Column{
+ Name: field.Name,
+ Type: convertDataType(field.Type()),
+ Nullable: field.Type().Nullable(),
+ Source: strings.ToLower(frame.RefID),
+ }
+ }
+
+ return schema
+}
+
+// Schema implements the mysql.Table interface
+func (ft *FrameTable) Schema() mysql.Schema {
+ if ft.schema == nil {
+ ft.schema = schemaFromFrame(ft.Frame)
+ }
+ return ft.schema
+}
+
+// Collation implements the mysql.Table interface
+func (ft *FrameTable) Collation() mysql.CollationID {
+ return mysql.Collation_Unspecified
+}
+
+// Partitions implements the mysql.Table interface
+func (ft *FrameTable) Partitions(ctx *mysql.Context) (mysql.PartitionIter, error) {
+ return &noopPartitionIter{}, nil
+}
+
+// PartitionRows implements the mysql.Table interface
+func (ft *FrameTable) PartitionRows(ctx *mysql.Context, _ mysql.Partition) (mysql.RowIter, error) {
+ return &rowIter{ft: ft, row: 0}, nil
+}
+
+type rowIter struct {
+ ft *FrameTable
+ row int
+}
+
+func (ri *rowIter) Next(_ *mysql.Context) (mysql.Row, error) {
+ // We assume each field in the Frame has the same number of rows.
+ numRows := 0
+ if len(ri.ft.Frame.Fields) > 0 {
+ numRows = ri.ft.Frame.Fields[0].Len()
+ }
+
+ // If we've already exhausted all rows, return EOF
+ if ri.row >= numRows {
+ return nil, io.EOF
+ }
+
+ // Construct a Row (which is []interface{} under the hood) by pulling
+ // the value from each column at the current row index.
+ row := make(mysql.Row, len(ri.ft.Frame.Fields))
+ for colIndex, field := range ri.ft.Frame.Fields {
+ if nilAt(*field, ri.row) {
+ continue
+ }
+ row[colIndex], _ = field.ConcreteAt(ri.row)
+ }
+
+ ri.row++
+ return row, nil
+}
+
+// Close implements the mysql.RowIter interface.
+// In this no-op example, there isn't anything to do here.
+func (ri *rowIter) Close(*mysql.Context) error {
+ return nil
+}
+
+type noopPartitionIter struct {
+ done bool
+}
+
+func (i *noopPartitionIter) Next(*mysql.Context) (mysql.Partition, error) {
+ if !i.done {
+ i.done = true
+ return noopParition, nil
+ }
+ return nil, io.EOF
+}
+
+func (i *noopPartitionIter) Close(*mysql.Context) error {
+ return nil
+}
+
+var noopParition = partition(nil)
+
+type partition []byte
+
+func (p partition) Key() []byte {
+ return p
+}
diff --git a/pkg/expr/sql/parser.go b/pkg/expr/sql/parser.go
index b5456b3b6ff..12269b4a68f 100644
--- a/pkg/expr/sql/parser.go
+++ b/pkg/expr/sql/parser.go
@@ -1,89 +1,62 @@
package sql
import (
- "encoding/json"
"fmt"
"sort"
- "strings"
+ "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/grafana/grafana/pkg/infra/log"
- "github.com/jeremywohl/flatten"
-)
-
-const (
- TABLE_NAME = "table_name"
- ERROR = ".error"
- ERROR_MESSAGE = ".error_message"
)
var logger = log.New("sql_expr")
// TablesList returns a list of tables for the sql statement
func TablesList(rawSQL string) ([]string, error) {
- db := NewInMemoryDB()
- rawSQL = strings.Replace(rawSQL, "'", "''", -1)
- cmd := fmt.Sprintf("SELECT json_serialize_sql('%s')", rawSQL)
- ret, err := db.RunCommands([]string{cmd})
+ stmt, err := sqlparser.Parse(rawSQL)
if err != nil {
- logger.Error("error serializing sql", "error", err.Error(), "sql", rawSQL, "cmd", cmd)
- return nil, fmt.Errorf("error serializing sql: %s", err.Error())
+ logger.Error("error parsing sql: %s", err.Error(), "sql", rawSQL)
+ return nil, fmt.Errorf("error parsing sql: %s", err.Error())
}
- ast := []map[string]any{}
- err = json.Unmarshal([]byte(ret), &ast)
- if err != nil {
- logger.Error("error converting json sql to ast", "error", err.Error(), "ret", ret)
- return nil, fmt.Errorf("error converting json to ast: %s", err.Error())
- }
+ tables := make(map[string]struct{})
- return tablesFromAST(ast)
-}
-
-// tablesFromAST returns a list of tables from the ast
-func tablesFromAST(ast []map[string]any) ([]string, error) {
- flat, err := flatten.Flatten(ast[0], "", flatten.DotStyle)
- if err != nil {
- logger.Error("error flattening ast", "error", err.Error(), "ast", ast)
- return nil, fmt.Errorf("error flattening ast: %s", err.Error())
- }
-
- tables := []string{}
- for k, v := range flat {
- if strings.HasSuffix(k, ERROR) {
- v, ok := v.(bool)
- if ok && v {
- logger.Error("error in sql", "error", k)
- return nil, astError(k, flat)
+ walkSubtree := func(node sqlparser.SQLNode) error {
+ err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+ switch v := node.(type) {
+ case *sqlparser.AliasedTableExpr:
+ if tableName, ok := v.Expr.(sqlparser.TableName); ok {
+ tables[tableName.Name.String()] = struct{}{}
+ }
+ case *sqlparser.TableName:
+ tables[v.Name.String()] = struct{}{}
}
+ return true, nil
+ }, node)
+
+ if err != nil {
+ logger.Error("error walking sql", "error", err, "node", node)
+ return fmt.Errorf("failed to parse SQL expression: %w", err)
}
- if strings.Contains(k, TABLE_NAME) {
- table, ok := v.(string)
- if ok && !existsInList(table, tables) {
- tables = append(tables, v.(string))
- }
+ return nil
+ }
+
+ if err := walkSubtree(stmt); err != nil {
+ return nil, err
+ }
+
+ result := make([]string, 0, len(tables))
+ for table := range tables {
+ // Remove 'dual' table if it exists
+ // This is a special table in MySQL that always returns a single row with a single column
+ // See: https://dev.mysql.com/doc/refman/5.7/en/select.html#:~:text=You%20are%20permitted%20to%20specify%20DUAL%20as%20a%20dummy%20table%20name%20in%20situations%20where%20no%20tables%20are%20referenced
+ if table != "dual" {
+ result = append(result, table)
}
}
- sort.Strings(tables)
+
+ sort.Strings(result)
logger.Debug("tables found in sql", "tables", tables)
- return tables, nil
-}
-
-func astError(k string, flat map[string]any) error {
- key := strings.Replace(k, ERROR, "", 1)
- message, ok := flat[key+ERROR_MESSAGE]
- if !ok {
- message = "unknown error in sql"
- }
- return fmt.Errorf("error in sql: %s", message)
-}
-
-func existsInList(table string, list []string) bool {
- for _, t := range list {
- if t == table {
- return true
- }
- }
- return false
+ return result, nil
}
diff --git a/pkg/expr/sql/parser_allow.go b/pkg/expr/sql/parser_allow.go
new file mode 100644
index 00000000000..8a2ad436920
--- /dev/null
+++ b/pkg/expr/sql/parser_allow.go
@@ -0,0 +1,136 @@
+package sql
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dolthub/vitess/go/vt/sqlparser"
+)
+
+// AllowQuery parses the query and checks it against an allow list of allowed SQL nodes
+// and functions.
+func AllowQuery(rawSQL string) (bool, error) {
+ s, err := sqlparser.Parse(rawSQL)
+ if err != nil {
+ return false, fmt.Errorf("error parsing sql: %s", err.Error())
+ }
+
+ walkSubtree := func(node sqlparser.SQLNode) error {
+ err := sqlparser.Walk(func(node sqlparser.SQLNode) (bool, error) {
+ if !allowedNode(node) {
+ if fT, ok := node.(*sqlparser.FuncExpr); ok {
+ return false, fmt.Errorf("blocked function %s - not supported in queries", fT.Name)
+ }
+ return false, fmt.Errorf("blocked node %T - not supported in queries", node)
+ }
+ return true, nil
+ }, node)
+
+ if err != nil {
+ return fmt.Errorf("failed to parse SQL expression: %w", err)
+ }
+
+ return nil
+ }
+
+ if err := walkSubtree(s); err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
+
+// nolint:gocyclo,nakedret
+func allowedNode(node sqlparser.SQLNode) (b bool) {
+ b = true // so don't have to return true in every case but default
+
+ switch v := node.(type) {
+ case *sqlparser.FuncExpr:
+ return allowedFunction(v)
+
+ case *sqlparser.AsOf:
+ return
+
+ case *sqlparser.AliasedExpr, *sqlparser.AliasedTableExpr:
+ return
+
+ case *sqlparser.BinaryExpr:
+ return
+
+ case sqlparser.ColIdent, *sqlparser.ColName, sqlparser.Columns:
+ return
+
+ case sqlparser.Comments: // TODO: understand why some are pointer vs not
+ return
+
+ case *sqlparser.CommonTableExpr:
+ return
+
+ case *sqlparser.ComparisonExpr:
+ return
+
+ case *sqlparser.ConvertExpr:
+ return
+
+ case sqlparser.GroupBy:
+ return
+
+ case *sqlparser.IndexHints:
+ return
+
+ case *sqlparser.Into:
+ return
+
+ case *sqlparser.JoinTableExpr, sqlparser.JoinCondition:
+ return
+
+ case *sqlparser.Select, sqlparser.SelectExprs:
+ return
+
+ case *sqlparser.StarExpr:
+ return
+
+ case *sqlparser.SQLVal:
+ return
+
+ case *sqlparser.Limit:
+ return
+
+ case *sqlparser.Order, sqlparser.OrderBy:
+ return
+
+ case *sqlparser.Over:
+ return
+
+ case *sqlparser.Subquery:
+ return
+
+ case sqlparser.TableName, sqlparser.TableExprs, sqlparser.TableIdent:
+ return
+
+ case *sqlparser.With:
+ return
+
+ case *sqlparser.Where:
+ return
+
+ default:
+ return false
+ }
+}
+
+// nolint:gocyclo,nakedret
+func allowedFunction(f *sqlparser.FuncExpr) (b bool) {
+ b = true // so don't have to return true in every case but default
+
+ switch strings.ToLower(f.Name.String()) {
+ case "sum", "avg", "count", "min", "max":
+ return
+
+ case "coalesce":
+ return
+
+ default:
+ return false
+ }
+}
diff --git a/pkg/expr/sql/parser_allow_test.go b/pkg/expr/sql/parser_allow_test.go
new file mode 100644
index 00000000000..aeeb1a40c69
--- /dev/null
+++ b/pkg/expr/sql/parser_allow_test.go
@@ -0,0 +1,80 @@
+package sql
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAllowQuery(t *testing.T) {
+ testCases := []struct {
+ name string
+ q string
+ err error
+ }{
+ {
+ name: "a big catch all for now",
+ q: example_metrics_query,
+ err: nil,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ _, err := AllowQuery(tc.q)
+ if tc.err != nil {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
+
+var example_metrics_query = `WITH
+ metrics_this_month AS (
+ SELECT
+ Month,
+ namespace,
+ sum(BillableSeries) AS billable_series
+ FROM metrics
+ WHERE
+ Month = "2024-11"
+ GROUP BY
+ Month,
+ namespace
+ ORDER BY billable_series DESC
+ ),
+ total_metrics AS (
+ SELECT SUM(billable_series) AS metrics_billable_series_total
+ FROM metrics_this_month
+ ),
+ total_traces AS (
+ -- "usage" is a reserved keyword in MySQL. Quote it with backticks.
+ SELECT SUM(value) AS traces_usage_total
+ FROM traces
+ ),
+ usage_by_team AS (
+ SELECT
+ COALESCE(teams.team, 'unaccounted') AS team,
+ 1 + 0 AS team_count,
+ -- Metrics
+ SUM(COALESCE(metrics_this_month.billable_series, 0)) AS metrics_billable_series,
+ -- Traces
+ SUM(COALESCE(traces.value, 0)) AS traces_usage
+ -- FROM teams
+ -- FULL OUTER JOIN metrics_this_month
+ FROM metrics_this_month
+ FULL OUTER JOIN teams
+ ON teams.namespace = metrics_this_month.namespace
+ FULL OUTER JOIN traces
+ ON teams.namespace = traces.namespace
+ GROUP BY
+ -- COALESCE(teams.team, 'unaccounted')
+ teams.team
+ ORDER BY metrics_billable_series DESC
+ )
+
+SELECT *
+FROM usage_by_team
+CROSS JOIN total_metrics
+CROSS JOIN total_traces`
diff --git a/pkg/expr/sql/parser_test.go b/pkg/expr/sql/parser_test.go
index 24303ce0178..17235684388 100644
--- a/pkg/expr/sql/parser_test.go
+++ b/pkg/expr/sql/parser_test.go
@@ -3,214 +3,131 @@ package sql
import (
"testing"
- "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
-func TestParse(t *testing.T) {
- t.Skip()
- sql := "select * from foo"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
+func TestTablesList(t *testing.T) {
+ tests := []struct {
+ name string
+ sql string
+ expected []string
+ expectError bool
+ }{
+ {
+ name: "simple select",
+ sql: "select * from foo",
+ expected: []string{"foo"},
+ },
+ {
+ name: "select with comma",
+ sql: "select * from foo,bar",
+ expected: []string{"bar", "foo"},
+ },
+ {
+ name: "select with multiple commas",
+ sql: "select * from foo,bar,baz",
+ expected: []string{"bar", "baz", "foo"},
+ },
+ {
+ name: "no table",
+ sql: "select 1 as 'n'",
+ expected: []string{},
+ },
+ {
+ name: "json array",
+ sql: "SELECT JSON_ARRAY(1, 2, 3) AS array_value",
+ expected: []string{},
+ },
+ {
+ name: "json extract",
+ sql: "SELECT JSON_EXTRACT(JSON_ARRAY(1, 2, 3), '$[0]') AS first_element;",
+ expected: []string{},
+ },
+ {
+ name: "json int array",
+ sql: "SELECT JSON_ARRAY(3, 2, 1) AS int_array;",
+ expected: []string{},
+ },
+ {
+ name: "subquery",
+ sql: "select * from (select * from people limit 1) AS subquery",
+ expected: []string{"people"},
+ },
+ {
+ name: "join",
+ sql: `select * from A
+ JOIN B ON A.name = B.name
+ LIMIT 10`,
+ expected: []string{"A", "B"},
+ },
+ {
+ name: "right join",
+ sql: `select * from A
+ RIGHT JOIN B ON A.name = B.name
+ LIMIT 10`,
+ expected: []string{"A", "B"},
+ },
+ {
+ name: "alias with join",
+ sql: `select * from A as X
+ RIGHT JOIN B ON A.name = X.name
+ LIMIT 10`,
+ expected: []string{"A", "B"},
+ },
+ {
+ name: "alias",
+ sql: "select * from A as X LIMIT 10",
+ expected: []string{"A"},
+ },
+ {
+ name: "error case",
+ sql: "select * from zzz aaa zzz",
+ expectError: true,
+ },
+ {
+ name: "parens",
+ sql: `SELECT t1.Col1,
+ t2.Col1,
+ t3.Col1
+ FROM table1 AS t1
+ LEFT JOIN (
+ table2 AS t2
+ INNER JOIN table3 AS t3 ON t3.Col1 = t2.Col1
+ ) ON t2.Col1 = t1.Col1;`,
+ expected: []string{"table1", "table2", "table3"},
+ },
+ {
+ name: "with clause",
+ sql: `WITH top_products AS (
+ SELECT * FROM products
+ ORDER BY price DESC
+ LIMIT 5
+ )
+ SELECT name, price
+ FROM top_products;`,
+ expected: []string{"products", "top_products"},
+ },
+ {
+ name: "with quote",
+ sql: "select *,'junk' from foo",
+ expected: []string{"foo"},
+ },
+ {
+ name: "with quote 2",
+ sql: "SELECT json_serialize_sql('SELECT 1')",
+ expected: []string{},
+ },
+ }
- assert.Equal(t, "foo", tables[0])
-}
-
-func TestParseWithComma(t *testing.T) {
- t.Skip()
- sql := "select * from foo,bar"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, "bar", tables[0])
- assert.Equal(t, "foo", tables[1])
-}
-
-func TestParseWithCommas(t *testing.T) {
- t.Skip()
- sql := "select * from foo,bar,baz"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, "bar", tables[0])
- assert.Equal(t, "baz", tables[1])
- assert.Equal(t, "foo", tables[2])
-}
-
-func TestArray(t *testing.T) {
- t.Skip()
- sql := "SELECT array_value(1, 2, 3)"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 0, len(tables))
-}
-
-func TestArray2(t *testing.T) {
- t.Skip()
- sql := "SELECT array_value(1, 2, 3)[2]"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 0, len(tables))
-}
-
-func TestXxx(t *testing.T) {
- t.Skip()
- sql := "SELECT [3, 2, 1]::INT[3];"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 0, len(tables))
-}
-
-func TestParseSubquery(t *testing.T) {
- t.Skip()
- sql := "select * from (select * from people limit 1)"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 1, len(tables))
- assert.Equal(t, "people", tables[0])
-}
-
-func TestJoin(t *testing.T) {
- t.Skip()
- sql := `select * from A
- JOIN B ON A.name = B.name
- LIMIT 10`
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 2, len(tables))
- assert.Equal(t, "A", tables[0])
- assert.Equal(t, "B", tables[1])
-}
-
-func TestRightJoin(t *testing.T) {
- t.Skip()
- sql := `select * from A
- RIGHT JOIN B ON A.name = B.name
- LIMIT 10`
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 2, len(tables))
- assert.Equal(t, "A", tables[0])
- assert.Equal(t, "B", tables[1])
-}
-
-func TestAliasWithJoin(t *testing.T) {
- t.Skip()
- sql := `select * from A as X
- RIGHT JOIN B ON A.name = X.name
- LIMIT 10`
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 2, len(tables))
- assert.Equal(t, "A", tables[0])
- assert.Equal(t, "B", tables[1])
-}
-
-func TestAlias(t *testing.T) {
- t.Skip()
- sql := `select * from A as X LIMIT 10`
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 1, len(tables))
- assert.Equal(t, "A", tables[0])
-}
-
-func TestError(t *testing.T) {
- t.Skip()
- sql := `select * from zzz aaa zzz`
- _, err := TablesList((sql))
- assert.NotNil(t, err)
-}
-
-func TestParens(t *testing.T) {
- t.Skip()
- sql := `SELECT t1.Col1,
- t2.Col1,
- t3.Col1
- FROM table1 AS t1
- LEFT JOIN (
- table2 AS t2
- INNER JOIN table3 AS t3 ON t3.Col1 = t2.Col1
- ) ON t2.Col1 = t1.Col1;`
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 3, len(tables))
- assert.Equal(t, "table1", tables[0])
- assert.Equal(t, "table2", tables[1])
- assert.Equal(t, "table3", tables[2])
-}
-
-func TestWith(t *testing.T) {
- t.Skip()
- sql := `WITH
-
- current_month AS (
- select
- distinct "Month(ISO)" as mth
- from A
- ORDER BY mth DESC
- LIMIT 1
- ),
-
- last_month_bill AS (
- select
- CAST (
- sum(
- CAST(BillableSeries AS INTEGER)
- ) AS INTEGER
- ) AS BillableSeries,
- "Month(ISO)",
- label_namespace
- -- , B.activeseries_count
- from A
- JOIN current_month
- ON current_month.mth = A."Month(ISO)"
- JOIN B
- ON B.namespace = A.label_namespace
- GROUP BY
- label_namespace,
- "Month(ISO)"
- ORDER BY BillableSeries DESC
- )
-
- SELECT
- last_month_bill.*,
- BEE.activeseries_count
- FROM last_month_bill
- JOIN BEE
- ON BEE.namespace = last_month_bill.label_namespace`
-
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 5, len(tables))
- assert.Equal(t, "A", tables[0])
- assert.Equal(t, "B", tables[1])
- assert.Equal(t, "BEE", tables[2])
-}
-
-func TestWithQuote(t *testing.T) {
- t.Skip()
- sql := "select *,'junk' from foo"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, "foo", tables[0])
-}
-
-func TestWithQuote2(t *testing.T) {
- t.Skip()
- sql := "SELECT json_serialize_sql('SELECT 1')"
- tables, err := TablesList((sql))
- assert.Nil(t, err)
-
- assert.Equal(t, 0, len(tables))
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ tables, err := TablesList(tc.sql)
+ if tc.expectError {
+ require.NotNil(t, err, "expected error for SQL: %s", tc.sql)
+ } else {
+ require.Nil(t, err, "unexpected error for SQL: %s", tc.sql)
+ require.Equal(t, tc.expected, tables, "mismatched tables for SQL: %s", tc.sql)
+ }
+ })
+ }
}
diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go
index 9f0e1faf3e8..60d23f6e82f 100644
--- a/pkg/expr/sql_command.go
+++ b/pkg/expr/sql_command.go
@@ -93,11 +93,10 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V
rsp := mathexp.Results{}
- db := sql.NewInMemoryDB()
- var frame = &data.Frame{}
+ db := sql.DB{}
logger.Debug("Executing query", "query", gr.query, "frames", len(allFrames))
- err := db.QueryFramesInto(gr.refID, gr.query, allFrames, frame)
+ frame, err := db.QueryFrames(ctx, gr.refID, gr.query, allFrames)
if err != nil {
logger.Error("Failed to query frames", "error", err.Error())
rsp.Error = err
@@ -105,12 +104,11 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V
}
logger.Debug("Done Executing query", "query", gr.query, "rows", frame.Rows())
- frame.RefID = gr.refID
-
if frame.Rows() == 0 {
rsp.Values = mathexp.Values{
mathexp.NoData{Frame: frame},
}
+ return rsp, nil
}
rsp.Values = mathexp.Values{
diff --git a/pkg/registry/apis/dashboard/large.go b/pkg/registry/apis/dashboard/large.go
index 88a5eb2d56d..9530b247d2f 100644
--- a/pkg/registry/apis/dashboard/large.go
+++ b/pkg/registry/apis/dashboard/large.go
@@ -31,7 +31,7 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme) *apistore.BasicLarge
dash.Spec = spec
dash.SetManagedFields(nil) // this could be bigger than the object!
- keep := []string{"title", "description", "schemaVersion"}
+ keep := []string{"title", "description", "tags", "schemaVersion"}
for _, k := range keep {
v, ok := old[k]
if ok {
diff --git a/pkg/registry/apis/dashboard/large_test.go b/pkg/registry/apis/dashboard/large_test.go
index a1f1094a935..45510304a65 100644
--- a/pkg/registry/apis/dashboard/large_test.go
+++ b/pkg/registry/apis/dashboard/large_test.go
@@ -54,7 +54,8 @@ func TestLargeDashboardSupport(t *testing.T) {
require.NoError(t, err)
require.JSONEq(t, `{
"schemaVersion": 33,
- "title": "Panel tests - All panels"
+ "title": "Panel tests - All panels",
+ "tags": ["gdev","panel-tests","all-panels"]
}`, string(small))
// Now make it big again
diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go
index 17aa5aa16a9..bd2e4a896a8 100644
--- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go
+++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go
@@ -2,12 +2,18 @@ package legacysearcher
import (
"context"
+ "encoding/json"
"fmt"
+ "strconv"
"strings"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
+ "github.com/grafana/grafana/pkg/apimachinery/utils"
+ "github.com/grafana/grafana/pkg/apis/dashboard"
+ folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"google.golang.org/grpc"
@@ -28,29 +34,93 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
return nil, err
}
- if req.Query == "*" {
- req.Query = ""
+ // the "*"s will be added in the k8s handler in dashboard_service.go in order to make search work
+ // in modes 3+. These "*"s will break the legacy sql query so we need to remove them here
+ if strings.Contains(req.Query, "*") {
+ req.Query = strings.ReplaceAll(req.Query, "*", "")
}
// TODO add missing support for the following query params:
- // - tag
- // - starred (won't support)
- // - page (check)
- // - type
- // - sort
- // - deleted
+ // - folderIds (won't support, must use folderUIDs)
// - permission
- // - dashboardIds
- // - dashboardUIDs
- // - folderIds
- // - folderUIDs
- // - sort (default by title)
query := &dashboards.FindPersistedDashboardsQuery{
- Title: req.Query,
- Limit: req.Limit,
- // FolderUIDs: req.FolderUIDs,
- Type: searchstore.TypeDashboard,
+ Title: req.Query,
+ Limit: req.Limit,
+ Page: req.Page,
SignedInUser: user,
+ IsDeleted: req.IsDeleted,
+ }
+
+ var queryType string
+ if req.Options.Key.Resource == dashboard.DASHBOARD_RESOURCE {
+ queryType = searchstore.TypeDashboard
+ } else if req.Options.Key.Resource == folderv0alpha1.RESOURCE {
+ queryType = searchstore.TypeFolder
+ } else {
+ return nil, fmt.Errorf("bad type request")
+ }
+
+ if len(req.Federated) > 1 {
+ return nil, fmt.Errorf("bad type request")
+ }
+
+ if len(req.Federated) == 1 &&
+ ((req.Federated[0].Resource == dashboard.DASHBOARD_RESOURCE && queryType == searchstore.TypeFolder) ||
+ (req.Federated[0].Resource == folderv0alpha1.RESOURCE && queryType == searchstore.TypeDashboard)) {
+ queryType = "" // makes the legacy store search across both
+ }
+
+ if queryType != "" {
+ query.Type = queryType
+ }
+
+ // technically, there exists the ability to register multiple ways of sorting using the legacy database
+ // see RegisterSortOption in pkg/services/search/sorting.go
+ // however, it doesn't look like we are taking advantage of that. And since by default the legacy
+ // sql will sort by title ascending, we only really need to handle the "alpha-desc" case
+ if req.SortBy != nil {
+ for _, sort := range req.SortBy {
+ if sort.Field == "title" && sort.Desc {
+ query.Sort = search.SortAlphaDesc
+ }
+ }
+ }
+ // handle deprecated dashboardIds query param
+ for _, field := range req.Options.Labels {
+ if field.Key == utils.LabelKeyDeprecatedInternalID {
+ values := field.GetValues()
+ dashboardIds := make([]int64, len(values))
+ for i, id := range values {
+ if n, err := strconv.ParseInt(id, 10, 64); err == nil {
+ dashboardIds[i] = n
+ }
+ }
+
+ query.DashboardIds = dashboardIds
+ }
+ }
+
+ for _, field := range req.Options.Fields {
+ switch field.Key {
+ case resource.SEARCH_FIELD_TAGS:
+ query.Tags = field.GetValues()
+ case resource.SEARCH_FIELD_NAME:
+ query.DashboardUIDs = field.GetValues()
+ query.DashboardIds = nil
+ case resource.SEARCH_FIELD_FOLDER:
+ vals := field.GetValues()
+ folders := make([]string, len(vals))
+
+ for i, val := range vals {
+ if val == "" {
+ folders[i] = "general"
+ } else {
+ folders[i] = val
+ }
+ }
+
+ query.FolderUIDs = folders
+ }
}
// TODO need to test this
@@ -75,26 +145,73 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
- // searchFields.Field(resource.SEARCH_FIELD_TAGS),
+ searchFields.Field(resource.SEARCH_FIELD_TAGS),
},
},
}
- for _, dashboard := range res {
+ hits := formatQueryResult(res)
+
+ for _, dashboard := range hits {
+ tags, err := json.Marshal(dashboard.Tags)
+ if err != nil {
+ return nil, err
+ }
+
list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{
- Key: &resource.ResourceKey{
- Namespace: "default",
- Group: "dashboard.grafana.app",
- Resource: "dashboards",
- Name: dashboard.UID,
- },
- Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID)}, // TODO add tag
+ Key: getResourceKey(dashboard, req.Options.Key.Namespace),
+ Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags},
})
}
return list, nil
}
+func getResourceKey(item *dashboards.DashboardSearchProjection, namespace string) *resource.ResourceKey {
+ if item.IsFolder {
+ return &resource.ResourceKey{
+ Namespace: namespace,
+ Group: folderv0alpha1.GROUP,
+ Resource: folderv0alpha1.RESOURCE,
+ Name: item.UID,
+ }
+ }
+
+ return &resource.ResourceKey{
+ Namespace: namespace,
+ Group: dashboard.GROUP,
+ Resource: dashboard.DASHBOARD_RESOURCE,
+ Name: item.UID,
+ }
+}
+
+func formatQueryResult(res []dashboards.DashboardSearchProjection) []*dashboards.DashboardSearchProjection {
+ hitList := make([]*dashboards.DashboardSearchProjection, 0)
+ hits := make(map[string]*dashboards.DashboardSearchProjection)
+
+ for _, item := range res {
+ key := fmt.Sprintf("%s-%d", item.UID, item.OrgID)
+ hit, exists := hits[key]
+ if !exists {
+ hit = &dashboards.DashboardSearchProjection{
+ UID: item.UID,
+ Title: item.Title,
+ FolderUID: item.FolderUID,
+ Tags: []string{},
+ IsFolder: item.IsFolder,
+ }
+ hitList = append(hitList, hit)
+ hits[key] = hit
+ }
+
+ if len(item.Term) > 0 {
+ hit.Tags = append(hit.Tags, item.Term)
+ }
+ }
+
+ return hitList
+}
+
func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
info, err := claims.ParseNamespace(req.Namespace)
if err != nil {
diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go
index fc170a36783..4d9b8137c85 100644
--- a/pkg/registry/apis/dashboard/search.go
+++ b/pkg/registry/apis/dashboard/search.go
@@ -18,7 +18,9 @@ import (
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/apimachinery/identity"
+ "github.com/grafana/grafana/pkg/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
@@ -199,7 +201,7 @@ func (s *SearchHandler) DoSortable(w http.ResponseWriter, r *http.Request) {
const rootFolder = "general"
-//nolint:gocyclo
+// nolint:gocyclo
func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
ctx, span := s.tracer.Start(r.Context(), "dashboard.search")
defer span.End()
@@ -231,6 +233,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
Query: queryParams.Get("query"),
Limit: int64(limit),
Offset: int64(offset),
+ Page: int64(offset), // on modes 0-2 (legacy) we use "Page" instead of "Offset"
Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false",
}
fields := []string{"title", "folder", "tags"}
@@ -262,10 +265,10 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
switch len(types) {
case 0:
// When no type specified, search for dashboards
- searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), "dashboards")
+ searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboard.DASHBOARD_RESOURCE)
// Currently a search query is across folders and dashboards
- if searchRequest.Query != "" {
- federate, err = asResourceKey(user.GetNamespace(), "folders")
+ if err == nil {
+ federate, err = asResourceKey(user.GetNamespace(), folderv0alpha1.RESOURCE)
}
case 1:
searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), types[0])
@@ -301,8 +304,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
}
// The facet term fields
- facets, ok := queryParams["facet"]
- if ok {
+ if facets, ok := queryParams["facet"]; ok {
searchRequest.Facet = make(map[string]*resource.ResourceSearchRequest_Facet)
for _, v := range facets {
searchRequest.Facet[v] = &resource.ResourceSearchRequest_Facet{
@@ -313,8 +315,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
}
// The tags filter
- tags, ok := queryParams["tag"]
- if ok {
+ if tags, ok := queryParams["tag"]; ok {
searchRequest.Options.Fields = []*resource.Requirement{{
Key: "tags",
Operator: "=",
@@ -323,8 +324,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
}
// The names filter
- names, ok := queryParams["name"]
- if ok {
+ if names, ok := queryParams["name"]; ok {
if searchRequest.Options.Fields == nil {
searchRequest.Options.Fields = []*resource.Requirement{}
}
@@ -366,30 +366,10 @@ func (s *SearchHandler) write(w http.ResponseWriter, obj any) {
// Given a namespace and type convert it to a search key
func asResourceKey(ns string, k string) (*resource.ResourceKey, error) {
- if ns == "" {
- return nil, apierrors.NewBadRequest("missing namespace")
+ key, err := resource.AsResourceKey(ns, k)
+ if err != nil {
+ return nil, apierrors.NewBadRequest(err.Error())
}
- switch k {
- case "folders", "folder":
- return &resource.ResourceKey{
- Namespace: ns,
- Group: "folder.grafana.app",
- Resource: "folders",
- }, nil
- case "dashboards", "dashboard":
- return &resource.ResourceKey{
- Namespace: ns,
- Group: dashboardv0alpha1.GROUP,
- Resource: "dashboards",
- }, nil
- // NOT really supported in the dashboard search UI, but useful for manual testing
- case "playlist", "playlists":
- return &resource.ResourceKey{
- Namespace: ns,
- Group: "playlist.grafana.app",
- Resource: "playlists",
- }, nil
- }
- return nil, apierrors.NewBadRequest("unknown resource type")
+ return key, nil
}
diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go
index 83e46a6e6c2..d48a6e7a802 100644
--- a/pkg/registry/apis/dashboard/search_test.go
+++ b/pkg/registry/apis/dashboard/search_test.go
@@ -9,19 +9,17 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/user"
+ "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
-/*
-Search Fallback was returning both Folders and Dashboards which resulted
-in issues with rendering the Folder UI. Also, filters are not implemented
-yet. For those reasons, we will be disabling Search Fallback for now
func TestSearchFallback(t *testing.T) {
t.Run("should hit legacy search handler on mode 0", func(t *testing.T) {
mockClient := &MockClient{}
@@ -179,7 +177,6 @@ func TestSearchFallback(t *testing.T) {
}
})
}
-*/
func TestSearchHandler(t *testing.T) {
// Create a mock client
diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go
index f86bf0fc75f..f3675514a25 100644
--- a/pkg/registry/apis/query/query.go
+++ b/pkg/registry/apis/query/query.go
@@ -160,6 +160,7 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O
// Actually run the query
rsp, err := b.execute(ctx, req)
if err != nil {
+ b.log.Error("hit unexpected error while executing query, this will show as an unhandled k8s status error", "err", err)
responder.Error(err)
return
}
@@ -368,8 +369,7 @@ func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, req parsedReque
if !ok {
dr, ok := qdr.Responses[refId]
if ok {
- allowLongFrames := false // TODO -- depends on input type and only if SQL?
- _, res, err := b.converter.Convert(ctx, req.RefIDTypes[refId], dr.Frames, allowLongFrames)
+ _, res, err := b.converter.Convert(ctx, req.RefIDTypes[refId], dr.Frames)
if err != nil {
expressionsLogger.Error("error converting frames for expressions", "error", err)
res.Error = err
@@ -409,13 +409,12 @@ func (b *QueryAPIBuilder) convertQueryWithoutExpression(ctx context.Context, req
if qdr == nil {
return nil, errors.New("queryDataResponse is nil")
}
- allowLongFrames := false
refID := req.Request.Queries[0].RefID
if _, exist := qdr.Responses[refID]; !exist {
return nil, fmt.Errorf("refID '%s' does not exist", refID)
}
frames := qdr.Responses[refID].Frames
- _, results, err := b.converter.Convert(ctx, req.PluginId, frames, allowLongFrames)
+ _, results, err := b.converter.Convert(ctx, req.PluginId, frames)
if err != nil {
results.Error = err
}
diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go
index 2f45e426bc3..9aaf573b7c8 100644
--- a/pkg/services/apiserver/client/client.go
+++ b/pkg/services/apiserver/client/client.go
@@ -179,10 +179,12 @@ func (h *k8sHandler) Search(ctx context.Context, orgID int64, in *resource.Resou
in.Options = &resource.ListOptions{}
}
- in.Options.Key = &resource.ResourceKey{
- Namespace: h.GetNamespace(orgID),
- Group: h.gvr.Group,
- Resource: h.gvr.Resource,
+ if in.Options.Key == nil {
+ in.Options.Key = &resource.ResourceKey{
+ Namespace: h.GetNamespace(orgID),
+ Group: h.gvr.Group,
+ Resource: h.gvr.Resource,
+ }
}
return h.searcher.Search(ctx, in)
diff --git a/pkg/services/authz/client.go b/pkg/services/authz/client.go
index 412a4e2acb9..506155df380 100644
--- a/pkg/services/authz/client.go
+++ b/pkg/services/authz/client.go
@@ -124,6 +124,10 @@ func newInProcLegacyClient(server *rbac.Service, tracer tracing.Tracer) (authlib
authzlib.WithGrpcConnectionClientOption(channel),
authzlib.WithDisableAccessTokenClientOption(),
authzlib.WithTracerClientOption(tracer),
+ authzlib.WithCacheClientOption(cache.NewLocalCache(cache.Config{
+ Expiry: 30 * time.Second,
+ CleanupInterval: 2 * time.Minute,
+ })),
)
}
@@ -147,6 +151,10 @@ func newGrpcLegacyClient(authCfg *Cfg, tracer tracing.Tracer) (authlib.AccessCli
grpc.WithStreamInterceptor(clientInterceptor.StreamClientInterceptor),
),
authzlib.WithTracerClientOption(tracer),
+ authzlib.WithCacheClientOption(cache.NewLocalCache(cache.Config{
+ Expiry: 30 * time.Second,
+ CleanupInterval: 2 * time.Minute,
+ })),
// TODO: remove this once access tokens are supported on-prem
authzlib.WithDisableAccessTokenClientOption(),
)
@@ -181,6 +189,10 @@ func newCloudLegacyClient(authCfg *Cfg, tracer tracing.Tracer) (authlib.AccessCl
grpc.WithUnaryInterceptor(clientInterceptor.UnaryClientInterceptor),
grpc.WithStreamInterceptor(clientInterceptor.StreamClientInterceptor),
),
+ authzlib.WithCacheClientOption(cache.NewLocalCache(cache.Config{
+ Expiry: 30 * time.Second,
+ CleanupInterval: 2 * time.Minute,
+ })),
authzlib.WithTracerClientOption(tracer),
)
if err != nil {
diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go
index 26bd352903c..349ccd53558 100644
--- a/pkg/services/authz/rbac/service.go
+++ b/pkg/services/authz/rbac/service.go
@@ -31,10 +31,10 @@ import (
)
const (
- shortCacheTTL = 1 * time.Minute
- shortCleanupInterval = 5 * time.Minute
- longCacheTTL = 5 * time.Minute
- longCleanupInterval = 10 * time.Minute
+ shortCacheTTL = 30 * time.Second
+ shortCleanupInterval = 2 * time.Minute
+ longCacheTTL = 2 * time.Minute
+ longCleanupInterval = 4 * time.Minute
)
type Service struct {
@@ -82,7 +82,7 @@ func NewService(
idCache: newCacheWrap[store.UserIdentifiers](cache, logger, longCacheTTL),
permCache: newCacheWrap[map[string]bool](cache, logger, shortCacheTTL),
teamCache: newCacheWrap[[]int64](cache, logger, shortCacheTTL),
- basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, longCacheTTL),
+ basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, shortCacheTTL),
folderCache: newCacheWrap[map[string]FolderNode](cache, logger, shortCacheTTL),
sf: new(singleflight.Group),
}
diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go
index 6e1154489bf..1efa21fc77c 100644
--- a/pkg/services/cloudmigration/api/dtos.go
+++ b/pkg/services/cloudmigration/api/dtos.go
@@ -127,6 +127,7 @@ const (
FolderDataType MigrateDataType = "FOLDER"
LibraryElementDataType MigrateDataType = "LIBRARY_ELEMENT"
AlertRuleType MigrateDataType = "ALERT_RULE"
+ AlertRuleGroupType MigrateDataType = "ALERT_RULE_GROUP"
ContactPointType MigrateDataType = "CONTACT_POINT"
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go
index 257d2e23a07..348bf8b1e98 100644
--- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go
+++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go
@@ -821,8 +821,11 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf
secretsService := secretsfakes.NewFakeSecretsService()
rr := routing.NewRouteRegister()
tracer := tracing.InitializeTracerForTest()
+
+ fakeFolder := &folder.Folder{UID: "folderUID", Title: "Folder"}
mockFolder := &foldertest.FakeService{
- ExpectedFolder: &folder.Folder{UID: "folderUID", Title: "Folder"},
+ ExpectedFolders: []*folder.Folder{fakeFolder},
+ ExpectedFolder: fakeFolder,
}
cfg := setting.NewCfg()
diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go
index 870c4949533..034c601f510 100644
--- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go
+++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go
@@ -43,6 +43,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{
cloudmigration.NotificationTemplateType,
cloudmigration.ContactPointType,
cloudmigration.NotificationPolicyType,
+ cloudmigration.AlertRuleGroupType,
cloudmigration.AlertRuleType,
cloudmigration.PluginDataType,
}
@@ -106,6 +107,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S
return nil, err
}
+ // Alerts: Alert Rule Groups
+ alertRuleGroups, err := s.getAlertRuleGroups(ctx, signedInUser)
+ if err != nil {
+ s.log.Error("Failed to get alert rule groups", "err", err)
+ return nil, err
+ }
+
// Alerts: Alert Rules
alertRules, err := s.getAlertRules(ctx, signedInUser)
if err != nil {
@@ -209,6 +217,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S
})
}
+ for _, alertRuleGroup := range alertRuleGroups {
+ migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
+ Type: cloudmigration.AlertRuleGroupType,
+ RefID: alertRuleGroup.Title, // no UID available
+ Name: alertRuleGroup.Title,
+ Data: alertRuleGroup,
+ })
+ }
+
for _, alertRule := range alertRules {
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
Type: cloudmigration.AlertRuleType,
diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go
index a848508b7eb..c75d8a35df0 100644
--- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go
+++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go
@@ -180,3 +180,61 @@ func (s *Service) getAlertRules(ctx context.Context, signedInUser *user.SignedIn
return provisionedAlertRules, nil
}
+
+type alertRuleGroup struct {
+ Title string `json:"title"`
+ FolderUID string `json:"folderUid"`
+ Interval int64 `json:"interval"`
+ Rules []alertRule `json:"rules"`
+}
+
+func (s *Service) getAlertRuleGroups(ctx context.Context, signedInUser *user.SignedInUser) ([]alertRuleGroup, error) {
+ alertRuleGroupsWithFolder, err := s.ngAlert.Api.AlertRules.GetAlertGroupsWithFolderFullpath(ctx, signedInUser, nil)
+ if err != nil {
+ return nil, fmt.Errorf("fetching alert rule groups with folders: %w", err)
+ }
+
+ settingAlertRulesPaused := s.cfg.CloudMigration.AlertRulesState == setting.GMSAlertRulesPaused
+
+ alertRuleGroups := make([]alertRuleGroup, 0, len(alertRuleGroupsWithFolder))
+
+ for _, ruleGroup := range alertRuleGroupsWithFolder {
+ provisionedAlertRules := make([]alertRule, 0, len(ruleGroup.Rules))
+
+ for _, rule := range ruleGroup.Rules {
+ isPaused := rule.IsPaused
+ if settingAlertRulesPaused {
+ isPaused = true
+ }
+
+ provisionedAlertRules = append(provisionedAlertRules, alertRule{
+ ID: rule.ID,
+ UID: rule.UID,
+ OrgID: rule.OrgID,
+ FolderUID: rule.NamespaceUID,
+ RuleGroup: rule.RuleGroup,
+ Title: rule.Title,
+ For: model.Duration(rule.For),
+ Condition: rule.Condition,
+ Data: ngalertapi.ApiAlertQueriesFromAlertQueries(rule.Data),
+ Updated: rule.Updated,
+ NoDataState: rule.NoDataState.String(),
+ ExecErrState: rule.ExecErrState.String(),
+ Annotations: rule.Annotations,
+ Labels: rule.Labels,
+ IsPaused: isPaused,
+ NotificationSettings: ngalertapi.AlertRuleNotificationSettingsFromNotificationSettings(rule.NotificationSettings),
+ Record: ngalertapi.ApiRecordFromModelRecord(rule.Record),
+ })
+ }
+
+ alertRuleGroups = append(alertRuleGroups, alertRuleGroup{
+ Title: ruleGroup.Title,
+ FolderUID: ruleGroup.FolderUID,
+ Interval: ruleGroup.Interval,
+ Rules: provisionedAlertRules,
+ })
+ }
+
+ return alertRuleGroups, nil
+}
diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go
index f8e99aed021..91d7ecc5cd7 100644
--- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go
+++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go
@@ -121,7 +121,7 @@ func TestGetAlertRules(t *testing.T) {
user := &user.SignedInUser{OrgID: 1}
- alertRule := createAlertRule(t, ctx, s, user, false)
+ alertRule := createAlertRule(t, ctx, s, user, false, "")
alertRules, err := s.getAlertRules(ctx, user)
require.NoError(t, err)
@@ -138,10 +138,10 @@ func TestGetAlertRules(t *testing.T) {
user := &user.SignedInUser{OrgID: 1}
- alertRulePaused := createAlertRule(t, ctx, s, user, true)
+ alertRulePaused := createAlertRule(t, ctx, s, user, true, "")
require.True(t, alertRulePaused.IsPaused)
- alertRuleUnpaused := createAlertRule(t, ctx, s, user, false)
+ alertRuleUnpaused := createAlertRule(t, ctx, s, user, false, "")
require.False(t, alertRuleUnpaused.IsPaused)
alertRules, err := s.getAlertRules(ctx, user)
@@ -152,6 +152,83 @@ func TestGetAlertRules(t *testing.T) {
})
}
+func TestGetAlertRuleGroups(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+
+ t.Run("it returns the alert rule groups", func(t *testing.T) {
+ s := setUpServiceTest(t, false).(*Service)
+
+ user := &user.SignedInUser{OrgID: 1}
+
+ ruleGroupTitle := "ruleGroupTitle"
+
+ alertRule1 := createAlertRule(t, ctx, s, user, true, ruleGroupTitle)
+ alertRule2 := createAlertRule(t, ctx, s, user, false, ruleGroupTitle)
+ alertRule3 := createAlertRule(t, ctx, s, user, false, "anotherRuleGroup")
+
+ createAlertRuleGroup(t, ctx, s, user, ruleGroupTitle, []models.AlertRule{alertRule1, alertRule2})
+
+ ruleGroups, err := s.getAlertRuleGroups(ctx, user)
+ require.NoError(t, err)
+ require.Len(t, ruleGroups, 2)
+
+ for _, ruleGroup := range ruleGroups {
+ alertRuleUIDs := make([]string, 0)
+ for _, alertRule := range ruleGroup.Rules {
+ alertRuleUIDs = append(alertRuleUIDs, alertRule.UID)
+ }
+
+ if ruleGroup.Title == ruleGroupTitle {
+ require.Len(t, ruleGroup.Rules, 2)
+ require.ElementsMatch(t, []string{alertRule1.UID, alertRule2.UID}, alertRuleUIDs)
+ } else {
+ require.Len(t, ruleGroup.Rules, 1)
+ require.ElementsMatch(t, []string{alertRule3.UID}, alertRuleUIDs)
+ }
+ }
+ })
+
+ t.Run("with the alert rules state set to paused, it returns the alert rule groups with alert rules paused", func(t *testing.T) {
+ alertRulesState := func(c *setting.Cfg) {
+ c.CloudMigration.AlertRulesState = setting.GMSAlertRulesPaused
+ }
+
+ s := setUpServiceTest(t, false, alertRulesState).(*Service)
+
+ user := &user.SignedInUser{OrgID: 1}
+
+ ruleGroupTitle := "ruleGroupTitle"
+
+ alertRule1 := createAlertRule(t, ctx, s, user, true, ruleGroupTitle)
+ alertRule2 := createAlertRule(t, ctx, s, user, false, ruleGroupTitle)
+ alertRule3 := createAlertRule(t, ctx, s, user, false, "anotherRuleGroup")
+
+ createAlertRuleGroup(t, ctx, s, user, ruleGroupTitle, []models.AlertRule{alertRule1, alertRule2})
+
+ ruleGroups, err := s.getAlertRuleGroups(ctx, user)
+ require.NoError(t, err)
+ require.Len(t, ruleGroups, 2)
+
+ for _, ruleGroup := range ruleGroups {
+ alertRuleUIDs := make([]string, 0)
+ for _, alertRule := range ruleGroup.Rules {
+ alertRuleUIDs = append(alertRuleUIDs, alertRule.UID)
+
+ require.True(t, alertRule.IsPaused)
+ }
+
+ if ruleGroup.Title == ruleGroupTitle {
+ require.Len(t, ruleGroup.Rules, 2)
+ require.ElementsMatch(t, []string{alertRule1.UID, alertRule2.UID}, alertRuleUIDs)
+ } else {
+ require.Len(t, ruleGroup.Rules, 1)
+ require.ElementsMatch(t, []string{alertRule3.UID}, alertRuleUIDs)
+ }
+ }
+ })
+}
+
func createMuteTiming(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser) definitions.MuteTimeInterval {
t.Helper()
@@ -267,12 +344,12 @@ func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Se
require.NoError(t, err)
}
-func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool) models.AlertRule {
+func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool, ruleGroup string) models.AlertRule {
t.Helper()
rule := models.AlertRule{
OrgID: user.GetOrgID(),
- Title: fmt.Sprintf("Alert Rule SLO (Paused: %v)", isPaused),
+ Title: fmt.Sprintf("Alert Rule SLO (Paused: %v) - %v", isPaused, ruleGroup),
NamespaceUID: "folderUID",
Condition: "A",
Data: []models.AlertQuery{
@@ -286,7 +363,7 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *
},
},
IsPaused: isPaused,
- RuleGroup: "ruleGroup",
+ RuleGroup: ruleGroup,
For: time.Minute,
IntervalSeconds: 60,
NoDataState: models.OK,
@@ -298,3 +375,19 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *
return createdRule
}
+
+func createAlertRuleGroup(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, title string, rules []models.AlertRule) models.AlertRuleGroup {
+ t.Helper()
+
+ group := models.AlertRuleGroup{
+ Title: title,
+ FolderUID: "folderUID",
+ Interval: 300,
+ Rules: rules,
+ }
+
+ err := service.ngAlert.Api.AlertRules.ReplaceRuleGroup(ctx, user, group, "")
+ require.NoError(t, err)
+
+ return group
+}
diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go
index 7d0b4b58f49..a46a3b46910 100644
--- a/pkg/services/cloudmigration/model.go
+++ b/pkg/services/cloudmigration/model.go
@@ -88,6 +88,7 @@ const (
FolderDataType MigrateDataType = "FOLDER"
LibraryElementDataType MigrateDataType = "LIBRARY_ELEMENT"
AlertRuleType MigrateDataType = "ALERT_RULE"
+ AlertRuleGroupType MigrateDataType = "ALERT_RULE_GROUP"
ContactPointType MigrateDataType = "CONTACT_POINT"
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go
index 759c60b2e4a..664874d872e 100644
--- a/pkg/services/dashboards/accesscontrol.go
+++ b/pkg/services/dashboards/accesscontrol.go
@@ -5,7 +5,7 @@ import (
"errors"
"strings"
- "github.com/grafana/grafana/pkg/infra/metrics"
+ "github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/folder"
"go.opentelemetry.io/otel"
@@ -65,18 +65,19 @@ func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Serv
return []string{ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)}, nil
}
- folder, err := folderDB.GetFolderByID(ctx, orgID, id)
- if err != nil {
- return nil, err
- }
+ return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) {
+ folder, err := folderDB.GetFolderByID(ctx, orgID, id)
+ if err != nil {
+ return nil, err
+ }
- result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc)
- if err != nil {
- return nil, err
- }
+ result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc)
+ if err != nil {
+ return nil, err
+ }
- result = append([]string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, result...)
- return result, nil
+ return append([]string{ScopeFoldersProvider.GetResourceScopeUID(folder.UID)}, result...), nil
+ })
})
}
@@ -97,17 +98,19 @@ func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttrib
return nil, err
}
- inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderSvc)
- if err != nil {
- return nil, err
- }
- return append(inheritedScopes, ScopeFoldersProvider.GetResourceScopeUID(uid)), nil
+ return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) {
+ inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderSvc)
+ if err != nil {
+ return nil, err
+ }
+ return append(inheritedScopes, ScopeFoldersProvider.GetResourceScopeUID(uid)), nil
+ })
})
}
// NewDashboardIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:id:"
// into uid based scopes for both dashboard and folder
-func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
+func NewDashboardIDScopeResolver(ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
prefix := ScopeDashboardsProvider.GetResourceScope("")
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
ctx, span := tracer.Start(ctx, "dashboards.NewDashboardIDScopeResolver")
@@ -122,18 +125,20 @@ func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardServic
return nil, err
}
- dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{ID: id, OrgID: orgID})
- if err != nil {
- return nil, err
- }
+ return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) {
+ dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{ID: id, OrgID: orgID})
+ if err != nil {
+ return nil, err
+ }
- return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc)
+ return resolveDashboardScope(ctx, orgID, dashboard, folderSvc)
+ })
})
}
// NewDashboardUIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:uid:"
// into uid based scopes for both dashboard and folder
-func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
+func NewDashboardUIDScopeResolver(ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
prefix := ScopeDashboardsProvider.GetResourceScopeUID("")
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
ctx, span := tracer.Start(ctx, "dashboards.NewDashboardUIDScopeResolver")
@@ -148,36 +153,26 @@ func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardServi
return nil, err
}
- dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{UID: uid, OrgID: orgID})
- if err != nil {
- return nil, err
- }
+ return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) {
+ dashboard, err := ds.GetDashboard(ctx, &GetDashboardQuery{UID: uid, OrgID: orgID})
+ if err != nil {
+ return nil, err
+ }
- return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc)
+ return resolveDashboardScope(ctx, orgID, dashboard, folderSvc)
+ })
})
}
-func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) {
+func resolveDashboardScope(ctx context.Context, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) {
ctx, span := tracer.Start(ctx, "dashboards.resolveDashboardScope")
span.End()
var folderUID string
- metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
- // nolint:staticcheck
- if dashboard.FolderID < 0 {
- return []string{ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)}, nil
- }
-
- metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
- // nolint:staticcheck
- if dashboard.FolderID == 0 {
+ if dashboard.FolderUID == "" {
folderUID = ac.GeneralFolderUID
} else {
- folder, err := folderDB.GetFolderByID(ctx, orgID, dashboard.FolderID)
- if err != nil {
- return nil, err
- }
- folderUID = folder.UID
+ folderUID = dashboard.FolderUID
}
result, err := GetInheritedScopes(ctx, orgID, folderUID, folderSvc)
diff --git a/pkg/services/dashboards/accesscontrol_test.go b/pkg/services/dashboards/accesscontrol_test.go
index 026081d0dd0..a08313052f8 100644
--- a/pkg/services/dashboards/accesscontrol_test.go
+++ b/pkg/services/dashboards/accesscontrol_test.go
@@ -60,12 +60,12 @@ func TestNewFolderIDScopeResolver(t *testing.T) {
func TestNewDashboardIDScopeResolver(t *testing.T) {
t.Run("prefix should be expected", func(t *testing.T) {
- prefix, _ := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService())
+ prefix, _ := NewDashboardIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService())
require.Equal(t, "dashboards:id:", prefix)
})
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
- _, resolver := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService())
+ _, resolver := NewDashboardIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService())
_, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:uid:123")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
@@ -73,12 +73,12 @@ func TestNewDashboardIDScopeResolver(t *testing.T) {
func TestNewDashboardUIDScopeResolver(t *testing.T) {
t.Run("prefix should be expected", func(t *testing.T) {
- prefix, _ := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService())
+ prefix, _ := NewDashboardUIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService())
require.Equal(t, "dashboards:uid:", prefix)
})
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
- _, resolver := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService())
+ _, resolver := NewDashboardUIDScopeResolver(&FakeDashboardService{}, foldertest.NewFakeService())
_, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:id:123")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go
index d4abb3627fc..f6b732b426f 100644
--- a/pkg/services/dashboards/service/dashboard_service.go
+++ b/pkg/services/dashboards/service/dashboard_service.go
@@ -27,7 +27,9 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
- "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/grafana/grafana/pkg/apis/dashboard"
+ dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
@@ -93,7 +95,7 @@ func ProvideDashboardServiceImpl(
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
) (*DashboardServiceImpl, error) {
- k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService)
+ k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService)
dashSvc := &DashboardServiceImpl{
cfg: cfg,
@@ -123,8 +125,8 @@ func ProvideDashboardServiceImpl(
return nil, err
}
- ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, folderSvc))
- ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, dashSvc, folderSvc))
+ ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashSvc, folderSvc))
+ ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashSvc, folderSvc))
if err := folderSvc.RegisterService(dashSvc); err != nil {
return nil, err
@@ -1249,20 +1251,24 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits))
// Create a small runtime cache for folders to avoid extra calls to the folder service
foldersMap := make(map[string]*folder.Folder)
+ serviceCtx, serviceIdent := identity.WithServiceIdentity(ctx, query.OrgId)
for i, hit := range response.Hits {
f, ok := foldersMap[hit.Folder]
if !ok {
- f, err = dr.folderService.Get(ctx, &folder.GetFolderQuery{
+ // We can get search result where user don't have access to parents. If that happens this thi
+ // will fail if we call it as the requesting user. To resolve this we call this as the service so we can
+ // garantuee that we can fetch the parent.
+ f, err = dr.folderService.Get(serviceCtx, &folder.GetFolderQuery{
UID: &hit.Folder,
OrgID: query.OrgId,
- SignedInUser: query.SignedInUser,
+ SignedInUser: serviceIdent,
})
if err != nil {
return nil, err
}
foldersMap[hit.Folder] = f
}
- finalResults[i] = dashboards.DashboardSearchProjection{
+ result := dashboards.DashboardSearchProjection{
ID: hit.Field.GetNestedInt64(search.DASHBOARD_LEGACY_ID),
UID: hit.Name,
OrgID: query.OrgId,
@@ -1273,6 +1279,12 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
FolderTitle: f.Title,
Tags: hit.Tags,
}
+
+ if hit.Resource == folderv0alpha1.RESOURCE {
+ result.IsFolder = true
+ }
+
+ finalResults[i] = result
}
return finalResults, nil
@@ -1640,7 +1652,7 @@ func (dr *DashboardServiceImpl) listDashboardsThroughK8s(ctx context.Context, or
return dashboards, nil
}
-func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (*v0alpha1.SearchResults, error) {
+func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (*dashboardv0alpha1.SearchResults, error) {
request := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Fields: []*resource.Requirement{},
@@ -1718,8 +1730,55 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex
request.Options.Fields = append(request.Options.Fields, req...)
}
- if query.Limit > 0 {
- request.Limit = query.Limit
+ if query.IsDeleted {
+ request.IsDeleted = query.IsDeleted
+ }
+
+ if query.Limit < 1 {
+ query.Limit = 1000
+ }
+
+ if query.Page < 1 {
+ query.Page = 1
+ }
+
+ request.Limit = query.Limit
+ request.Page = query.Page
+ request.Offset = query.Page - 1 // bleve's offset is 0 indexed
+
+ namespace := dr.k8sclient.GetNamespace(query.OrgId)
+ var err error
+ var federate *resource.ResourceKey
+ switch query.Type {
+ case "":
+ // When no type specified, search for dashboards
+ request.Options.Key, err = resource.AsResourceKey(namespace, dashboard.DASHBOARD_RESOURCE)
+ // Currently a search query is across folders and dashboards
+ if err == nil {
+ federate, err = resource.AsResourceKey(namespace, folderv0alpha1.RESOURCE)
+ }
+ case searchstore.TypeDashboard, searchstore.TypeAnnotation:
+ request.Options.Key, err = resource.AsResourceKey(namespace, dashboard.DASHBOARD_RESOURCE)
+ case searchstore.TypeFolder, searchstore.TypeAlertFolder:
+ request.Options.Key, err = resource.AsResourceKey(namespace, folderv0alpha1.RESOURCE)
+ default:
+ err = fmt.Errorf("bad type request")
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ if federate != nil {
+ request.Federated = []*resource.ResourceKey{federate}
+ }
+
+ // technically, there exists the ability to register multiple ways of sorting using the legacy database
+ // see RegisterSortOption in pkg/services/search/sorting.go
+ // however, it doesn't look like we are taking advantage of that. And since by default the legacy
+ // sql will sort by title ascending, we only really need to handle the "alpha-desc" case
+ if query.Sort.Name == "alpha-desc" {
+ request.SortBy = append(request.SortBy, &resource.ResourceSearchRequest_Sort{Field: resource.SEARCH_FIELD_TITLE, Desc: true})
}
res, err := dr.k8sclient.Search(ctx, query.OrgId, request)
@@ -1766,7 +1825,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
var mu sync.Mutex
g, ctx := errgroup.WithContext(ctx)
for _, h := range searchResults.Hits {
- func(hit v0alpha1.DashboardHit) {
+ func(hit dashboardv0alpha1.DashboardHit) {
g.Go(func() error {
out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId, v1.GetOptions{})
if err != nil {
@@ -1996,7 +2055,7 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names
finalObj.Object["spec"] = obj
finalObj.SetName(uid)
finalObj.SetNamespace(namespace)
- finalObj.SetGroupVersionKind(v0alpha1.DashboardResourceInfo.GroupVersionKind())
+ finalObj.SetGroupVersionKind(dashboardv0alpha1.DashboardResourceInfo.GroupVersionKind())
meta, err := utils.MetaAccessor(&finalObj)
if err != nil {
diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go
index 58cc555bfd3..5e9167c1f50 100644
--- a/pkg/services/dashboards/service/dashboard_service_test.go
+++ b/pkg/services/dashboards/service/dashboard_service_test.go
@@ -333,6 +333,7 @@ func TestGetDashboard(t *testing.T) {
OrgID: 1,
}
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
@@ -538,6 +539,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled and get from relevant org", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
@@ -636,6 +638,7 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled and get from whatever org it is in", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
@@ -725,6 +728,7 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
@@ -812,6 +816,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled, delete across all orgs, but only delete file based provisioned dashboards", func(t *testing.T) {
_, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid", OrgID: 1}).Return(nil).Once()
fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid3", OrgID: 2}).Return(nil).Once()
@@ -1044,6 +1049,7 @@ func TestGetDashboardsByPluginID(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
@@ -1263,6 +1269,7 @@ func TestDeleteDashboard(t *testing.T) {
t.Run("If UID is not passed in, it should retrieve that first", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
fakeStore.On("CleanupAfterDelete", mock.Anything, mock.Anything).Return(nil).Once()
fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
@@ -1394,6 +1401,7 @@ func TestSearchDashboards(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -1513,6 +1521,7 @@ func TestGetDashboards(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything, mock.Anything).Return(uid1Unstructured, nil)
k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(uid2Unstructured, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
@@ -1595,6 +1604,7 @@ func TestGetDashboardUIDByID(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -1892,6 +1902,7 @@ func TestCountInFolders(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(dashs, nil).Once()
result, err := service.CountInFolders(ctx, 1, []string{"folder1"}, &user.SignedInUser{})
require.NoError(t, err)
@@ -1906,6 +1917,7 @@ func TestSearchDashboardsThroughK8sRaw(t *testing.T) {
query := &dashboards.FindPersistedDashboardsQuery{
OrgId: 1,
}
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -1972,6 +1984,7 @@ func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) {
},
"spec": map[string]any{},
}}
+ k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go
index 85bb0739e32..13520e2a02b 100644
--- a/pkg/services/featuremgmt/codeowners.go
+++ b/pkg/services/featuremgmt/codeowners.go
@@ -29,4 +29,5 @@ const (
grafanaDatabasesFrontend codeowner = "@grafana/databases-frontend"
grafanaOSSBigTent codeowner = "@grafana/oss-big-tent"
growthAndOnboarding codeowner = "@grafana/growth-and-onboarding"
+ grafanaDatasourcesCoreServicesSquad codeowner = "@grafana/grafana-datasources-core-services"
)
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index f0def056b77..ac2eaffc940 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1023,9 +1023,10 @@ var (
{
Name: "alertingSaveStateCompressed",
Description: "Enables the compressed protobuf-based alert state storage",
- Stage: FeatureStageExperimental,
+ Stage: FeatureStagePublicPreview,
FrontendOnly: false,
Owner: grafanaAlertingSquad,
+ Expression: "false",
},
{
Name: "scopeApi",
@@ -1054,7 +1055,7 @@ var (
Description: "Enables using SQL and DuckDB functions as Expressions.",
Stage: FeatureStageExperimental,
FrontendOnly: false,
- Owner: grafanaAppPlatformSquad,
+ Owner: grafanaDatasourcesCoreServicesSquad,
},
{
Name: "nodeGraphDotLayout",
@@ -1784,6 +1785,15 @@ var (
FrontendOnly: true,
Owner: grafanaObservabilityLogsSquad,
},
+ {
+ Name: "grafanaconThemes",
+ Description: "Enables the temporary themes for GrafanaCon",
+ Stage: FeatureStageExperimental,
+ Owner: grafanaFrontendPlatformSquad,
+ HideFromAdminPage: true,
+ HideFromDocs: true,
+ RequiresRestart: true,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index ad6203ebdc7..e88c9c3d176 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -134,11 +134,11 @@ newFolderPicker,experimental,@grafana/grafana-frontend-platform,false,false,true
jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false
onPremToCloudMigrations,preview,@grafana/grafana-operator-experience-squad,false,false,false
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false
-alertingSaveStateCompressed,experimental,@grafana/alerting-squad,false,false,false
+alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false
scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
promQLScope,GA,@grafana/oss-big-tent,false,false,false
logQLScope,privatePreview,@grafana/observability-logs,false,false,false
-sqlExpressions,experimental,@grafana/grafana-app-platform-squad,false,false,false
+sqlExpressions,experimental,@grafana/grafana-datasources-core-services,false,false,false
nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,false,false,true
groupToNestedTableTransformation,GA,@grafana/dataviz-squad,false,false,true
newPDFRendering,GA,@grafana/sharing-squad,false,false,false
@@ -237,3 +237,4 @@ fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false
alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,true,false
alertingAlertmanagerExtraDedupStageStopPipeline,experimental,@grafana/alerting-squad,false,true,false
newLogsPanel,experimental,@grafana/observability-logs,false,false,true
+grafanaconThemes,experimental,@grafana/grafana-frontend-platform,false,true,false
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 8c36acbcc51..e910ed15857 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -958,4 +958,8 @@ const (
// FlagNewLogsPanel
// Enables the new logs panel in Explore
FlagNewLogsPanel = "newLogsPanel"
+
+ // FlagGrafanaconThemes
+ // Enables the temporary themes for GrafanaCon
+ FlagGrafanaconThemes = "grafanaconThemes"
)
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index e1673e7c8ab..607d0581270 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -373,23 +373,27 @@
{
"metadata": {
"name": "alertingSaveStateCompressed",
- "resourceVersion": "1737472824047",
+ "resourceVersion": "1738604435531",
"creationTimestamp": "2025-01-17T18:17:20Z",
"annotations": {
- "grafana.app/updatedTimestamp": "2025-01-21 15:20:24.047499 +0000 UTC"
+ "grafana.app/updatedTimestamp": "2025-02-03 17:40:35.53174 +0000 UTC"
}
},
"spec": {
"description": "Enables the compressed protobuf-based alert state storage",
- "stage": "experimental",
- "codeowner": "@grafana/alerting-squad"
+ "stage": "preview",
+ "codeowner": "@grafana/alerting-squad",
+ "expression": "false"
}
},
{
"metadata": {
"name": "alertingSaveStatePeriodic",
- "resourceVersion": "1718727528075",
- "creationTimestamp": "2024-01-23T16:03:30Z"
+ "resourceVersion": "1738604155684",
+ "creationTimestamp": "2024-01-23T16:03:30Z",
+ "annotations": {
+ "grafana.app/updatedTimestamp": "2025-02-03 17:35:55.684572 +0000 UTC"
+ }
},
"spec": {
"description": "Writes the state periodically to the database, asynchronous to rule evaluation",
@@ -1865,6 +1869,21 @@
"hideFromDocs": true
}
},
+ {
+ "metadata": {
+ "name": "grafanaconThemes",
+ "resourceVersion": "1738661140740",
+ "creationTimestamp": "2025-02-04T09:25:40Z"
+ },
+ "spec": {
+ "description": "Enables the temporary themes for GrafanaCon",
+ "stage": "experimental",
+ "codeowner": "@grafana/grafana-frontend-platform",
+ "requiresRestart": true,
+ "hideFromAdminPage": true,
+ "hideFromDocs": true
+ }
+ },
{
"metadata": {
"name": "groupAttributeSync",
@@ -3643,13 +3662,16 @@
{
"metadata": {
"name": "sqlExpressions",
- "resourceVersion": "1718727528075",
- "creationTimestamp": "2024-02-27T21:16:00Z"
+ "resourceVersion": "1738589190784",
+ "creationTimestamp": "2024-02-27T21:16:00Z",
+ "annotations": {
+ "grafana.app/updatedTimestamp": "2025-02-03 13:26:30.784245615 +0000 UTC"
+ }
},
"spec": {
"description": "Enables using SQL and DuckDB functions as Expressions.",
"stage": "experimental",
- "codeowner": "@grafana/grafana-app-platform-squad"
+ "codeowner": "@grafana/grafana-datasources-core-services"
}
},
{
diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go
index 59ac7d49828..5de4e91da24 100644
--- a/pkg/services/guardian/accesscontrol_guardian_test.go
+++ b/pkg/services/guardian/accesscontrol_guardian_test.go
@@ -962,7 +962,7 @@ func setupAccessControlGuardianTest(
folderStore := foldertest.NewFakeFolderStore(t)
- ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, fakeDashboardService, folderSvc))
+ ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(fakeDashboardService, folderSvc))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(folderSvc))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, folderSvc))
diff --git a/pkg/services/ngalert/notifier/email_test.go b/pkg/services/ngalert/notifier/email_test.go
index beb0251e17d..70e2a8543af 100644
--- a/pkg/services/ngalert/notifier/email_test.go
+++ b/pkg/services/ngalert/notifier/email_test.go
@@ -222,16 +222,26 @@ func (e emailSender) SendWebhook(ctx context.Context, cmd *receivers.SendWebhook
}
func (e emailSender) SendEmail(ctx context.Context, cmd *receivers.SendEmailSettings) error {
+ sendEmailCommand := notifications.SendEmailCommand{
+ To: cmd.To,
+ SingleEmail: cmd.SingleEmail,
+ Template: cmd.Template,
+ Subject: cmd.Subject,
+ Data: cmd.Data,
+ ReplyTo: cmd.ReplyTo,
+ EmbeddedFiles: cmd.EmbeddedFiles,
+ }
+ if len(cmd.EmbeddedContents) > 0 {
+ sendEmailCommand.EmbeddedContents = make([]notifications.EmbeddedContent, len(cmd.EmbeddedContents))
+ for i, ec := range cmd.EmbeddedContents {
+ sendEmailCommand.EmbeddedContents[i] = notifications.EmbeddedContent{
+ Name: ec.Name,
+ Content: ec.Content,
+ }
+ }
+ }
return e.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{
- SendEmailCommand: notifications.SendEmailCommand{
- To: cmd.To,
- SingleEmail: cmd.SingleEmail,
- Template: cmd.Template,
- Subject: cmd.Subject,
- Data: cmd.Data,
- ReplyTo: cmd.ReplyTo,
- EmbeddedFiles: cmd.EmbeddedFiles,
- },
+ SendEmailCommand: sendEmailCommand,
})
}
diff --git a/pkg/services/ngalert/notifier/sender.go b/pkg/services/ngalert/notifier/sender.go
index be2f58b523d..67e68b2abea 100644
--- a/pkg/services/ngalert/notifier/sender.go
+++ b/pkg/services/ngalert/notifier/sender.go
@@ -27,15 +27,25 @@ func (s sender) SendWebhook(ctx context.Context, cmd *receivers.SendWebhookSetti
}
func (s sender) SendEmail(ctx context.Context, cmd *receivers.SendEmailSettings) error {
+ sendEmailCommand := notifications.SendEmailCommand{
+ To: cmd.To,
+ SingleEmail: cmd.SingleEmail,
+ Template: cmd.Template,
+ Subject: cmd.Subject,
+ Data: cmd.Data,
+ ReplyTo: cmd.ReplyTo,
+ EmbeddedFiles: cmd.EmbeddedFiles,
+ }
+ if len(cmd.EmbeddedContents) > 0 {
+ sendEmailCommand.EmbeddedContents = make([]notifications.EmbeddedContent, len(cmd.EmbeddedContents))
+ for i, ec := range cmd.EmbeddedContents {
+ sendEmailCommand.EmbeddedContents[i] = notifications.EmbeddedContent{
+ Name: ec.Name,
+ Content: ec.Content,
+ }
+ }
+ }
return s.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{
- SendEmailCommand: notifications.SendEmailCommand{
- To: cmd.To,
- SingleEmail: cmd.SingleEmail,
- Template: cmd.Template,
- Subject: cmd.Subject,
- Data: cmd.Data,
- ReplyTo: cmd.ReplyTo,
- EmbeddedFiles: cmd.EmbeddedFiles,
- },
+ SendEmailCommand: sendEmailCommand,
})
}
diff --git a/pkg/services/preference/themes.go b/pkg/services/preference/themes.go
index f1332d33f0b..4e88235d5d2 100644
--- a/pkg/services/preference/themes.go
+++ b/pkg/services/preference/themes.go
@@ -11,6 +11,16 @@ var themes = []ThemeDTO{
{ID: "dark", Type: "dark"},
{ID: "system", Type: "dark"},
{ID: "debug", Type: "dark", IsExtra: true},
+ {ID: "aubergine", Type: "dark", IsExtra: true},
+ {ID: "desertbloom", Type: "light", IsExtra: true},
+ {ID: "gildedgrove", Type: "dark", IsExtra: true},
+ {ID: "mars", Type: "dark", IsExtra: true},
+ {ID: "matrix", Type: "dark", IsExtra: true},
+ {ID: "sapphiredusk", Type: "dark", IsExtra: true},
+ {ID: "synthwave", Type: "dark", IsExtra: true},
+ {ID: "tron", Type: "dark", IsExtra: true},
+ {ID: "victorian", Type: "dark", IsExtra: true},
+ {ID: "zen", Type: "light", IsExtra: true},
}
func GetThemeByID(id string) *ThemeDTO {
diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go
index 744f5e09e18..89370542de1 100644
--- a/pkg/services/rendering/rendering.go
+++ b/pkg/services/rendering/rendering.go
@@ -424,7 +424,7 @@ func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) {
// getGrafanaCallbackURL creates a URL to send to the image rendering as callback for rendering a Grafana resource
func (rs *RenderingService) getGrafanaCallbackURL(path string) string {
- if rs.Cfg.RendererUrl != "" {
+ if rs.Cfg.RendererUrl != "" || rs.Cfg.RendererCallbackUrl != "" {
// The backend rendering service can potentially be remote.
// So we need to use the root_url to ensure the rendering service
// can reach this Grafana instance.
@@ -433,11 +433,6 @@ func (rs *RenderingService) getGrafanaCallbackURL(path string) string {
return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path)
}
- if rs.Cfg.RendererCallbackUrl != "" {
- // &render=1 signals to the legacy redirect layer to
- return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path)
- }
-
protocol := rs.Cfg.Protocol
switch protocol {
case setting.HTTPScheme:
diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go
index 1b48756ea07..137cf29db12 100644
--- a/pkg/services/search/service.go
+++ b/pkg/services/search/service.go
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/search/model"
- "github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@@ -114,20 +113,6 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model.
dashboardQuery.Sort = sortOpt
}
- // if folders are stored in unified storage, we need to use the folder service to query for folders
- if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) && (query.Type == searchstore.TypeFolder || query.Type == searchstore.TypeAlertFolder) {
- hits, err := s.folderService.SearchFolders(ctx, folder.SearchFoldersQuery{
- OrgID: query.OrgId,
- UIDs: query.FolderUIDs,
- IDs: query.FolderIds,
- Title: query.Title,
- Limit: query.Limit,
- SignedInUser: query.SignedInUser,
- })
-
- return sortedHits(hits), err
- }
-
hits, err := s.dashboardService.SearchDashboards(ctx, &dashboardQuery)
if err != nil {
return nil, err
diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod
index 7e3edf8535b..5b5d4913d87 100644
--- a/pkg/storage/unified/apistore/go.mod
+++ b/pkg/storage/unified/apistore/go.mod
@@ -117,6 +117,11 @@ require (
github.com/distribution/reference v0.6.0 // indirect
github.com/dlmiddlecote/sqlstats v1.0.2 // indirect
github.com/docker/go-units v0.5.0 // indirect
+ github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect
+ github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 // indirect
+ github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 // indirect
+ github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect
+ github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elazarl/goproxy v1.3.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
@@ -170,7 +175,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
- github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 // indirect
+ github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 // indirect
github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
@@ -205,7 +210,6 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.2 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
- github.com/jeremywohl/flatten v1.0.1 // indirect
github.com/jessevdk/go-flags v1.5.0 // indirect
github.com/jhump/protoreflect v1.15.1 // indirect
github.com/jmespath-community/go-jmespath v1.1.1 // indirect
@@ -220,6 +224,7 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
+ github.com/lestrrat-go/strftime v1.0.4 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
@@ -286,6 +291,7 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
@@ -296,6 +302,7 @@ require (
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/tetratelabs/wazero v1.8.2 // indirect
github.com/tjhop/slog-gokit v0.1.3 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
@@ -352,6 +359,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
+ gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.32.1 // indirect
diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum
index e74589ac323..0ec94d6bec8 100644
--- a/pkg/storage/unified/apistore/go.sum
+++ b/pkg/storage/unified/apistore/go.sum
@@ -312,6 +312,16 @@ github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
+github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY=
+github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM=
+github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA=
+github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 h1:b9ygz2zMlMv2AcJ9mLL0b60tJ9Qd+bzfsAY5ZDTu9VY=
+github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7/go.mod h1:jYEJ8tNkA7K3k39X8iMqaX3MSMmViRgh222JSLHDgVc=
+github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ=
+github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
+github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa h1:kyoPzxViSXAyqfO0Mab7Qo1UogFIrxZKKyBU6kBOl+E=
+github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
@@ -547,8 +557,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 h1:dmsycYQzl5JexuV8UxQpT3B79maSvhiIahid4/tezAM=
-github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
+github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 h1:J3PIK9OL3ZHPypYHlcK+nBREwYL3ROZ3fJyNMsTYlpk=
+github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY=
github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8=
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo=
@@ -654,8 +664,6 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
-github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
-github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
@@ -725,6 +733,10 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
+github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
+github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -1004,6 +1016,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
+github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
+github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o=
github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs=
@@ -1306,6 +1320,7 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1536,6 +1551,8 @@ gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc=
+gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod
index c63fc1d605d..2437b312dd4 100644
--- a/pkg/storage/unified/resource/go.mod
+++ b/pkg/storage/unified/resource/go.mod
@@ -17,6 +17,7 @@ require (
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
github.com/grafana/grafana-plugin-sdk-go v0.263.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d
+ github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/prometheus/client_golang v1.20.5
@@ -116,12 +117,11 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
- github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 // indirect
+ github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect
- github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/grafana/sqlds/v4 v4.1.3 // indirect
diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum
index ae8f767831d..38be5e15f37 100644
--- a/pkg/storage/unified/resource/go.sum
+++ b/pkg/storage/unified/resource/go.sum
@@ -214,6 +214,16 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
+github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY=
+github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM=
+github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA=
+github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 h1:b9ygz2zMlMv2AcJ9mLL0b60tJ9Qd+bzfsAY5ZDTu9VY=
+github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7/go.mod h1:jYEJ8tNkA7K3k39X8iMqaX3MSMmViRgh222JSLHDgVc=
+github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ=
+github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
+github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa h1:kyoPzxViSXAyqfO0Mab7Qo1UogFIrxZKKyBU6kBOl+E=
+github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
@@ -411,8 +421,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65 h1:dmsycYQzl5JexuV8UxQpT3B79maSvhiIahid4/tezAM=
-github.com/grafana/alerting v0.0.0-20250130152446-d49e2e0b7d65/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
+github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64 h1:J3PIK9OL3ZHPypYHlcK+nBREwYL3ROZ3fJyNMsTYlpk=
+github.com/grafana/alerting v0.0.0-20250205180645-995709fe8d64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029 h1:0C0a1FEkecxDk60su4uuOozqBzqz/4nmfNFSxXnCViY=
github.com/grafana/authlib v0.0.0-20250204100101-00a7f40e4029/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8=
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo=
@@ -498,8 +508,6 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
-github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
-github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
@@ -562,6 +570,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
+github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
+github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -757,6 +767,8 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJV
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
@@ -798,6 +810,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
+github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
+github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o=
github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs=
@@ -1133,6 +1147,8 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
+gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc=
+gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go
index 9a41f4c4e5a..0e72212de6f 100644
--- a/pkg/storage/unified/resource/resource.pb.go
+++ b/pkg/storage/unified/resource/resource.pb.go
@@ -1814,7 +1814,9 @@ type ResourceSearchRequest struct {
// the return fields (empty will return everything)
Fields []string `protobuf:"bytes,8,rep,name=fields,proto3" json:"fields,omitempty"`
// explain each result (added to the each row)
- Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"`
+ Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"`
+ IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"`
+ Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1912,6 +1914,20 @@ func (x *ResourceSearchRequest) GetExplain() bool {
return false
}
+func (x *ResourceSearchRequest) GetIsDeleted() bool {
+ if x != nil {
+ return x.IsDeleted
+ }
+ return false
+}
+
+func (x *ResourceSearchRequest) GetPage() int64 {
+ if x != nil {
+ return x.Page
+ }
+ return 0
+}
+
type ResourceSearchResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Error details
@@ -3810,7 +3826,7 @@ var file_resource_proto_rawDesc = []byte{
0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74,
- 0x22, 0xbb, 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61,
+ 0x22, 0xee, 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61,
0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f,
@@ -3833,321 +3849,324 @@ var file_resource_proto_rawDesc = []byte{
0x61, 0x63, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x08,
0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07,
0x65, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65,
- 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14,
- 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66,
- 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65,
+ 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c,
+ 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65,
+ 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72,
0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x1a, 0x5f, 0x0a,
- 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
- 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a,
- 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x61,
- 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea,
- 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63,
- 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72,
- 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52,
- 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
- 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c,
- 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, 0x73,
- 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, 0x74,
- 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18,
- 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, 0x74,
- 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20,
- 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a,
- 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46,
- 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74,
- 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69,
- 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64,
- 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
- 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e,
- 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67,
- 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
- 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
+ 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46,
+ 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69,
+ 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
+ 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
+ 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
+ 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
+ 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65,
+ 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05,
+ 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75,
+ 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b,
+ 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65,
+ 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68,
+ 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c,
+ 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f,
+ 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x43,
+ 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65,
+ 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65,
+ 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, 0x72,
- 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12,
- 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74,
- 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, 0x63,
- 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c,
- 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72,
- 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74,
- 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x78, 0x0a, 0x1c, 0x4c,
- 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a,
- 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e,
- 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f,
- 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
- 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xda, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
- 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73,
- 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79,
- 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e,
- 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e,
- 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f,
- 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72,
- 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
- 0x1a, 0x9f, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06, 0x6f, 0x62, 0x6a,
- 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79,
- 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04,
- 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68,
- 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
- 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x06, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f,
- 0x6c, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64,
- 0x65, 0x72, 0x22, 0x51, 0x0a, 0x1d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73,
- 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
- 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x1e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52,
- 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73,
- 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d,
- 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f,
- 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52,
- 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72,
- 0x72, 0x6f, 0x72, 0x1a, 0x77, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43,
- 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f,
- 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69,
- 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18,
- 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2e, 0x0a, 0x12,
- 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a,
- 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
- 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75,
- 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72,
- 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e,
- 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49,
- 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56,
- 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45,
- 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x87, 0x02, 0x0a, 0x0d, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x41, 0x0a, 0x07,
- 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e,
- 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69,
- 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12,
- 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
- 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12,
- 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b,
- 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61,
- 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28,
- 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69,
- 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f,
- 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
- 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43,
- 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69,
- 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x04, 0x74, 0x79,
- 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c,
- 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f,
- 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79,
- 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x03,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79, 0x12, 0x20, 0x0a,
- 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12,
- 0x52, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75,
- 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f,
- 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74,
- 0x69, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18,
- 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x1a,
- 0xae, 0x01, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x23,
- 0x0a, 0x0d, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x56, 0x61, 0x6c,
- 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x74, 0x65, 0x78, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, 0x65, 0x65, 0x54, 0x65, 0x78, 0x74,
- 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c, 0x65,
- 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x64,
- 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01,
- 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65,
- 0x22, 0x95, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12,
- 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10,
- 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a,
- 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e,
- 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x10, 0x04,
- 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x44,
- 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x45, 0x10,
- 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x08,
- 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x09, 0x12, 0x0a, 0x0a, 0x06,
- 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x12, 0x27, 0x0a,
- 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65,
- 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
- 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
- 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c,
- 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63,
- 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22,
- 0x64, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15,
- 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65,
- 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
- 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f,
- 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05,
- 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
- 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
- 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12,
- 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18,
- 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79,
- 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
- 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68,
- 0x6f, 0x64, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04,
- 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, 0x50, 0x75, 0x74, 0x42, 0x6c,
- 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72,
- 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
- 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c,
- 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x73,
- 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12,
- 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68,
- 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65,
- 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65,
- 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0x98, 0x01, 0x0a, 0x0e, 0x47,
- 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a,
- 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72,
- 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x6d,
- 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18,
- 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79,
- 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f,
- 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72,
- 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52,
- 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74,
- 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
- 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76,
- 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
- 0x65, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72,
- 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74,
- 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45,
- 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64,
- 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
- 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72,
- 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06,
- 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
- 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74,
- 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c,
- 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44,
- 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72,
- 0x65, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
- 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15,
- 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a,
- 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14,
- 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45,
- 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72,
- 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74,
- 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x32, 0xe8, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72,
- 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x6b, 0x0a, 0x16, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52,
- 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73,
- 0x12, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e,
+ 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x61,
+ 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a,
+ 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69,
+ 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73,
+ 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73,
+ 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05,
+ 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63,
+ 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a,
+ 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+ 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05,
+ 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
+ 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61,
+ 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x78,
+ 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79,
+ 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26,
+ 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65,
+ 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67,
+ 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
+ 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73,
+ 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xda, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73,
0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63,
- 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69,
- 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73,
- 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x26, 0x2e, 0x72,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f,
- 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
- 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62,
- 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8b, 0x01,
- 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x50,
- 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42,
- 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47,
- 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42,
- 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44,
- 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73,
- 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
+ 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x69, 0x74,
+ 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74,
+ 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26,
+ 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65,
+ 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67,
+ 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72,
+ 0x72, 0x6f, 0x72, 0x1a, 0x9f, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x2d, 0x0a, 0x06,
+ 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70,
+ 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12,
+ 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68,
+ 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65,
+ 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x16, 0x0a,
+ 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66,
+ 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x51, 0x0a, 0x1d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65,
+ 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
+ 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73,
+ 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x1e, 0x43, 0x6f, 0x75,
+ 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65,
+ 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69,
+ 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73,
+ 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x75,
+ 0x6e, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72,
+ 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52,
+ 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x77, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
+ 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73,
+ 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70,
+ 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a,
+ 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75,
+ 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22,
+ 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22,
+ 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+ 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
- 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61,
- 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75,
- 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62,
- 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74,
+ 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d,
+ 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a,
+ 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45,
+ 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53,
+ 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56,
+ 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0x87, 0x02,
+ 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12,
+ 0x41, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44,
+ 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d,
+ 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x1a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f,
+ 0x77, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f,
+ 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78,
+ 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65,
+ 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69,
+ 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74,
+ 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf1, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44,
+ 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
+ 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a,
+ 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54,
+ 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69,
+ 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52,
+ 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61,
+ 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79,
+ 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18,
+ 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73,
+ 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x43,
+ 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e,
+ 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70,
+ 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69,
+ 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69,
+ 0x74, 0x79, 0x1a, 0xae, 0x01, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65,
+ 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75,
+ 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65,
+ 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x74,
+ 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x66, 0x72, 0x65, 0x65, 0x54,
+ 0x65, 0x78, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61, 0x62, 0x6c,
+ 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x61,
+ 0x62, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18,
+ 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x12, 0x23,
+ 0x0a, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
+ 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61,
+ 0x6c, 0x75, 0x65, 0x22, 0x95, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x54, 0x79,
+ 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x54, 0x59,
+ 0x50, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01,
+ 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a,
+ 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36,
+ 0x34, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x10, 0x05, 0x12, 0x0a,
+ 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41,
+ 0x54, 0x45, 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d,
+ 0x45, 0x10, 0x08, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x09, 0x12,
+ 0x0a, 0x0a, 0x06, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0a, 0x22, 0x94, 0x01, 0x0a, 0x10,
+ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x77,
+ 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
+ 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72,
+ 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20,
+ 0x03, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62,
+ 0x6a, 0x65, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65,
+ 0x63, 0x74, 0x22, 0x64, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a,
+ 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
+ 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x74,
+ 0x6f, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65,
+ 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c,
+ 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73,
+ 0x69, 0x6f, 0x6e, 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
+ 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52,
+ 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74,
+ 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68,
+ 0x6f, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79,
+ 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+ 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d,
+ 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12,
+ 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x22, 0xc1, 0x01, 0x0a, 0x0f, 0x50, 0x75,
+ 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
+ 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73,
+ 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69,
+ 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03,
+ 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12,
+ 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69,
+ 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74,
+ 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54,
+ 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0x98, 0x01,
+ 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f,
+ 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28,
+ 0x0a, 0x10, 0x6d, 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74,
+ 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72,
+ 0x6f, 0x78, 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74,
+ 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05,
+ 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75,
+ 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63,
+ 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14,
+ 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c,
+ 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09,
+ 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52,
+ 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
+ 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70,
+ 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06,
+ 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74,
+ 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x73,
+ 0x74, 0x6f, 0x72, 0x65, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
+ 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19,
+ 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72,
+ 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73,
+ 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73,
+ 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74,
+ 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53,
+ 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53,
+ 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
+ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
+ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe8, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69,
+ 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x6b, 0x0a, 0x16, 0x43, 0x6f, 0x75,
+ 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65,
+ 0x63, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43,
+ 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62,
+ 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70,
+ 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
+ 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12,
+ 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52,
+ 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
+ 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72,
+ 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x32, 0x8b, 0x01, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e,
+ 0x0a, 0x07, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50,
+ 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e,
+ 0x0a, 0x07, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47,
+ 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57,
+ 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a,
+ 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63,
+ 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75,
+ 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72,
+ 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
+ 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto
index ae7d067ff5a..2624e36dc94 100644
--- a/pkg/storage/unified/resource/resource.proto
+++ b/pkg/storage/unified/resource/resource.proto
@@ -225,9 +225,9 @@ enum ResourceVersionMatch {
}
message ListRequest {
- enum Source {
+ enum Source {
STORE = 0; // the standard place
- HISTORY = 1;
+ HISTORY = 1;
TRASH = 2;
}
@@ -248,7 +248,7 @@ message ListRequest {
ListOptions options = 5;
// Select values from history or trash
- Source source = 6;
+ Source source = 6;
}
message ListResponse {
@@ -329,7 +329,7 @@ message ResourceStatsRequest {
// NOTE, this query may need to federate across a few storage instances
repeated string kinds = 2;
- // Limit the stats within a folder (not recursive!)
+ // Limit the stats within a folder (not recursive!)
string folder = 3;
}
@@ -356,7 +356,7 @@ message ResourceSearchRequest {
string field = 1;
bool desc = 2; // defaults to ascending
}
-
+
message Facet {
string field = 1;
int64 limit = 2;
@@ -370,8 +370,8 @@ message ResourceSearchRequest {
// To search additional resource types, add additional keys to this list
// NOTE: queries will only support federation across kinds with common fields
- repeated ResourceKey federated = 2;
-
+ repeated ResourceKey federated = 2;
+
// When a query exists, it is parsed and used to influence
// query string for chosen implementation (currently just bleve)
// The score is only relevant when a query exists
@@ -394,6 +394,10 @@ message ResourceSearchRequest {
// explain each result (added to the each row)
bool explain = 9;
+
+ bool is_deleted = 10;
+
+ int64 page = 11;
}
message ResourceSearchResponse {
@@ -406,7 +410,7 @@ message ResourceSearchResponse {
// Top term stats
repeated TermFacet terms = 4;
// numeric range
- // date range facets
+ // date range facets
}
message TermFacet {
@@ -444,7 +448,7 @@ message ListRepositoryObjectsRequest {
// Namespace (tenant)
string namespace = 2;
-
+
// The name of the repository
string name = 3;
}
@@ -456,16 +460,16 @@ message ListRepositoryObjectsResponse {
// Hash for the resource
string path = 2;
-
+
// Verification hash from the origin
string hash = 3;
-
+
// Change time from the origin
int64 time = 5;
// Title inside the payload
string title = 6;
-
+
// The name of the folder in metadata
string folder = 7;
}
@@ -484,7 +488,7 @@ message ListRepositoryObjectsResponse {
message CountRepositoryObjectsRequest {
// Namespace (tenant)
string namespace = 1;
-
+
// The name of the repository
// empty to count across all repositories
string name = 2;
@@ -559,16 +563,16 @@ message ResourceTableColumnDefinition {
STRING = 1;
BOOLEAN = 2;
INT32 = 3;
- INT64 = 4;
- FLOAT = 5;
- DOUBLE = 6;
- DATE = 7;
- DATE_TIME = 8;
- BINARY = 9;
+ INT64 = 4;
+ FLOAT = 5;
+ DOUBLE = 6;
+ DATE = 7;
+ DATE_TIME = 8;
+ BINARY = 9;
OBJECT = 10; // map[string]any
}
- // These values are not part of standard k8s format
+ // These values are not part of standard k8s format
// however these are useful when indexing and analyzing results
message Properties {
// All values in this columns should be unique
@@ -618,7 +622,7 @@ message ResourceTableRow {
// The resource version for the given values
int64 resource_version = 2;
- // Cells will be as wide as the column definitions array
+ // Cells will be as wide as the column definitions array
// Numeric values will be encoded using big endian bytes
// All arrays will be JSON encoded
repeated bytes cells = 3;
@@ -638,7 +642,7 @@ message ResourceTableRow {
message RestoreRequest {
// Full key must be set
ResourceKey key = 1;
-
+
// The resource version to restore
int64 resource_version = 2;
}
@@ -759,7 +763,7 @@ service ResourceIndex {
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
}
-// Query repository info from the search index.
+// Query repository info from the search index.
// Results access control is based on access to the repository *not* the items
service RepositoryIndex {
// Describe how many resources of each type exist within a repository
diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go
index f0d18960dc5..dc990d499eb 100644
--- a/pkg/storage/unified/resource/search.go
+++ b/pkg/storage/unified/resource/search.go
@@ -10,6 +10,8 @@ import (
"sync"
"time"
+ dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/hashicorp/golang-lru/v2/expirable"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -651,3 +653,34 @@ func (s *builderCache) get(ctx context.Context, key NamespacedResource) (Documen
}
return s.defaultBuilder, nil
}
+
+// AsResourceKey converts the given namespace and type to a search key
+func AsResourceKey(ns string, t string) (*ResourceKey, error) {
+ if ns == "" {
+ return nil, fmt.Errorf("missing namespace")
+ }
+ switch t {
+ case "folders", "folder":
+ return &ResourceKey{
+ Namespace: ns,
+ Group: folderv0alpha1.GROUP,
+ Resource: folderv0alpha1.RESOURCE,
+ }, nil
+ case "dashboards", "dashboard":
+ return &ResourceKey{
+ Namespace: ns,
+ Group: dashboardv0alpha1.GROUP,
+ Resource: dashboardv0alpha1.RESOURCE,
+ }, nil
+
+ // NOT really supported in the dashboard search UI, but useful for manual testing
+ case "playlist", "playlists":
+ return &ResourceKey{
+ Namespace: ns,
+ Group: "playlist.grafana.app",
+ Resource: "playlists",
+ }, nil
+ }
+
+ return nil, fmt.Errorf("unknown resource type")
+}
diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go
index a9c9c95ef9d..43e0faa7b1e 100644
--- a/pkg/storage/unified/resource/search_client.go
+++ b/pkg/storage/unified/resource/search_client.go
@@ -1,24 +1,20 @@
package resource
import (
+ "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/setting"
)
-// Search Fallback was returning both Folders and Dashboards which resulted
-// in issues with rendering the Folder UI. Also, filters are not implemented
-// yet. For those reasons, we will be disabling Search Fallback for now
func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient {
- // config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey]
- // if !ok {
- // return legacyClient
- // }
+ config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey]
+ if !ok {
+ return legacyClient
+ }
- // switch config.DualWriterMode {
- // case rest.Mode0, rest.Mode1, rest.Mode2:
- // return legacyClient
- // default:
- // return unifiedClient
- // }
-
- return unifiedClient
+ switch config.DualWriterMode {
+ case rest.Mode0, rest.Mode1, rest.Mode2:
+ return legacyClient
+ default:
+ return unifiedClient
+ }
}
diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json
index 7667a82756c..e2e8df6926b 100644
--- a/public/api-enterprise-spec.json
+++ b/public/api-enterprise-spec.json
@@ -5670,6 +5670,7 @@
"FOLDER",
"LIBRARY_ELEMENT",
"ALERT_RULE",
+ "ALERT_RULE_GROUP",
"CONTACT_POINT",
"NOTIFICATION_POLICY",
"NOTIFICATION_TEMPLATE",
diff --git a/public/api-merged.json b/public/api-merged.json
index e063c8c959c..3e2239131d4 100644
--- a/public/api-merged.json
+++ b/public/api-merged.json
@@ -17182,6 +17182,7 @@
"FOLDER",
"LIBRARY_ELEMENT",
"ALERT_RULE",
+ "ALERT_RULE_GROUP",
"CONTACT_POINT",
"NOTIFICATION_POLICY",
"NOTIFICATION_TEMPLATE",
diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
index a24bd4b027b..16d91551060 100644
--- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx
+++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
@@ -17,6 +17,7 @@ import {
FeatureBadge,
Combobox,
ComboboxOption,
+ TextLink,
} from '@grafana/ui';
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { t, Trans } from 'app/core/internationalization';
@@ -80,7 +81,20 @@ export class SharedPreferences extends PureComponent {
navbar: { bookmarkUrls: [] },
};
- this.themeOptions = getBuiltInThemes(config.featureToggles.extraThemes).map((theme) => ({
+ const allowedExtraThemes = [];
+
+ if (config.featureToggles.extraThemes) {
+ allowedExtraThemes.push('debug');
+ }
+
+ if (config.featureToggles.grafanaconThemes) {
+ allowedExtraThemes.push('desertbloom');
+ allowedExtraThemes.push('gildedgrove');
+ allowedExtraThemes.push('sapphiredusk');
+ allowedExtraThemes.push('tron');
+ }
+
+ this.themeOptions = getBuiltInThemes(allowedExtraThemes).map((theme) => ({
value: theme.id,
label: getTranslatedThemeName(theme),
}));
@@ -168,6 +182,20 @@ export class SharedPreferences extends PureComponent {
loading={isLoading}
disabled={isLoading}
label={t('shared-preferences.fields.theme-label', 'Interface theme')}
+ description={
+ config.featureToggles.grafanaconThemes ? (
+
+ Enjoying the limited edition themes? Tell us what you'd like to see{' '}
+
+ here.
+
+
+ ) : undefined
+ }
>
{
return this._initialState;
}
- public getNextPanelId(): number {
- return this.state.body.getMaxPanelId() + 1;
- }
-
public addPanel(vizPanel: VizPanel): void {
if (!this.state.isEditing) {
this.onEnterEditMode();
@@ -503,7 +503,7 @@ export class DashboardScene extends SceneObjectBase {
}
public duplicatePanel(vizPanel: VizPanel) {
- this.state.body.duplicatePanel(vizPanel);
+ getLayoutManagerFor(vizPanel).duplicatePanel(vizPanel);
}
public copyPanel(vizPanel: VizPanel) {
@@ -537,7 +537,7 @@ export class DashboardScene extends SceneObjectBase {
}
public removePanel(panel: VizPanel) {
- this.state.body.removePanel(panel);
+ getLayoutManagerFor(panel).removePanel(panel);
}
public unlinkLibraryPanel(panel: VizPanel) {
diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx
index 794cbe2b2d8..e30068924e7 100644
--- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx
@@ -5,7 +5,6 @@ import { PageLayoutType } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
-import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
import { useSelector } from 'app/types';
import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitter';
@@ -15,7 +14,7 @@ import { PanelSearchLayout } from './PanelSearchLayout';
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
export function DashboardSceneRenderer({ model }: SceneComponentProps) {
- const { controls, overlay, editview, editPanel, isEmpty, viewPanelScene, panelSearch, panelsPerRow, isEditing } =
+ const { controls, overlay, editview, editPanel, viewPanelScene, panelSearch, panelsPerRow, isEditing } =
model.useState();
const { type } = useParams();
const location = useLocation();
@@ -56,9 +55,6 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps
- {isEmpty && (
-
- )}
>
);
diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx
index 8a06e7c9031..fdb9dd9fc38 100644
--- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx
@@ -26,22 +26,6 @@ describe('DefaultGridLayoutManager', () => {
});
});
- describe('getMaxPanelId', () => {
- it('should get max panel id in a simple 3 panel layout', () => {
- const { manager } = setup();
- const id = manager.getMaxPanelId();
-
- expect(id).toBe(3);
- });
-
- it('should return 0 if no panels are found', () => {
- const { manager } = setup({ gridItems: [] });
- const id = manager.getMaxPanelId();
-
- expect(id).toBe(0);
- });
- });
-
describe('addPanel', () => {
it('Should add a new panel', () => {
const { manager } = setup();
diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx
index d714bc87d4d..a1416c5deb0 100644
--- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx
@@ -9,11 +9,14 @@ import {
sceneUtils,
SceneComponentProps,
SceneGridItemLike,
+ useSceneObjectState,
} from '@grafana/scenes';
import { GRID_COLUMN_COUNT } from 'app/core/constants';
import { t } from 'app/core/internationalization';
+import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
import { isClonedKey, joinCloneKeys } from '../../utils/clone';
+import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import {
forceRenderChildren,
getPanelIdForVizPanel,
@@ -72,7 +75,7 @@ export class DefaultGridLayoutManager
}
public addPanel(vizPanel: VizPanel): void {
- const panelId = this.getNextPanelId();
+ const panelId = dashboardSceneGraph.getNextPanelId(this);
vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
vizPanel.clearParent();
@@ -95,7 +98,8 @@ export class DefaultGridLayoutManager
* Adds a new empty row
*/
public addNewRow(): SceneGridRow {
- const id = this.getNextPanelId();
+ const id = dashboardSceneGraph.getNextPanelId(this);
+
const row = new SceneGridRow({
key: getVizPanelKeyForPanelId(id),
title: 'Row title',
@@ -183,7 +187,7 @@ export class DefaultGridLayoutManager
let panelData;
let newGridItem;
- const newPanelId = this.getNextPanelId();
+ const newPanelId = dashboardSceneGraph.getNextPanelId(this);
const grid = this.state.grid;
if (gridItem instanceof DashboardGridItem) {
@@ -248,53 +252,6 @@ export class DefaultGridLayoutManager
return panels;
}
- public getMaxPanelId(): number {
- let max = 0;
-
- for (const child of this.state.grid.state.children) {
- if (child instanceof DashboardGridItem) {
- const vizPanel = child.state.body;
-
- if (vizPanel) {
- const panelId = getPanelIdForVizPanel(vizPanel);
-
- if (panelId > max) {
- max = panelId;
- }
- }
- }
-
- if (child instanceof SceneGridRow) {
- //rows follow the same key pattern --- e.g.: `panel-6`
- const panelId = getPanelIdForVizPanel(child);
-
- if (panelId > max) {
- max = panelId;
- }
-
- for (const rowChild of child.state.children) {
- if (rowChild instanceof DashboardGridItem) {
- const vizPanel = rowChild.state.body;
-
- if (vizPanel) {
- const panelId = getPanelIdForVizPanel(vizPanel);
-
- if (panelId > max) {
- max = panelId;
- }
- }
- }
- }
- }
- }
-
- return max;
- }
-
- public getNextPanelId(): number {
- return getDashboardSceneFor(this).getNextPanelId();
- }
-
public collapseAllRows(): void {
this.state.grid.state.children.forEach((child) => {
if (!(child instanceof SceneGridRow)) {
@@ -493,6 +450,16 @@ export class DefaultGridLayoutManager
}
public static Component = ({ model }: SceneComponentProps) => {
+ const { children } = useSceneObjectState(model.state.grid, { shouldActivateOrKeepAlive: true });
+ const dashboard = getDashboardSceneFor(model);
+
+ // If we are top level layout and have no children, show empty state
+ if (model.parent === dashboard && children.length === 0) {
+ return (
+
+ );
+ }
+
return ;
};
}
diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
index a1e4798b6c2..91fcbec0a48 100644
--- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
@@ -4,7 +4,8 @@ import { Select } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
-import { getDashboardSceneFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils';
+import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
+import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
@@ -33,10 +34,17 @@ export class ResponsiveGridLayoutManager
public readonly descriptor = ResponsiveGridLayoutManager.descriptor;
+ public constructor(state: ResponsiveGridLayoutManagerState) {
+ super(state);
+
+ //@ts-ignore
+ this.state.layout.getDragClassCancel = () => 'drag-cancel';
+ }
+
public editModeChanged(isEditing: boolean): void {}
public addPanel(vizPanel: VizPanel): void {
- const panelId = this.getNextPanelId();
+ const panelId = dashboardSceneGraph.getNextPanelId(this);
vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
vizPanel.clearParent();
@@ -52,33 +60,35 @@ export class ResponsiveGridLayoutManager
getDashboardSceneFor(this).switchLayout(rowsLayout);
}
- public getMaxPanelId(): number {
- let max = 0;
-
- for (const child of this.state.layout.state.children) {
- if (child instanceof VizPanel) {
- let panelId = getPanelIdForVizPanel(child);
-
- if (panelId > max) {
- max = panelId;
- }
- }
- }
-
- return max;
- }
-
- public getNextPanelId(): number {
- return getDashboardSceneFor(this).getNextPanelId();
- }
-
public removePanel(panel: VizPanel) {
const element = panel.parent;
this.state.layout.setState({ children: this.state.layout.state.children.filter((child) => child !== element) });
}
public duplicatePanel(panel: VizPanel): void {
- throw new Error('Method not implemented.');
+ const gridItem = panel.parent;
+ if (!(gridItem instanceof ResponsiveGridItem)) {
+ console.error('Trying to duplicate a panel that is not inside a DashboardGridItem');
+ return;
+ }
+
+ const newPanelId = dashboardSceneGraph.getNextPanelId(this);
+ const grid = this.state.layout;
+
+ const newGridItem = gridItem.clone({
+ key: getGridItemKeyForPanelId(newPanelId),
+ body: panel.clone({
+ key: getVizPanelKeyForPanelId(newPanelId),
+ }),
+ });
+
+ const sourceIndex = grid.state.children.indexOf(gridItem);
+ const newChildren = [...grid.state.children];
+
+ // insert after
+ newChildren.splice(sourceIndex + 1, 0, newGridItem);
+
+ grid.setState({ children: newChildren });
}
public getVizPanels(): VizPanel[] {
@@ -124,9 +134,10 @@ export class ResponsiveGridLayoutManager
});
}
- activateRepeaters?(): void {
- throw new Error('Method not implemented.');
- }
+ /**
+ * Might as well implement this as a no-top function as vs-code very eagerly adds optional functions that throw Method not implemented
+ */
+ public activateRepeaters(): void {}
public static Component = ({ model }: SceneComponentProps) => {
return ;
diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
index 10ff3f66550..dc9154d3b2b 100644
--- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
@@ -75,14 +75,6 @@ export class RowsLayoutManager extends SceneObjectBase i
});
}
- public getMaxPanelId(): number {
- return Math.max(...this.state.rows.map((row) => row.getLayout().getMaxPanelId()));
- }
-
- public getNextPanelId(): number {
- return 0;
- }
-
public removePanel(panel: VizPanel) {}
public removeRow(row: RowItem) {
diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts
index 572d7da9549..fbcd4200d36 100644
--- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts
+++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts
@@ -38,11 +38,6 @@ export interface DashboardLayoutManager extends SceneObject {
*/
getVizPanels(): VizPanel[];
- /**
- * Returns the highest panel id in the layout
- */
- getMaxPanelId(): number;
-
/**
* Add row
*/
diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap
index a62ba3c1daf..7695bba0ab6 100644
--- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap
+++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap
@@ -222,7 +222,13 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model
"multi": true,
"name": "queryVar",
"options": [],
- "query": "query1",
+ "query": {
+ "kind": "prometheus",
+ "spec": {
+ "expr": "label_values(node_boot_time_seconds)",
+ "refId": "A",
+ },
+ },
"refresh": "onDashboardLoad",
"regex": "regex1",
"skipUrlSync": false,
diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
index 2cbb9a03550..d7514139230 100644
--- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
+++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
@@ -775,7 +775,12 @@ describe('sceneVariablesSetToVariables', () => {
"multi": true,
"name": "test",
"options": [],
- "query": "query",
+ "query": {
+ "kind": "fake-std",
+ "spec": {
+ "__legacyStringValue": "query",
+ },
+ },
"refresh": "onDashboardLoad",
"regex": "",
"skipUrlSync": false,
diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
index bb9c410c7ec..c7d86f4c910 100644
--- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
+++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
@@ -27,6 +27,7 @@ import {
transformVariableRefreshToEnum,
transformVariableHideToEnum,
transformSortVariableToEnum,
+ LEGACY_STRING_VALUE_KEY,
} from './transformToV2TypesUtils';
/**
* Converts a SceneVariables object into an array of VariableModel objects.
@@ -269,16 +270,20 @@ export function sceneVariablesSetToSchemaV2Variables(
if (transformVariableRefreshToEnum(variable.state.refresh) === 'never' || keepQueryOptions) {
options = variableValueOptionsToVariableOptions(variable.state);
}
- //query: DataQueryKind | string;
const query = variable.state.query;
let dataQuery: DataQueryKind | string;
if (typeof query !== 'string') {
dataQuery = {
- kind: getDataQueryKind(query),
+ kind: variable.state.datasource?.type ?? getDataQueryKind(query),
spec: getDataQuerySpec(query),
};
} else {
- dataQuery = query;
+ dataQuery = {
+ kind: variable.state.datasource?.type ?? getDataQueryKind(query),
+ spec: {
+ [LEGACY_STRING_VALUE_KEY]: query,
+ },
+ };
}
const queryVariable: QueryVariableKind = {
kind: 'QueryVariable',
diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
index 75be7ae3a42..61ed7495a8c 100644
--- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
@@ -88,7 +88,7 @@ import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior
import { RowActions } from '../scene/layout-default/row-actions/RowActions';
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
-import { getDashboardSceneFor, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils';
+import { getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils';
import { GRID_ROW_HEIGHT } from './const';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
@@ -100,6 +100,7 @@ import {
transformVariableHideToEnumV1,
transformVariableRefreshToEnumV1,
} from './transformToV1TypesUtils';
+import { LEGACY_STRING_VALUE_KEY } from './transformToV2TypesUtils';
const DEFAULT_DATASOURCE = 'default';
@@ -181,7 +182,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo {
- if (n.children.length !== p.children.length || n.children !== p.children) {
- getDashboardSceneFor(grid).setState({ isEmpty: n.children.length === 0 });
- }
- });
-
- return () => {
- sub.unsubscribe();
- };
-}
-
function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
if (!panel.spec.data?.spec.queries?.length) {
return undefined;
@@ -650,12 +636,12 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
}
function getDataQueryForVariable(variable: QueryVariableKind) {
- return typeof variable.spec.query !== 'string'
- ? {
+ return LEGACY_STRING_VALUE_KEY in variable.spec.query.spec
+ ? (variable.spec.query.spec[LEGACY_STRING_VALUE_KEY] ?? '')
+ : {
...variable.spec.query.spec,
refId: variable.spec.query.spec.refId ?? 'A',
- }
- : (variable.spec.query ?? '');
+ };
}
export function getCurrentValueForOldIntervalModel(variable: IntervalVariableKind, intervals: string[]): string {
diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
index 77960f558ce..b5ca18ea717 100644
--- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
@@ -50,7 +50,7 @@ import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
import { DashboardInteractions } from '../utils/interactions';
-import { getDashboardSceneFor, getVizPanelKeyForPanelId } from '../utils/utils';
+import { getVizPanelKeyForPanelId } from '../utils/utils';
import { createVariablesForDashboard, createVariablesForSnapshot } from '../utils/variables';
import { getAngularPanelMigrationHandler } from './angularMigration';
@@ -267,7 +267,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
grid: new SceneGridLayout({
isLazy: !(dto.preload || contextSrv.user.authenticatedBy === 'render'),
children: createSceneObjectsForPanels(oldModel.panels),
- $behaviors: [trackIfEmpty],
}),
}),
$timeRange: new SceneTimeRange({
@@ -426,20 +425,6 @@ export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => {
}
};
-function trackIfEmpty(grid: SceneGridLayout) {
- getDashboardSceneFor(grid).setState({ isEmpty: grid.state.children.length === 0 });
-
- const sub = grid.subscribeToState((n, p) => {
- if (n.children.length !== p.children.length || n.children !== p.children) {
- getDashboardSceneFor(grid).setState({ isEmpty: n.children.length === 0 });
- }
- });
-
- return () => {
- sub.unsubscribe();
- };
-}
-
function getDashboardInteractionCallback(uid: string, title: string) {
return (e: SceneInteractionProfileEvent) => {
let interactionType = '';
diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts
index 43121f54d8b..c87a4a0a687 100644
--- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts
@@ -225,7 +225,10 @@ describe('transformSceneToSaveModelSchemaV2', () => {
hide: VariableHideV1.hideLabel,
value: 'value1',
text: 'text1',
- query: 'query1',
+ query: {
+ expr: 'label_values(node_boot_time_seconds)',
+ refId: 'A',
+ },
definition: 'definition1',
datasource: { uid: 'datasource1', type: 'prometheus' },
sort: VariableSortV1.alphabeticalDesc,
diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
index 67cae374214..928ce92733f 100644
--- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
@@ -403,8 +403,11 @@ function getVizPanelQueries(vizPanel: VizPanel): PanelQueryKind[] {
return queries;
}
-export function getDataQueryKind(query: SceneDataQuery): string {
- // If the query has a datasource, use the datasource type, otherwise return empty kind
+export function getDataQueryKind(query: SceneDataQuery | string): string {
+ if (typeof query === 'string') {
+ return getDefaultDataSourceRef()?.type ?? '';
+ }
+
return query.datasource?.type ?? getDefaultDataSourceRef()?.type ?? '';
}
@@ -616,19 +619,15 @@ export function getAnnotationQueryKind(annotationQuery: AnnotationQuery): string
}
}
-export function getDefaultDataSourceRef(): DataSourceRef | undefined {
+export function getDefaultDataSourceRef(): DataSourceRef {
// we need to return the default datasource configured in the BootConfig
const defaultDatasource = config.bootData.settings.defaultDatasource;
// get default datasource type
- const dsList = config.bootData.settings.datasources ?? {};
+ const dsList = config.bootData.settings.datasources;
const ds = dsList[defaultDatasource];
- if (ds) {
- return { type: ds.meta.id, uid: ds.name }; // in the datasource list from bootData "id" is the type
- }
-
- return undefined;
+ return { type: ds.meta.id, uid: ds.name }; // in the datasource list from bootData "id" is the type
}
// Function to know if the dashboard transformed is a valid DashboardV2Spec
diff --git a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts
index ca7c056d752..582b6b4cfcf 100644
--- a/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts
+++ b/public/app/features/dashboard-scene/serialization/transformToV2TypesUtils.ts
@@ -19,6 +19,9 @@ import {
FieldColorModeId as FieldColorModeIdV2,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
+// used for QueryVariableKind's query prop - in schema V2 we've deprecated string type and support only DataQuery
+export const LEGACY_STRING_VALUE_KEY = '__legacyStringValue';
+
export function transformCursorSynctoEnum(cursorSync?: DashboardCursorSyncV1): DashboardCursorSync {
switch (cursorSync) {
case 0:
diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts
index 6e0b078ea22..2aa39746339 100644
--- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts
+++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts
@@ -9,7 +9,7 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
-import { dashboardSceneGraph } from './dashboardSceneGraph';
+import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph';
import { findVizPanelByKey } from './utils';
describe('dashboardSceneGraph', () => {
@@ -22,7 +22,7 @@ describe('dashboardSceneGraph', () => {
it('should resolve VizPanelLinks object', () => {
const scene = buildTestScene();
- const panelWithNoLinks = findVizPanelByKey(scene, 'panel-with-links')!;
+ const panelWithNoLinks = findVizPanelByKey(scene, 'panel-2')!;
expect(dashboardSceneGraph.getPanelLinks(panelWithNoLinks)).toBeInstanceOf(VizPanelLinks);
});
});
@@ -66,6 +66,24 @@ describe('dashboardSceneGraph', () => {
expect(cursorSync).toBeUndefined();
});
});
+
+ describe('getNextPanelId', () => {
+ it('should get next panel id in a simple 3 panel layout', () => {
+ const scene = buildTestScene();
+ const id = getNextPanelId(scene);
+
+ expect(id).toBe(3);
+ });
+
+ it('should return 1 if no panels are found', () => {
+ const scene = buildTestScene();
+
+ const grid = scene.state.body as DefaultGridLayoutManager;
+ grid.state.grid.setState({ children: [] });
+ const id = getNextPanelId(scene);
+ expect(id).toBe(1);
+ });
+ });
});
function buildTestScene(overrides?: Partial) {
@@ -111,7 +129,7 @@ function buildTestScene(overrides?: Partial) {
new DashboardGridItem({
body: new VizPanel({
title: 'Panel D',
- key: 'panel-with-links',
+ key: 'panel-2',
pluginId: 'table',
$data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }),
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
index 10edc9904d7..8d1a8176754 100644
--- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
+++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
@@ -1,10 +1,11 @@
-import { VizPanel, sceneGraph, behaviors } from '@grafana/scenes';
+import { VizPanel, sceneGraph, behaviors, SceneObject, SceneGridRow } from '@grafana/scenes';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardScene } from '../scene/DashboardScene';
import { VizPanelLinks } from '../scene/PanelLinks';
-import { getLayoutManagerFor } from './utils';
+import { isClonedKey } from './clone';
+import { getLayoutManagerFor, getPanelIdForVizPanel } from './utils';
function getTimePicker(scene: DashboardScene) {
return scene.state.controls?.state.timePicker;
@@ -28,6 +29,29 @@ function getVizPanels(scene: DashboardScene): VizPanel[] {
return scene.state.body.getVizPanels();
}
+/**
+ * Will look for all panels in the entire scene starting from root
+ * and find the next free panel id
+ */
+export function getNextPanelId(scene: SceneObject): number {
+ let max = 0;
+
+ sceneGraph
+ .findAllObjects(scene.getRoot(), (obj) => obj instanceof VizPanel || obj instanceof SceneGridRow)
+ .forEach((panel) => {
+ if (isClonedKey(panel.state.key!)) {
+ return;
+ }
+
+ const panelId = getPanelIdForVizPanel(panel);
+ if (panelId > max) {
+ max = panelId;
+ }
+ });
+
+ return max + 1;
+}
+
function getDataLayers(scene: DashboardScene): DashboardDataLayerSet {
const data = sceneGraph.getData(scene);
@@ -56,4 +80,5 @@ export const dashboardSceneGraph = {
getDataLayers,
getCursorSync,
getLayoutManagerFor,
+ getNextPanelId,
};
diff --git a/public/app/features/dashboard-scene/v2schema/test-helpers.ts b/public/app/features/dashboard-scene/v2schema/test-helpers.ts
index 9c37f1df5d5..9f901bf67e4 100644
--- a/public/app/features/dashboard-scene/v2schema/test-helpers.ts
+++ b/public/app/features/dashboard-scene/v2schema/test-helpers.ts
@@ -53,7 +53,7 @@ export function validateVariable<
}
if (sceneVariable instanceof QueryVariable && variableKind.kind === 'QueryVariable') {
expect(sceneVariable?.state.datasource).toBe(variableKind.spec.datasource);
- expect(sceneVariable?.state.query).toBe(variableKind.spec.query);
+ expect(sceneVariable?.state.query).toEqual(variableKind.spec.query.spec);
}
if (sceneVariable instanceof CustomVariable && variableKind.kind === 'CustomVariable') {
expect(sceneVariable?.state.query).toBe(variableKind.spec.query);
diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts
index 3a677bf658e..1dfbd14eb04 100644
--- a/public/app/features/dashboard/api/ResponseTransformers.test.ts
+++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts
@@ -19,6 +19,7 @@ import {
} from 'app/features/apiserver/types';
import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2';
import {
+ LEGACY_STRING_VALUE_KEY,
transformVariableHideToEnum,
transformVariableRefreshToEnum,
} from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils';
@@ -915,7 +916,7 @@ describe('ResponseTransformers', () => {
expect(v2.spec.datasource).toEqual(v1.datasource);
if (typeof v1.query === 'string') {
- expect(v2.spec.query).toEqual(v1.query);
+ expect(v2.spec.query.spec[LEGACY_STRING_VALUE_KEY]).toEqual(v1.query);
} else {
expect(v2.spec.query).toEqual({
kind: v1.datasource?.type,
diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts
index 535265f606d..1b72d6b3c08 100644
--- a/public/app/features/dashboard/api/ResponseTransformers.ts
+++ b/public/app/features/dashboard/api/ResponseTransformers.ts
@@ -62,6 +62,7 @@ import {
transformVariableRefreshToEnumV1,
} from 'app/features/dashboard-scene/serialization/transformToV1TypesUtils';
import {
+ LEGACY_STRING_VALUE_KEY,
transformCursorSynctoEnum,
transformDataTopic,
transformSortVariableToEnum,
@@ -504,8 +505,12 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables']
let query = v.query || {};
if (typeof query === 'string') {
- console.error('Query variable query is a string. It needs to extend DataQuery.');
- query = {};
+ console.warn(
+ 'Query variable query is a string which is deprecated in the schema v2. It should extend DataQuery'
+ );
+ query = {
+ [LEGACY_STRING_VALUE_KEY]: query,
+ };
}
const qv: QueryVariableKind = {
@@ -527,7 +532,7 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables']
query: {
kind: v.datasource?.type || getDefaultDatasourceType(),
spec: {
- ...query,
+ ...v.query,
},
},
},
@@ -708,7 +713,10 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] {
...commonProperties,
current: v.spec.current,
options: v.spec.options,
- query: typeof v.spec.query === 'string' ? v.spec.query : v.spec.query.spec,
+ query:
+ LEGACY_STRING_VALUE_KEY in v.spec.query.spec
+ ? v.spec.query.spec[LEGACY_STRING_VALUE_KEY]
+ : v.spec.query.spec,
datasource: v.spec.datasource,
sort: transformSortVariableToEnumV1(v.spec.sort),
refresh: transformVariableRefreshToEnumV1(v.spec.refresh),
diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts
index f1b2fdd4ffb..6229f0d1a02 100644
--- a/public/app/features/datasources/state/actions.ts
+++ b/public/app/features/datasources/state/actions.ts
@@ -5,6 +5,7 @@ import {
TestDataSourceResponse,
DataSourceTestSucceeded,
DataSourceTestFailed,
+ DataSourceApi,
} from '@grafana/data';
import {
config,
@@ -125,6 +126,11 @@ export const initDataSourceSettings = (
};
};
+const getPluginVersion = (dsApi: DataSourceApi) => {
+ const isCorePlugin = (dsApi?.meta?.module || '').startsWith('core');
+ return isCorePlugin ? config?.buildInfo?.version : dsApi?.meta?.info?.version;
+};
+
export const testDataSource = (
dataSourceName: string,
editRoute = DATASOURCES_ROUTES.Edit,
@@ -153,6 +159,7 @@ export const testDataSource = (
trackDataSourceTested({
grafana_version: config.buildInfo.version,
plugin_id: dsApi.type,
+ plugin_version: getPluginVersion(dsApi),
datasource_uid: dsApi.uid,
success: true,
path: editLink,
@@ -165,6 +172,7 @@ export const testDataSource = (
trackDataSourceTested({
grafana_version: config.buildInfo.version,
plugin_id: dsApi.type,
+ plugin_version: getPluginVersion(dsApi),
datasource_uid: dsApi.uid,
success: false,
path: editLink,
diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts
index e24b6b424f0..98e962b61e0 100644
--- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts
+++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts
@@ -194,6 +194,7 @@ export type MigrateDataResponseItemDto = {
| 'FOLDER'
| 'LIBRARY_ELEMENT'
| 'ALERT_RULE'
+ | 'ALERT_RULE_GROUP'
| 'CONTACT_POINT'
| 'NOTIFICATION_POLICY'
| 'NOTIFICATION_TEMPLATE'
diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx
index c6b8f78f04b..3080a26fdd9 100644
--- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx
@@ -231,6 +231,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
return ;
case 'ALERT_RULE':
return ;
+ case 'ALERT_RULE_GROUP':
+ return ;
case 'PLUGIN':
if (pluginLogo) {
return
;
diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
index 399945cdaba..00d576db9c5 100644
--- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
@@ -23,6 +23,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) {
return t('migrate-to-cloud.resource-type.notification_policy', 'Notification Policy');
case 'ALERT_RULE':
return t('migrate-to-cloud.resource-type.alert_rule', 'Alert Rule');
+ case 'ALERT_RULE_GROUP':
+ return t('migrate-to-cloud.resource-type.alert_rule_group', 'Alert Rule Group');
case 'PLUGIN':
return t('migrate-to-cloud.resource-type.plugin', 'Plugin');
default:
diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx
index e21fd83e815..e98a8221bb6 100644
--- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx
@@ -62,6 +62,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) {
types.push(t('migrate-to-cloud.migrated-counts.notification_policies', 'notification policies'));
} else if (type === 'ALERT_RULE') {
types.push(t('migrate-to-cloud.migrated-counts.alert_rules', 'alert rules'));
+ } else if (type === 'ALERT_RULE_GROUP') {
+ types.push(t('migrate-to-cloud.migrated-counts.alert_rule_groups', 'alert rule groups'));
} else if (type === 'PLUGIN') {
types.push(t('migrate-to-cloud.migrated-counts.plugins', 'plugins'));
}
diff --git a/public/app/features/trails/migrations/otelDeploymentEnvironment.ts b/public/app/features/trails/migrations/otelDeploymentEnvironment.ts
index 969c95bf662..517ce88ce2c 100644
--- a/public/app/features/trails/migrations/otelDeploymentEnvironment.ts
+++ b/public/app/features/trails/migrations/otelDeploymentEnvironment.ts
@@ -32,8 +32,15 @@ export function migrateOtelDeploymentEnvironment(trail: DataTrail, urlParams: Ur
) {
return;
}
- // if there is no dep env, does not need to be migrated
- if (!deploymentEnv) {
+
+ // check that there is a deployment environment variable value to migrate
+ // in some cases the deployment environment may not present
+ // but due to this change it is now always present and the value is undefined
+ // https://github.com/grafana/scenes/pull/1033
+ if (
+ !deploymentEnv ||
+ (Array.isArray(deploymentEnv) && deploymentEnv.length > 0 && deploymentEnv[0] === 'undefined')
+ ) {
return;
}
diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx
index bbd8f242906..10253f5851a 100644
--- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx
+++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, waitFor } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CoreApp, LogSortOrderChangeEvent, LogsSortOrder, store } from '@grafana/data';
@@ -214,42 +214,42 @@ describe('LokiQueryBuilderOptions', () => {
});
describe('Query direction', () => {
- it("initializes query direction when it's empty", async () => {
+ it("initializes query direction when it's empty in Explore or Dashboards", () => {
const onChange = jest.fn();
- setup({ expr: '{foo="bar"}' }, onChange);
- await waitFor(() =>
- expect(onChange).toHaveBeenCalledWith({
- expr: '{foo="bar"}',
- refId: 'A',
- direction: LokiQueryDirection.Backward,
- })
- );
+ setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore });
+ expect(onChange).toHaveBeenCalledWith({
+ expr: '{foo="bar"}',
+ refId: 'A',
+ direction: LokiQueryDirection.Backward,
+ });
});
- it('uses backward as default in Explore with no previous stored preference', async () => {
+ it('does not change direction on initialization elsewhere', () => {
+ const onChange = jest.fn();
+ setup({ expr: '{foo="bar"}' }, onChange, { app: undefined });
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ it('uses backward as default in Explore with no previous stored preference', () => {
const onChange = jest.fn();
store.delete('grafana.explore.logs.sortOrder');
setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore });
- await waitFor(() =>
- expect(onChange).toHaveBeenCalledWith({
- expr: '{foo="bar"}',
- refId: 'A',
- direction: LokiQueryDirection.Backward,
- })
- );
+ expect(onChange).toHaveBeenCalledWith({
+ expr: '{foo="bar"}',
+ refId: 'A',
+ direction: LokiQueryDirection.Backward,
+ });
});
- it('uses the stored sorting option to determine direction in Explore', async () => {
+ it('uses the stored sorting option to determine direction in Explore', () => {
store.set('grafana.explore.logs.sortOrder', LogsSortOrder.Ascending);
const onChange = jest.fn();
setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore });
- await waitFor(() =>
- expect(onChange).toHaveBeenCalledWith({
- expr: '{foo="bar"}',
- refId: 'A',
- direction: LokiQueryDirection.Forward,
- })
- );
+ expect(onChange).toHaveBeenCalledWith({
+ expr: '{foo="bar"}',
+ refId: 'A',
+ direction: LokiQueryDirection.Forward,
+ });
store.delete('grafana.explore.logs.sortOrder');
});
diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx
index 2d36a70f1af..cb4fed551f5 100644
--- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx
+++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx
@@ -39,6 +39,9 @@ export const LokiQueryBuilderOptions = React.memo(
const [splitDurationValid, setSplitDurationValid] = useState(true);
useEffect(() => {
+ if (app !== CoreApp.Explore && app !== CoreApp.Dashboard && app !== CoreApp.PanelEditor) {
+ return;
+ }
// Initialize the query direction according to the current environment.
if (!query.direction) {
onChange({ ...query, direction: getDefaultQueryDirection(app) });
@@ -182,7 +185,11 @@ export const LokiQueryBuilderOptions = React.memo(
/>
-
+
>
)}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 380c5917c9c..66492eac6b1 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -2117,6 +2117,7 @@
"title": "Let us help you migrate to this stack"
},
"migrated-counts": {
+ "alert_rule_groups": "alert rule groups",
"alert_rules": "alert rules",
"contact_points": "contact points",
"dashboards": "dashboards",
@@ -2221,6 +2222,7 @@
},
"resource-type": {
"alert_rule": "Alert Rule",
+ "alert_rule_group": "Alert Rule Group",
"contact_point": "Contact Point",
"dashboard": "Dashboard",
"datasource": "Data source",
@@ -3398,6 +3400,7 @@
"home-dashboard-placeholder": "Default dashboard",
"locale-label": "Language",
"locale-placeholder": "Choose language",
+ "theme-description": "Enjoying the limited edition themes? Tell us what you'd like to see <2>here.2>",
"theme-label": "Interface theme",
"week-start-label": "Week start"
},
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index e3194c0093e..193643a5111 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -2117,6 +2117,7 @@
"title": "Ŀęŧ ūş ĥęľp yőū mįģřäŧę ŧő ŧĥįş şŧäčĸ"
},
"migrated-counts": {
+ "alert_rule_groups": "äľęřŧ řūľę ģřőūpş",
"alert_rules": "äľęřŧ řūľęş",
"contact_points": "čőʼnŧäčŧ pőįʼnŧş",
"dashboards": "đäşĥþőäřđş",
@@ -2221,6 +2222,7 @@
},
"resource-type": {
"alert_rule": "Åľęřŧ Ŗūľę",
+ "alert_rule_group": "Åľęřŧ Ŗūľę Ğřőūp",
"contact_point": "Cőʼnŧäčŧ Pőįʼnŧ",
"dashboard": "Đäşĥþőäřđ",
"datasource": "Đäŧä şőūřčę",
@@ -3398,6 +3400,7 @@
"home-dashboard-placeholder": "Đęƒäūľŧ đäşĥþőäřđ",
"locale-label": "Ŀäʼnģūäģę",
"locale-placeholder": "Cĥőőşę ľäʼnģūäģę",
+ "theme-description": "Ēʼnĵőyįʼnģ ŧĥę ľįmįŧęđ ęđįŧįőʼn ŧĥęmęş? Ŧęľľ ūş ŵĥäŧ yőū'đ ľįĸę ŧő şęę <2>ĥęřę.2>",
"theme-label": "Ĩʼnŧęřƒäčę ŧĥęmę",
"week-start-label": "Ŵęęĸ şŧäřŧ"
},
diff --git a/public/openapi3.json b/public/openapi3.json
index f326616d62d..89612da9cf1 100644
--- a/public/openapi3.json
+++ b/public/openapi3.json
@@ -7250,6 +7250,7 @@
"FOLDER",
"LIBRARY_ELEMENT",
"ALERT_RULE",
+ "ALERT_RULE_GROUP",
"CONTACT_POINT",
"NOTIFICATION_POLICY",
"NOTIFICATION_TEMPLATE",
diff --git a/scripts/drone/events/pr.star b/scripts/drone/events/pr.star
index 9a57ec4cc53..7d42325bef3 100644
--- a/scripts/drone/events/pr.star
+++ b/scripts/drone/events/pr.star
@@ -116,6 +116,7 @@ def pr_pipelines():
"public/app/plugins/**/plugin.json",
"docs/sources/setup-grafana/configure-grafana/feature-toggles/**",
"devenv/**",
+ "apps/**",
],
),
ver_mode,
@@ -134,6 +135,7 @@ def pr_pipelines():
"public/app/plugins/**/plugin.json",
"devenv/**",
".bingo/**",
+ "apps/**",
],
),
ver_mode,
diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star
index 01147f6df17..6eeb6cfa03c 100644
--- a/scripts/drone/variables.star
+++ b/scripts/drone/variables.star
@@ -2,7 +2,7 @@
global variables
"""
-grabpl_version = "v3.1.1"
+grabpl_version = "v3.1.2"
golang_version = "1.23.5"
# nodejs_version should match what's in ".nvmrc", but without the v prefix.
diff --git a/yarn.lock b/yarn.lock
index 3ebde167b43..1c4fee5ed1d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4129,7 +4129,6 @@ __metadata:
slate-plain-serializer: "npm:0.7.13"
slate-react: "npm:0.22.10"
storybook: "npm:^8.4.2"
- storybook-dark-mode: "npm:4.0.1"
style-loader: "npm:4.0.0"
tinycolor2: "npm:1.6.0"
tslib: "npm:2.8.1"
@@ -7524,7 +7523,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/components@npm:8.4.4, @storybook/components@npm:^8.0.0, @storybook/components@npm:^8.4.2":
+"@storybook/components@npm:8.4.4, @storybook/components@npm:^8.4.2":
version: 8.4.4
resolution: "@storybook/components@npm:8.4.4"
peerDependencies:
@@ -7533,7 +7532,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/core-events@npm:^8.0.0, @storybook/core-events@npm:^8.4.2":
+"@storybook/core-events@npm:^8.4.2":
version: 8.4.4
resolution: "@storybook/core-events@npm:8.4.4"
peerDependencies:
@@ -7605,7 +7604,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/icons@npm:^1.2.12, @storybook/icons@npm:^1.2.5":
+"@storybook/icons@npm:^1.2.12":
version: 1.2.12
resolution: "@storybook/icons@npm:1.2.12"
peerDependencies:
@@ -7615,7 +7614,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/manager-api@npm:8.4.4, @storybook/manager-api@npm:^8.0.0, @storybook/manager-api@npm:^8.4.2":
+"@storybook/manager-api@npm:8.4.4, @storybook/manager-api@npm:^8.4.2":
version: 8.4.4
resolution: "@storybook/manager-api@npm:8.4.4"
peerDependencies:
@@ -7766,7 +7765,7 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/theming@npm:8.4.4, @storybook/theming@npm:^8.0.0, @storybook/theming@npm:^8.4.2":
+"@storybook/theming@npm:8.4.4, @storybook/theming@npm:^8.4.2":
version: 8.4.4
resolution: "@storybook/theming@npm:8.4.4"
peerDependencies:
@@ -28886,22 +28885,6 @@ __metadata:
languageName: node
linkType: hard
-"storybook-dark-mode@npm:4.0.1":
- version: 4.0.1
- resolution: "storybook-dark-mode@npm:4.0.1"
- dependencies:
- "@storybook/components": "npm:^8.0.0"
- "@storybook/core-events": "npm:^8.0.0"
- "@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.5"
- "@storybook/manager-api": "npm:^8.0.0"
- "@storybook/theming": "npm:^8.0.0"
- fast-deep-equal: "npm:^3.1.3"
- memoizerific: "npm:^1.11.3"
- checksum: 10/3225e5bdaba0ea76b65d642202d9712d7de234e3b5673fb46e444892ab114be207dd287778e2002b662ec35bb8153d2624ff280ce51c5299fb13c711431dad40
- languageName: node
- linkType: hard
-
"storybook@npm:^8.4.2":
version: 8.4.4
resolution: "storybook@npm:8.4.4"