merge main

This commit is contained in:
Ryan McKinley
2024-06-13 20:05:01 +03:00
52 changed files with 5675 additions and 6944 deletions
+3
View File
@@ -7124,6 +7124,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
],
"public/app/plugins/panel/logs/LogsPanel.test.tsx:5381": [
[0, 0, 0, "* import is invalid because \'Layout,HorizontalGroup,VerticalGroup\' from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
],
"public/app/plugins/panel/logs/types.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./panelcfg.gen\`)", "0"]
],
+1 -1
View File
@@ -79,7 +79,7 @@
/pkg/components/apikeygen/ @grafana/identity-access-team
/pkg/components/satokengen/ @grafana/identity-access-team
/pkg/components/dashdiffs/ @grafana/grafana-app-platform-squad
/pkg/components/imguploader/ @grafana/grafana-backend-group
/pkg/components/imguploader/ @grafana/alerting-backend
/pkg/components/loki/ @grafana/grafana-backend-group
/pkg/components/null/ @grafana/grafana-backend-group
/pkg/components/simplejson/ @grafana/grafana-backend-group
+17
View File
@@ -350,6 +350,23 @@ The deprecated `email` field to send a report via `/api/reports/email` endpoint
- **Grafana UI:** Add code variant to Text component. [#82318](https://github.com/grafana/grafana/issues/82318), [@tskarhed](https://github.com/tskarhed)
<!-- 11.0.0-preview END -->
<!-- 10.4.4 START -->
# 10.4.4 (2024-06-13)
### Bug fixes
- **BrowseDashboards:** Prepend subpath to New Browse Dashboard actions. [#89129](https://github.com/grafana/grafana/issues/89129), [@joshhunt](https://github.com/joshhunt)
- **Alerting:** Fix rule storage to filter by group names using case-sensitive comparison. [#89061](https://github.com/grafana/grafana/issues/89061), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Fix editing Grafana folder via alert rule editor. [#88907](https://github.com/grafana/grafana/issues/88907), [@gillesdemey](https://github.com/gillesdemey)
- **AzureMonitor:** Fix bug detecting app insights queries. [#88786](https://github.com/grafana/grafana/issues/88786), [@aangelisc](https://github.com/aangelisc)
- **AuthN:** Fix signout redirect url. [#88749](https://github.com/grafana/grafana/issues/88749), [@kalleep](https://github.com/kalleep)
- **SSE:** Fix threshold unmarshal to avoid panic. [#88650](https://github.com/grafana/grafana/issues/88650), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Fix typo in JSON response for rule export. [#88094](https://github.com/grafana/grafana/issues/88094), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **CloudMonitoring:** Fix query type selection issue. [#88023](https://github.com/grafana/grafana/issues/88023), [@aangelisc](https://github.com/aangelisc)
- **Provisioning:** Add override option to role provisioning. (Enterprise)
<!-- 10.4.4 END -->
<!-- 10.4.3 START -->
# 10.4.3 (2024-05-13)
@@ -48,6 +48,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `dashgpt` | Enable AI powered features in dashboards | Yes |
| `alertingInsights` | Show the new alerting insights landing page | Yes |
| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes |
| `transformationsVariableSupport` | Allows using variables in transformations | Yes |
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | Yes |
| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes |
| `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes |
@@ -91,7 +92,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `reportingRetries` | Enables rendering retries for the reporting feature |
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
| `formatString` | Enable format string transformer |
| `transformationsVariableSupport` | Allows using variables in transformations |
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
| `teamHttpHeaders` | Enables Team LBAC for datasources to apply team headers to the client requests |
| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation |
+3 -3
View File
@@ -99,15 +99,15 @@ require (
github.com/grafana/grafana-azure-sdk-go/v2 v2.0.4 // @grafana/partner-datasources
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
github.com/grafana/grafana-plugin-sdk-go v0.234.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240612163934-ed400f0bbfe4 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana-plugin-sdk-go v0.235.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240613084659-7e9e5f534676 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad
// This needs to be here for other projects that import grafana/grafana
// For local development grafana/grafana will always use the local files
// Check go.work file for details
github.com/grafana/grafana/pkg/promlib v0.0.6 // @grafana/observability-metrics
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
github.com/grafana/pyroscope-go/godeltaprof v0.1.6 // @grafana/observability-traces-and-profiling
github.com/grafana/pyroscope-go/godeltaprof v0.1.7 // @grafana/observability-traces-and-profiling
github.com/grafana/pyroscope/api v0.3.0 // @grafana/observability-traces-and-profiling
github.com/grafana/tempo v1.5.1-0.20230524121406-1dc1bfe7085b // @grafana/observability-traces-and-profiling
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // @grafana/plugins-platform-backend
+3 -4175
View File
File diff suppressed because it is too large Load Diff
+3863 -985
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -66,6 +66,7 @@
"classnames": "2.5.1",
"d3": "7.9.0",
"date-fns": "3.6.0",
"downshift": "^9.0.6",
"hoist-non-react-statics": "3.3.2",
"i18next": "^23.0.0",
"i18next-browser-languagedetector": "^7.0.2",
@@ -0,0 +1,47 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryFn } from '@storybook/react';
import React, { useState } from 'react';
import { Combobox } from './Combobox';
const meta: Meta<typeof Combobox> = {
title: 'Forms/Combobox',
component: Combobox,
args: {
loading: undefined,
invalid: undefined,
placeholder: 'Select an option...',
options: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Carrot', value: 'carrot' },
{ label: 'Dill', value: 'dill' },
{ label: 'Eggplant', value: 'eggplant' },
{ label: 'Fennel', value: 'fennel' },
{ label: 'Grape', value: 'grape' },
{ label: 'Honeydew', value: 'honeydew' },
{ label: 'Iceberg Lettuce', value: 'iceberg-lettuce' },
{ label: 'Jackfruit', value: 'jackfruit' },
{ label: '1', value: 1 },
{ label: '2', value: 2 },
{ label: '3', value: 3 },
],
value: 'banana',
},
};
export const Basic: StoryFn<typeof Combobox> = (args) => {
const [value, setValue] = useState(args.value);
return (
<Combobox
{...args}
value={value}
onChange={(val) => {
setValue(val.value);
action('onChange')(val);
}}
/>
);
};
export default meta;
@@ -0,0 +1,64 @@
import { useCombobox } from 'downshift';
import React, { useMemo, useState } from 'react';
import { Icon } from '../Icon/Icon';
import { Input, Props as InputProps } from '../Input/Input';
type Value = string | number;
type Option = {
label: string;
value: Value;
};
interface ComboboxProps
extends Omit<InputProps, 'width' | 'prefix' | 'suffix' | 'value' | 'addonBefore' | 'addonAfter' | 'onChange'> {
onChange: (val: Option) => void;
value: Value;
options: Option[];
}
function itemToString(item: Option | null) {
return item?.label || '';
}
function itemFilter(inputValue: string) {
const lowerCasedInputValue = inputValue.toLowerCase();
return (item: Option) => {
return (
!inputValue ||
item?.label?.toLowerCase().includes(lowerCasedInputValue) ||
item?.value?.toString().toLowerCase().includes(lowerCasedInputValue)
);
};
}
export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => {
const [items, setItems] = useState(options);
const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]);
const { getInputProps, getMenuProps, getItemProps, isOpen } = useCombobox({
items,
itemToString,
selectedItem,
onInputValueChange: ({ inputValue }) => {
setItems(options.filter(itemFilter(inputValue)));
},
onSelectedItemChange: ({ selectedItem }) => onChange(selectedItem),
});
return (
<div>
<Input suffix={<Icon name={isOpen ? 'search' : 'angle-down'} />} {...restProps} {...getInputProps()} />
<ul {...getMenuProps()}>
{isOpen &&
items.map((item, index) => {
return (
<li key={item.value} {...getItemProps({ item, index })}>
{item.label}
</li>
);
})}
</ul>
</div>
);
};
+3 -1
View File
@@ -5,7 +5,7 @@ go 1.21.10
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/gorilla/mux v1.8.1
github.com/grafana/grafana-plugin-sdk-go v0.234.0
github.com/grafana/grafana-plugin-sdk-go v0.235.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f
github.com/prometheus/client_golang v1.19.0
github.com/stretchr/testify v1.9.0
@@ -60,6 +60,8 @@ require (
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240416155748-26353dc0451f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.7 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
+3 -1
View File
@@ -117,9 +117,11 @@ 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.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/grafana-plugin-sdk-go v0.234.0 h1:p20XfGKB3Z/8aZ6jut+FIU/0cXw+dLkcGFnxJbyFd+k=
github.com/grafana/grafana-plugin-sdk-go v0.235.0 h1:UnZ/iBDvCkfDgwR94opi8trAWJXv4V8Qr1ocJKRRmqA=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f h1:+CK3tH3XrAAqx5urmVqpgSxMrL2MlpTOnLVSU4w4IjY=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f/go.mod h1:ZxIaCOlDmFupiL55aLU+Qp7O1dgwkDMBAQBK7wnEVBg=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/pyroscope-go/godeltaprof v0.1.7 h1:C11j63y7gymiW8VugJ9ZW0pWfxTZugdSJyC48olk5KY=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
+2 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -119,7 +120,7 @@ func (c *ResultConverter) Convert(ctx context.Context,
}, nil
}
func getResponseFrame(resp *backend.QueryDataResponse, refID string) (data.Frames, error) {
func getResponseFrame(logger *log.ConcreteLogger, resp *backend.QueryDataResponse, refID string) (data.Frames, error) {
response, ok := resp.Responses[refID]
if !ok {
// This indicates that the RefID of the request was not included to the response, i.e. some problem in the data source plugin
+1 -1
View File
@@ -124,7 +124,7 @@ func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s *
data = &backend.QueryDataResponse{Responses: map[string]backend.DataResponse{}}
}
dataFrames, err := getResponseFrame(data, m.refID)
dataFrames, err := getResponseFrame(logger, data, m.refID)
if err != nil {
return mathexp.Results{}, MakeQueryError(m.refID, "ml", err)
}
+2 -2
View File
@@ -325,7 +325,7 @@ func executeDSNodesGrouped(ctx context.Context, now time.Time, vars mathexp.Vars
}
for _, dn := range nodeGroup {
dataFrames, err := getResponseFrame(resp, dn.refID)
dataFrames, err := getResponseFrame(logger, resp, dn.refID)
if err != nil {
vars[dn.refID] = mathexp.Results{Error: MakeQueryError(dn.refID, dn.datasource.UID, err)}
instrument(err, "")
@@ -395,7 +395,7 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s
return mathexp.Results{}, MakeQueryError(dn.refID, dn.datasource.UID, err)
}
dataFrames, err := getResponseFrame(resp, dn.refID)
dataFrames, err := getResponseFrame(logger, resp, dn.refID)
if err != nil {
return mathexp.Results{}, MakeQueryError(dn.refID, dn.datasource.UID, err)
}
+3 -1
View File
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/promlib
go 1.21.10
require (
github.com/grafana/grafana-plugin-sdk-go v0.234.0
github.com/grafana/grafana-plugin-sdk-go v0.235.0
github.com/json-iterator/go v1.1.12
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.19.0
@@ -52,6 +52,8 @@ require (
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.7 // indirect
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
+3 -1
View File
@@ -80,7 +80,9 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/grafana-plugin-sdk-go v0.234.0 h1:p20XfGKB3Z/8aZ6jut+FIU/0cXw+dLkcGFnxJbyFd+k=
github.com/grafana/grafana-plugin-sdk-go v0.235.0 h1:UnZ/iBDvCkfDgwR94opi8trAWJXv4V8Qr1ocJKRRmqA=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/pyroscope-go/godeltaprof v0.1.7 h1:C11j63y7gymiW8VugJ9ZW0pWfxTZugdSJyC48olk5KY=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
+2 -1
View File
@@ -717,8 +717,9 @@ var (
Name: "transformationsVariableSupport",
Description: "Allows using variables in transformations",
FrontendOnly: true,
Stage: FeatureStagePublicPreview,
Stage: FeatureStageGeneralAvailability,
Owner: grafanaDatavizSquad,
Expression: "true", // Enabled by default
},
{
Name: "kubernetesPlaylists",
+1 -1
View File
@@ -94,7 +94,7 @@ externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false
panelMonitoring,GA,@grafana/dataviz-squad,false,false,true
enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false
formatString,preview,@grafana/dataviz-squad,false,false,true
transformationsVariableSupport,preview,@grafana/dataviz-squad,false,false,true
transformationsVariableSupport,GA,@grafana/dataviz-squad,false,false,true
kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false
kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
94 panelMonitoring GA @grafana/dataviz-squad false false true
95 enableNativeHTTPHistogram experimental @grafana/hosted-grafana-team false false false
96 formatString preview @grafana/dataviz-squad false false true
97 transformationsVariableSupport preview GA @grafana/dataviz-squad false false true
98 kubernetesPlaylists GA @grafana/grafana-app-platform-squad false true false
99 kubernetesSnapshots experimental @grafana/grafana-app-platform-squad false true false
100 kubernetesDashboards experimental @grafana/grafana-app-platform-squad false false true
+32 -29
View File
@@ -293,6 +293,20 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "authZGRPCServer",
"resourceVersion": "1718093439898",
"creationTimestamp": "2024-06-11T08:10:39Z"
},
"spec": {
"description": "Enables the gRPC server for authorization",
"stage": "experimental",
"codeowner": "@grafana/identity-access-team",
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "autoMigrateGraphPanel",
@@ -1662,6 +1676,18 @@
"codeowner": "@grafana/grafana-backend-group"
}
},
{
"metadata": {
"name": "pinNavItems",
"resourceVersion": "1718017263521",
"creationTimestamp": "2024-06-10T11:01:03Z"
},
"spec": {
"description": "Enables pinning of nav items",
"stage": "experimental",
"codeowner": "@grafana/grafana-frontend-platform"
}
},
{
"metadata": {
"name": "pluginProxyPreserveTrailingSlash",
@@ -2193,12 +2219,15 @@
{
"metadata": {
"name": "transformationsVariableSupport",
"resourceVersion": "1717578796182",
"creationTimestamp": "2023-10-04T14:28:46Z"
"resourceVersion": "1718190346540",
"creationTimestamp": "2023-10-04T14:28:46Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-06-12 11:05:46.540869 +0000 UTC"
}
},
"spec": {
"description": "Allows using variables in transformations",
"stage": "preview",
"stage": "GA",
"codeowner": "@grafana/dataviz-squad",
"frontend": true
}
@@ -2252,32 +2281,6 @@
"stage": "experimental",
"codeowner": "@grafana/hosted-grafana-team"
}
},
{
"metadata": {
"name": "pinNavItems",
"resourceVersion": "1718017263521",
"creationTimestamp": "2024-06-10T11:01:03Z"
},
"spec": {
"description": "Enables pinning of nav items",
"stage": "experimental",
"codeowner": "@grafana/grafana-frontend-platform"
}
},
{
"metadata": {
"name": "authZGRPCServer",
"resourceVersion": "1718093439898",
"creationTimestamp": "2024-06-11T08:10:39Z"
},
"spec": {
"description": "Enables the gRPC server for authorization",
"stage": "experimental",
"codeowner": "@grafana/identity-access-team",
"hideFromAdminPage": true,
"hideFromDocs": true
}
}
]
}
+1 -1
View File
@@ -297,7 +297,7 @@ func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Respo
return response.Err(err)
}
return response.JSON(http.StatusOK, nil)
return response.Empty(http.StatusOK)
}
// Copied from pkg/api/metrics.go
+94 -67
View File
@@ -2,6 +2,7 @@ package permissions
import (
"bytes"
"context"
"fmt"
"slices"
"strings"
@@ -25,10 +26,12 @@ type clause struct {
}
type accessControlDashboardPermissionFilter struct {
user identity.Requester
dashboardAction string
folderAction string
features featuremgmt.FeatureToggles
user identity.Requester
dashboardAction string
dashboardActionSets []string
folderAction string
folderActionSets []string
features featuremgmt.FeatureToggles
where clause
// any recursive CTE queries (if supported)
@@ -53,30 +56,63 @@ func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissi
var folderAction string
var dashboardAction string
var folderActionSets []string
var dashboardActionSets []string
if queryType == searchstore.TypeFolder {
folderAction = dashboards.ActionFoldersRead
//folderAction = append(folderAction, dashboards.ActionFoldersRead)
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
}
if needEdit {
folderAction = dashboards.ActionDashboardsCreate
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
}
}
} else if queryType == searchstore.TypeDashboard {
dashboardAction = dashboards.ActionDashboardsRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
}
if needEdit {
dashboardAction = dashboards.ActionDashboardsWrite
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
}
}
} else if queryType == searchstore.TypeAlertFolder {
folderAction = accesscontrol.ActionAlertingRuleRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
}
if needEdit {
folderAction = accesscontrol.ActionAlertingRuleCreate
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
}
}
} else if queryType == searchstore.TypeAnnotation {
dashboardAction = accesscontrol.ActionAnnotationsRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
}
} else {
folderAction = dashboards.ActionFoldersRead
dashboardAction = dashboards.ActionDashboardsRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
}
if needEdit {
folderAction = dashboards.ActionDashboardsCreate
dashboardAction = dashboards.ActionDashboardsWrite
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
}
}
}
@@ -84,13 +120,13 @@ func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissi
if features.IsEnabledGlobally(featuremgmt.FlagPermissionsFilterRemoveSubquery) {
f = &accessControlDashboardPermissionFilterNoFolderSubquery{
accessControlDashboardPermissionFilter: accessControlDashboardPermissionFilter{
user: user, folderAction: folderAction, dashboardAction: dashboardAction, features: features,
recursiveQueriesAreSupported: recursiveQueriesAreSupported,
user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets,
features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported,
},
}
} else {
f = &accessControlDashboardPermissionFilter{user: user, folderAction: folderAction, dashboardAction: dashboardAction, features: features,
recursiveQueriesAreSupported: recursiveQueriesAreSupported,
f = &accessControlDashboardPermissionFilter{user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets,
features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported,
}
}
f.buildClauses()
@@ -149,20 +185,24 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
// currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana)
useSelfContainedPermissions := f.user.IsAuthenticatedBy(login.ExtendedJWTModule)
if len(f.dashboardAction) > 0 {
toCheck := actionsToCheck(f.dashboardAction, f.user.GetPermissions(), dashWildcards, folderWildcards)
if f.dashboardAction != "" {
toCheckDashboards := actionsToCheck(f.dashboardAction, f.dashboardActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards)
toCheckFolders := actionsToCheck(f.dashboardAction, f.folderActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards)
if len(toCheck) > 0 {
if len(toCheckDashboards) > 0 {
if !useSelfContainedPermissions {
builder.WriteString("(dashboard.uid IN (SELECT identifier FROM permission WHERE kind = 'dashboards' AND attribute = 'uid'")
builder.WriteString(rolesFilter)
args = append(args, params...)
builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)")
args = append(args, toCheck[0])
if len(toCheckDashboards) == 1 {
builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)")
args = append(args, toCheckDashboards[0])
} else {
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")) AND NOT dashboard.is_folder)")
args = append(args, toCheckDashboards...)
}
} else {
actions := parseStringSliceFromInterfaceSlice(toCheck)
args = getAllowedUIDs(actions, f.user, dashboards.ScopeDashboardsPrefix)
args = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeDashboardsPrefix)
// Only add the IN clause if we have any dashboards to check
if len(args) > 0 {
@@ -179,12 +219,15 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if len(toCheckDashboards) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheckDashboards[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")")
permSelectorArgs = append(permSelectorArgs, toCheckFolders...)
}
} else {
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
permSelectorArgs = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeFoldersPrefix)
// Only add the IN clause if we have any folders to check
if len(permSelectorArgs) > 0 {
@@ -229,7 +272,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
builder.WriteString(") AND NOT dashboard.is_folder)")
// Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder)
if hasAccessToRoot(toCheck, f.user) {
if hasAccessToRoot(f.dashboardAction, f.user) {
builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)")
}
} else {
@@ -241,23 +284,27 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
permSelector.Reset()
permSelectorArgs = permSelectorArgs[:0]
if len(f.folderAction) > 0 {
if len(f.dashboardAction) > 0 {
if f.folderAction != "" {
if f.dashboardAction != "" {
builder.WriteString(" OR ")
}
toCheck := actionsToCheck(f.folderAction, f.user.GetPermissions(), folderWildcards)
toCheck := actionsToCheck(f.folderAction, f.folderActionSets, f.user.GetPermissions(), folderWildcards)
if len(toCheck) > 0 {
if !useSelfContainedPermissions {
permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ")")
permSelectorArgs = append(permSelectorArgs, toCheck...)
}
} else {
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
permSelectorArgs = getAllowedUIDs(f.folderAction, f.user, dashboards.ScopeFoldersPrefix)
if len(permSelectorArgs) > 0 {
permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
@@ -342,7 +389,7 @@ func (f *accessControlDashboardPermissionFilter) addRecQry(queryName string, whe
})
}
func actionsToCheck(action string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []any {
func actionsToCheck(action string, actionSets []string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []any {
for _, scope := range permissions[action] {
for _, w := range wildcards {
if w.Contains(scope) {
@@ -351,7 +398,12 @@ func actionsToCheck(action string, permissions map[string][]string, wildcards ..
}
}
return []any{action}
toCheck := []any{action}
for _, a := range actionSets {
toCheck = append(toCheck, a)
}
return toCheck
}
func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTable string, leftCol string, rightTableCol string, orgID int64) (string, []any) {
@@ -382,46 +434,21 @@ func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSele
return strings.Join(wheres, ") OR "), args
}
func parseStringSliceFromInterfaceSlice(slice []any) []string {
result := make([]string, 0, len(slice))
for _, s := range slice {
result = append(result, s.(string))
}
return result
}
func getAllowedUIDs(actions []string, user identity.Requester, scopePrefix string) []any {
uidToActions := make(map[string]map[string]struct{})
for _, action := range actions {
for _, uidScope := range user.GetPermissions()[action] {
if !strings.HasPrefix(uidScope, scopePrefix) {
continue
}
uid := strings.TrimPrefix(uidScope, scopePrefix)
if _, exists := uidToActions[uid]; !exists {
uidToActions[uid] = make(map[string]struct{})
}
uidToActions[uid][action] = struct{}{}
func getAllowedUIDs(action string, user identity.Requester, scopePrefix string) []any {
var args []any
for _, uidScope := range user.GetPermissions()[action] {
if !strings.HasPrefix(uidScope, scopePrefix) {
continue
}
uid := strings.TrimPrefix(uidScope, scopePrefix)
args = append(args, uid)
}
// args max capacity is the length of the different uids
args := make([]any, 0, len(uidToActions))
for uid, assignedActions := range uidToActions {
if len(assignedActions) == len(actions) {
args = append(args, uid)
}
}
return args
}
// Checks if the user has the required permissions on the root (used to be the General folder)
func hasAccessToRoot(actionsToCheck []any, user identity.Requester) bool {
func hasAccessToRoot(actionToCheck string, user identity.Requester) bool {
generalFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)
for _, action := range actionsToCheck {
if !slices.Contains(user.GetPermissions()[action.(string)], generalFolderScope) {
return false
}
}
return true
return slices.Contains(user.GetPermissions()[actionToCheck], generalFolderScope)
}
@@ -53,20 +53,24 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
// currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana)
useSelfContainedPermissions := f.user.GetAuthenticatedBy() == login.ExtendedJWTModule
if len(f.dashboardAction) > 0 {
toCheck := actionsToCheck(f.dashboardAction, f.user.GetPermissions(), dashWildcards, folderWildcards)
if f.dashboardAction != "" {
toCheckDashboards := actionsToCheck(f.dashboardAction, f.dashboardActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards)
toCheckFolders := actionsToCheck(f.dashboardAction, f.folderActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards)
if len(toCheck) > 0 {
if len(toCheckDashboards) > 0 {
if !useSelfContainedPermissions {
builder.WriteString("(dashboard.uid IN (SELECT identifier FROM permission WHERE kind = 'dashboards' AND attribute = 'uid'")
builder.WriteString(rolesFilter)
args = append(args, params...)
builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)")
args = append(args, toCheck[0])
if len(toCheckDashboards) == 1 {
builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)")
args = append(args, toCheckDashboards[0])
} else {
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")) AND NOT dashboard.is_folder)")
args = append(args, toCheckDashboards...)
}
} else {
actions := parseStringSliceFromInterfaceSlice(toCheck)
args = getAllowedUIDs(actions, f.user, dashboards.ScopeDashboardsPrefix)
args = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeDashboardsPrefix)
// Only add the IN clause if we have any dashboards to check
if len(args) > 0 {
@@ -83,12 +87,15 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if len(toCheckFolders) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheckFolders[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckFolders)-1) + ")")
permSelectorArgs = append(permSelectorArgs, toCheckFolders...)
}
} else {
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
permSelectorArgs = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeFoldersPrefix)
// Only add the IN clause if we have any folders to check
if len(permSelectorArgs) > 0 {
@@ -133,7 +140,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
}
// Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder)
if hasAccessToRoot(toCheck, f.user) {
if hasAccessToRoot(f.dashboardAction, f.user) {
builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)")
}
} else {
@@ -145,23 +152,26 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
permSelector.Reset()
permSelectorArgs = permSelectorArgs[:0]
if len(f.folderAction) > 0 {
if len(f.dashboardAction) > 0 {
if f.folderAction != "" {
if f.dashboardAction != "" {
builder.WriteString(" OR ")
}
toCheck := actionsToCheck(f.folderAction, f.user.GetPermissions(), folderWildcards)
toCheck := actionsToCheck(f.folderAction, f.folderActionSets, f.user.GetPermissions(), folderWildcards)
if len(toCheck) > 0 {
if !useSelfContainedPermissions {
permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ")")
permSelectorArgs = append(permSelectorArgs, toCheck...)
}
} else {
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
permSelectorArgs = getAllowedUIDs(f.folderAction, f.user, dashboards.ScopeFoldersPrefix)
if len(permSelectorArgs) > 0 {
permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
@@ -416,8 +416,8 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"},
},
{
desc: "Should be able to view inherited folders if nested folders are enabled",
@@ -458,7 +458,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.permissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagAccessActionSets)...), featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -530,7 +530,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"},
expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"},
},
{
desc: "Should be able to view inherited folders if nested folders are enabled",
@@ -608,6 +608,125 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
}
}
func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T) {
testCases := []struct {
desc string
queryType string
permission dashboardaccess.PermissionType
signedInUserPermissions []accesscontrol.Permission
expectedResult []string
features []any
}{
{
desc: "Should not list any dashboards if user has no permissions",
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: nil,
},
{
desc: "Should not list any folders if user has no permissions",
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: nil,
},
{
desc: "Should be able to view folders if user has `folders:read` access to them",
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"parent", "subfolder"},
},
{
desc: "Should be able to view folders if user has action set access to them",
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: "folders:view", Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"parent", "subfolder"},
},
{
desc: "Should be able to view only the subfolder if user has action set access to it",
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: "folders:admin", Scope: "folders:uid:subfolder", Kind: "folders", Identifier: "subfolder"},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"subfolder"},
},
{
desc: "Should be able to filter for folders that user has write access to",
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_EDIT,
signedInUserPermissions: []accesscontrol.Permission{
{Action: "folders:edit", Scope: "folders:uid:subfolder", Kind: "folders", Identifier: "subfolder"},
{Action: "folders:view", Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"subfolder"},
},
}
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
t.Cleanup(func() {
guardian.New = origNewGuardian
})
var orgID int64 = 1
for _, tc := range testCases {
tc.signedInUserPermissions = append(tc.signedInUserPermissions, accesscontrol.Permission{
Action: dashboards.ActionFoldersCreate,
}, accesscontrol.Permission{
Action: dashboards.ActionFoldersWrite,
Scope: dashboards.ScopeFoldersAll,
}, accesscontrol.Permission{
Action: dashboards.ActionFoldersRead,
Scope: "folders:uid:unrelated"}, accesscontrol.Permission{
Action: dashboards.ActionDashboardsCreate,
Scope: "folders:uid:unrelated"})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.signedInUserPermissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
t.Run(tc.desc+" with features "+strings.Join(keys, ","), func(t *testing.T) {
db := setupNestedTest(t, usr, tc.signedInUserPermissions, orgID, features)
recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
require.NoError(t, err)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported)
var result []string
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()
recQry, recQryParams := filter.With()
params = append(recQryParams, params...)
s := recQry + "\nSELECT dashboard.title FROM dashboard WHERE " + q
leftJoin := filter.LeftJoin()
if leftJoin != "" {
s = recQry + "\nSELECT dashboard.title FROM dashboard LEFT OUTER JOIN " + leftJoin + " WHERE " + q + "ORDER BY dashboard.id ASC"
}
err := sess.SQL(s, params...).Find(&result)
return err
})
require.NoError(t, err)
assert.Equal(t, tc.expectedResult, result)
})
}
}
}
func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB {
t.Helper()
@@ -724,6 +843,15 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
})
require.NoError(t, err)
// create a root level dashboard
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: orgID,
Dashboard: simplejson.NewFromAny(map[string]any{
"title": "dashboard under the root",
}),
})
require.NoError(t, err)
// create dashboard under parent folder
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: orgID,
+4 -4
View File
@@ -16,19 +16,19 @@ import (
flowcontrolrequest "k8s.io/apiserver/pkg/util/flowcontrol/request"
"k8s.io/client-go/tools/cache"
entityStore "github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
var _ generic.RESTOptionsGetter = (*RESTOptionsGetter)(nil)
type RESTOptionsGetter struct {
cfg *setting.Cfg
store entityStore.EntityStoreClient
store resource.ResourceStoreClient
Codec runtime.Codec
}
func NewRESTOptionsGetter(cfg *setting.Cfg, store entityStore.EntityStoreClient, codec runtime.Codec) *RESTOptionsGetter {
func NewRESTOptionsGetter(cfg *setting.Cfg, store resource.ResourceStoreClient, codec runtime.Codec) *RESTOptionsGetter {
return &RESTOptionsGetter{
cfg: cfg,
store: store,
@@ -38,7 +38,7 @@ func NewRESTOptionsGetter(cfg *setting.Cfg, store entityStore.EntityStoreClient,
func (f *RESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
// build connection string to uniquely identify the storage backend
connectionInfo, err := json.Marshal(f.cfg.SectionWithEnvOverrides("entity_api").KeysHash())
connectionInfo, err := json.Marshal(f.cfg.SectionWithEnvOverrides("resource_store").KeysHash())
if err != nil {
return generic.RESTOptions{}, err
}
-89
View File
@@ -1,89 +0,0 @@
package apistore
import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
const SortByKey = "grafana.app/sortBy"
const ListDeletedKey = "grafana.app/listDeleted"
const ListHistoryKey = "grafana.app/listHistory"
type Requirements struct {
// Equals folder
Folder *string
// SortBy is a list of fields to sort by
SortBy []string
// ListDeleted is a flag to list deleted entities
ListDeleted bool
// ListHistory is a resource name to list the history of
ListHistory string
// ListOriginKeys needs to include the origin key of a given entity in order for it to be selected.
ListOriginKeys []string
}
func ReadLabelSelectors(selector labels.Selector) (Requirements, labels.Selector, error) {
requirements := Requirements{}
newSelector := labels.NewSelector()
if selector == nil {
return requirements, newSelector, nil
}
labelSelectors, _ := selector.Requirements()
for _, r := range labelSelectors {
switch r.Key() {
case utils.AnnoKeyFolder:
if (r.Operator() != selection.Equals) && (r.Operator() != selection.DoubleEquals) {
return requirements, newSelector, apierrors.NewBadRequest(utils.AnnoKeyFolder + " label selector only supports equality")
}
folder := r.Values().List()[0]
requirements.Folder = &folder
case SortByKey:
if r.Operator() != selection.In {
return requirements, newSelector, apierrors.NewBadRequest(SortByKey + " label selector only supports in")
}
requirements.SortBy = r.Values().List()
case utils.AnnoKeyOriginKey:
if r.Operator() != selection.In {
return requirements, newSelector, apierrors.NewBadRequest(SortByKey + " label selector only supports in")
}
requirements.ListOriginKeys = r.Values().List()
case ListDeletedKey:
if r.Operator() != selection.Equals {
return requirements, newSelector, apierrors.NewBadRequest(ListDeletedKey + " label selector only supports equality")
}
if len(r.Values().List()) != 1 {
return requirements, newSelector, apierrors.NewBadRequest(ListDeletedKey + " label selector only supports one value")
}
if r.Values().List()[0] != "true" && r.Values().List()[0] != "false" {
return requirements, newSelector, apierrors.NewBadRequest(ListDeletedKey + " label selector only supports true or false")
}
requirements.ListDeleted = r.Values().List()[0] == "true"
case ListHistoryKey:
if r.Operator() != selection.Equals {
return requirements, newSelector, apierrors.NewBadRequest(ListHistoryKey + " label selector only supports equality")
}
if len(r.Values().List()) != 1 {
return requirements, newSelector, apierrors.NewBadRequest(ListHistoryKey + " label selector only supports one value")
}
if r.Values().List()[0] == "" {
return requirements, newSelector, apierrors.NewBadRequest(ListHistoryKey + " label selector must not be empty")
}
requirements.ListHistory = r.Values().List()[0]
// add all unregonized label selectors to the new selector list, these will be processed by the entity store
default:
newSelector = newSelector.Add(r)
}
}
if requirements.ListDeleted && requirements.ListHistory != "" {
return requirements, newSelector, apierrors.NewBadRequest("cannot list deleted and history at the same time")
}
return requirements, newSelector, nil
}
+263 -383
View File
@@ -6,6 +6,7 @@
package apistore
import (
"bytes"
"context"
"errors"
"fmt"
@@ -21,7 +22,6 @@ import (
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage"
@@ -29,15 +29,17 @@ import (
"k8s.io/apiserver/pkg/storage/storagebackend/factory"
"k8s.io/klog/v2"
entityStore "github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
const SortByKey = "grafana.app/sortBy"
var _ storage.Interface = (*Storage)(nil)
// Storage implements storage.Interface and stores resources in unified storage
type Storage struct {
config *storagebackend.ConfigForResource
store entityStore.EntityStoreClient
store resource.ResourceStoreClient
gr schema.GroupResource
codec runtime.Codec
keyFunc func(obj runtime.Object) (string, error)
@@ -51,7 +53,7 @@ type Storage struct {
func NewStorage(
config *storagebackend.ConfigForResource,
gr schema.GroupResource,
store entityStore.EntityStoreClient,
store resource.ResourceStoreClient,
codec runtime.Codec,
keyFunc func(obj runtime.Object) (string, error),
newFunc func() runtime.Object,
@@ -70,41 +72,85 @@ func NewStorage(
}, nil, nil
}
func errorWrap(err error, status *resource.StatusResult) error {
if err != nil {
return err
}
if status != nil {
return &apierrors.StatusError{metav1.Status{
Status: metav1.StatusFailure,
Code: status.Code,
Reason: metav1.StatusReason(status.Reason),
Message: status.Message,
}}
}
return nil
}
func getKey(ctx context.Context) (*resource.ResourceKey, error) {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return nil, apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
key := &resource.ResourceKey{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
}
if key.Group == "" {
return nil, apierrors.NewInternalError(fmt.Errorf("missing group in request"))
}
if key.Resource == "" {
return nil, apierrors.NewInternalError(fmt.Errorf("missing resource in request"))
}
return key, nil
}
// Create adds a new object at a key unless it already exists. 'ttl' is time-to-live
// in seconds (0 means forever). If no error is returned and out is not nil, out will be
// set to the read value from database.
func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, out runtime.Object, ttl uint64) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
if err := s.Versioner().PrepareObjectForStorage(obj); err != nil {
return err
}
e, err := resourceToEntity(obj, requestInfo, s.codec)
func (s *Storage) Create(ctx context.Context, _ string, obj runtime.Object, out runtime.Object, ttl uint64) error {
key, err := getKey(ctx)
if err != nil {
return err
}
req := &entityStore.CreateEntityRequest{
Entity: e,
}
rsp, err := s.store.Create(ctx, req)
err = s.Versioner().PrepareObjectForStorage(obj)
if err != nil {
return err
}
if rsp.Status != entityStore.CreateEntityResponse_CREATED {
return fmt.Errorf("this was not a create operation... (%s)", rsp.Status.String())
}
err = entityToResource(rsp.Entity, out, s.codec)
var buf bytes.Buffer
err = s.codec.Encode(obj, &buf)
if err != nil {
return apierrors.NewInternalError(err)
return err
}
cmd := &resource.CreateRequest{
Key: key,
Value: buf.Bytes(),
}
// TODO?? blob from context?
rsp, err := s.store.Create(ctx, cmd)
err = errorWrap(err, rsp.Status)
if err != nil {
return err
}
if rsp.Status != nil {
return fmt.Errorf("error in status %+v", rsp.Status)
}
// Copy the output bits
out = obj
after, err := meta.Accessor(out)
if err != nil {
return err
}
after.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10))
return nil
}
@@ -113,38 +159,34 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou
// If 'cachedExistingObject' is non-nil, it can be used as a suggestion about the
// current version of the object to avoid read operation from storage to get it.
// However, the implementations have to retry in case suggestion is stale.
func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
}
previousVersion := int64(0)
if preconditions != nil && preconditions.ResourceVersion != nil {
previousVersion, _ = strconv.ParseInt(*preconditions.ResourceVersion, 10, 64)
}
rsp, err := s.store.Delete(ctx, &entityStore.DeleteEntityRequest{
Key: k.String(),
PreviousVersion: previousVersion,
})
func (s *Storage) Delete(ctx context.Context, _ string, out runtime.Object, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error {
key, err := getKey(ctx)
if err != nil {
return err
}
err = entityToResource(rsp.Entity, out, s.codec)
if err != nil {
return apierrors.NewInternalError(err)
if validateDeletion != nil {
return fmt.Errorf("not supported (validate deletion)")
}
cmd := &resource.DeleteRequest{Key: key}
if preconditions != nil {
if preconditions.ResourceVersion != nil {
key.ResourceVersion, err = strconv.ParseInt(*preconditions.ResourceVersion, 10, 64)
if err != nil {
return err
}
}
if preconditions.UID != nil {
cmd.Uid = string(*preconditions.UID)
}
}
rsp, err := s.store.Delete(ctx, cmd)
err = errorWrap(err, rsp.Status)
if err != nil {
return err
}
return nil
}
@@ -155,121 +197,38 @@ func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, pr
// (e.g. reconnecting without missing any updates).
// If resource version is "0", this interface will get current object at given key
// and send it in an "ADDED" event, before watch starts.
func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return nil, apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
}
if opts.Predicate.Field != nil {
// check for metadata.name field selector
if v, ok := opts.Predicate.Field.RequiresExactMatch("metadata.name"); ok && k.Name == "" {
// just watch the specific key if we have a name field selector
k.Name = v
}
// check for metadata.namespace field selector
if v, ok := opts.Predicate.Field.RequiresExactMatch("metadata.namespace"); ok && k.Namespace == "" {
// just watch the specific namespace if we have a namespace field selector
k.Namespace = v
}
}
// translate grafana.app/* label selectors into field requirements
requirements, newSelector, err := ReadLabelSelectors(opts.Predicate.Label)
func (s *Storage) Watch(ctx context.Context, _ string, opts storage.ListOptions) (watch.Interface, error) {
listopts, _, err := toListRequest(ctx, opts)
if err != nil {
return nil, err
}
// Update the selector to remove the unneeded requirements
opts.Predicate.Label = newSelector
// if we got a listHistory label selector, watch the specified resource
if requirements.ListHistory != "" {
if k.Name != "" && k.Name != requirements.ListHistory {
return nil, apierrors.NewBadRequest("name field selector does not match listHistory")
}
k.Name = requirements.ListHistory
if listopts == nil {
return watch.NewEmptyWatch(), nil
}
if len(listopts.Sort) > 0 {
return nil, apierrors.NewBadRequest("sorting not supported in watch")
}
req := &entityStore.EntityWatchRequest{
Action: entityStore.EntityWatchRequest_START,
Key: []string{
k.String(),
},
Labels: map[string]string{},
WithBody: true,
WithStatus: true,
cmd := &resource.WatchRequest{
Since: listopts.ResourceVersion,
Options: listopts.Options,
SendInitialEvents: false,
AllowWatchBookmarks: opts.Predicate.AllowWatchBookmarks,
}
if opts.ResourceVersion != "" {
rv, err := strconv.ParseInt(opts.ResourceVersion, 10, 64)
if err != nil {
return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %s", opts.ResourceVersion))
}
req.Since = rv
if opts.SendInitialEvents != nil {
cmd.SendInitialEvents = *opts.SendInitialEvents
}
if opts.SendInitialEvents == nil && req.Since == 0 {
req.SendInitialEvents = true
} else if opts.SendInitialEvents != nil {
req.SendInitialEvents = *opts.SendInitialEvents
}
if requirements.Folder != nil {
req.Folder = *requirements.Folder
}
// translate "equals" label selectors to storage label conditions
labelRequirements, selectable := opts.Predicate.Label.Requirements()
if !selectable {
return nil, apierrors.NewBadRequest("label selector is not selectable")
}
for _, r := range labelRequirements {
if r.Operator() == selection.Equals {
req.Labels[r.Key()] = r.Values().List()[0]
}
}
client, err := s.store.Watch(ctx)
client, err := s.store.Watch(ctx, cmd)
if err != nil {
// if the context was canceled, just return a new empty watch
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.EOF) {
return watch.NewEmptyWatch(), nil
}
return nil, err
}
err = client.Send(req)
if err != nil {
err2 := client.CloseSend()
if err2 != nil {
klog.Errorf("watch close failed: %s\n", err2)
}
// if the context was canceled, just return a new empty watch
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.EOF) {
return watch.NewEmptyWatch(), nil
}
return nil, err
}
reporter := apierrors.NewClientErrorReporter(500, "WATCH", "")
decoder := &Decoder{
client: client,
newFunc: s.newFunc,
@@ -277,9 +236,7 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption
codec: s.codec,
}
w := watch.NewStreamWatcher(decoder, reporter)
return w, nil
return watch.NewStreamWatcher(decoder, reporter), nil
}
// Get unmarshals object found at key into objPtr. On a not found error, will either
@@ -287,53 +244,84 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption
// Treats empty responses and nil response nodes exactly like a not found error.
// The returned contents may be delayed, but it is guaranteed that they will
// match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'.
func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
}
resourceVersion := int64(0)
var err error
if opts.ResourceVersion != "" {
resourceVersion, err = strconv.ParseInt(opts.ResourceVersion, 10, 64)
if err != nil {
return apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %s", opts.ResourceVersion))
}
}
rsp, err := s.store.Read(ctx, &entityStore.ReadEntityRequest{
Key: k.String(),
WithBody: true,
WithStatus: true,
ResourceVersion: resourceVersion,
})
func (s *Storage) Get(ctx context.Context, _ string, opts storage.GetOptions, objPtr runtime.Object) error {
key, err := getKey(ctx)
if err != nil {
return err
}
if rsp.Key == "" {
if opts.IgnoreNotFound {
return nil
rsp, err := s.store.Read(ctx, &resource.ReadRequest{Key: key, IgnoreBlob: true})
err = errorWrap(err, rsp.Status)
if err != nil {
return err
}
_, _, err = s.codec.Decode(rsp.Value, &schema.GroupVersionKind{}, objPtr)
if err != nil {
return err
}
obj, err := meta.Accessor(objPtr)
if err != nil {
return err
}
obj.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10))
return nil
}
func toListRequest(ctx context.Context, opts storage.ListOptions) (*resource.ListRequest, storage.SelectionPredicate, error) {
predicate := opts.Predicate
key, err := getKey(ctx)
if err != nil {
return nil, predicate, err
}
req := &resource.ListRequest{
Limit: opts.Predicate.Limit,
Options: &resource.ListOptions{
Key: key,
},
}
if opts.Predicate.Label != nil && !opts.Predicate.Label.Empty() {
requirements, selectable := opts.Predicate.Label.Requirements()
if !selectable {
return nil, predicate, nil // not selectable
}
return apierrors.NewNotFound(s.gr, k.Name)
for _, r := range requirements {
v := r.Key()
if v == SortByKey {
// TODO! Must update the predicate!
continue
}
req.Options.Labels = append(req.Options.Labels, &resource.Requirement{
Key: v,
Operator: string(r.Operator()),
Values: r.Values().List(),
})
}
}
err = entityToResource(rsp, objPtr, s.codec)
if err != nil {
return apierrors.NewInternalError(err)
if opts.ResourceVersion != "" {
rv, err := strconv.ParseInt(opts.ResourceVersion, 10, 64)
if err != nil {
return nil, predicate, apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %s", opts.ResourceVersion))
}
req.ResourceVersion = rv
}
switch opts.ResourceVersionMatch {
case metav1.ResourceVersionMatchNotOlderThan:
req.VersionMatch = resource.ResourceVersionMatch_NotOlderThan
case metav1.ResourceVersionMatchExact:
req.VersionMatch = resource.ResourceVersionMatch_Exact
default:
return nil, predicate, apierrors.NewBadRequest(
fmt.Sprintf("unsupported version match: %v", opts.ResourceVersionMatch),
)
}
return nil
return req, predicate, nil
}
// GetList unmarshalls objects found at key into a *List api object (an object
@@ -342,18 +330,15 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions,
// is true, 'key' is used as a prefix.
// The returned contents may be delayed, but it is guaranteed that they will
// match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'.
func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
func (s *Storage) GetList(ctx context.Context, _ string, opts storage.ListOptions, listObj runtime.Object) error {
req, predicate, err := toListRequest(ctx, opts)
if err != nil {
return err
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
rsp, err := s.store.List(ctx, req)
if err != nil {
return err
}
listPtr, err := meta.GetItemsPtr(listObj)
@@ -365,117 +350,21 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti
return err
}
// translate grafana.app/* label selectors into field requirements
requirements, newSelector, err := ReadLabelSelectors(opts.Predicate.Label)
if err != nil {
return err
}
for _, item := range rsp.Items {
tmp := s.newFunc()
// Update the selector to remove the unneeded requirements
opts.Predicate.Label = newSelector
if requirements.ListHistory != "" {
k.Name = requirements.ListHistory
req := &entityStore.EntityHistoryRequest{
Key: k.String(),
WithBody: true,
WithStatus: true,
NextPageToken: opts.Predicate.Continue,
Limit: opts.Predicate.Limit,
Sort: requirements.SortBy,
}
rsp, err := s.store.History(ctx, req)
if err != nil {
return apierrors.NewInternalError(err)
}
for _, r := range rsp.Versions {
res := s.newFunc()
err := entityToResource(r, res, s.codec)
if err != nil {
return apierrors.NewInternalError(err)
}
// apply any predicates not handled in storage
matches, err := opts.Predicate.Matches(res)
if err != nil {
return apierrors.NewInternalError(err)
}
if !matches {
continue
}
v.Set(reflect.Append(v, reflect.ValueOf(res).Elem()))
}
listAccessor, err := meta.ListAccessor(listObj)
tmp, _, err = s.codec.Decode(item.Value, nil, tmp)
if err != nil {
return err
}
if rsp.NextPageToken != "" {
listAccessor.SetContinue(rsp.NextPageToken)
}
listAccessor.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10))
return nil
}
req := &entityStore.EntityListRequest{
Key: []string{
k.String(),
},
WithBody: true,
WithStatus: true,
NextPageToken: opts.Predicate.Continue,
Limit: opts.Predicate.Limit,
Labels: map[string]string{},
}
if requirements.Folder != nil {
req.Folder = *requirements.Folder
}
if len(requirements.SortBy) > 0 {
req.Sort = requirements.SortBy
}
if len(requirements.ListOriginKeys) > 0 {
req.OriginKeys = requirements.ListOriginKeys
}
if requirements.ListDeleted {
req.Deleted = true
}
// translate "equals" label selectors to storage label conditions
labelRequirements, selectable := opts.Predicate.Label.Requirements()
if !selectable {
return apierrors.NewBadRequest("label selector is not selectable")
}
for _, r := range labelRequirements {
if r.Operator() == selection.Equals {
req.Labels[r.Key()] = r.Values().List()[0]
}
}
rsp, err := s.store.List(ctx, req)
if err != nil {
return apierrors.NewInternalError(err)
}
for _, r := range rsp.Results {
res := s.newFunc()
err := entityToResource(r, res, s.codec)
obj, err := meta.Accessor(tmp)
if err != nil {
return apierrors.NewInternalError(err)
return err
}
obj.SetResourceVersion(strconv.FormatInt(item.ResourceVersion, 10))
// apply any predicates not handled in storage
matches, err := opts.Predicate.Matches(res)
matches, err := predicate.Matches(tmp)
if err != nil {
return apierrors.NewInternalError(err)
}
@@ -483,20 +372,20 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti
continue
}
v.Set(reflect.Append(v, reflect.ValueOf(res).Elem()))
v.Set(reflect.Append(v, reflect.ValueOf(tmp).Elem()))
}
listAccessor, err := meta.ListAccessor(listObj)
if err != nil {
return err
}
if rsp.NextPageToken != "" {
listAccessor.SetContinue(rsp.NextPageToken)
}
if rsp.RemainingItemCount > 0 {
listAccessor.SetRemainingItemCount(&rsp.RemainingItemCount)
}
listAccessor.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10))
return nil
}
@@ -515,33 +404,26 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti
// However, the implementations have to retry in case suggestion is stale.
func (s *Storage) GuaranteedUpdate(
ctx context.Context,
key string,
_ string,
destination runtime.Object,
ignoreNotFound bool,
preconditions *storage.Preconditions,
tryUpdate storage.UpdateFunc,
cachedExistingObject runtime.Object,
) error {
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return apierrors.NewInternalError(fmt.Errorf("could not get request info"))
key, err := getKey(ctx)
if err != nil {
return err
}
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: requestInfo.Name,
Subresource: requestInfo.Subresource,
}
getErr := s.Get(ctx, k.String(), storage.GetOptions{}, destination)
if getErr != nil {
if ignoreNotFound && apierrors.IsNotFound(getErr) {
// Get the current version
err = s.Get(ctx, "<ignored>", storage.GetOptions{}, destination)
if err != nil {
if ignoreNotFound && apierrors.IsNotFound(err) {
// destination is already set to zero value
// we'll create the resource
} else {
return getErr
return err
}
}
@@ -549,9 +431,25 @@ func (s *Storage) GuaranteedUpdate(
if err != nil {
return err
}
// Early optimistic locking failure
previousVersion, _ := strconv.ParseInt(accessor.GetResourceVersion(), 10, 64)
if preconditions != nil && preconditions.ResourceVersion != nil {
previousVersion, _ = strconv.ParseInt(*preconditions.ResourceVersion, 10, 64)
if preconditions != nil {
if preconditions.ResourceVersion != nil {
rv, err := strconv.ParseInt(*preconditions.ResourceVersion, 10, 64)
if err != nil {
return err
}
if rv != previousVersion {
return fmt.Errorf("optimistic locking mismatch (previousVersion mismatch)")
}
}
if preconditions.UID != nil {
if accessor.GetUID() != *preconditions.UID {
return fmt.Errorf("optimistic locking mismatch (UID mismatch)")
}
}
}
res := &storage.ResponseMeta{}
@@ -564,54 +462,30 @@ func (s *Storage) GuaranteedUpdate(
return statusErr
}
}
return apierrors.NewInternalError(fmt.Errorf("could not successfully update object. key=%s, err=%s", k.String(), err.Error()))
return apierrors.NewInternalError(
fmt.Errorf("could not successfully update object. key=%s, err=%s", key.String(), err.Error()),
)
}
e, err := resourceToEntity(updatedObj, requestInfo, s.codec)
accessor, err = meta.Accessor(updatedObj)
if err != nil {
return apierrors.NewInternalError(err)
}
var buf bytes.Buffer
err = s.codec.Encode(updatedObj, &buf)
if err != nil {
return err
}
// if we have a non-nil getErr, then we've ignored a not found error
if getErr != nil {
// object does not exist, create it
req := &entityStore.CreateEntityRequest{
Entity: e,
}
rsp, err := s.store.Create(ctx, req)
if err != nil {
return err
}
err = entityToResource(rsp.Entity, destination, s.codec)
if err != nil {
return apierrors.NewInternalError(err)
}
return nil
}
// update the existing object
req := &entityStore.UpdateEntityRequest{
Entity: e,
PreviousVersion: previousVersion,
}
req := &resource.UpdateRequest{Key: key, Value: buf.Bytes()}
// TODO... message
rsp, err := s.store.Update(ctx, req)
err = errorWrap(err, rsp.Status)
if err != nil {
return err // continue???
}
if rsp.Status == entityStore.UpdateEntityResponse_UNCHANGED {
return nil // destination is already set
}
err = entityToResource(rsp.Entity, destination, s.codec)
if err != nil {
return apierrors.NewInternalError(err)
return err
}
accessor.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10))
return nil
}
@@ -630,12 +504,24 @@ func (s *Storage) RequestWatchProgress(ctx context.Context) error {
}
type Decoder struct {
client entityStore.EntityStore_WatchClient
client resource.ResourceStore_WatchClient
newFunc func() runtime.Object
opts storage.ListOptions
codec runtime.Codec
}
func (d *Decoder) toObject(w *resource.ResourceWrapper) (runtime.Object, error) {
obj, _, err := d.codec.Decode(w.Value, nil, d.newFunc())
if err == nil {
accessor, err := meta.Accessor(obj)
if err != nil {
return nil, err
}
accessor.SetResourceVersion(strconv.FormatInt(w.ResourceVersion, 10))
}
return obj, err
}
func (d *Decoder) Decode() (action watch.EventType, object runtime.Object, err error) {
decode:
for {
@@ -659,14 +545,14 @@ decode:
return watch.Error, nil, err
}
if resp.Entity == nil {
if resp.Resource == nil {
klog.Errorf("client: received nil entity\n")
continue decode
}
obj := d.newFunc()
if resp.Resource.Operation == resource.ResourceOperation_BOOKMARK {
obj := d.newFunc()
if resp.Entity.Action == entityStore.Entity_BOOKMARK {
// here k8s expects an empty object with just resource version and k8s.io/initial-events-end annotation
accessor, err := meta.Accessor(obj)
if err != nil {
@@ -674,20 +560,20 @@ decode:
return watch.Error, nil, err
}
accessor.SetResourceVersion(fmt.Sprintf("%d", resp.Entity.ResourceVersion))
accessor.SetResourceVersion(strconv.FormatInt(resp.Resource.ResourceVersion, 10))
accessor.SetAnnotations(map[string]string{"k8s.io/initial-events-end": "true"})
return watch.Bookmark, obj, nil
}
err = entityToResource(resp.Entity, obj, d.codec)
obj, err := d.toObject(resp.Resource)
if err != nil {
klog.Errorf("error decoding entity: %s", err)
return watch.Error, nil, err
}
var watchAction watch.EventType
switch resp.Entity.Action {
case entityStore.Entity_CREATED:
switch resp.Resource.Operation {
case resource.ResourceOperation_CREATED:
// apply any predicates not handled in storage
matches, err := d.opts.Predicate.Matches(obj)
if err != nil {
@@ -697,9 +583,9 @@ decode:
if !matches {
continue decode
}
watchAction = watch.Added
case entityStore.Entity_UPDATED:
case resource.ResourceOperation_UPDATED:
watchAction = watch.Modified
// apply any predicates not handled in storage
@@ -710,10 +596,10 @@ decode:
}
// if we have a previous object, check if it matches
var prevObj runtime.Object
prevMatches := false
prevObj := d.newFunc()
if resp.Previous != nil {
err = entityToResource(resp.Previous, prevObj, d.codec)
prevObj, err := d.toObject(resp.Previous)
if err != nil {
klog.Errorf("error decoding entity: %s", err)
return watch.Error, nil, err
@@ -743,23 +629,17 @@ decode:
klog.Errorf("error getting object accessor: %s", err)
return watch.Error, nil, err
}
accessor.SetResourceVersion(fmt.Sprintf("%d", resp.Entity.ResourceVersion))
accessor.SetResourceVersion(strconv.FormatInt(resp.Previous.ResourceVersion, 10))
} else if !prevMatches {
// if the object didn't previously match, send an Added event
watchAction = watch.Added
}
case entityStore.Entity_DELETED:
case resource.ResourceOperation_DELETED:
watchAction = watch.Deleted
// if we have a previous object, return that in the deleted event
if resp.Previous != nil {
err = entityToResource(resp.Previous, obj, d.codec)
if err != nil {
klog.Errorf("error decoding entity: %s", err)
return watch.Error, nil, err
}
// here k8s expects the previous object but with the new resource version
accessor, err := meta.Accessor(obj)
if err != nil {
@@ -767,7 +647,7 @@ decode:
return watch.Error, nil, err
}
accessor.SetResourceVersion(fmt.Sprintf("%d", resp.Entity.ResourceVersion))
accessor.SetResourceVersion(strconv.FormatInt(resp.Previous.ResourceVersion, 10))
}
// apply any predicates not handled in storage
@@ -9,7 +9,6 @@ import (
"context"
"testing"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/apitesting"
@@ -25,6 +24,8 @@ import (
"k8s.io/apiserver/pkg/storage/storagebackend/factory"
storagetesting "k8s.io/apiserver/pkg/storage/testing"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/apiserver/storage/entity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
-182
View File
@@ -1,182 +0,0 @@
package apistore
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"time"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/apimachinery/utils"
entityStore "github.com/grafana/grafana/pkg/services/store/entity"
)
func entityToResource(rsp *entityStore.Entity, res runtime.Object, codec runtime.Codec) error {
var err error
// Read the body first -- it includes old resourceVersion!
if len(rsp.Body) > 0 {
decoded, _, err := codec.Decode(rsp.Body, &schema.GroupVersionKind{Group: rsp.Group, Version: rsp.GroupVersion}, res)
if err != nil {
return err
}
res = decoded
}
metaAccessor, err := meta.Accessor(res)
if err != nil {
return err
}
if len(rsp.Meta) > 0 {
err = json.Unmarshal(rsp.Meta, res)
if err != nil {
return err
}
}
metaAccessor.SetName(rsp.Name)
metaAccessor.SetNamespace(rsp.Namespace)
metaAccessor.SetUID(types.UID(rsp.Guid))
metaAccessor.SetResourceVersion(fmt.Sprintf("%d", rsp.ResourceVersion))
metaAccessor.SetCreationTimestamp(metav1.Unix(rsp.CreatedAt/1000, rsp.CreatedAt%1000*1000000))
grafanaAccessor, err := utils.MetaAccessor(metaAccessor)
if err != nil {
return err
}
if rsp.Folder != "" {
grafanaAccessor.SetFolder(rsp.Folder)
}
if rsp.CreatedBy != "" {
grafanaAccessor.SetCreatedBy(rsp.CreatedBy)
}
if rsp.UpdatedBy != "" {
grafanaAccessor.SetUpdatedBy(rsp.UpdatedBy)
}
if rsp.UpdatedAt != 0 {
updatedAt := time.UnixMilli(rsp.UpdatedAt).UTC()
grafanaAccessor.SetUpdatedTimestamp(&updatedAt)
}
grafanaAccessor.SetSlug(rsp.Slug)
if rsp.Origin != nil {
originTime := time.UnixMilli(rsp.Origin.Time).UTC()
grafanaAccessor.SetOriginInfo(&utils.ResourceOriginInfo{
Name: rsp.Origin.Source,
Key: rsp.Origin.Key,
// Path: rsp.Origin.Path,
Timestamp: &originTime,
})
}
if len(rsp.Labels) > 0 {
metaAccessor.SetLabels(rsp.Labels)
}
// TODO fields?
if len(rsp.Status) > 0 {
status := reflect.ValueOf(res).Elem().FieldByName("Status")
if status != (reflect.Value{}) && status.CanSet() {
err = json.Unmarshal(rsp.Status, status.Addr().Interface())
if err != nil {
return err
}
}
}
return nil
}
func resourceToEntity(res runtime.Object, requestInfo *request.RequestInfo, codec runtime.Codec) (*entityStore.Entity, error) {
metaAccessor, err := meta.Accessor(res)
if err != nil {
return nil, err
}
grafanaAccessor, err := utils.MetaAccessor(metaAccessor)
if err != nil {
return nil, err
}
rv, _ := strconv.ParseInt(metaAccessor.GetResourceVersion(), 10, 64)
k := &entityStore.Key{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Namespace: requestInfo.Namespace,
Name: metaAccessor.GetName(),
Subresource: requestInfo.Subresource,
}
rsp := &entityStore.Entity{
Group: k.Group,
GroupVersion: requestInfo.APIVersion,
Resource: k.Resource,
Subresource: k.Subresource,
Namespace: k.Namespace,
Key: k.String(),
Name: k.Name,
Guid: string(metaAccessor.GetUID()),
ResourceVersion: rv,
Folder: grafanaAccessor.GetFolder(),
CreatedAt: metaAccessor.GetCreationTimestamp().Time.UnixMilli(),
CreatedBy: grafanaAccessor.GetCreatedBy(),
UpdatedBy: grafanaAccessor.GetUpdatedBy(),
Slug: grafanaAccessor.GetSlug(),
Title: grafanaAccessor.FindTitle(metaAccessor.GetName()),
Origin: &entityStore.EntityOriginInfo{
Source: grafanaAccessor.GetOriginName(),
Key: grafanaAccessor.GetOriginKey(),
// Path: grafanaAccessor.GetOriginPath(),
},
Labels: metaAccessor.GetLabels(),
}
t, err := grafanaAccessor.GetUpdatedTimestamp()
if err != nil {
return nil, err
}
if t != nil {
rsp.UpdatedAt = t.UnixMilli()
}
t, err = grafanaAccessor.GetOriginTimestamp()
if err != nil {
return nil, err
}
if t != nil {
rsp.Origin.Time = t.UnixMilli()
}
rsp.Meta, err = json.Marshal(meta.AsPartialObjectMetadata(metaAccessor))
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = codec.Encode(res, &buf)
if err != nil {
return nil, err
}
rsp.Body = buf.Bytes()
status := reflect.ValueOf(res).Elem().FieldByName("Status")
if status != (reflect.Value{}) {
rsp.Status, err = json.Marshal(status.Interface())
if err != nil {
return nil, err
}
}
return rsp, nil
}
-216
View File
@@ -1,216 +0,0 @@
package apistore
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"
entityStore "github.com/grafana/grafana/pkg/services/store/entity"
)
func TestResourceToEntity(t *testing.T) {
createdAt := metav1.Now()
createdAtStr := createdAt.UTC().Format(time.RFC3339)
// truncate here because RFC3339 doesn't support millisecond precision
// consider updating accessor to use RFC3339Nano to encode timestamps
updatedAt := createdAt.Add(time.Hour).Truncate(time.Second)
updatedAtStr := updatedAt.UTC().Format(time.RFC3339)
Scheme := runtime.NewScheme()
Scheme.AddKnownTypes(v0alpha1.PlaylistResourceInfo.GroupVersion(), &v0alpha1.Playlist{})
Codecs := serializer.NewCodecFactory(Scheme)
testCases := []struct {
requestInfo *request.RequestInfo
resource runtime.Object
codec runtime.Codec
expectedKey string
expectedGroupVersion string
expectedName string
expectedNamespace string
expectedTitle string
expectedGuid string
expectedVersion string
expectedFolder string
expectedCreatedAt int64
expectedUpdatedAt int64
expectedCreatedBy string
expectedUpdatedBy string
expectedSlug string
expectedOrigin *entityStore.EntityOriginInfo
expectedLabels map[string]string
expectedMeta []byte
expectedBody []byte
}{
{
requestInfo: &request.RequestInfo{
APIGroup: "playlist.grafana.app",
APIVersion: "v0alpha1",
Resource: "playlists",
Namespace: "default",
Name: "test-name",
},
resource: &v0alpha1.Playlist{
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: createdAt,
Labels: map[string]string{"label1": "value1", "label2": "value2"},
Name: "test-name",
ResourceVersion: "1",
UID: "test-uid",
Annotations: map[string]string{
"grafana.app/createdBy": "test-created-by",
"grafana.app/updatedBy": "test-updated-by",
"grafana.app/updatedTimestamp": updatedAtStr,
"grafana.app/folder": "test-folder",
"grafana.app/slug": "test-slug",
},
},
Spec: v0alpha1.Spec{
Title: "A playlist",
Interval: "5m",
Items: []v0alpha1.Item{
{Type: v0alpha1.ItemTypeDashboardByTag, Value: "panel-tests"},
{Type: v0alpha1.ItemTypeDashboardByUid, Value: "vmie2cmWz"},
},
},
},
expectedKey: "/playlist.grafana.app/playlists/namespaces/default/test-name",
expectedGroupVersion: "v0alpha1",
expectedName: "test-name",
expectedNamespace: "default",
expectedTitle: "A playlist",
expectedGuid: "test-uid",
expectedVersion: "1",
expectedFolder: "test-folder",
expectedCreatedAt: createdAt.UnixMilli(),
expectedUpdatedAt: updatedAt.UnixMilli(),
expectedCreatedBy: "test-created-by",
expectedUpdatedBy: "test-updated-by",
expectedSlug: "test-slug",
expectedOrigin: &entityStore.EntityOriginInfo{Source: "", Key: ""},
expectedLabels: map[string]string{"label1": "value1", "label2": "value2"},
expectedMeta: []byte(fmt.Sprintf(`{"metadata":{"name":"test-name","uid":"test-uid","resourceVersion":"1","creationTimestamp":%q,"labels":{"label1":"value1","label2":"value2"},"annotations":{"grafana.app/createdBy":"test-created-by","grafana.app/folder":"test-folder","grafana.app/slug":"test-slug","grafana.app/updatedBy":"test-updated-by","grafana.app/updatedTimestamp":%q}}}`, createdAtStr, updatedAtStr)),
expectedBody: []byte(fmt.Sprintf(`{"kind":"Playlist","apiVersion":"playlist.grafana.app/v0alpha1","metadata":{"name":"test-name","uid":"test-uid","resourceVersion":"1","creationTimestamp":%q,"labels":{"label1":"value1","label2":"value2"},"annotations":{"grafana.app/createdBy":"test-created-by","grafana.app/folder":"test-folder","grafana.app/slug":"test-slug","grafana.app/updatedBy":"test-updated-by","grafana.app/updatedTimestamp":%q}},"spec":{"title":"A playlist","interval":"5m","items":[{"type":"dashboard_by_tag","value":"panel-tests"},{"type":"dashboard_by_uid","value":"vmie2cmWz"}]}}`, createdAtStr, updatedAtStr)),
},
}
for _, tc := range testCases {
t.Run(tc.resource.GetObjectKind().GroupVersionKind().Kind+" to entity conversion should succeed", func(t *testing.T) {
entity, err := resourceToEntity(tc.resource, tc.requestInfo, Codecs.LegacyCodec(v0alpha1.PlaylistResourceInfo.GroupVersion()))
require.NoError(t, err)
assert.Equal(t, tc.expectedKey, entity.Key)
assert.Equal(t, tc.expectedName, entity.Name)
assert.Equal(t, tc.expectedNamespace, entity.Namespace)
assert.Equal(t, tc.expectedTitle, entity.Title)
assert.Equal(t, tc.expectedGroupVersion, entity.GroupVersion)
assert.Equal(t, tc.expectedName, entity.Name)
assert.Equal(t, tc.expectedGuid, entity.Guid)
assert.Equal(t, tc.expectedFolder, entity.Folder)
assert.Equal(t, tc.expectedCreatedAt, entity.CreatedAt)
assert.Equal(t, tc.expectedUpdatedAt, entity.UpdatedAt)
assert.Equal(t, tc.expectedCreatedBy, entity.CreatedBy)
assert.Equal(t, tc.expectedUpdatedBy, entity.UpdatedBy)
assert.Equal(t, tc.expectedSlug, entity.Slug)
assert.Equal(t, tc.expectedOrigin, entity.Origin)
assert.Equal(t, tc.expectedLabels, entity.Labels)
assert.Equal(t, tc.expectedMeta, entity.Meta)
assert.Equal(t, tc.expectedBody, entity.Body[:len(entity.Body)-1]) // remove trailing newline
})
}
}
func TestEntityToResource(t *testing.T) {
createdAt := metav1.Now()
createdAtStr := createdAt.UTC().Format(time.RFC3339)
updatedAt := createdAt.Add(time.Hour)
updatedAtStr := updatedAt.UTC().Format(time.RFC3339)
Scheme := runtime.NewScheme()
Scheme.AddKnownTypes(v0alpha1.PlaylistResourceInfo.GroupVersion(), &v0alpha1.Playlist{})
Codecs := serializer.NewCodecFactory(Scheme)
testCases := []struct {
entity *entityStore.Entity
codec runtime.Codec
expectedApiVersion string
expectedCreationTimestamp metav1.Time
expectedLabels map[string]string
expectedName string
expectedResourceVersion string
expectedUid string
expectedTitle string
expectedAnnotations map[string]string
expectedSpec any
}{
{
entity: &entityStore.Entity{
Key: "/playlist.grafana.app/playlists/namespaces/default/test-uid",
GroupVersion: "v0alpha1",
Name: "test-uid",
Title: "A playlist",
Guid: "test-guid",
Folder: "test-folder",
CreatedBy: "test-created-by",
CreatedAt: createdAt.UnixMilli(),
UpdatedAt: updatedAt.UnixMilli(),
UpdatedBy: "test-updated-by",
Slug: "test-slug",
Origin: &entityStore.EntityOriginInfo{},
Labels: map[string]string{"label1": "value1", "label2": "value2"},
Meta: []byte(fmt.Sprintf(`{"metadata":{"name":"test-name","uid":"test-uid","resourceVersion":"1","creationTimestamp":%q,"labels":{"label1":"value1","label2":"value2"},"annotations":{"grafana.app/createdBy":"test-created-by","grafana.app/folder":"test-folder","grafana.app/slug":"test-slug","grafana.app/updatedTimestamp":%q,"grafana.app/updatedBy":"test-updated-by"}}}`, createdAtStr, updatedAtStr)),
Body: []byte(fmt.Sprintf(`{"kind":"Playlist","apiVersion":"playlist.grafana.app/v0alpha1","metadata":{"name":"test-name","uid":"test-uid","resourceVersion":"1","creationTimestamp":%q,"labels":{"label1":"value1","label2":"value2"},"annotations":{"grafana.app/createdBy":"test-created-by","grafana.app/folder":"test-folder","grafana.app/slug":"test-slug","grafana.app/updatedBy":"test-updated-by","grafana.app/updatedTimestamp":%q}},"spec":{"title":"A playlist","interval":"5m","items":[{"type":"dashboard_by_tag","value":"panel-tests"},{"type":"dashboard_by_uid","value":"vmie2cmWz"}]}}`, createdAtStr, updatedAtStr)),
ResourceVersion: 1,
Action: entityStore.Entity_CREATED,
},
codec: runtime.Codec(nil),
expectedApiVersion: "playlist.grafana.app/v0alpha1",
expectedCreationTimestamp: createdAt,
expectedLabels: map[string]string{"label1": "value1", "label2": "value2"},
expectedName: "test-uid",
expectedTitle: "test-name",
expectedResourceVersion: "1",
expectedUid: "test-guid",
expectedAnnotations: map[string]string{
"grafana.app/createdBy": "test-created-by",
"grafana.app/folder": "test-folder",
"grafana.app/slug": "test-slug",
"grafana.app/updatedBy": "test-updated-by",
"grafana.app/updatedTimestamp": updatedAtStr,
},
expectedSpec: v0alpha1.Spec{
Title: "A playlist",
Interval: "5m",
Items: []v0alpha1.Item{
{Type: v0alpha1.ItemTypeDashboardByTag, Value: "panel-tests"},
{Type: v0alpha1.ItemTypeDashboardByUid, Value: "vmie2cmWz"},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.entity.Key+" to resource conversion should succeed", func(t *testing.T) {
var p v0alpha1.Playlist
err := entityToResource(tc.entity, &p, Codecs.LegacyCodec(v0alpha1.PlaylistResourceInfo.GroupVersion()))
require.NoError(t, err)
assert.Equal(t, tc.expectedApiVersion, p.TypeMeta.APIVersion)
assert.Equal(t, tc.expectedCreationTimestamp.Unix(), p.ObjectMeta.CreationTimestamp.Unix())
assert.Equal(t, tc.expectedLabels, p.ObjectMeta.Labels)
assert.Equal(t, tc.expectedName, p.ObjectMeta.Name)
assert.Equal(t, tc.expectedResourceVersion, p.ObjectMeta.ResourceVersion)
assert.Equal(t, tc.expectedUid, string(p.ObjectMeta.UID))
assert.Equal(t, tc.expectedAnnotations, p.ObjectMeta.Annotations)
assert.Equal(t, tc.expectedSpec, p.Spec)
})
}
}
+391 -326
View File
@@ -75,6 +75,52 @@ func (ResourceOperation) EnumDescriptor() ([]byte, []int) {
return file_resource_proto_rawDescGZIP(), []int{0}
}
type ResourceVersionMatch int32
const (
ResourceVersionMatch_NotOlderThan ResourceVersionMatch = 0
ResourceVersionMatch_Exact ResourceVersionMatch = 1
)
// Enum value maps for ResourceVersionMatch.
var (
ResourceVersionMatch_name = map[int32]string{
0: "NotOlderThan",
1: "Exact",
}
ResourceVersionMatch_value = map[string]int32{
"NotOlderThan": 0,
"Exact": 1,
}
)
func (x ResourceVersionMatch) Enum() *ResourceVersionMatch {
p := new(ResourceVersionMatch)
*p = x
return p
}
func (x ResourceVersionMatch) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ResourceVersionMatch) Descriptor() protoreflect.EnumDescriptor {
return file_resource_proto_enumTypes[1].Descriptor()
}
func (ResourceVersionMatch) Type() protoreflect.EnumType {
return &file_resource_proto_enumTypes[1]
}
func (x ResourceVersionMatch) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ResourceVersionMatch.Descriptor instead.
func (ResourceVersionMatch) EnumDescriptor() ([]byte, []int) {
return file_resource_proto_rawDescGZIP(), []int{1}
}
type LinkBlob_Action int32
const (
@@ -114,11 +160,11 @@ func (x LinkBlob_Action) String() string {
}
func (LinkBlob_Action) Descriptor() protoreflect.EnumDescriptor {
return file_resource_proto_enumTypes[1].Descriptor()
return file_resource_proto_enumTypes[2].Descriptor()
}
func (LinkBlob_Action) Type() protoreflect.EnumType {
return &file_resource_proto_enumTypes[1]
return &file_resource_proto_enumTypes[2]
}
func (x LinkBlob_Action) Number() protoreflect.EnumNumber {
@@ -160,11 +206,11 @@ func (x Sort_Order) String() string {
}
func (Sort_Order) Descriptor() protoreflect.EnumDescriptor {
return file_resource_proto_enumTypes[2].Descriptor()
return file_resource_proto_enumTypes[3].Descriptor()
}
func (Sort_Order) Type() protoreflect.EnumType {
return &file_resource_proto_enumTypes[2]
return &file_resource_proto_enumTypes[3]
}
func (x Sort_Order) Number() protoreflect.EnumNumber {
@@ -212,11 +258,11 @@ func (x HealthCheckResponse_ServingStatus) String() string {
}
func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor {
return file_resource_proto_enumTypes[3].Descriptor()
return file_resource_proto_enumTypes[4].Descriptor()
}
func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType {
return &file_resource_proto_enumTypes[3]
return &file_resource_proto_enumTypes[4]
}
func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber {
@@ -1158,9 +1204,11 @@ type ReadResponse struct {
ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
// The properties
Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"`
// The the commit message
Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"`
// A Signed URL that will let you fetch the blob
// If this value starts with # you must read the bytes using the GetResourceBlob request
BlobUrl string `protobuf:"bytes,4,opt,name=blob_url,json=blobUrl,proto3" json:"blob_url,omitempty"`
BlobUrl string `protobuf:"bytes,5,opt,name=blob_url,json=blobUrl,proto3" json:"blob_url,omitempty"`
}
func (x *ReadResponse) Reset() {
@@ -1216,6 +1264,13 @@ func (x *ReadResponse) GetValue() []byte {
return nil
}
func (x *ReadResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ReadResponse) GetBlobUrl() string {
if x != nil {
return x.BlobUrl
@@ -1461,17 +1516,13 @@ type ListOptions struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Maximum number of items to return
// NOTE responses will also be limited by the response payload size
Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
// Namespace+Group+Resource+etc
Key *ResourceKey `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
// This must *not* include the resource version
Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Match label
Labels []*Requirement `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty"`
Labels []*Requirement `protobuf:"bytes,2,rep,name=labels,proto3" json:"labels,omitempty"`
// Match fields (not yet supported)
Fields []*Requirement `protobuf:"bytes,5,rep,name=fields,proto3" json:"fields,omitempty"`
// Limit results to items in a specific folder (not a query for everything under that folder!)
Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"`
Fields []*Requirement `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"`
}
func (x *ListOptions) Reset() {
@@ -1506,13 +1557,6 @@ func (*ListOptions) Descriptor() ([]byte, []int) {
return file_resource_proto_rawDescGZIP(), []int{18}
}
func (x *ListOptions) GetLimit() int64 {
if x != nil {
return x.Limit
}
return 0
}
func (x *ListOptions) GetKey() *ResourceKey {
if x != nil {
return x.Key
@@ -1534,13 +1578,6 @@ func (x *ListOptions) GetFields() []*Requirement {
return nil
}
func (x *ListOptions) GetFolder() string {
if x != nil {
return x.Folder
}
return ""
}
type ListRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1548,10 +1585,17 @@ type ListRequest struct {
// Starting from the requested page (other query parameters must match!)
NextPageToken string `protobuf:"bytes,1,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
// The resource version
ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
// List options
VersionMatch ResourceVersionMatch `protobuf:"varint,3,opt,name=version_match,json=versionMatch,proto3,enum=resource.ResourceVersionMatch" json:"version_match,omitempty"`
// Maximum number of items to return
// NOTE responses will also be limited by the response payload size
Limit int64 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"`
// Filtering
Options *ListOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
Options *ListOptions `protobuf:"bytes,5,opt,name=options,proto3" json:"options,omitempty"`
// Sorting instructions `field ASC/DESC`
Sort []*Sort `protobuf:"bytes,3,rep,name=sort,proto3" json:"sort,omitempty"`
Sort []*Sort `protobuf:"bytes,6,rep,name=sort,proto3" json:"sort,omitempty"`
}
func (x *ListRequest) Reset() {
@@ -1593,6 +1637,27 @@ func (x *ListRequest) GetNextPageToken() string {
return ""
}
func (x *ListRequest) GetResourceVersion() int64 {
if x != nil {
return x.ResourceVersion
}
return 0
}
func (x *ListRequest) GetVersionMatch() ResourceVersionMatch {
if x != nil {
return x.VersionMatch
}
return ResourceVersionMatch_NotOlderThan
}
func (x *ListRequest) GetLimit() int64 {
if x != nil {
return x.Limit
}
return 0
}
func (x *ListRequest) GetOptions() *ListOptions {
if x != nil {
return x.Options
@@ -1697,8 +1762,6 @@ type WatchRequest struct {
// ResourceVersion of last changes. Empty will default to full history
Since int64 `protobuf:"varint,1,opt,name=since,proto3" json:"since,omitempty"`
// Watch specific entities
Key *ResourceKey `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
// Additional options
Options *ListOptions `protobuf:"bytes,3,opt,name=options,proto3" json:"options,omitempty"`
// Return initial events
@@ -1746,13 +1809,6 @@ func (x *WatchRequest) GetSince() int64 {
return 0
}
func (x *WatchRequest) GetKey() *ResourceKey {
if x != nil {
return x.Key
}
return nil
}
func (x *WatchRequest) GetOptions() *ListOptions {
if x != nil {
return x.Options
@@ -2427,7 +2483,7 @@ var file_resource_proto_rawDesc = []byte{
0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65,
0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x62,
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x42, 0x6c,
0x6f, 0x62, 0x22, 0x9a, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6f, 0x62, 0x22, 0xb4, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61,
@@ -2435,214 +2491,222 @@ var file_resource_proto_rawDesc = []byte{
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, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x75, 0x72, 0x6c,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x6c, 0x6f, 0x62, 0x55, 0x72, 0x6c, 0x22,
0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 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, 0x22, 0x7f, 0x0a, 0x0f, 0x47, 0x65,
0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a,
0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x52,
0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08,
0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73,
0x22, 0x64, 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, 0x2a,
0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x2e, 0x4f, 0x72,
0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x1a, 0x0a, 0x05, 0x4f, 0x72,
0x64, 0x65, 0x72, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x53, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04,
0x44, 0x45, 0x53, 0x43, 0x10, 0x01, 0x22, 0xc2, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f,
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18,
0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19,
0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x62, 0x6c, 0x6f, 0x62, 0x55, 0x72, 0x6c, 0x22, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74,
0x42, 0x6c, 0x6f, 0x62, 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, 0x22, 0x7f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12,
0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03,
0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x64, 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, 0x2a, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65,
0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f,
0x72, 0x64, 0x65, 0x72, 0x22, 0x1a, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x07, 0x0a,
0x03, 0x41, 0x53, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x45, 0x53, 0x43, 0x10, 0x01,
0x22, 0x94, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
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, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62,
0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c,
0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52,
0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0x90, 0x02, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74,
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,
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, 0x43, 0x0a, 0x0d, 0x76, 0x65,
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
0x0e, 0x32, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63,
0x68, 0x52, 0x0c, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12,
0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x18, 0x05, 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, 0x6e, 0x73, 0x52, 0x07, 0x6f,
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x06,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x53, 0x6f, 0x72, 0x74, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0xc4, 0x01, 0x0a, 0x0c, 0x4c,
0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69,
0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72,
0x61, 0x70, 0x70, 0x65, 0x72, 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, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 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, 0x04, 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, 0xb9, 0x01, 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x03, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x18, 0x03, 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, 0x6e, 0x73,
0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x6e,
0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x73, 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x69, 0x74,
0x69, 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x6c, 0x6c,
0x6f, 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x62, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72,
0x6b, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x57,
0x61, 0x74, 0x63, 0x68, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x22, 0x9b, 0x01,
0x0a, 0x0d, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01,
0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x35, 0x0a,
0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73,
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65,
0x72, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x0e,
0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 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, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03,
0x6b, 0x65, 0x79, 0x18, 0x03, 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, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18,
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x6c, 0x61,
0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x05,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x66, 0x69, 0x65,
0x6c, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x8a, 0x01, 0x0a, 0x0b,
0x4c, 0x69, 0x73, 0x74, 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, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02,
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, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x6f,
0x72, 0x74, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0xc4, 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73,
0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65,
0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70,
0x70, 0x65, 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65,
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f,
0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x92, 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73,
0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05,
0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d,
0x65, 0x74, 0x61, 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, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 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, 0x04, 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,
0xe2, 0x01, 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 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,
0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 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, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c,
0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x73,
0x65, 0x6e, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
0x12, 0x32, 0x0a, 0x15, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x5f,
0x62, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52,
0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x57, 0x61, 0x74, 0x63, 0x68, 0x42, 0x6f, 0x6f, 0x6b, 0x6d,
0x61, 0x72, 0x6b, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x0d, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65,
0x72, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x70,
0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f,
0x75, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 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, 0x14, 0x0a,
0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69,
0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 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, 0x21, 0x0a, 0x0c,
0x73, 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01,
0x28, 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22,
0x92, 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 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, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 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, 0x14,
0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c,
0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 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, 0x16, 0x0a,
0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f,
0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 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, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12,
0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68,
0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12,
0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01,
0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01,
0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 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, 0x29, 0x0a, 0x10,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 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, 0x2a, 0x55, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e,
0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54,
0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10,
0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c,
0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, 0x4b, 0x10, 0x04, 0x32, 0xba, 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, 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, 0x3a, 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, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 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, 0x32, 0xce, 0x02, 0x0a, 0x0e, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x35, 0x0a, 0x04,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x01,
0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 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, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a,
0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 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, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5,
0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69,
0x6e, 0x49, 0x6e, 0x66, 0x6f, 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, 0x23,
0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f,
0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67,
0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e,
0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01, 0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69,
0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65,
0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67,
0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 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, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
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, 0x2a, 0x55,
0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00,
0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a,
0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45,
0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d,
0x41, 0x52, 0x4b, 0x10, 0x04, 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, 0xba, 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, 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, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18,
0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72,
0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52,
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, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
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,
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, 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, 0x3a, 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, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61,
0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 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, 0x32, 0xce, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 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, 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, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 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,
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 (
@@ -2657,101 +2721,102 @@ func file_resource_proto_rawDescGZIP() []byte {
return file_resource_proto_rawDescData
}
var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
var file_resource_proto_goTypes = []interface{}{
(ResourceOperation)(0), // 0: resource.ResourceOperation
(LinkBlob_Action)(0), // 1: resource.LinkBlob.Action
(Sort_Order)(0), // 2: resource.Sort.Order
(HealthCheckResponse_ServingStatus)(0), // 3: resource.HealthCheckResponse.ServingStatus
(*ResourceKey)(nil), // 4: resource.ResourceKey
(*ResourceWrapper)(nil), // 5: resource.ResourceWrapper
(*ResourceMeta)(nil), // 6: resource.ResourceMeta
(*BlobInfo)(nil), // 7: resource.BlobInfo
(*StatusResult)(nil), // 8: resource.StatusResult
(*LinkBlob)(nil), // 9: resource.LinkBlob
(*CreateRequest)(nil), // 10: resource.CreateRequest
(*CreateResponse)(nil), // 11: resource.CreateResponse
(*UpdateRequest)(nil), // 12: resource.UpdateRequest
(*UpdateResponse)(nil), // 13: resource.UpdateResponse
(*DeleteRequest)(nil), // 14: resource.DeleteRequest
(*DeleteResponse)(nil), // 15: resource.DeleteResponse
(*ReadRequest)(nil), // 16: resource.ReadRequest
(*ReadResponse)(nil), // 17: resource.ReadResponse
(*GetBlobRequest)(nil), // 18: resource.GetBlobRequest
(*GetBlobResponse)(nil), // 19: resource.GetBlobResponse
(*Requirement)(nil), // 20: resource.Requirement
(*Sort)(nil), // 21: resource.Sort
(*ListOptions)(nil), // 22: resource.ListOptions
(*ListRequest)(nil), // 23: resource.ListRequest
(*ListResponse)(nil), // 24: resource.ListResponse
(*WatchRequest)(nil), // 25: resource.WatchRequest
(*WatchResponse)(nil), // 26: resource.WatchResponse
(*HistoryRequest)(nil), // 27: resource.HistoryRequest
(*HistoryResponse)(nil), // 28: resource.HistoryResponse
(*OriginRequest)(nil), // 29: resource.OriginRequest
(*ResourceOriginInfo)(nil), // 30: resource.ResourceOriginInfo
(*OriginResponse)(nil), // 31: resource.OriginResponse
(*HealthCheckRequest)(nil), // 32: resource.HealthCheckRequest
(*HealthCheckResponse)(nil), // 33: resource.HealthCheckResponse
(ResourceVersionMatch)(0), // 1: resource.ResourceVersionMatch
(LinkBlob_Action)(0), // 2: resource.LinkBlob.Action
(Sort_Order)(0), // 3: resource.Sort.Order
(HealthCheckResponse_ServingStatus)(0), // 4: resource.HealthCheckResponse.ServingStatus
(*ResourceKey)(nil), // 5: resource.ResourceKey
(*ResourceWrapper)(nil), // 6: resource.ResourceWrapper
(*ResourceMeta)(nil), // 7: resource.ResourceMeta
(*BlobInfo)(nil), // 8: resource.BlobInfo
(*StatusResult)(nil), // 9: resource.StatusResult
(*LinkBlob)(nil), // 10: resource.LinkBlob
(*CreateRequest)(nil), // 11: resource.CreateRequest
(*CreateResponse)(nil), // 12: resource.CreateResponse
(*UpdateRequest)(nil), // 13: resource.UpdateRequest
(*UpdateResponse)(nil), // 14: resource.UpdateResponse
(*DeleteRequest)(nil), // 15: resource.DeleteRequest
(*DeleteResponse)(nil), // 16: resource.DeleteResponse
(*ReadRequest)(nil), // 17: resource.ReadRequest
(*ReadResponse)(nil), // 18: resource.ReadResponse
(*GetBlobRequest)(nil), // 19: resource.GetBlobRequest
(*GetBlobResponse)(nil), // 20: resource.GetBlobResponse
(*Requirement)(nil), // 21: resource.Requirement
(*Sort)(nil), // 22: resource.Sort
(*ListOptions)(nil), // 23: resource.ListOptions
(*ListRequest)(nil), // 24: resource.ListRequest
(*ListResponse)(nil), // 25: resource.ListResponse
(*WatchRequest)(nil), // 26: resource.WatchRequest
(*WatchResponse)(nil), // 27: resource.WatchResponse
(*HistoryRequest)(nil), // 28: resource.HistoryRequest
(*HistoryResponse)(nil), // 29: resource.HistoryResponse
(*OriginRequest)(nil), // 30: resource.OriginRequest
(*ResourceOriginInfo)(nil), // 31: resource.ResourceOriginInfo
(*OriginResponse)(nil), // 32: resource.OriginResponse
(*HealthCheckRequest)(nil), // 33: resource.HealthCheckRequest
(*HealthCheckResponse)(nil), // 34: resource.HealthCheckResponse
}
var file_resource_proto_depIdxs = []int32{
0, // 0: resource.ResourceWrapper.operation:type_name -> resource.ResourceOperation
0, // 1: resource.ResourceMeta.operation:type_name -> resource.ResourceOperation
4, // 2: resource.CreateRequest.key:type_name -> resource.ResourceKey
9, // 3: resource.CreateRequest.blob:type_name -> resource.LinkBlob
8, // 4: resource.CreateResponse.status:type_name -> resource.StatusResult
4, // 5: resource.UpdateRequest.key:type_name -> resource.ResourceKey
9, // 6: resource.UpdateRequest.blob:type_name -> resource.LinkBlob
8, // 7: resource.UpdateResponse.status:type_name -> resource.StatusResult
4, // 8: resource.DeleteRequest.key:type_name -> resource.ResourceKey
8, // 9: resource.DeleteResponse.status:type_name -> resource.StatusResult
4, // 10: resource.ReadRequest.key:type_name -> resource.ResourceKey
8, // 11: resource.ReadResponse.status:type_name -> resource.StatusResult
4, // 12: resource.GetBlobRequest.key:type_name -> resource.ResourceKey
8, // 13: resource.GetBlobResponse.status:type_name -> resource.StatusResult
7, // 14: resource.GetBlobResponse.info:type_name -> resource.BlobInfo
2, // 15: resource.Sort.order:type_name -> resource.Sort.Order
4, // 16: resource.ListOptions.key:type_name -> resource.ResourceKey
20, // 17: resource.ListOptions.labels:type_name -> resource.Requirement
20, // 18: resource.ListOptions.fields:type_name -> resource.Requirement
22, // 19: resource.ListRequest.options:type_name -> resource.ListOptions
21, // 20: resource.ListRequest.sort:type_name -> resource.Sort
5, // 21: resource.ListResponse.items:type_name -> resource.ResourceWrapper
4, // 22: resource.WatchRequest.key:type_name -> resource.ResourceKey
22, // 23: resource.WatchRequest.options:type_name -> resource.ListOptions
5, // 24: resource.WatchResponse.resource:type_name -> resource.ResourceWrapper
5, // 25: resource.WatchResponse.previous:type_name -> resource.ResourceWrapper
4, // 26: resource.HistoryRequest.key:type_name -> resource.ResourceKey
6, // 27: resource.HistoryResponse.items:type_name -> resource.ResourceMeta
4, // 28: resource.OriginRequest.key:type_name -> resource.ResourceKey
4, // 29: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey
30, // 30: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo
3, // 31: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
16, // 32: resource.ResourceStore.Read:input_type -> resource.ReadRequest
10, // 33: resource.ResourceStore.Create:input_type -> resource.CreateRequest
12, // 34: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
14, // 35: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
23, // 36: resource.ResourceStore.List:input_type -> resource.ListRequest
25, // 37: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
32, // 38: resource.ResourceStore.IsHealthy:input_type -> resource.HealthCheckRequest
16, // 39: resource.ResourceSearch.Read:input_type -> resource.ReadRequest
18, // 40: resource.ResourceSearch.GetBlob:input_type -> resource.GetBlobRequest
27, // 41: resource.ResourceSearch.History:input_type -> resource.HistoryRequest
29, // 42: resource.ResourceSearch.Origin:input_type -> resource.OriginRequest
32, // 43: resource.ResourceSearch.IsHealthy:input_type -> resource.HealthCheckRequest
17, // 44: resource.ResourceStore.Read:output_type -> resource.ReadResponse
11, // 45: resource.ResourceStore.Create:output_type -> resource.CreateResponse
13, // 46: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
15, // 47: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
24, // 48: resource.ResourceStore.List:output_type -> resource.ListResponse
26, // 49: resource.ResourceStore.Watch:output_type -> resource.WatchResponse
33, // 50: resource.ResourceStore.IsHealthy:output_type -> resource.HealthCheckResponse
17, // 51: resource.ResourceSearch.Read:output_type -> resource.ReadResponse
19, // 52: resource.ResourceSearch.GetBlob:output_type -> resource.GetBlobResponse
28, // 53: resource.ResourceSearch.History:output_type -> resource.HistoryResponse
31, // 54: resource.ResourceSearch.Origin:output_type -> resource.OriginResponse
33, // 55: resource.ResourceSearch.IsHealthy:output_type -> resource.HealthCheckResponse
5, // 2: resource.CreateRequest.key:type_name -> resource.ResourceKey
10, // 3: resource.CreateRequest.blob:type_name -> resource.LinkBlob
9, // 4: resource.CreateResponse.status:type_name -> resource.StatusResult
5, // 5: resource.UpdateRequest.key:type_name -> resource.ResourceKey
10, // 6: resource.UpdateRequest.blob:type_name -> resource.LinkBlob
9, // 7: resource.UpdateResponse.status:type_name -> resource.StatusResult
5, // 8: resource.DeleteRequest.key:type_name -> resource.ResourceKey
9, // 9: resource.DeleteResponse.status:type_name -> resource.StatusResult
5, // 10: resource.ReadRequest.key:type_name -> resource.ResourceKey
9, // 11: resource.ReadResponse.status:type_name -> resource.StatusResult
5, // 12: resource.GetBlobRequest.key:type_name -> resource.ResourceKey
9, // 13: resource.GetBlobResponse.status:type_name -> resource.StatusResult
8, // 14: resource.GetBlobResponse.info:type_name -> resource.BlobInfo
3, // 15: resource.Sort.order:type_name -> resource.Sort.Order
5, // 16: resource.ListOptions.key:type_name -> resource.ResourceKey
21, // 17: resource.ListOptions.labels:type_name -> resource.Requirement
21, // 18: resource.ListOptions.fields:type_name -> resource.Requirement
1, // 19: resource.ListRequest.version_match:type_name -> resource.ResourceVersionMatch
23, // 20: resource.ListRequest.options:type_name -> resource.ListOptions
22, // 21: resource.ListRequest.sort:type_name -> resource.Sort
6, // 22: resource.ListResponse.items:type_name -> resource.ResourceWrapper
23, // 23: resource.WatchRequest.options:type_name -> resource.ListOptions
6, // 24: resource.WatchResponse.resource:type_name -> resource.ResourceWrapper
6, // 25: resource.WatchResponse.previous:type_name -> resource.ResourceWrapper
5, // 26: resource.HistoryRequest.key:type_name -> resource.ResourceKey
7, // 27: resource.HistoryResponse.items:type_name -> resource.ResourceMeta
5, // 28: resource.OriginRequest.key:type_name -> resource.ResourceKey
5, // 29: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey
31, // 30: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo
4, // 31: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
17, // 32: resource.ResourceStore.Read:input_type -> resource.ReadRequest
11, // 33: resource.ResourceStore.Create:input_type -> resource.CreateRequest
13, // 34: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
15, // 35: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
24, // 36: resource.ResourceStore.List:input_type -> resource.ListRequest
26, // 37: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
33, // 38: resource.ResourceStore.IsHealthy:input_type -> resource.HealthCheckRequest
17, // 39: resource.ResourceSearch.Read:input_type -> resource.ReadRequest
19, // 40: resource.ResourceSearch.GetBlob:input_type -> resource.GetBlobRequest
28, // 41: resource.ResourceSearch.History:input_type -> resource.HistoryRequest
30, // 42: resource.ResourceSearch.Origin:input_type -> resource.OriginRequest
33, // 43: resource.ResourceSearch.IsHealthy:input_type -> resource.HealthCheckRequest
18, // 44: resource.ResourceStore.Read:output_type -> resource.ReadResponse
12, // 45: resource.ResourceStore.Create:output_type -> resource.CreateResponse
14, // 46: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
16, // 47: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
25, // 48: resource.ResourceStore.List:output_type -> resource.ListResponse
27, // 49: resource.ResourceStore.Watch:output_type -> resource.WatchResponse
34, // 50: resource.ResourceStore.IsHealthy:output_type -> resource.HealthCheckResponse
18, // 51: resource.ResourceSearch.Read:output_type -> resource.ReadResponse
20, // 52: resource.ResourceSearch.GetBlob:output_type -> resource.GetBlobResponse
29, // 53: resource.ResourceSearch.History:output_type -> resource.HistoryResponse
32, // 54: resource.ResourceSearch.Origin:output_type -> resource.OriginResponse
34, // 55: resource.ResourceSearch.IsHealthy:output_type -> resource.HealthCheckResponse
44, // [44:56] is the sub-list for method output_type
32, // [32:44] is the sub-list for method input_type
32, // [32:32] is the sub-list for extension type_name
@@ -3131,7 +3196,7 @@ func file_resource_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_resource_proto_rawDesc,
NumEnums: 4,
NumEnums: 5,
NumMessages: 30,
NumExtensions: 0,
NumServices: 2,
+24 -15
View File
@@ -203,9 +203,12 @@ message ReadResponse {
// The properties
bytes value = 3;
// The the commit message
string message = 4;
// A Signed URL that will let you fetch the blob
// If this value starts with # you must read the bytes using the GetResourceBlob request
string blob_url = 4;
string blob_url = 5;
}
message GetBlobRequest {
@@ -245,32 +248,41 @@ message Sort {
}
message ListOptions {
// Maximum number of items to return
// NOTE responses will also be limited by the response payload size
int64 limit = 2;
// Namespace+Group+Resource+etc
ResourceKey key = 3;
// This must *not* include the resource version
ResourceKey key = 1;
// Match label
repeated Requirement labels = 4;
repeated Requirement labels = 2;
// Match fields (not yet supported)
repeated Requirement fields = 5;
repeated Requirement fields = 3;
}
// Limit results to items in a specific folder (not a query for everything under that folder!)
string folder = 6;
enum ResourceVersionMatch {
NotOlderThan = 0;
Exact = 1;
}
message ListRequest {
// Starting from the requested page (other query parameters must match!)
string next_page_token = 1;
// The resource version
int64 resource_version = 2;
// List options
ResourceVersionMatch version_match = 3;
// Maximum number of items to return
// NOTE responses will also be limited by the response payload size
int64 limit = 4;
// Filtering
ListOptions options = 2;
ListOptions options = 5;
// Sorting instructions `field ASC/DESC`
repeated Sort sort = 3;
repeated Sort sort = 6;
}
message ListResponse {
@@ -299,9 +311,6 @@ message WatchRequest {
// ResourceVersion of last changes. Empty will default to full history
int64 since = 1;
// Watch specific entities
ResourceKey key = 2;
// Additional options
ListOptions options = 3;
@@ -385,8 +385,8 @@ const (
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// Unlike ResourceStore, this service does not have strict read after write guarantees
// Clients can use this service directly
// NOTE: This is read only, and no read afer write guarantees
type ResourceSearchClient interface {
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
// Get the raw blob bytes and metadata
@@ -461,8 +461,8 @@ func (c *resourceSearchClient) IsHealthy(ctx context.Context, in *HealthCheckReq
// All implementations should embed UnimplementedResourceSearchServer
// for forward compatibility
//
// Unlike ResourceStore, this service does not have strict read after write guarantees
// Clients can use this service directly
// NOTE: This is read only, and no read afer write guarantees
type ResourceSearchServer interface {
Read(context.Context, *ReadRequest) (*ReadResponse, error)
// Get the raw blob bytes and metadata
+66
View File
@@ -3127,6 +3127,14 @@
}
}
},
"CloudMigrationRequest": {
"type": "object",
"properties": {
"authToken": {
"type": "string"
}
}
},
"CloudMigrationResponse": {
"type": "object",
"properties": {
@@ -4630,6 +4638,29 @@
"$ref": "#/definitions/Frame"
}
},
"GetAccessTokenResponseDTO": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"displayName": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"firstUsedAt": {
"type": "string"
},
"id": {
"type": "string"
},
"lastUsedAt": {
"type": "string"
}
}
},
"GetAnnotationTagsResponse": {
"type": "object",
"title": "GetAnnotationTagsResponse is a response struct for FindTagsResult.",
@@ -6610,6 +6641,32 @@
}
}
},
"SearchDTO": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"basicRole": {
"type": "string"
},
"onlyRoles": {
"type": "boolean"
},
"roleName": {
"type": "string"
},
"scope": {
"type": "string"
},
"teamId": {
"type": "string"
},
"userId": {
"type": "string"
}
}
},
"SearchDeviceQueryResult": {
"type": "object",
"properties": {
@@ -8276,6 +8333,15 @@
"$ref": "#/definitions/CreateAccessTokenResponseDTO"
}
},
"cloudMigrationDeleteTokenResponse": {
"description": ""
},
"cloudMigrationGetTokenResponse": {
"description": "",
"schema": {
"$ref": "#/definitions/GetAccessTokenResponseDTO"
}
},
"cloudMigrationListResponse": {
"description": "",
"schema": {
+26
View File
@@ -19350,6 +19350,32 @@
}
}
},
"SearchDTO": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"basicRole": {
"type": "string"
},
"onlyRoles": {
"type": "boolean"
},
"roleName": {
"type": "string"
},
"scope": {
"type": "string"
},
"teamId": {
"type": "string"
},
"userId": {
"type": "string"
}
}
},
"SearchDeviceQueryResult": {
"type": "object",
"properties": {
@@ -1,76 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { Button, Drawer, Spinner, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ScopesFiltersScene } from './ScopesFiltersScene';
import { ScopesTreeLevel } from './ScopesTreeLevel';
export function ScopesFiltersAdvancedSelector({ model }: SceneComponentProps<ScopesFiltersScene>) {
const styles = useStyles2(getStyles);
const { nodes, loadingNodeName, dirtyScopeNames, isLoadingScopes, isAdvancedOpened } = model.useState();
if (!isAdvancedOpened) {
return null;
}
return (
<Drawer
title={t('scopes.advancedSelector.title', 'Select scopes')}
size="sm"
onClose={() => {
model.closeAdvancedSelector();
model.resetDirtyScopeNames();
}}
>
{isLoadingScopes ? (
<Spinner data-testid="scopes-advanced-loading" />
) : (
<ScopesTreeLevel
showQuery={true}
nodes={nodes}
nodePath={['']}
loadingNodeName={loadingNodeName}
scopeNames={dirtyScopeNames}
onNodeUpdate={(path, isExpanded, query) => model.updateNode(path, isExpanded, query)}
onNodeSelectToggle={(path) => model.toggleNodeSelect(path)}
/>
)}
<div className={styles.buttonGroup}>
<Button
variant="primary"
data-testid="scopes-advanced-apply"
onClick={() => {
model.closeAdvancedSelector();
model.updateScopes();
}}
>
<Trans i18nKey="scopes.advancedSelector.apply">Apply</Trans>
</Button>
<Button
variant="secondary"
data-testid="scopes-advanced-cancel"
onClick={() => {
model.closeAdvancedSelector();
model.resetDirtyScopeNames();
}}
>
<Trans i18nKey="scopes.advancedSelector.cancel">Cancel</Trans>
</Button>
</div>
</Drawer>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
buttonGroup: css({
display: 'flex',
gap: theme.spacing(1),
marginTop: theme.spacing(8),
}),
};
};
@@ -1,110 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { Icon, IconButton, Input, Spinner, Toggletip, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ScopesFiltersScene } from './ScopesFiltersScene';
import { ScopesTreeLevel } from './ScopesTreeLevel';
export function ScopesFiltersBasicSelector({ model }: SceneComponentProps<ScopesFiltersScene>) {
const styles = useStyles2(getStyles);
const { nodes, loadingNodeName, scopes, dirtyScopeNames, isLoadingScopes, isBasicOpened } = model.useState();
const { isViewing } = model.scopesParent.useState();
const scopesTitles = scopes.map(({ spec: { title } }) => title).join(', ');
return (
<div className={styles.container} data-testid="scopes-basic-container">
<Toggletip
show={isBasicOpened}
closeButton={false}
content={
<div className={styles.innerContainer} data-testid="scopes-basic-inner-container">
{isLoadingScopes ? (
<Spinner data-testid="scopes-basic-loading" />
) : (
<ScopesTreeLevel
showQuery={false}
nodes={nodes}
nodePath={['']}
loadingNodeName={loadingNodeName}
scopeNames={dirtyScopeNames}
onNodeUpdate={(path, isExpanded, query) => model.updateNode(path, isExpanded, query)}
onNodeSelectToggle={(path) => model.toggleNodeSelect(path)}
/>
)}
</div>
}
footer={
<button
className={styles.openAdvancedButton}
data-testid="scopes-basic-open-advanced"
onClick={() => model.openAdvancedSelector()}
>
<Trans i18nKey="scopes.basicSelector.openAdvanced">
Open advanced scope selector <Icon name="arrow-right" />
</Trans>
</button>
}
onOpen={() => model.openBasicSelector()}
onClose={() => {
model.closeBasicSelector();
model.updateScopes();
}}
>
<Input
readOnly
placeholder={t('scopes.basicSelector.placeholder', 'Select scopes...')}
loading={isLoadingScopes}
value={scopesTitles}
aria-label={t('scopes.basicSelector.placeholder', 'Select scopes...')}
data-testid="scopes-basic-input"
suffix={
scopes.length > 0 && !isViewing ? (
<IconButton
aria-label={t('scopes.basicSelector.removeAll', 'Remove all scopes')}
name="times"
onClick={() => model.removeAllScopes()}
/>
) : undefined
}
/>
</Toggletip>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
width: '100%',
'& > div': css({
padding: 0,
'& > div': css({
padding: 0,
margin: 0,
}),
}),
}),
innerContainer: css({
minWidth: 400,
padding: theme.spacing(0, 1),
}),
openAdvancedButton: css({
backgroundColor: theme.colors.secondary.main,
border: 'none',
borderTop: `1px solid ${theme.colors.secondary.border}`,
display: 'block',
fontSize: theme.typography.pxToRem(12),
margin: 0,
padding: theme.spacing(1.5),
textAlign: 'right',
width: '100%',
}),
};
};
@@ -1,8 +1,9 @@
import { css } from '@emotion/css';
import { isEqual } from 'lodash';
import React from 'react';
import { finalize, from, Subscription } from 'rxjs';
import { Scope } from '@grafana/data';
import { GrafanaTheme2, Scope } from '@grafana/data';
import {
SceneComponentProps,
sceneGraph,
@@ -12,10 +13,11 @@ import {
SceneObjectUrlValues,
SceneObjectWithUrlSync,
} from '@grafana/scenes';
import { Button, Drawer, IconButton, Input, Spinner, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ScopesFiltersAdvancedSelector } from './ScopesFiltersAdvancedSelector';
import { ScopesFiltersBasicSelector } from './ScopesFiltersBasicSelector';
import { ScopesScene } from './ScopesScene';
import { ScopesTreeLevel } from './ScopesTreeLevel';
import { fetchNodes, fetchScope, fetchScopes } from './api';
import { NodesMap } from './types';
@@ -25,8 +27,7 @@ export interface ScopesFiltersSceneState extends SceneObjectState {
scopes: Scope[];
dirtyScopeNames: string[];
isLoadingScopes: boolean;
isBasicOpened: boolean;
isAdvancedOpened: boolean;
isOpened: boolean;
}
export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState> implements SceneObjectWithUrlSync {
@@ -58,8 +59,7 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
scopes: [],
dirtyScopeNames: [],
isLoadingScopes: false,
isBasicOpened: false,
isAdvancedOpened: false,
isOpened: false,
});
this.addActivationHandler(() => {
@@ -153,24 +153,14 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
}
}
public openBasicSelector() {
public open() {
if (!this.scopesParent.state.isViewing) {
this.setState({ isBasicOpened: true, isAdvancedOpened: false });
this.setState({ isOpened: true });
}
}
public closeBasicSelector() {
this.setState({ isBasicOpened: false });
}
public openAdvancedSelector() {
if (!this.scopesParent.state.isViewing) {
this.setState({ isBasicOpened: false, isAdvancedOpened: true });
}
}
public closeAdvancedSelector() {
this.setState({ isAdvancedOpened: false });
public close() {
this.setState({ isOpened: false });
}
public getSelectedScopes(): Scope[] {
@@ -196,7 +186,7 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
}
public enterViewMode() {
this.setState({ isBasicOpened: false, isAdvancedOpened: false });
this.setState({ isOpened: false });
}
private getScopeNames(): string[] {
@@ -205,10 +195,88 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
}
export function ScopesFiltersSceneRenderer({ model }: SceneComponentProps<ScopesFiltersScene>) {
const styles = useStyles2(getStyles);
const { nodes, loadingNodeName, dirtyScopeNames, isLoadingScopes, isOpened, scopes } = model.useState();
const { isViewing } = model.scopesParent.useState();
const scopesTitles = scopes.map(({ spec: { title } }) => title).join(', ');
return (
<>
<ScopesFiltersBasicSelector model={model} />
<ScopesFiltersAdvancedSelector model={model} />
<Input
readOnly
placeholder={t('scopes.filters.input.placeholder', 'Select scopes...')}
loading={isLoadingScopes}
value={scopesTitles}
aria-label={t('scopes.filters.input.placeholder', 'Select scopes...')}
data-testid="scopes-filters-input"
suffix={
scopes.length > 0 && !isViewing ? (
<IconButton
aria-label={t('scopes.filters.input.removeAll', 'Remove all scopes')}
name="times"
onClick={() => model.removeAllScopes()}
/>
) : undefined
}
onClick={() => model.open()}
/>
{isOpened && (
<Drawer
title={t('scopes.filters.title', 'Select scopes')}
size="sm"
onClose={() => {
model.close();
model.resetDirtyScopeNames();
}}
>
{isLoadingScopes ? (
<Spinner data-testid="scopes-filters-loading" />
) : (
<ScopesTreeLevel
nodes={nodes}
nodePath={['']}
loadingNodeName={loadingNodeName}
scopeNames={dirtyScopeNames}
onNodeUpdate={(path, isExpanded, query) => model.updateNode(path, isExpanded, query)}
onNodeSelectToggle={(path) => model.toggleNodeSelect(path)}
/>
)}
<div className={styles.buttonGroup}>
<Button
variant="primary"
data-testid="scopes-filters-apply"
onClick={() => {
model.close();
model.updateScopes();
}}
>
<Trans i18nKey="scopes.filters.apply">Apply</Trans>
</Button>
<Button
variant="secondary"
data-testid="scopes-filters-cancel"
onClick={() => {
model.close();
model.resetDirtyScopeNames();
}}
>
<Trans i18nKey="scopes.filters.cancel">Cancel</Trans>
</Button>
</div>
</Drawer>
)}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
buttonGroup: css({
display: 'flex',
gap: theme.spacing(1),
marginTop: theme.spacing(8),
}),
};
};
@@ -13,8 +13,6 @@ import {
fetchNodesSpy,
fetchScopeSpy,
fetchScopesSpy,
getAdvancedApply,
getAdvancedCancel,
getApplicationsClustersExpand,
getApplicationsClustersSelect,
getApplicationsExpand,
@@ -22,29 +20,28 @@ import {
getApplicationsSlothPictureFactorySelect,
getApplicationsSlothPictureFactoryTitle,
getApplicationsSlothVoteTrackerSelect,
getBasicInnerContainer,
getBasicInput,
getBasicOpenAdvanced,
getFiltersApply,
getFiltersCancel,
getFiltersInput,
getClustersExpand,
getClustersSelect,
getClustersSlothClusterNorthSelect,
getClustersSlothClusterSouthSelect,
getDashboard,
getDashboardsContainer,
getDashboardsExpand,
getDashboardsSearch,
getRootExpand,
mocksNodes,
mocksScopeDashboardBindings,
mocksScopes,
queryAdvancedApply,
queryFiltersApply,
queryApplicationsClustersSlothClusterNorthTitle,
queryApplicationsClustersTitle,
queryApplicationsSlothPictureFactoryTitle,
queryApplicationsSlothVoteTrackerTitle,
queryBasicInnerContainer,
queryDashboard,
queryDashboardsContainer,
queryRootExpand,
queryDashboardsExpand,
renderDashboard,
} from './testUtils';
@@ -123,14 +120,14 @@ describe('ScopesScene', () => {
describe('Tree', () => {
it('Navigates through scopes nodes', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsClustersExpand());
await userEvents.click(getApplicationsExpand());
});
it('Fetches scope details on select', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothVoteTrackerSelect());
await waitFor(() => expect(fetchScopeSpy).toHaveBeenCalledTimes(1));
@@ -138,24 +135,24 @@ describe('ScopesScene', () => {
it('Selects the proper scopes', async () => {
await act(async () => filtersScene.updateScopes(['slothPictureFactory', 'slothVoteTracker']));
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
expect(getApplicationsSlothVoteTrackerSelect()).toBeChecked();
expect(getApplicationsSlothPictureFactorySelect()).toBeChecked();
});
it('Can select scopes from same level', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothVoteTrackerSelect());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getApplicationsClustersSelect());
await userEvents.click(getBasicInput());
expect(getBasicInput().value).toBe('slothVoteTracker, slothPictureFactory, Cluster Index Helper');
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('slothVoteTracker, slothPictureFactory, Cluster Index Helper');
});
it("Can't navigate deeper than the level where scopes are selected", async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothVoteTrackerSelect());
await userEvents.click(getApplicationsClustersExpand());
@@ -163,25 +160,24 @@ describe('ScopesScene', () => {
});
it('Can select a node from an upper level', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothVoteTrackerSelect());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getClustersSelect());
await userEvents.click(getBasicInput());
expect(getBasicInput().value).toBe('Cluster Index Helper');
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('Cluster Index Helper');
});
it('Respects only one select per container', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getClustersExpand());
await userEvents.click(getClustersSlothClusterNorthSelect());
expect(getClustersSlothClusterSouthSelect()).toBeDisabled();
});
it('Search works', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getBasicOpenAdvanced());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.type(getApplicationsSearch(), 'Clusters');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
@@ -197,43 +193,16 @@ describe('ScopesScene', () => {
});
});
describe('Basic selector', () => {
describe('Filters', () => {
it('Opens', async () => {
await userEvents.click(getBasicInput());
expect(getBasicInnerContainer()).toBeInTheDocument();
await userEvents.click(getFiltersInput());
expect(getFiltersApply()).toBeInTheDocument();
});
it('Fetches scope details on save', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getClustersSelect());
await userEvents.click(getBasicInput());
await waitFor(() => expect(fetchScopesSpy).toHaveBeenCalled());
expect(filtersScene.getSelectedScopes()).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'indexHelperCluster')
);
});
it('Shows selected scopes', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getClustersSelect());
await userEvents.click(getBasicInput());
expect(getBasicInput().value).toEqual('Cluster Index Helper');
});
});
describe('Advanced selector', () => {
it('Opens', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getBasicOpenAdvanced());
expect(queryBasicInnerContainer()).not.toBeInTheDocument();
expect(getAdvancedApply()).toBeInTheDocument();
});
it('Fetches scope details on save', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getBasicOpenAdvanced());
await userEvents.click(getClustersSelect());
await userEvents.click(getAdvancedApply());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchScopesSpy).toHaveBeenCalled());
expect(filtersScene.getSelectedScopes()).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'indexHelperCluster')
@@ -241,85 +210,73 @@ describe('ScopesScene', () => {
});
it("Doesn't save the scopes on close", async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getBasicOpenAdvanced());
await userEvents.click(getFiltersInput());
await userEvents.click(getClustersSelect());
await userEvents.click(getAdvancedCancel());
await userEvents.click(getFiltersCancel());
await waitFor(() => expect(fetchScopesSpy).not.toHaveBeenCalled());
expect(filtersScene.getSelectedScopes()).toEqual([]);
});
});
describe('Selectors interoperability', () => {
it('Replicates the same structure from basic to advanced selector', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getBasicOpenAdvanced());
expect(getApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
});
it('Replicates the same structure from advanced to basic selector', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getBasicOpenAdvanced());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getAdvancedApply());
await userEvents.click(getBasicInput());
expect(getApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
it('Shows selected scopes', async () => {
await userEvents.click(getFiltersInput());
await userEvents.click(getClustersSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toEqual('Cluster Index Helper');
});
});
describe('Dashboards list', () => {
it('Toggles expanded state', async () => {
await userEvents.click(getRootExpand());
await userEvents.click(getDashboardsExpand());
expect(getDashboardsContainer()).toBeInTheDocument();
});
it('Does not fetch dashboards list when the list is not expanded', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchDashboardsSpy).not.toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded', async () => {
await userEvents.click(getRootExpand());
await userEvents.click(getBasicInput());
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded after scope selection', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getRootExpand());
await userEvents.click(getFiltersApply());
await userEvents.click(getDashboardsExpand());
await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled());
});
it('Shows dashboards for multiple scopes', async () => {
await userEvents.click(getRootExpand());
await userEvents.click(getBasicInput());
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(queryDashboard('3')).not.toBeInTheDocument();
expect(queryDashboard('4')).not.toBeInTheDocument();
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsSlothVoteTrackerSelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(getDashboard('3')).toBeInTheDocument();
expect(getDashboard('4')).toBeInTheDocument();
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
expect(queryDashboard('1')).not.toBeInTheDocument();
expect(queryDashboard('2')).not.toBeInTheDocument();
expect(getDashboard('3')).toBeInTheDocument();
@@ -327,11 +284,11 @@ describe('ScopesScene', () => {
});
it('Filters the dashboards list', async () => {
await userEvents.click(getRootExpand());
await userEvents.click(getBasicInput());
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
await userEvents.type(getDashboardsSearch(), '1');
@@ -346,43 +303,36 @@ describe('ScopesScene', () => {
expect(scopesScene.state.isExpanded).toEqual(false);
});
it('Closes basic selector on enter', async () => {
await userEvents.click(getBasicInput());
it('Closes filters on enter', async () => {
await userEvents.click(getFiltersInput());
await act(async () => dashboardScene.onEnterEditMode());
expect(queryBasicInnerContainer()).not.toBeInTheDocument();
});
it('Closes advanced selector on enter', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getBasicOpenAdvanced());
await act(async () => dashboardScene.onEnterEditMode());
expect(queryAdvancedApply()).not.toBeInTheDocument();
expect(queryFiltersApply()).not.toBeInTheDocument();
});
it('Closes dashboards list on enter', async () => {
await userEvents.click(getRootExpand());
await userEvents.click(getDashboardsExpand());
await act(async () => dashboardScene.onEnterEditMode());
expect(queryDashboardsContainer()).not.toBeInTheDocument();
});
it('Does not open basic selector when view mode is active', async () => {
it('Does not open filters when view mode is active', async () => {
await act(async () => dashboardScene.onEnterEditMode());
await userEvents.click(getBasicInput());
expect(queryBasicInnerContainer()).not.toBeInTheDocument();
await userEvents.click(getFiltersInput());
expect(queryFiltersApply()).not.toBeInTheDocument();
});
it('Hides the expand button when view mode is active', async () => {
await act(async () => dashboardScene.onEnterEditMode());
expect(queryRootExpand()).not.toBeInTheDocument();
expect(queryDashboardsExpand()).not.toBeInTheDocument();
});
});
describe('Enrichers', () => {
it('Data requests', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
@@ -390,9 +340,9 @@ describe('ScopesScene', () => {
);
});
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsSlothVoteTrackerSelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
@@ -402,9 +352,9 @@ describe('ScopesScene', () => {
);
});
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(
@@ -414,19 +364,19 @@ describe('ScopesScene', () => {
});
it('Filters requests', async () => {
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'slothPictureFactory')
);
});
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsSlothVoteTrackerSelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(
@@ -435,9 +385,9 @@ describe('ScopesScene', () => {
);
});
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersInput());
await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getBasicInput());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
mocksScopes.filter(({ metadata: { name } }) => name === 'slothVoteTracker')
@@ -94,10 +94,10 @@ export function ScopesSceneRenderer({ model }: SceneComponentProps<ScopesScene>)
className={cx(!isExpanded && styles.iconNotExpanded)}
aria-label={
isExpanded
? t('scopes.root.collapse', 'Collapse scope filters')
: t('scopes.root.expand', 'Expand scope filters')
? t('scopes.suggestedDashboards.toggle.collapse', 'Collapse scope filters')
: t('scopes.suggestedDashboards.toggle..expand', 'Expand scope filters')
}
data-testid="scopes-root-expand"
data-testid="scopes-dashboards-expand"
onClick={() => model.toggleIsExpanded()}
/>
)}
@@ -10,7 +10,6 @@ import { t } from 'app/core/internationalization';
import { NodesMap } from './types';
export interface ScopesTreeLevelProps {
showQuery: boolean;
nodes: NodesMap;
nodePath: string[];
loadingNodeName: string | undefined;
@@ -20,7 +19,6 @@ export interface ScopesTreeLevelProps {
}
export function ScopesTreeLevel({
showQuery,
nodes,
nodePath,
loadingNodeName,
@@ -43,7 +41,7 @@ export function ScopesTreeLevel({
return (
<>
{showQuery && !anyChildExpanded && (
{!anyChildExpanded && (
<Input
prefix={<Icon name="filter" />}
className={styles.searchInput}
@@ -101,7 +99,6 @@ export function ScopesTreeLevel({
<div className={styles.itemChildren}>
{childNode.isExpanded && (
<ScopesTreeLevel
showQuery={showQuery}
nodes={node.nodes}
nodePath={childNodePath}
loadingNodeName={loadingNodeName}
@@ -229,29 +229,21 @@ export const fetchScopesSpy = jest.spyOn(api, 'fetchScopes');
export const fetchDashboardsSpy = jest.spyOn(api, 'fetchDashboards');
const selectors = {
root: {
expand: 'scopes-root-expand',
},
tree: {
search: (nodeId: string) => `scopes-tree-${nodeId}-search`,
select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
title: (nodeId: string) => `scopes-tree-${nodeId}-title`,
},
basicSelector: {
container: 'scopes-basic-container',
innerContainer: 'scopes-basic-inner-container',
loading: 'scopes-basic-loading',
openAdvanced: 'scopes-basic-open-advanced',
input: 'scopes-basic-input',
},
advancedSelector: {
container: 'scopes-advanced-container',
loading: 'scopes-advanced-loading',
apply: 'scopes-advanced-apply',
cancel: 'scopes-advanced-cancel',
filters: {
input: 'scopes-filters-input',
container: 'scopes-filters-container',
loading: 'scopes-filters-loading',
apply: 'scopes-filters-apply',
cancel: 'scopes-filters-cancel',
},
dashboards: {
expand: 'scopes-dashboards-expand',
container: 'scopes-dashboards-container',
search: 'scopes-dashboards-search',
loading: 'scopes-dashboards-loading',
@@ -259,18 +251,13 @@ const selectors = {
},
};
export const queryRootExpand = () => screen.queryByTestId(selectors.root.expand);
export const getRootExpand = () => screen.getByTestId(selectors.root.expand);
export const queryBasicInnerContainer = () => screen.queryByTestId(selectors.basicSelector.innerContainer);
export const getBasicInnerContainer = () => screen.getByTestId(selectors.basicSelector.innerContainer);
export const getBasicInput = () => screen.getByTestId<HTMLInputElement>(selectors.basicSelector.input);
export const getBasicOpenAdvanced = () => screen.getByTestId(selectors.basicSelector.openAdvanced);
export const queryAdvancedApply = () => screen.queryByTestId(selectors.advancedSelector.apply);
export const getAdvancedApply = () => screen.getByTestId(selectors.advancedSelector.apply);
export const getAdvancedCancel = () => screen.getByTestId(selectors.advancedSelector.cancel);
export const getFiltersInput = () => screen.getByTestId<HTMLInputElement>(selectors.filters.input);
export const queryFiltersApply = () => screen.queryByTestId(selectors.filters.apply);
export const getFiltersApply = () => screen.getByTestId(selectors.filters.apply);
export const getFiltersCancel = () => screen.getByTestId(selectors.filters.cancel);
export const queryDashboardsExpand = () => screen.queryByTestId(selectors.dashboards.expand);
export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand);
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container);
export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search);
@@ -23,6 +23,7 @@ import { DashboardInteractions } from 'app/features/dashboard-scene/utils/intera
import { AccessControlAction } from 'app/types';
import { getDashboardSceneFor } from '../../../utils/utils';
import { ShareDrawerConfirmAction } from '../../ShareDrawer/ShareDrawerConfirmAction';
import { useShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext';
import { EmailSharing } from './EmailShare/EmailSharing';
@@ -65,22 +66,62 @@ export class ShareExternally extends SceneObjectBase {
}
function ShareExternallyRenderer({ model }: SceneComponentProps<ShareExternally>) {
const dashboard = getDashboardSceneFor(model);
const { data: publicDashboard, isLoading } = useGetPublicDashboardQuery(dashboard.state.uid!);
const [showRevokeAccess, setShowRevokeAccess] = useState(false);
const styles = useStyles2(getStyles);
const dashboard = getDashboardSceneFor(model);
const { data: publicDashboard, isLoading } = useGetPublicDashboardQuery(dashboard.state.uid!);
const [deletePublicDashboard, { isLoading: isDeleteLoading }] = useDeletePublicDashboardMutation();
const onRevokeClick = () => {
setShowRevokeAccess(true);
};
const onDeleteClick = async () => {
DashboardInteractions.revokePublicDashboardClicked();
await deletePublicDashboard({
dashboard,
uid: publicDashboard!.uid,
dashboardUid: dashboard.state.uid!,
}).unwrap();
setShowRevokeAccess(false);
};
if (isLoading) {
return <Loader />;
}
if (showRevokeAccess) {
return (
<ShareDrawerConfirmAction
title={t('public-dashboard.share-externally.revoke-access-button', 'Revoke access')}
confirmButtonLabel={t('public-dashboard.share-externally.revoke-access-button', 'Revoke access')}
onConfirm={onDeleteClick}
onDismiss={() => setShowRevokeAccess(false)}
description={t(
'public-dashboard.share-externally.revoke-access-description',
'Are you sure you want to revoke this access? The dashboard can no longer be shared.'
)}
isActionLoading={isDeleteLoading}
/>
);
}
return (
<div className={styles.container}>
<ShareExternallyBase publicDashboard={publicDashboard} />
<ShareExternallyBase publicDashboard={publicDashboard} onRevokeClick={onRevokeClick} />
</div>
);
}
function ShareExternallyBase({ publicDashboard }: { publicDashboard?: PublicDashboard }) {
function ShareExternallyBase({
publicDashboard,
onRevokeClick,
}: {
publicDashboard?: PublicDashboard;
onRevokeClick: () => void;
}) {
const options = getShareExternallyOptions();
const getShareType = useMemo(() => {
if (publicDashboard && isEmailSharingEnabled()) {
@@ -105,24 +146,21 @@ function ShareExternallyBase({ publicDashboard }: { publicDashboard?: PublicDash
<Stack direction="column" gap={2} data-testid={selectors.container}>
<ShareAlerts publicDashboard={publicDashboard} />
<ShareTypeSelect setShareType={setShareType} value={shareType} options={options} />
{Config}
{publicDashboard && (
<>
<Divider spacing={0} />
<Actions publicDashboard={publicDashboard} />
<Actions publicDashboard={publicDashboard} onRevokeClick={onRevokeClick} />
</>
)}
</Stack>
);
}
function Actions({ publicDashboard }: { publicDashboard: PublicDashboard }) {
function Actions({ publicDashboard, onRevokeClick }: { publicDashboard: PublicDashboard; onRevokeClick: () => void }) {
const { dashboard } = useShareDrawerContext();
const [update, { isLoading: isUpdateLoading }] = usePauseOrResumePublicDashboardMutation();
const [deletePublicDashboard, { isLoading: isDeleteLoading }] = useDeletePublicDashboardMutation();
const styles = useStyles2(getStyles);
const isLoading = isUpdateLoading || isDeleteLoading;
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
function onCopyURL() {
@@ -142,15 +180,6 @@ function Actions({ publicDashboard }: { publicDashboard: PublicDashboard }) {
});
};
const onDeleteClick = () => {
DashboardInteractions.revokePublicDashboardClicked();
deletePublicDashboard({
dashboard,
uid: publicDashboard!.uid,
dashboardUid: dashboard.state.uid!,
});
};
return (
<Stack alignItems="center" direction={{ xs: 'column', sm: 'row' }}>
<div className={styles.actionsContainer}>
@@ -169,8 +198,8 @@ function Actions({ publicDashboard }: { publicDashboard: PublicDashboard }) {
icon="trash-alt"
variant="destructive"
fill="outline"
disabled={isLoading || !hasWritePermissions}
onClick={onDeleteClick}
disabled={isUpdateLoading || !hasWritePermissions}
onClick={onRevokeClick}
>
<Trans i18nKey="public-dashboard.share-externally.revoke-access-button">Revoke access</Trans>
</Button>
@@ -187,7 +216,7 @@ function Actions({ publicDashboard }: { publicDashboard: PublicDashboard }) {
: ''
}
onClick={onPauseOrResumeClick}
disabled={isLoading || !hasWritePermissions}
disabled={isUpdateLoading || !hasWritePermissions}
>
{publicDashboard.isEnabled ? (
<Trans i18nKey="public-dashboard.share-externally.pause-access-button">Pause access</Trans>
@@ -197,7 +226,7 @@ function Actions({ publicDashboard }: { publicDashboard: PublicDashboard }) {
</Button>
</Stack>
</div>
{isLoading && <Spinner />}
{isUpdateLoading && <Spinner />}
</Stack>
);
}
@@ -0,0 +1,59 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Spinner, Stack, Text } from '@grafana/ui';
import { IconButton, useStyles2 } from '@grafana/ui/';
import { ConfirmContent, ConfirmContentProps } from '@grafana/ui/src/components/ConfirmModal/ConfirmContent';
import { t } from 'app/core/internationalization';
export function ShareDrawerConfirmAction({
onConfirm,
onDismiss,
description,
confirmButtonLabel,
title,
isActionLoading,
}: { title: string; isActionLoading: boolean } & Pick<
ConfirmContentProps,
'description' | 'onConfirm' | 'onDismiss' | 'confirmButtonLabel'
>) {
const styles = useStyles2(getStyles);
const ConfirmBody = () => (
<div className={styles.bodyContainer}>
<Stack justifyContent="space-between">
<Stack gap={1} alignItems="center">
<IconButton
size="xl"
name="angle-left"
aria-label={t('share-drawer.confirm-action.back-arrow-button', 'Back button')}
onClick={onDismiss}
/>
<Text variant="h4">{title}</Text>
</Stack>
{isActionLoading && <Spinner />}
</Stack>
</div>
);
return (
<ConfirmContent
body={<ConfirmBody />}
description={description}
confirmButtonLabel={confirmButtonLabel}
confirmButtonVariant="destructive"
dismissButtonLabel="Cancel"
dismissButtonVariant="secondary"
justifyButtons="flex-start"
onConfirm={onConfirm}
onDismiss={onDismiss}
/>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
bodyContainer: css({
marginBottom: theme.spacing(2),
}),
});
@@ -1,4 +1,4 @@
import { cloneDeep, find, first as _first, isNumber, isObject, isString, map as _map } from 'lodash';
import { cloneDeep, first as _first, isNumber, isObject, isString, map as _map, find } from 'lodash';
import { from, generate, lastValueFrom, Observable, of } from 'rxjs';
import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty, tap } from 'rxjs/operators';
import { SemVer } from 'semver';
@@ -121,7 +121,6 @@ export class ElasticDatasource
queryBuilder: ElasticQueryBuilder;
indexPattern: IndexPattern;
intervalPattern?: Interval;
logMessageField?: string;
logLevelField?: string;
dataLinks: DataLinkConfig[];
languageProvider: LanguageProvider;
@@ -140,7 +139,7 @@ export class ElasticDatasource
this.name = instanceSettings.name;
this.isProxyAccess = instanceSettings.access === 'proxy';
const settingsData = instanceSettings.jsonData || {};
// instanceSettings.database is deprecated and should be removed in the future
this.index = settingsData.index ?? instanceSettings.database ?? '';
this.timeField = settingsData.timeField;
this.indexPattern = new IndexPattern(this.index, settingsData.interval);
@@ -150,19 +149,15 @@ export class ElasticDatasource
this.queryBuilder = new ElasticQueryBuilder({
timeField: this.timeField,
});
this.logMessageField = settingsData.logMessageField || '';
this.logLevelField = settingsData.logLevelField || '';
this.dataLinks = settingsData.dataLinks || [];
this.includeFrozen = settingsData.includeFrozen ?? false;
// we want to cache the database version so we don't have to ask for it every time
this.databaseVersion = null;
this.annotations = {
QueryEditor: ElasticsearchAnnotationsQueryEditor,
};
if (this.logMessageField === '') {
this.logMessageField = undefined;
}
if (this.logLevelField === '') {
this.logLevelField = undefined;
}
@@ -181,6 +176,11 @@ export class ElasticDatasource
return this.postResource(path, data, resourceOptions);
}
/**
* Implemented as part of DataSourceWithQueryImportSupport.
* Imports queries from AbstractQuery objects when switching between different data source types.
* @returns A Promise that resolves to an array of ES queries.
*/
async importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise<ElasticsearchQuery[]> {
return abstractQueries.map((abstractQuery) => this.languageProvider.importFromAbstractQuery(abstractQuery));
}
@@ -235,6 +235,11 @@ export class ElasticDatasource
);
}
/**
* Implemented as part of the DataSourceAPI. It allows the datasource to serve as a source of annotations for a dashboard.
* @returns A promise that resolves to an array of AnnotationEvent objects representing the annotations for the dashboard.
* @todo This is deprecated and it is recommended to use the `AnnotationSupport` feature for annotations.
*/
annotationQuery(options: any): Promise<AnnotationEvent[]> {
const payload = this.prepareAnnotationRequest(options);
trackAnnotationQuery(options.annotation);
@@ -255,6 +260,7 @@ export class ElasticDatasource
);
}
// Private method used in the `annotationQuery` to prepare the payload for the Elasticsearch annotation request
private prepareAnnotationRequest(options: {
annotation: ElasticsearchAnnotationQuery;
// Should be DashboardModel but cannot import that here from the main app. This is a temporary solution as we need to move from deprecated annotations.
@@ -348,6 +354,7 @@ export class ElasticDatasource
return payload;
}
// Private method used in the `annotationQuery` to process Elasticsearch hits into AnnotationEvents
private processHitsToAnnotationEvents(annotation: ElasticsearchAnnotationQuery, hits: ElasticsearchHits) {
const timeField = annotation.timeField || '@timestamp';
const timeEndField = annotation.timeEndField || null;
@@ -416,10 +423,15 @@ export class ElasticDatasource
return list;
}
// Replaces variables in a Lucene query string
interpolateLuceneQuery(queryString: string, scopedVars?: ScopedVars) {
return this.templateSrv.replace(queryString, scopedVars, 'lucene');
}
/**
* Implemented as a part of DataSourceApi. Interpolates variables and adds ad hoc filters to a list of ES queries.
* @returns An array of ES queries with interpolated variables and ad hoc filters using `applyTemplateVariables`.
*/
interpolateVariablesInQueries(
queries: ElasticsearchQuery[],
scopedVars: ScopedVars,
@@ -428,6 +440,9 @@ export class ElasticDatasource
return queries.map((q) => this.applyTemplateVariables(q, scopedVars, filters));
}
/**
* @todo Remove as we have health checks in the backend
*/
async testDatasource() {
// we explicitly ask for uncached, "fresh" data here
const dbVersion = await this.getDatabaseVersion(false);
@@ -456,7 +471,8 @@ export class ElasticDatasource
);
}
getQueryHeader(searchType: string, timeFrom?: DateTime, timeTo?: DateTime): string {
// Private method used in `getTerms` to get the header for the Elasticsearch query
private getQueryHeader(searchType: string, timeFrom?: DateTime, timeTo?: DateTime): string {
const queryHeader = {
search_type: searchType,
ignore_unavailable: true,
@@ -466,6 +482,11 @@ export class ElasticDatasource
return JSON.stringify(queryHeader);
}
/**
* Implemented as part of DataSourceApi. Converts a ES query to a simple text string.
* Used, for example, in Query history.
* @returns A text representation of the query.
*/
getQueryDisplayText(query: ElasticsearchQuery) {
// TODO: This might be refactored a bit.
const metricAggs = query.metrics;
@@ -517,6 +538,10 @@ export class ElasticDatasource
return text;
}
/**
* Part of `DataSourceWithLogsContextSupport`, used to retrieve log context for a log row.
* @returns A promise that resolves to an object containing the log context data as DataFrames.
*/
getLogRowContext = async (row: LogRowModel, options?: LogRowContextOptions): Promise<{ data: DataFrame[] }> => {
const contextRequest = this.makeLogContextDataRequest(row, options);
return lastValueFrom(
@@ -552,10 +577,20 @@ export class ElasticDatasource
}
}
/**
* Implemented for DataSourceWithSupplementaryQueriesSupport.
* It returns the supplementary types that the data source supports.
* @returns An array of supported supplementary query types.
*/
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] {
return [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample];
}
/**
* Implemented for DataSourceWithSupplementaryQueriesSupport.
* It retrieves supplementary queries based on the provided options and ES query.
* @returns A supplemented ES query or undefined if unsupported.
*/
getSupplementaryQuery(options: SupplementaryQueryOptions, query: ElasticsearchQuery): ElasticsearchQuery | undefined {
let isQuerySuitable = false;
@@ -628,6 +663,10 @@ export class ElasticDatasource
}
}
/**
* Private method used in the `getDataProvider` for DataSourceWithSupplementaryQueriesSupport, specifically for Logs volume queries.
* @returns An Observable of DataQueryResponse or undefined if no suitable queries are found.
*/
private getLogsVolumeDataProvider(
request: DataQueryRequest<ElasticsearchQuery>
): DataQueryRequest<ElasticsearchQuery> | undefined {
@@ -643,6 +682,10 @@ export class ElasticDatasource
return { ...logsVolumeRequest, targets };
}
/**
* Private method used in the `getDataProvider` for DataSourceWithSupplementaryQueriesSupport, specifically for Logs sample queries.
* @returns An Observable of DataQueryResponse or undefined if no suitable queries are found.
*/
private getLogsSampleDataProvider(
request: DataQueryRequest<ElasticsearchQuery>
): DataQueryRequest<ElasticsearchQuery> | undefined {
@@ -659,6 +702,10 @@ export class ElasticDatasource
return { ...logsSampleRequest, targets: elasticQueries };
}
/**
* Required by DataSourceApi. It executes queries based on the provided DataQueryRequest.
* @returns An Observable of DataQueryResponse containing the query results.
*/
query(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> {
const start = new Date();
return super.query(request).pipe(
@@ -672,6 +719,11 @@ export class ElasticDatasource
);
}
/**
* Filters out queries that are hidden. Used when running queries through backend.
* It is called from DatasourceWithBackend.
* @returns `true` if the query is not hidden.
*/
filterQuery(query: ElasticsearchQuery): boolean {
if (query.hide) {
return false;
@@ -679,13 +731,17 @@ export class ElasticDatasource
return true;
}
isMetadataField(fieldName: string) {
// Private method used in the `getFields` to check if a field is a metadata field.
private isMetadataField(fieldName: string) {
return ELASTIC_META_FIELDS.includes(fieldName);
}
// TODO: instead of being a string, this could be a custom type representing all the elastic types
// FIXME: This doesn't seem to return actual MetricFindValues, we should either change the return type
// or fix the implementation.
/**
* Get the list of the fields to display in query editor or used for example in getTagKeys.
* @todo instead of being a string, this could be a custom type representing all the elastic types
* @fixme This doesn't seem to return actual MetricFindValues, we should either change the return type
* or fix the implementation.
*/
getFields(type?: string[], range?: TimeRange): Observable<MetricFindValue[]> {
const typeMap: Record<string, string> = {
float: 'number',
@@ -767,6 +823,10 @@ export class ElasticDatasource
);
}
/**
* Get values for a given field.
* Used for example in getTagValues.
*/
getTerms(queryDef: TermsQuery, range = getDefaultTimeRange()): Observable<MetricFindValue[]> {
const searchType = 'query_then_fetch';
const header = this.getQueryHeader(searchType, range.from, range.to);
@@ -798,6 +858,7 @@ export class ElasticDatasource
);
}
// Method used to create URL that includes correct parameters based on ES data source config.
getMultiSearchUrl() {
const searchParams = new URLSearchParams();
@@ -812,6 +873,10 @@ export class ElasticDatasource
return ('_msearch?' + searchParams.toString()).replace(/\?$/, '');
}
/**
* Implemented as part of DataSourceAPI and used for template variable queries.
* @returns A Promise that resolves to an array of results from the metric find query.
*/
metricFindQuery(query: string, options?: { range: TimeRange }): Promise<MetricFindValue[]> {
const range = options?.range;
const parsedQuery = JSON.parse(query);
@@ -831,14 +896,26 @@ export class ElasticDatasource
return Promise.resolve([]);
}
/**
* Implemented as part of the DataSourceAPI. Retrieves tag keys that can be used for ad-hoc filtering.
* @returns A Promise that resolves to an array of label names represented as MetricFindValue objects.
*/
getTagKeys() {
return lastValueFrom(this.getFields());
}
/**
* Implemented as part of the DataSourceAPI. Retrieves tag values that can be used for ad-hoc filtering.
* @returns A Promise that resolves to an array of label values represented as MetricFindValue objects
*/
getTagValues(options: DataSourceGetTagValuesOptions<ElasticsearchQuery>) {
return lastValueFrom(this.getTerms({ field: options.key }, options.timeRange));
}
/**
* Implemented as part of the DataSourceAPI.
* Used by alerting to check if query contains template variables.
*/
targetContainsTemplate(target: ElasticsearchQuery) {
if (this.templateSrv.containsTemplate(target.query) || this.templateSrv.containsTemplate(target.alias)) {
return true;
@@ -875,6 +952,7 @@ export class ElasticDatasource
return false;
}
// Private method used in the `targetContainsTemplate` to check if an object contains template variables.
private objectContainsTemplate(obj: any) {
if (typeof obj === 'string') {
return this.templateSrv.containsTemplate(obj);
@@ -898,6 +976,11 @@ export class ElasticDatasource
return false;
}
/**
* Implemented for `DataSourceWithToggleableQueryFiltersSupport`. Toggles a filter on or off based on the provided filter action.
* It is used for example in Explore to toggle fields on and off trough log details.
* @returns A new ES query with the filter toggled as specified.
*/
toggleQueryFilter(query: ElasticsearchQuery, filter: ToggleFilterAction): ElasticsearchQuery {
let expression = query.query ?? '';
switch (filter.type) {
@@ -921,11 +1004,20 @@ export class ElasticDatasource
return { ...query, query: expression };
}
/**
* Implemented for `DataSourceWithToggleableQueryFiltersSupport`. Checks if a query expression contains a filter based on the provided filter options.
* @returns A boolean value indicating whether the filter exists in the query expression.
*/
queryHasFilter(query: ElasticsearchQuery, options: QueryFilterOptions): boolean {
let expression = query.query ?? '';
return queryHasFilter(expression, options.key, options.value);
}
/**
* Implemented as part of `DataSourceWithQueryModificationSupport`. Used to modify a query based on the provided action.
* It is used, for example, in the Query Builder to apply hints such as parsers, operations, etc.
* @returns A new ES query with the specified modification applied.
*/
modifyQuery(query: ElasticsearchQuery, action: QueryFixAction): ElasticsearchQuery {
if (!action.options) {
return query;
@@ -954,10 +1046,18 @@ export class ElasticDatasource
return { ...query, query: expression };
}
/**
* Implemented as part of `DataSourceWithQueryModificationSupport`. Returns a list of operation
* types that are supported by `modifyQuery()`.
*/
getSupportedQueryModifications() {
return ['ADD_FILTER', 'ADD_FILTER_OUT', 'ADD_STRING_FILTER', 'ADD_STRING_FILTER_OUT'];
}
/**
* Adds ad hoc filters to a query expression, handling proper escaping of filter values.
* @returns The query expression with ad hoc filters and correctly escaped values.
*/
addAdHocFilters(query: string, adhocFilters?: AdHocVariableFilter[]) {
if (!adhocFilters) {
return query;
@@ -970,7 +1070,11 @@ export class ElasticDatasource
return finalQuery;
}
// Used when running queries through backend
/**
* Applies template variables and add hoc filters to a query. Used when running queries through backend.
* It is called from DatasourceWithBackend.
* @returns A modified ES query with template variables and ad hoc filters applied.
*/
applyTemplateVariables(
query: ElasticsearchQuery,
scopedVars: ScopedVars,
@@ -1006,6 +1110,7 @@ export class ElasticDatasource
return finalQuery;
}
// Private method used in the `getDatabaseVersion` to get the database version from the Elasticsearch API.
private getDatabaseVersionUncached(): Promise<SemVer | null> {
// we want this function to never fail
const getDbVersionObservable = from(this.getResourceRequest(''));
@@ -1029,6 +1134,11 @@ export class ElasticDatasource
);
}
/**
* Method used to get the database version from cache or from the Elasticsearch API.
* Elasticsearch data source supports only certain versions of Elasticsearch and we
* want to check the version and notify the user if the version is not supported.
* */
async getDatabaseVersion(useCachedData = true): Promise<SemVer | null> {
if (useCachedData) {
const cached = this.databaseVersion;
@@ -1042,6 +1152,7 @@ export class ElasticDatasource
return freshDatabaseVersion;
}
// private method used in the `getLogRowContext` to create a log context data request.
private makeLogContextDataRequest = (row: LogRowModel, options?: LogRowContextOptions) => {
const direction = options?.direction || LogRowContextQueryDirection.Backward;
const logQuery: Logs = {
@@ -1087,6 +1198,7 @@ export class ElasticDatasource
};
}
// Function to enhance the data frame with data links configured in the data source settings.
export function enhanceDataFrameWithDataLinks(dataFrame: DataFrame, dataLinks: DataLinkConfig[]) {
if (!dataLinks.length) {
return;
@@ -0,0 +1,31 @@
# ElasticSearch data source in Grafana
ElasticSearch is the built-in core data source and one of the oldest and most popular data sources in Grafana. When refactoring and improving, it's important to consider that many users have legacy dashboards, annotations and configs that need to remain compatible.
## Running queries using backend
Queries in the ElasticSearch data source are now exclusively run through the backend. This change is detailed in [this document](https://docs.google.com/document/d/1oLfVh54gReZEN9FdlJ0Wuo7Ja8XhSbjJ15FkRisPGs8/edit#heading=h.nuqzkh8bfixf). The `enableElasticSearchBackendQuerying` feature toggle, which allowed switching between frontend and backend modes, was removed in Grafana 11.1.0. In case of reported issues, please refer to the linked document.
## Development
When developing for ElasticSearch, use `make devenv sources=elastic`. To specify a version, use `make devenv sources=elastic elastic_version=7.17.0`. In `devenv/docker/blocks/elastic/data/data.js`, you can update data to suit your debugging and testing needs. Additionally, ElasticSearch has a couple of debugging dashboards located in `devenv/dev-dashboards/datasource-ElasticSearch`.
## Instrumentation
The ElasticSearch data source has improved instrumentation with logs, metrics, traces, and dashboards. When debugging issues, it is useful to review the available telemetry signals.
## Technical debt
Here is a list of our current technical debt.
### Database field
Previously, users stored ElasticSearch indices in the `database` field, which has since been deprecated. It is now stored in `jsonData` (implemented in https://github.com/grafana/grafana/pull/62808), though we continue to support both fields. Eventually, support for the `database` field will need to be removed.
## Supported Explore and Log features
Many Explore and Log features are implemented through `DataSourceWithXXXSupport`, making it clear which functionalities are supported.
## Supported ES Versions and version changes
The supported ElasticSearch version is documented at https://grafana.com/docs/grafana/latest/datasources/ElasticSearch/#supported-ElasticSearch-versions. We typically update it with major Grafana versions, following ElasticSearch [Elastic Product End of Life Dates](https://www.elastic.co/support/eol) to the last supported versions of ElasticSearch available at the time of Grafana's release.
@@ -13,6 +13,7 @@ import {
LogsDedupStrategy,
EventBusSrv,
} from '@grafana/data';
import * as grafanaUI from '@grafana/ui';
import * as styles from 'app/features/logs/components/getLogRowStyles';
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
@@ -347,7 +348,7 @@ describe('LogsPanel', () => {
}),
];
it('allow to filter for a value or filter out a value', async () => {
it('allows to filter for a value or filter out a value', async () => {
const filterForMock = jest.fn();
const filterOutMock = jest.fn();
const isFilterLabelActiveMock = jest.fn();
@@ -380,6 +381,50 @@ describe('LogsPanel', () => {
expect(isFilterLabelActiveMock).toHaveBeenCalledTimes(1);
});
describe('invalid handlers', () => {
it('does not show the controls if onAddAdHocFilter is not defined', async () => {
jest.spyOn(grafanaUI, 'usePanelContext').mockReturnValue({
eventsScope: 'global',
eventBus: new EventBusSrv(),
});
setup({
data: {
series,
},
});
expect(await screen.findByRole('row')).toBeInTheDocument();
await userEvent.click(screen.getByText('logline text'));
expect(screen.queryByLabelText('Filter for value in query A')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Filter out value in query A')).not.toBeInTheDocument();
});
it('shows the controls if onAddAdHocFilter is defined', async () => {
jest.spyOn(grafanaUI, 'usePanelContext').mockReturnValue({
eventsScope: 'global',
eventBus: new EventBusSrv(),
onAddAdHocFilter: jest.fn(),
});
setup({
data: {
series,
},
});
expect(await screen.findByRole('row')).toBeInTheDocument();
await userEvent.click(screen.getByText('logline text'));
expect(await screen.findByText('common_app')).toBeInTheDocument();
expect(screen.getByLabelText('Filter for value in query A')).toBeInTheDocument();
expect(screen.getByLabelText('Filter out value in query A')).toBeInTheDocument();
});
});
});
});
@@ -414,7 +459,7 @@ const setup = (propsOverrides?: {}) => {
prettifyLogMessage: false,
sortOrder: LogsSortOrder.Descending,
dedupStrategy: LogsDedupStrategy.none,
enableLogDetails: false,
enableLogDetails: true,
showLogContextToggle: false,
},
title: 'Logs panel',
+6 -2
View File
@@ -249,7 +249,7 @@ export const LogsPanel = ({
[scrollElement]
);
const defaultOnClickFilterLabel = useCallback(
const handleOnClickFilterLabel = useCallback(
(key: string, value: string) => {
onAddAdHocFilter?.({
key,
@@ -260,7 +260,7 @@ export const LogsPanel = ({
[onAddAdHocFilter]
);
const defaultOnClickFilterOutLabel = useCallback(
const handleOnClickFilterOutLabel = useCallback(
(key: string, value: string) => {
onAddAdHocFilter?.({
key,
@@ -285,6 +285,10 @@ export const LogsPanel = ({
</div>
);
// Passing callbacks control the display of the filtering buttons. We want to pass it only if onAddAdHocFilter is defined.
const defaultOnClickFilterLabel = onAddAdHocFilter ? handleOnClickFilterLabel : undefined;
const defaultOnClickFilterOutLabel = onAddAdHocFilter ? handleOnClickFilterOutLabel : undefined;
return (
<>
{contextRow && (
+17 -12
View File
@@ -1487,7 +1487,8 @@
"public-share-type-option-description": "Anyone with the link can access",
"public-share-type-option-label": "Anyone with the link",
"resume-access-button": "Resume access",
"revoke-access-button": "Revoke access"
"revoke-access-button": "Revoke access",
"revoke-access-description": "Are you sure you want to revoke this access? The dashboard can no longer be shared."
},
"sharing": {
"success-creation": "Dashboard is public!"
@@ -1594,23 +1595,22 @@
"dismissable-button": "Close"
},
"scopes": {
"advancedSelector": {
"filters": {
"apply": "Apply",
"cancel": "Cancel",
"input": {
"placeholder": "Select scopes...",
"removeAll": "Remove all scopes"
},
"title": "Select scopes"
},
"basicSelector": {
"openAdvanced": "Open advanced scope selector <1></1>",
"placeholder": "Select scopes...",
"removeAll": "Remove all scopes"
},
"root": {
"collapse": "Collapse scope filters",
"expand": "Expand scope filters"
},
"suggestedDashboards": {
"loading": "Loading dashboards",
"search": "Filter"
"search": "Filter",
"toggle": {
"collapse": "Collapse scope filters",
"expand": "Expand scope filters"
}
},
"tree": {
"collapse": "Collapse",
@@ -1655,6 +1655,11 @@
"title": "You haven't created any service accounts yet"
}
},
"share-drawer": {
"confirm-action": {
"back-arrow-button": "Back button"
}
},
"share-modal": {
"dashboard": {
"title": "Share"
+17 -12
View File
@@ -1487,7 +1487,8 @@
"public-share-type-option-description": "Åʼnyőʼnę ŵįŧĥ ŧĥę ľįʼnĸ čäʼn äččęşş",
"public-share-type-option-label": "Åʼnyőʼnę ŵįŧĥ ŧĥę ľįʼnĸ",
"resume-access-button": "Ŗęşūmę äččęşş",
"revoke-access-button": "Ŗęvőĸę äččęşş"
"revoke-access-button": "Ŗęvőĸę äččęşş",
"revoke-access-description": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő řęvőĸę ŧĥįş äččęşş? Ŧĥę đäşĥþőäřđ čäʼn ʼnő ľőʼnģęř þę şĥäřęđ."
},
"sharing": {
"success-creation": "Đäşĥþőäřđ įş pūþľįč!"
@@ -1594,23 +1595,22 @@
"dismissable-button": "Cľőşę"
},
"scopes": {
"advancedSelector": {
"filters": {
"apply": "Åppľy",
"cancel": "Cäʼnčęľ",
"input": {
"placeholder": "Ŝęľęčŧ şčőpęş...",
"removeAll": "Ŗęmővę äľľ şčőpęş"
},
"title": "Ŝęľęčŧ şčőpęş"
},
"basicSelector": {
"openAdvanced": "Øpęʼn äđväʼnčęđ şčőpę şęľęčŧőř <1></1>",
"placeholder": "Ŝęľęčŧ şčőpęş...",
"removeAll": "Ŗęmővę äľľ şčőpęş"
},
"root": {
"collapse": "Cőľľäpşę şčőpę ƒįľŧęřş",
"expand": "Ēχpäʼnđ şčőpę ƒįľŧęřş"
},
"suggestedDashboards": {
"loading": "Ŀőäđįʼnģ đäşĥþőäřđş",
"search": "Fįľŧęř"
"search": "Fįľŧęř",
"toggle": {
"collapse": "Cőľľäpşę şčőpę ƒįľŧęřş",
"expand": "Ēχpäʼnđ şčőpę ƒįľŧęřş"
}
},
"tree": {
"collapse": "Cőľľäpşę",
@@ -1655,6 +1655,11 @@
"title": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny şęřvįčę äččőūʼnŧş yęŧ"
}
},
"share-drawer": {
"confirm-action": {
"back-arrow-button": "ßäčĸ þūŧŧőʼn"
}
},
"share-modal": {
"dashboard": {
"title": "Ŝĥäřę"
+26
View File
@@ -9729,6 +9729,32 @@
},
"type": "object"
},
"SearchDTO": {
"properties": {
"action": {
"type": "string"
},
"basicRole": {
"type": "string"
},
"onlyRoles": {
"type": "boolean"
},
"roleName": {
"type": "string"
},
"scope": {
"type": "string"
},
"teamId": {
"type": "string"
},
"userId": {
"type": "string"
}
},
"type": "object"
},
"SearchDeviceQueryResult": {
"properties": {
"devices": {
+25 -2
View File
@@ -3684,6 +3684,7 @@ __metadata:
csstype: "npm:3.1.3"
d3: "npm:7.9.0"
date-fns: "npm:3.6.0"
downshift: "npm:^9.0.6"
esbuild: "npm:0.20.2"
expose-loader: "npm:5.0.0"
hoist-non-react-statics: "npm:3.3.2"
@@ -12460,6 +12461,13 @@ __metadata:
languageName: node
linkType: hard
"compute-scroll-into-view@npm:^3.1.0":
version: 3.1.0
resolution: "compute-scroll-into-view@npm:3.1.0"
checksum: 10/cc5211d49bced5ad23385da5c2eaf69b6045628581b0dcb9f4dd407bfee51bbd26d2bce426be26edf2feaf8c243706f5a7c3759827d89cc5a01a5cf7d299a5eb
languageName: node
linkType: hard
"concat-map@npm:0.0.1":
version: 0.0.1
resolution: "concat-map@npm:0.0.1"
@@ -14380,6 +14388,21 @@ __metadata:
languageName: node
linkType: hard
"downshift@npm:^9.0.6":
version: 9.0.6
resolution: "downshift@npm:9.0.6"
dependencies:
"@babel/runtime": "npm:^7.24.5"
compute-scroll-into-view: "npm:^3.1.0"
prop-types: "npm:^15.8.1"
react-is: "npm:18.2.0"
tslib: "npm:^2.6.2"
peerDependencies:
react: ">=16.12.0"
checksum: 10/e84ceba61429694395e6c2ab7213e76d6807a87ceb52b0db08642120ac6d6affc74426772431df29741d571743143316a11e6a815280bed4bbc1113cd83849b1
languageName: node
linkType: hard
"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2":
version: 0.1.2
resolution: "duplexer@npm:0.1.2"
@@ -25292,7 +25315,7 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.2.0":
"react-is@npm:18.2.0, react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.2.0":
version: 18.2.0
resolution: "react-is@npm:18.2.0"
checksum: 10/200cd65bf2e0be7ba6055f647091b725a45dd2a6abef03bf2380ce701fd5edccee40b49b9d15edab7ac08a762bf83cb4081e31ec2673a5bfb549a36ba21570df
@@ -29156,7 +29179,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:2.6.3, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1":
"tslib@npm:2.6.3, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.6.2":
version: 2.6.3
resolution: "tslib@npm:2.6.3"
checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c