Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
No files matched your search
@@ -6377,6 +6377,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
|
||||
],
|
||||
"public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
||||
],
|
||||
"public/app/features/trails/MetricsHeader.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
@@ -6389,6 +6392,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/trails/banners/NativeHistogramBanner.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/data/src/transformations/transformers/filterByValue\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
|
||||
@@ -233,6 +233,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `k8SFolderMove` | Enable folder's api server move |
|
||||
| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams |
|
||||
| `queryLibraryDashboards` | Enables Query Library feature in Dashboards |
|
||||
| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
||||
@@ -253,4 +253,5 @@ export interface FeatureToggles {
|
||||
ABTestFeatureToggleA?: boolean;
|
||||
ABTestFeatureToggleB?: boolean;
|
||||
queryLibraryDashboards?: boolean;
|
||||
elasticsearchImprovedParsing?: boolean;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc-gen-go v1.36.1
|
||||
// protoc (unknown)
|
||||
// source: rendererv2.proto
|
||||
|
||||
@@ -21,11 +21,10 @@ const (
|
||||
)
|
||||
|
||||
type StringList struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StringList) Reset() {
|
||||
@@ -66,10 +65,7 @@ func (x *StringList) GetValues() []string {
|
||||
}
|
||||
|
||||
type RenderRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Width int32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"`
|
||||
Height int32 `protobuf:"varint,3,opt,name=height,proto3" json:"height,omitempty"`
|
||||
@@ -79,9 +75,11 @@ type RenderRequest struct {
|
||||
Domain string `protobuf:"bytes,7,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Timeout int32 `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
||||
Timezone string `protobuf:"bytes,9,opt,name=timezone,proto3" json:"timezone,omitempty"`
|
||||
Headers map[string]*StringList `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Headers map[string]*StringList `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
AuthToken string `protobuf:"bytes,11,opt,name=authToken,proto3" json:"authToken,omitempty"`
|
||||
Encoding string `protobuf:"bytes,12,opt,name=encoding,proto3" json:"encoding,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenderRequest) Reset() {
|
||||
@@ -199,11 +197,10 @@ func (x *RenderRequest) GetEncoding() string {
|
||||
}
|
||||
|
||||
type RenderResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenderResponse) Reset() {
|
||||
@@ -244,18 +241,17 @@ func (x *RenderResponse) GetError() string {
|
||||
}
|
||||
|
||||
type RenderCSVRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
FilePath string `protobuf:"bytes,2,opt,name=filePath,proto3" json:"filePath,omitempty"`
|
||||
RenderKey string `protobuf:"bytes,3,opt,name=renderKey,proto3" json:"renderKey,omitempty"`
|
||||
Domain string `protobuf:"bytes,4,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Timeout int32 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
||||
Timezone string `protobuf:"bytes,6,opt,name=timezone,proto3" json:"timezone,omitempty"`
|
||||
Headers map[string]*StringList `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
AuthToken string `protobuf:"bytes,8,opt,name=authToken,proto3" json:"authToken,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
FilePath string `protobuf:"bytes,2,opt,name=filePath,proto3" json:"filePath,omitempty"`
|
||||
RenderKey string `protobuf:"bytes,3,opt,name=renderKey,proto3" json:"renderKey,omitempty"`
|
||||
Domain string `protobuf:"bytes,4,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Timeout int32 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
||||
Timezone string `protobuf:"bytes,6,opt,name=timezone,proto3" json:"timezone,omitempty"`
|
||||
Headers map[string]*StringList `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
AuthToken string `protobuf:"bytes,8,opt,name=authToken,proto3" json:"authToken,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenderCSVRequest) Reset() {
|
||||
@@ -345,12 +341,11 @@ func (x *RenderCSVRequest) GetAuthToken() string {
|
||||
}
|
||||
|
||||
type RenderCSVResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
FileName string `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
FileName string `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenderCSVResponse) Reset() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc-gen-go v1.36.1
|
||||
// protoc (unknown)
|
||||
// source: sanitizer.proto
|
||||
|
||||
@@ -21,14 +21,13 @@ const (
|
||||
)
|
||||
|
||||
type SanitizeRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"`
|
||||
Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
|
||||
ConfigType string `protobuf:"bytes,3,opt,name=configType,proto3" json:"configType,omitempty"` // DOMPurify, ...
|
||||
Config []byte `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"`
|
||||
Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
|
||||
ConfigType string `protobuf:"bytes,3,opt,name=configType,proto3" json:"configType,omitempty"` // DOMPurify, ...
|
||||
Config []byte `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SanitizeRequest) Reset() {
|
||||
@@ -90,12 +89,11 @@ func (x *SanitizeRequest) GetConfig() []byte {
|
||||
}
|
||||
|
||||
type SanitizeResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
Sanitized []byte `protobuf:"bytes,2,opt,name=sanitized,proto3" json:"sanitized,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
Sanitized []byte `protobuf:"bytes,2,opt,name=sanitized,proto3" json:"sanitized,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SanitizeResponse) Reset() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc-gen-go v1.36.1
|
||||
// protoc (unknown)
|
||||
// source: secretsmanager.proto
|
||||
|
||||
@@ -21,13 +21,12 @@ const (
|
||||
)
|
||||
|
||||
type Key struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
OrgId int64 `protobuf:"varint,1,opt,name=orgId,proto3" json:"orgId,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
OrgId int64 `protobuf:"varint,1,opt,name=orgId,proto3" json:"orgId,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Key) Reset() {
|
||||
@@ -82,12 +81,11 @@ func (x *Key) GetType() string {
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Key *Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Key *Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Item) Reset() {
|
||||
@@ -135,11 +133,10 @@ func (x *Item) GetValue() string {
|
||||
}
|
||||
|
||||
type GetSecretRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetSecretRequest) Reset() {
|
||||
@@ -180,13 +177,12 @@ func (x *GetSecretRequest) GetKeyDescriptor() *Key {
|
||||
}
|
||||
|
||||
type GetSecretResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"`
|
||||
Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"`
|
||||
Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetSecretResponse) Reset() {
|
||||
@@ -241,12 +237,11 @@ func (x *GetSecretResponse) GetExists() bool {
|
||||
}
|
||||
|
||||
type SetSecretRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetSecretRequest) Reset() {
|
||||
@@ -294,11 +289,10 @@ func (x *SetSecretRequest) GetValue() string {
|
||||
}
|
||||
|
||||
type SetSecretResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetSecretResponse) Reset() {
|
||||
@@ -339,11 +333,10 @@ func (x *SetSecretResponse) GetUserFriendlyError() string {
|
||||
}
|
||||
|
||||
type DeleteSecretRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteSecretRequest) Reset() {
|
||||
@@ -384,11 +377,10 @@ func (x *DeleteSecretRequest) GetKeyDescriptor() *Key {
|
||||
}
|
||||
|
||||
type DeleteSecretResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteSecretResponse) Reset() {
|
||||
@@ -429,12 +421,11 @@ func (x *DeleteSecretResponse) GetUserFriendlyError() string {
|
||||
}
|
||||
|
||||
type ListSecretsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListSecretsRequest) Reset() {
|
||||
@@ -482,12 +473,11 @@ func (x *ListSecretsRequest) GetAllOrganizations() bool {
|
||||
}
|
||||
|
||||
type ListSecretsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListSecretsResponse) Reset() {
|
||||
@@ -535,9 +525,9 @@ func (x *ListSecretsResponse) GetKeys() []*Key {
|
||||
}
|
||||
|
||||
type GetAllSecretsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetAllSecretsRequest) Reset() {
|
||||
@@ -571,12 +561,11 @@ func (*GetAllSecretsRequest) Descriptor() ([]byte, []int) {
|
||||
}
|
||||
|
||||
type GetAllSecretsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
Items []*Item `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
Items []*Item `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetAllSecretsResponse) Reset() {
|
||||
@@ -624,12 +613,11 @@ func (x *GetAllSecretsResponse) GetItems() []*Item {
|
||||
}
|
||||
|
||||
type RenameSecretRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
NewNamespace string `protobuf:"bytes,2,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
NewNamespace string `protobuf:"bytes,2,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenameSecretRequest) Reset() {
|
||||
@@ -677,11 +665,10 @@ func (x *RenameSecretRequest) GetNewNamespace() string {
|
||||
}
|
||||
|
||||
type RenameSecretResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenameSecretResponse) Reset() {
|
||||
|
||||
@@ -42,11 +42,6 @@ func (d *directResourceClient) GetStats(ctx context.Context, in *resource.Resour
|
||||
return d.server.GetStats(ctx, in)
|
||||
}
|
||||
|
||||
// History implements ResourceClient.
|
||||
func (d *directResourceClient) History(ctx context.Context, in *resource.HistoryRequest, opts ...grpc.CallOption) (*resource.HistoryResponse, error) {
|
||||
return d.server.History(ctx, in)
|
||||
}
|
||||
|
||||
// IsHealthy implements ResourceClient.
|
||||
func (d *directResourceClient) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) {
|
||||
return d.server.IsHealthy(ctx, in)
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
@@ -251,6 +249,7 @@ func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.ResourceS
|
||||
return nil, fmt.Errorf("not yet (filter)")
|
||||
}
|
||||
|
||||
/**
|
||||
func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryRequest) (*resource.HistoryResponse, error) {
|
||||
info, err := claims.ParseNamespace(req.Key.Namespace)
|
||||
if err == nil {
|
||||
@@ -329,6 +328,7 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
**/
|
||||
|
||||
func (a *dashboardSqlAccess) ListRepositoryObjects(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
|
||||
@@ -94,7 +94,3 @@ func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequ
|
||||
func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockClient) History(ctx context.Context, in *resource.HistoryRequest, opts ...grpc.CallOption) (*resource.HistoryResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package v0alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -146,14 +145,6 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
||||
|
||||
storage := map[string]rest.Storage{}
|
||||
storage[dash.StoragePath()] = legacyStore
|
||||
storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
|
||||
b.legacy.Server, // as client???
|
||||
dashboardv0alpha1.DashboardResourceInfo.GroupResource(),
|
||||
)
|
||||
|
||||
if optsGetter == nil {
|
||||
return errors.New("missing RESTOptionsGetter")
|
||||
}
|
||||
|
||||
// Dual writes if a RESTOptionsGetter is provided
|
||||
if dualWriteBuilder != nil {
|
||||
|
||||
@@ -142,13 +142,9 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
||||
|
||||
storage := map[string]rest.Storage{}
|
||||
storage[dash.StoragePath()] = legacyStore
|
||||
storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
|
||||
b.legacy.Server, // as client???
|
||||
dashboardv1alpha1.DashboardResourceInfo.GroupResource(),
|
||||
)
|
||||
|
||||
// Dual writes if a RESTOptionsGetter is provided
|
||||
if optsGetter != nil && dualWriteBuilder != nil {
|
||||
if dualWriteBuilder != nil {
|
||||
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -143,13 +143,9 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
||||
|
||||
storage := map[string]rest.Storage{}
|
||||
storage[dash.StoragePath()] = legacyStore
|
||||
storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
|
||||
b.legacy.Server, // as client???
|
||||
dashboardv2alpha1.DashboardResourceInfo.GroupResource(),
|
||||
)
|
||||
|
||||
// Dual writes if a RESTOptionsGetter is provided
|
||||
if optsGetter != nil && dualWriteBuilder != nil {
|
||||
if dualWriteBuilder != nil {
|
||||
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -127,13 +127,6 @@ func (s *ExtendedJWT) authenticateAsUser(
|
||||
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject)
|
||||
}
|
||||
|
||||
// For use in service layer, allow higher privilege
|
||||
namespace := accessTokenClaims.Rest.Namespace
|
||||
if len(s.cfg.StackID) > 0 {
|
||||
// For single-tenant cloud use, choose the lower of the two (id token will always have the specific namespace)
|
||||
namespace = idTokenClaims.Rest.Namespace
|
||||
}
|
||||
|
||||
return &authn.Identity{
|
||||
ID: id,
|
||||
Type: t,
|
||||
@@ -142,7 +135,7 @@ func (s *ExtendedJWT) authenticateAsUser(
|
||||
IDTokenClaims: &idTokenClaims,
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: accessTokenClaims.Subject,
|
||||
Namespace: namespace,
|
||||
Namespace: idTokenClaims.Rest.Namespace,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
|
||||
@@ -283,7 +283,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should authenticate as user using wildcard namespace for access token",
|
||||
name: "should authenticate as user in the user namespace",
|
||||
accessToken: &validAccessTokenClaimsWildcard,
|
||||
idToken: &validIDTokenClaims,
|
||||
orgID: 1,
|
||||
@@ -293,7 +293,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
OrgID: 1,
|
||||
AccessTokenClaims: &validAccessTokenClaimsWildcard,
|
||||
IDTokenClaims: &validIDTokenClaims,
|
||||
Namespace: "*",
|
||||
Namespace: "default",
|
||||
AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid",
|
||||
ClientParams: authn.ClientParams{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc-gen-go v1.36.1
|
||||
// protoc (unknown)
|
||||
// source: extention.proto
|
||||
|
||||
@@ -24,14 +24,13 @@ const (
|
||||
)
|
||||
|
||||
type TupleKey struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
|
||||
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
|
||||
Condition *RelationshipCondition `protobuf:"bytes,4,opt,name=condition,proto3" json:"condition,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
|
||||
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
|
||||
Condition *RelationshipCondition `protobuf:"bytes,4,opt,name=condition,proto3" json:"condition,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TupleKey) Reset() {
|
||||
@@ -93,12 +92,11 @@ func (x *TupleKey) GetCondition() *RelationshipCondition {
|
||||
}
|
||||
|
||||
type Tuple struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Key *TupleKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Key *TupleKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Tuple) Reset() {
|
||||
@@ -146,13 +144,12 @@ func (x *Tuple) GetTimestamp() *timestamppb.Timestamp {
|
||||
}
|
||||
|
||||
type TupleKeyWithoutCondition struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
|
||||
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
|
||||
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TupleKeyWithoutCondition) Reset() {
|
||||
@@ -207,12 +204,11 @@ func (x *TupleKeyWithoutCondition) GetObject() string {
|
||||
}
|
||||
|
||||
type RelationshipCondition struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Context *structpb.Struct `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Context *structpb.Struct `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RelationshipCondition) Reset() {
|
||||
@@ -260,14 +256,13 @@ func (x *RelationshipCondition) GetContext() *structpb.Struct {
|
||||
}
|
||||
|
||||
type ReadRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
TupleKey *ReadRequestTupleKey `protobuf:"bytes,2,opt,name=tuple_key,json=tupleKey,proto3" json:"tuple_key,omitempty"`
|
||||
PageSize *wrapperspb.Int32Value `protobuf:"bytes,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||
ContinuationToken string `protobuf:"bytes,4,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReadRequest) Reset() {
|
||||
@@ -329,13 +324,12 @@ func (x *ReadRequest) GetContinuationToken() string {
|
||||
}
|
||||
|
||||
type ReadRequestTupleKey struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
|
||||
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
|
||||
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReadRequestTupleKey) Reset() {
|
||||
@@ -390,12 +384,11 @@ func (x *ReadRequestTupleKey) GetObject() string {
|
||||
}
|
||||
|
||||
type ReadResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Tuples []*Tuple `protobuf:"bytes,1,rep,name=tuples,proto3" json:"tuples,omitempty"`
|
||||
ContinuationToken string `protobuf:"bytes,2,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Tuples []*Tuple `protobuf:"bytes,1,rep,name=tuples,proto3" json:"tuples,omitempty"`
|
||||
ContinuationToken string `protobuf:"bytes,2,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReadResponse) Reset() {
|
||||
@@ -443,11 +436,10 @@ func (x *ReadResponse) GetContinuationToken() string {
|
||||
}
|
||||
|
||||
type WriteRequestWrites struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
TupleKeys []*TupleKey `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
TupleKeys []*TupleKey `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WriteRequestWrites) Reset() {
|
||||
@@ -488,11 +480,10 @@ func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey {
|
||||
}
|
||||
|
||||
type WriteRequestDeletes struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
TupleKeys []*TupleKeyWithoutCondition `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
TupleKeys []*TupleKeyWithoutCondition `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WriteRequestDeletes) Reset() {
|
||||
@@ -533,13 +524,12 @@ func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition {
|
||||
}
|
||||
|
||||
type WriteRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Writes *WriteRequestWrites `protobuf:"bytes,2,opt,name=writes,proto3" json:"writes,omitempty"`
|
||||
Deletes *WriteRequestDeletes `protobuf:"bytes,3,opt,name=deletes,proto3" json:"deletes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Writes *WriteRequestWrites `protobuf:"bytes,2,opt,name=writes,proto3" json:"writes,omitempty"`
|
||||
Deletes *WriteRequestDeletes `protobuf:"bytes,3,opt,name=deletes,proto3" json:"deletes,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WriteRequest) Reset() {
|
||||
@@ -594,9 +584,9 @@ func (x *WriteRequest) GetDeletes() *WriteRequestDeletes {
|
||||
}
|
||||
|
||||
type WriteResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WriteResponse) Reset() {
|
||||
@@ -630,13 +620,12 @@ func (*WriteResponse) Descriptor() ([]byte, []int) {
|
||||
}
|
||||
|
||||
type BatchCheckRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Items []*BatchCheckItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Items []*BatchCheckItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BatchCheckRequest) Reset() {
|
||||
@@ -691,16 +680,15 @@ func (x *BatchCheckRequest) GetItems() []*BatchCheckItem {
|
||||
}
|
||||
|
||||
type BatchCheckItem struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Verb string `protobuf:"bytes,1,opt,name=verb,proto3" json:"verb,omitempty"`
|
||||
Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"`
|
||||
Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"`
|
||||
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Subresource string `protobuf:"bytes,5,opt,name=subresource,proto3" json:"subresource,omitempty"`
|
||||
Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Verb string `protobuf:"bytes,1,opt,name=verb,proto3" json:"verb,omitempty"`
|
||||
Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"`
|
||||
Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"`
|
||||
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Subresource string `protobuf:"bytes,5,opt,name=subresource,proto3" json:"subresource,omitempty"`
|
||||
Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) Reset() {
|
||||
@@ -776,11 +764,10 @@ func (x *BatchCheckItem) GetFolder() string {
|
||||
}
|
||||
|
||||
type BatchCheckResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Groups map[string]*BatchCheckGroupResource `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Groups map[string]*BatchCheckGroupResource `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BatchCheckResponse) Reset() {
|
||||
@@ -821,11 +808,10 @@ func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource {
|
||||
}
|
||||
|
||||
type BatchCheckGroupResource struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Items map[string]bool `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Items map[string]bool `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BatchCheckGroupResource) Reset() {
|
||||
|
||||
@@ -1750,6 +1750,12 @@ var (
|
||||
Owner: grafanaFrontendPlatformSquad,
|
||||
AllowSelfServe: false,
|
||||
},
|
||||
{
|
||||
Name: "elasticsearchImprovedParsing",
|
||||
Description: "Enables less memory intensive Elasticsearch result parsing",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: awsDatasourcesSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -234,3 +234,4 @@ teamHttpHeadersMimir,experimental,@grafana/identity-access-team,false,false,fals
|
||||
ABTestFeatureToggleA,experimental,@grafana/sharing-squad,false,false,false
|
||||
ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false
|
||||
queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false
|
||||
|
@@ -946,4 +946,8 @@ const (
|
||||
// FlagQueryLibraryDashboards
|
||||
// Enables Query Library feature in Dashboards
|
||||
FlagQueryLibraryDashboards = "queryLibraryDashboards"
|
||||
|
||||
// FlagElasticsearchImprovedParsing
|
||||
// Enables less memory intensive Elasticsearch result parsing
|
||||
FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing"
|
||||
)
|
||||
@@ -1321,6 +1321,21 @@
|
||||
"codeowner": "@grafana/aws-datasources"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "elasticsearchImprovedParsing",
|
||||
"resourceVersion": "1736808262603",
|
||||
"creationTimestamp": "2025-01-13T20:32:35Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-01-13 22:44:22.603729 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables less memory intensive Elasticsearch result parsing",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/aws-datasources"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "enableDatagridEditing",
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
alertingImages "github.com/grafana/alerting/images"
|
||||
alertingModels "github.com/grafana/alerting/models"
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
@@ -32,7 +33,7 @@ func (i imageProvider) GetImage(ctx context.Context, uri string) (*alertingImage
|
||||
image, err := i.getImageFromURI(ctx, uri)
|
||||
if err != nil {
|
||||
if errors.Is(err, models.ErrImageNotFound) {
|
||||
i.logger.Info("Image not found in database")
|
||||
i.logger.Info("Image not found in database", "uri", uri)
|
||||
return nil, alertingImages.ErrImageNotFound
|
||||
}
|
||||
return nil, err
|
||||
|
||||
@@ -24,6 +24,8 @@ var (
|
||||
ResendDelay = 30 * time.Second
|
||||
)
|
||||
|
||||
type takeImageFn func(reason string) *ngModels.Image
|
||||
|
||||
// AlertInstanceManager defines the interface for querying the current alert instances.
|
||||
type AlertInstanceManager interface {
|
||||
GetAll(orgID int64) []*State
|
||||
@@ -327,21 +329,21 @@ func (st *Manager) ProcessEvalResults(
|
||||
logger := st.log.FromContext(ctx)
|
||||
|
||||
// lazy evaluation of takeImage only once and only if it is requested.
|
||||
var fn func() *ngModels.Image
|
||||
var fn takeImageFn
|
||||
{
|
||||
var image *ngModels.Image
|
||||
var imageTaken bool
|
||||
fn = func() *ngModels.Image {
|
||||
fn = func(reason string) *ngModels.Image {
|
||||
if imageTaken {
|
||||
return image
|
||||
}
|
||||
logger.Debug("Taking image", "dashboard", alertRule.GetDashboardUID(), "panel", alertRule.GetPanelID())
|
||||
logger.Debug("Taking image", "dashboard", alertRule.GetDashboardUID(), "panel", alertRule.GetPanelID(), "reason", reason)
|
||||
img, err := takeImage(ctx, st.images, alertRule)
|
||||
imageTaken = true
|
||||
if err != nil {
|
||||
logger.Warn("Failed to take an image",
|
||||
"dashboard", alertRule.GetDashboardUID(),
|
||||
"panel", alertRule.GetPanelID(),
|
||||
"panel", alertRule.GetPanelID(), "reason", reason,
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
@@ -395,7 +397,7 @@ func (st *Manager) updateLastSentAt(states StateTransitions, evaluatedAt time.Ti
|
||||
return result
|
||||
}
|
||||
|
||||
func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels, logger log.Logger, takeImageFn func() *ngModels.Image) []StateTransition {
|
||||
func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels, logger log.Logger, takeImageFn takeImageFn) []StateTransition {
|
||||
if st.applyNoDataAndErrorToAllStates && results.IsNoData() && (alertRule.NoDataState == ngModels.Alerting || alertRule.NoDataState == ngModels.OK || alertRule.NoDataState == ngModels.KeepLast) { // If it is no data, check the mapping and switch all results to the new state
|
||||
// aggregate UID of datasources that returned NoData into one and provide as auxiliary info via annotationa. See: https://github.com/grafana/grafana/issues/88184
|
||||
var refIds strings.Builder
|
||||
@@ -445,7 +447,7 @@ func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels.
|
||||
return transitions
|
||||
}
|
||||
|
||||
func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval.Result, logger log.Logger, extraAnnotations data.Labels, takeImageFn func() *ngModels.Image) []StateTransition {
|
||||
func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval.Result, logger log.Logger, extraAnnotations data.Labels, takeImageFn takeImageFn) []StateTransition {
|
||||
currentStates := st.cache.getStatesForRuleUID(alertRule.OrgID, alertRule.UID, false)
|
||||
transitions := make([]StateTransition, 0, len(currentStates))
|
||||
updated := ruleStates{
|
||||
@@ -462,7 +464,7 @@ func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval
|
||||
}
|
||||
|
||||
// Set the current state based on evaluation results
|
||||
func (st *Manager) setNextState(alertRule *ngModels.AlertRule, currentState *State, result eval.Result, extraAnnotations data.Labels, logger log.Logger, takeImageFn func() *ngModels.Image) StateTransition {
|
||||
func (st *Manager) setNextState(alertRule *ngModels.AlertRule, currentState *State, result eval.Result, extraAnnotations data.Labels, logger log.Logger, takeImageFn takeImageFn) StateTransition {
|
||||
start := st.clock.Now()
|
||||
|
||||
currentState.LastEvaluationTime = result.EvaluatedAt
|
||||
@@ -538,8 +540,8 @@ func (st *Manager) setNextState(alertRule *ngModels.AlertRule, currentState *Sta
|
||||
currentState.ResolvedAt = nil
|
||||
}
|
||||
|
||||
if shouldTakeImage(currentState.State, oldState, currentState.Image, newlyResolved) {
|
||||
image := takeImageFn()
|
||||
if reason := shouldTakeImage(currentState.State, oldState, currentState.Image, newlyResolved); reason != "" {
|
||||
image := takeImageFn(reason)
|
||||
if image != nil {
|
||||
currentState.Image = image
|
||||
}
|
||||
@@ -606,7 +608,7 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State {
|
||||
}
|
||||
}
|
||||
|
||||
func (st *Manager) deleteStaleStatesFromCache(logger log.Logger, evaluatedAt time.Time, alertRule *ngModels.AlertRule, takeImageFn func() *ngModels.Image) []StateTransition {
|
||||
func (st *Manager) deleteStaleStatesFromCache(logger log.Logger, evaluatedAt time.Time, alertRule *ngModels.AlertRule, takeImageFn takeImageFn) []StateTransition {
|
||||
// If we are removing two or more stale series it makes sense to share the resolved image as the alert rule is the same.
|
||||
// TODO: We will need to change this when we support images without screenshots as each series will have a different image
|
||||
staleStates := st.cache.deleteRuleStates(alertRule.GetKey(), func(s *State) bool {
|
||||
@@ -626,7 +628,7 @@ func (st *Manager) deleteStaleStatesFromCache(logger log.Logger, evaluatedAt tim
|
||||
|
||||
if oldState == eval.Alerting {
|
||||
s.ResolvedAt = &evaluatedAt
|
||||
image := takeImageFn()
|
||||
image := takeImageFn("stale state")
|
||||
if image != nil {
|
||||
s.Image = image
|
||||
}
|
||||
|
||||
@@ -575,13 +575,24 @@ func (a *State) IsStale() bool {
|
||||
return a.StateReason == models.StateReasonMissingSeries
|
||||
}
|
||||
|
||||
// shouldTakeImage returns true if the state just has transitioned to alerting from another state,
|
||||
// transitioned to alerting in a previous evaluation but does not have a screenshot, or has just
|
||||
// been resolved.
|
||||
func shouldTakeImage(state, previousState eval.State, previousImage *models.Image, resolved bool) bool {
|
||||
return resolved ||
|
||||
state == eval.Alerting && previousState != eval.Alerting ||
|
||||
state == eval.Alerting && previousImage == nil
|
||||
// shouldTakeImage determines whether a new image should be taken for a given transition. This should return true when
|
||||
// newly transitioning to an alerting state, when no valid image exists, or when the alert has been resolved.
|
||||
func shouldTakeImage(state, previousState eval.State, previousImage *models.Image, resolved bool) string {
|
||||
if resolved {
|
||||
return "resolved"
|
||||
}
|
||||
if state == eval.Alerting {
|
||||
if previousState != eval.Alerting {
|
||||
return "transition to alerting"
|
||||
}
|
||||
if previousImage == nil {
|
||||
return "no image"
|
||||
}
|
||||
if previousImage.HasExpired() {
|
||||
return "expired image"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// takeImage takes an image for the alert rule. It returns nil if screenshots are disabled or
|
||||
|
||||
@@ -602,15 +602,21 @@ func TestShouldTakeImage(t *testing.T) {
|
||||
state: eval.Pending,
|
||||
previousState: eval.Normal,
|
||||
}, {
|
||||
name: "should not take image for alerting state with image",
|
||||
name: "should not take image for alerting state with valid image",
|
||||
state: eval.Alerting,
|
||||
previousState: eval.Alerting,
|
||||
previousImage: &ngmodels.Image{URL: "https://example.com/foo.png"},
|
||||
previousImage: &ngmodels.Image{URL: "https://example.com/foo.png", ExpiresAt: time.Now().Add(time.Hour)},
|
||||
}, {
|
||||
name: "should take image for alerting state with expired image",
|
||||
state: eval.Alerting,
|
||||
previousState: eval.Alerting,
|
||||
previousImage: &ngmodels.Image{URL: "https://example.com/foo.png", ExpiresAt: time.Now().Add(-time.Hour)},
|
||||
expected: true,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, shouldTakeImage(test.state, test.previousState, test.previousImage, test.resolved))
|
||||
assert.Equal(t, test.expected, shouldTakeImage(test.state, test.previousState, test.previousImage, test.resolved) != "")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package apistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
type HistoryConnector interface {
|
||||
rest.Storage
|
||||
rest.Connecter
|
||||
rest.StorageMetadata
|
||||
}
|
||||
|
||||
func NewHistoryConnector(index resource.ResourceIndexServer, gr schema.GroupResource) HistoryConnector {
|
||||
return &historyREST{
|
||||
index: index,
|
||||
gr: gr,
|
||||
}
|
||||
}
|
||||
|
||||
type historyREST struct {
|
||||
index resource.ResourceIndexServer // should be a client!
|
||||
gr schema.GroupResource
|
||||
}
|
||||
|
||||
func (r *historyREST) New() runtime.Object {
|
||||
return &metav1.PartialObjectMetadataList{}
|
||||
}
|
||||
|
||||
func (r *historyREST) Destroy() {
|
||||
}
|
||||
|
||||
func (r *historyREST) ConnectMethods() []string {
|
||||
return []string{"GET"}
|
||||
}
|
||||
|
||||
func (r *historyREST) ProducesMIMETypes(verb string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *historyREST) ProducesObject(verb string) interface{} {
|
||||
return &metav1.PartialObjectMetadataList{}
|
||||
}
|
||||
|
||||
func (r *historyREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
func (r *historyREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
info, err := NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &resource.ResourceKey{
|
||||
Namespace: info.Value,
|
||||
Group: r.gr.Group,
|
||||
Resource: r.gr.Resource,
|
||||
Name: uid,
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
query := req.URL.Query()
|
||||
rsp, err := r.index.History(ctx, &resource.HistoryRequest{
|
||||
NextPageToken: query.Get("token"),
|
||||
Limit: 100, // TODO, from query
|
||||
Key: key,
|
||||
})
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
list := &metav1.PartialObjectMetadataList{
|
||||
ListMeta: metav1.ListMeta{
|
||||
Continue: rsp.NextPageToken,
|
||||
},
|
||||
}
|
||||
if rsp.ResourceVersion > 0 {
|
||||
list.ResourceVersion = strconv.FormatInt(rsp.ResourceVersion, 10)
|
||||
}
|
||||
for _, v := range rsp.Items {
|
||||
partial := metav1.PartialObjectMetadata{}
|
||||
err = json.Unmarshal(v.PartialObjectMeta, &partial)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
list.Items = append(list.Items, partial)
|
||||
}
|
||||
responder.Object(http.StatusOK, list)
|
||||
}), nil
|
||||
}
|
||||
|
||||
// TODO: This is a temporary copy of the function from pkg/services/apiserver/endpoints/request/namespace.go
|
||||
func NamespaceInfoFrom(ctx context.Context, requireOrgID bool) (claims.NamespaceInfo, error) {
|
||||
info, err := claims.ParseNamespace(request.NamespaceValue(ctx))
|
||||
if err == nil && requireOrgID && info.OrgID < 1 {
|
||||
return info, fmt.Errorf("expected valid orgId in namespace")
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
@@ -22,22 +22,6 @@ message ResourceWrapper {
|
||||
bytes value = 2;
|
||||
}
|
||||
|
||||
// The history and trash commands need access to commit messages
|
||||
message ResourceMeta {
|
||||
// The resource version
|
||||
int64 resource_version = 1;
|
||||
|
||||
// Size of the full resource body
|
||||
int32 size = 3;
|
||||
|
||||
// Hash for the resource
|
||||
string hash = 4;
|
||||
|
||||
// The kubernetes metadata section (not the full resource)
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.30.2/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go#L1496
|
||||
bytes partial_object_meta = 6;
|
||||
}
|
||||
|
||||
// Status structure is copied from:
|
||||
// https://github.com/kubernetes/apimachinery/blob/v0.30.1/pkg/apis/meta/v1/generated.proto#L979
|
||||
// However, this is only used for error handling, never for succesful results
|
||||
@@ -443,33 +427,6 @@ message ResourceSearchResponse {
|
||||
map<string,Facet> facet = 7;
|
||||
}
|
||||
|
||||
message HistoryRequest {
|
||||
// Starting from the requested page (other query parameters must match!)
|
||||
string next_page_token = 1;
|
||||
|
||||
// Maximum number of items to return
|
||||
int64 limit = 2;
|
||||
|
||||
// Resource identifier
|
||||
ResourceKey key = 3;
|
||||
|
||||
// List the deleted values (eg, show trash)
|
||||
bool show_deleted = 4;
|
||||
}
|
||||
|
||||
message HistoryResponse {
|
||||
repeated ResourceMeta items = 1;
|
||||
|
||||
// More results exist... pass this in the next request
|
||||
string next_page_token = 2;
|
||||
|
||||
// ResourceVersion of the list response
|
||||
int64 resource_version = 3;
|
||||
|
||||
// Error details
|
||||
ErrorResult error = 4;
|
||||
}
|
||||
|
||||
// List items within a resource type & repository name
|
||||
// Access control is managed above this request
|
||||
message ListRepositoryObjectsRequest {
|
||||
@@ -791,9 +748,6 @@ service ResourceIndex {
|
||||
|
||||
// Get the resource stats
|
||||
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
|
||||
|
||||
// Show resource history (and trash)
|
||||
rpc History(HistoryRequest) returns (HistoryResponse);
|
||||
}
|
||||
|
||||
// Query repository info from the search index.
|
||||
|
||||
@@ -388,7 +388,6 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{
|
||||
const (
|
||||
ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search"
|
||||
ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats"
|
||||
ResourceIndex_History_FullMethodName = "/resource.ResourceIndex/History"
|
||||
)
|
||||
|
||||
// ResourceIndexClient is the client API for ResourceIndex service.
|
||||
@@ -401,8 +400,6 @@ type ResourceIndexClient interface {
|
||||
Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error)
|
||||
// Get the resource stats
|
||||
GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error)
|
||||
// Show resource history (and trash)
|
||||
History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error)
|
||||
}
|
||||
|
||||
type resourceIndexClient struct {
|
||||
@@ -433,16 +430,6 @@ func (c *resourceIndexClient) GetStats(ctx context.Context, in *ResourceStatsReq
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *resourceIndexClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(HistoryResponse)
|
||||
err := c.cc.Invoke(ctx, ResourceIndex_History_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ResourceIndexServer is the server API for ResourceIndex service.
|
||||
// All implementations should embed UnimplementedResourceIndexServer
|
||||
// for forward compatibility
|
||||
@@ -453,8 +440,6 @@ type ResourceIndexServer interface {
|
||||
Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error)
|
||||
// Get the resource stats
|
||||
GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error)
|
||||
// Show resource history (and trash)
|
||||
History(context.Context, *HistoryRequest) (*HistoryResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedResourceIndexServer should be embedded to have forward compatible implementations.
|
||||
@@ -467,9 +452,6 @@ func (UnimplementedResourceIndexServer) Search(context.Context, *ResourceSearchR
|
||||
func (UnimplementedResourceIndexServer) GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
|
||||
}
|
||||
func (UnimplementedResourceIndexServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method History not implemented")
|
||||
}
|
||||
|
||||
// UnsafeResourceIndexServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ResourceIndexServer will
|
||||
@@ -518,24 +500,6 @@ func _ResourceIndex_GetStats_Handler(srv interface{}, ctx context.Context, dec f
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ResourceIndex_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HistoryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ResourceIndexServer).History(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ResourceIndex_History_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ResourceIndexServer).History(ctx, req.(*HistoryRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ResourceIndex_ServiceDesc is the grpc.ServiceDesc for ResourceIndex service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -551,10 +515,6 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetStats",
|
||||
Handler: _ResourceIndex_GetStats_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "History",
|
||||
Handler: _ResourceIndex_History_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "resource.proto",
|
||||
|
||||
@@ -133,11 +133,6 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access authz.A
|
||||
return support, err
|
||||
}
|
||||
|
||||
// History implements ResourceIndexServer.
|
||||
func (s *searchSupport) History(context.Context, *HistoryRequest) (*HistoryResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented yet... likely should not be the search server")
|
||||
}
|
||||
|
||||
func (s *searchSupport) ListRepositoryObjects(ctx context.Context, req *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error) {
|
||||
if req.NextPageToken != "" {
|
||||
return &ListRepositoryObjectsResponse{
|
||||
|
||||
@@ -1073,11 +1073,6 @@ func (s *server) GetStats(ctx context.Context, req *ResourceStatsRequest) (*Reso
|
||||
return s.search.GetStats(ctx, req)
|
||||
}
|
||||
|
||||
// History implements ResourceServer.
|
||||
func (s *server) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) {
|
||||
return s.search.History(ctx, req)
|
||||
}
|
||||
|
||||
func (s *server) ListRepositoryObjects(ctx context.Context, req *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error) {
|
||||
return s.search.ListRepositoryObjects(ctx, req)
|
||||
}
|
||||
|
||||
@@ -53,16 +53,6 @@ func (b *backend) GetStats(ctx context.Context, req *resource.ResourceStatsReque
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
// History implements resource.ResourceIndexServer.
|
||||
func (b *backend) History(context.Context, *resource.HistoryRequest) (*resource.HistoryResponse, error) {
|
||||
return &resource.HistoryResponse{
|
||||
Error: &resource.ErrorResult{
|
||||
Code: http.StatusNotImplemented,
|
||||
Message: "SQL backend does not implement History",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *backend) RepositoryList(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
|
||||
return nil, fmt.Errorf("SQL backend does not implement RepositoryList")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -267,8 +268,23 @@ func (e *AzureLogAnalyticsDatasource) buildQuery(ctx context.Context, query back
|
||||
|
||||
func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.DataResponse, error) {
|
||||
// If azureLogAnalyticsSameAs is defined and set to false, return an error
|
||||
if sameAs, ok := dsInfo.JSONData["azureLogAnalyticsSameAs"]; ok && !sameAs.(bool) {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("credentials for Log Analytics are no longer supported. Go to the data source configuration to update Azure Monitor credentials"))
|
||||
if sameAs, ok := dsInfo.JSONData["azureLogAnalyticsSameAs"]; ok {
|
||||
sameAsValue, ok := sameAs.(bool)
|
||||
if !ok {
|
||||
stringVal, ok := sameAs.(string)
|
||||
if !ok {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("unknown value for Log Analytics credentials. Go to the data source configuration to update Azure Monitor credentials"))
|
||||
}
|
||||
|
||||
var err error
|
||||
sameAsValue, err = strconv.ParseBool(stringVal)
|
||||
if err != nil {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("unknown value for Log Analytics credentials. Go to the data source configuration to update Azure Monitor credentials"))
|
||||
}
|
||||
}
|
||||
if !sameAsValue {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("credentials for Log Analytics are no longer supported. Go to the data source configuration to update Azure Monitor credentials"))
|
||||
}
|
||||
}
|
||||
|
||||
queryJSONModel := dataquery.AzureMonitorQuery{}
|
||||
|
||||
@@ -708,13 +708,26 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) {
|
||||
query := &AzureLogAnalyticsQuery{
|
||||
TimeRange: backend.TimeRange{},
|
||||
}
|
||||
_, err := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services["Azure Log Analytics"].URL)
|
||||
if err == nil {
|
||||
t.Fatal("expecting an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "credentials for Log Analytics are no longer supported") {
|
||||
t.Error("expecting the error to inform of bad credentials")
|
||||
}
|
||||
t.Run("errors with azureLogAnalyticsSameAs set to false (boolean)", func(t *testing.T) {
|
||||
_, err := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services["Azure Log Analytics"].URL)
|
||||
if err == nil {
|
||||
t.Fatal("expecting an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "credentials for Log Analytics are no longer supported") {
|
||||
t.Error("expecting the error to inform of bad credentials")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("errors with azureLogAnalyticsSameAs set to false (boolean)", func(t *testing.T) {
|
||||
dsInfo.JSONData["azureLogAnalyticsSameAs"] = "false"
|
||||
_, err := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services["Azure Log Analytics"].URL)
|
||||
if err == nil {
|
||||
t.Fatal("expecting an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "credentials for Log Analytics are no longer supported") {
|
||||
t.Error("expecting the error to inform of bad credentials")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_exemplarsFeatureToggle(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// Used in logging to mark a stage
|
||||
@@ -202,8 +204,6 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
||||
c.logger.Info("Response received from Elasticsearch", "status", "ok", "statusCode", res.StatusCode, "contentLength", res.ContentLength, "duration", time.Since(start), "stage", StageDatabaseRequest)
|
||||
|
||||
start = time.Now()
|
||||
var msr MultiSearchResponse
|
||||
dec := json.NewDecoder(res.Body)
|
||||
_, resSpan := tracing.DefaultTracer().Start(c.ctx, "datasource.elasticsearch.queryData.executeMultisearch.decodeResponse")
|
||||
defer func() {
|
||||
if err != nil {
|
||||
@@ -212,19 +212,218 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
||||
}
|
||||
resSpan.End()
|
||||
}()
|
||||
err = dec.Decode(&msr)
|
||||
|
||||
var msr MultiSearchResponse
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing)
|
||||
if improvedParsingEnabled {
|
||||
err = StreamMultiSearchResponse(res.Body, &msr)
|
||||
} else {
|
||||
dec := json.NewDecoder(res.Body)
|
||||
err = dec.Decode(&msr)
|
||||
}
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to decode response from Elasticsearch", "error", err, "duration", time.Since(start))
|
||||
c.logger.Error("Failed to decode response from Elasticsearch", "error", err, "duration", time.Since(start), "improvedParsingEnabled", improvedParsingEnabled)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.logger.Debug("Completed decoding of response from Elasticsearch", "duration", time.Since(start))
|
||||
c.logger.Debug("Completed decoding of response from Elasticsearch", "duration", time.Since(start), "improvedParsingEnabled", improvedParsingEnabled)
|
||||
|
||||
msr.Status = res.StatusCode
|
||||
|
||||
return &msr, nil
|
||||
}
|
||||
|
||||
// StreamMultiSearchResponse processes the JSON response in a streaming fashion
|
||||
func StreamMultiSearchResponse(body io.Reader, msr *MultiSearchResponse) error {
|
||||
dec := json.NewDecoder(body)
|
||||
|
||||
_, err := dec.Token() // reads the `{` opening brace
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for dec.More() {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tok == "responses" {
|
||||
_, err := dec.Token() // reads the `[` opening bracket for responses array
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for dec.More() {
|
||||
var sr SearchResponse
|
||||
|
||||
_, err := dec.Token() // reads `{` for each SearchResponse
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for dec.More() {
|
||||
field, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "hits":
|
||||
sr.Hits = &SearchResponseHits{}
|
||||
err := processHits(dec, &sr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "aggregations":
|
||||
err := dec.Decode(&sr.Aggregations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "error":
|
||||
err := dec.Decode(&sr.Error)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// skip over unknown fields
|
||||
err := skipUnknownField(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msr.Responses = append(msr.Responses, &sr)
|
||||
|
||||
_, err = dec.Token() // reads `}` closing for each SearchResponse
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dec.Token() // reads the `]` closing bracket for responses array
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := skipUnknownField(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dec.Token() // reads the `}` closing brace for the entire JSON
|
||||
return err
|
||||
}
|
||||
|
||||
// processHits processes the hits in the JSON response incrementally.
|
||||
func processHits(dec *json.Decoder, sr *SearchResponse) error {
|
||||
tok, err := dec.Token() // reads the `{` opening brace for the hits object
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tok != json.Delim('{') {
|
||||
return fmt.Errorf("expected '{' for hits object, got %v", tok)
|
||||
}
|
||||
|
||||
for dec.More() {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tok == "hits" {
|
||||
if err := streamHitsArray(dec, sr); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// ignore these fields as they are not used in the current implementation
|
||||
err := skipUnknownField(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read the closing `}` for the hits object
|
||||
_, err = dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// streamHitsArray processes the hits array field incrementally.
|
||||
func streamHitsArray(dec *json.Decoder, sr *SearchResponse) error {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read the opening `[` for the hits array
|
||||
if tok != json.Delim('[') {
|
||||
return fmt.Errorf("expected '[' for hits array, got %v", tok)
|
||||
}
|
||||
|
||||
for dec.More() {
|
||||
var hit map[string]interface{}
|
||||
err = dec.Decode(&hit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sr.Hits.Hits = append(sr.Hits.Hits, hit)
|
||||
}
|
||||
|
||||
// read the closing bracket `]` for the hits array
|
||||
tok, err = dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tok != json.Delim(']') {
|
||||
return fmt.Errorf("expected ']' for closing hits array, got %v", tok)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// skipUnknownField skips over an unknown JSON field's value in the stream.
|
||||
func skipUnknownField(dec *json.Decoder) error {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tok {
|
||||
case json.Delim('{'):
|
||||
// skip everything inside the object until we reach the closing `}`
|
||||
for dec.More() {
|
||||
if err := skipUnknownField(dec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = dec.Token() // read the closing `}`
|
||||
return err
|
||||
case json.Delim('['):
|
||||
// skip everything inside the array until we reach the closing `]`
|
||||
for dec.More() {
|
||||
if err := skipUnknownField(dec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = dec.Token() // read the closing `]`
|
||||
return err
|
||||
default:
|
||||
// no further action needed for primitives
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchRequest) []*multiRequest {
|
||||
multiRequests := []*multiRequest{}
|
||||
|
||||
@@ -264,3 +463,7 @@ func (c *baseClientImpl) getMultiSearchQueryParameters() string {
|
||||
func (c *baseClientImpl) MultiSearch() *MultiSearchRequestBuilder {
|
||||
return NewMultiSearchRequestBuilder()
|
||||
}
|
||||
|
||||
func isFeatureEnabled(ctx context.Context, feature string) bool {
|
||||
return backend.GrafanaConfigFromContext(ctx).FeatureToggles().IsEnabled(feature)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -286,6 +287,129 @@ func TestClient_Index(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamMultiSearchResponse_Success(t *testing.T) {
|
||||
jsonBody := `
|
||||
{
|
||||
"responses": [
|
||||
{ "hits": { "hits": [] } },
|
||||
{ "hits": { "hits": [] } }
|
||||
]
|
||||
}`
|
||||
|
||||
msr := &MultiSearchResponse{}
|
||||
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(msr.Responses) != 2 {
|
||||
t.Errorf("expected 2 responses, got %d", len(msr.Responses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamMultiSearchResponse_MalformedJSON(t *testing.T) {
|
||||
jsonBody := `
|
||||
{
|
||||
"responses": [
|
||||
{ "hits": { "hits": [] } }
|
||||
` // Missing closing braces
|
||||
|
||||
msr := &MultiSearchResponse{}
|
||||
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamMultiSearchResponse_MissingResponses(t *testing.T) {
|
||||
jsonBody := `
|
||||
{
|
||||
"something_else": [
|
||||
{ "hits": { "hits": [] } }
|
||||
]
|
||||
}`
|
||||
|
||||
msr := &MultiSearchResponse{}
|
||||
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(msr.Responses) != 0 {
|
||||
t.Errorf("expected 0 responses, got %d", len(msr.Responses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamMultiSearchResponse_EmptyBody(t *testing.T) {
|
||||
jsonBody := `{}`
|
||||
|
||||
msr := &MultiSearchResponse{}
|
||||
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(msr.Responses) != 0 {
|
||||
t.Errorf("expected 0 responses, got %d", len(msr.Responses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamMultiSearchResponse_InvalidJSONStart(t *testing.T) {
|
||||
jsonBody := `invalid_json`
|
||||
|
||||
msr := &MultiSearchResponse{}
|
||||
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error due to invalid JSON, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamMultiSearchResponse_InvalidHitsField(t *testing.T) {
|
||||
jsonBody := `
|
||||
{
|
||||
"responses": [
|
||||
{ "hits": "invalid_string_value" }
|
||||
]
|
||||
}`
|
||||
|
||||
msr := &MultiSearchResponse{}
|
||||
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error due to invalid 'hits' field, got none")
|
||||
}
|
||||
|
||||
if err.Error() != "expected '{' for hits object, got invalid_string_value" {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamMultiSearchResponse_InvalidHitElement(t *testing.T) {
|
||||
jsonBody := `
|
||||
{
|
||||
"responses": [
|
||||
{ "hits": { "hits": ["invalid_element"] } }
|
||||
]
|
||||
}`
|
||||
|
||||
msr := &MultiSearchResponse{}
|
||||
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error due to invalid element in 'hits' array, got none")
|
||||
}
|
||||
|
||||
expected := "json: cannot unmarshal string into Go value of type map[string]interface {}"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("unexpected error message: expected %v, got %v", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func createMultisearchForTest(t *testing.T, c Client, timeRange backend.TimeRange) (*MultiSearchRequest, error) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -968,13 +968,13 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{visualisationType === 'logs' && (
|
||||
<div
|
||||
className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows}
|
||||
data-testid="logRows"
|
||||
ref={logsContainerRef}
|
||||
>
|
||||
{hasData && (
|
||||
{visualisationType === 'logs' && hasData && (
|
||||
<>
|
||||
<div
|
||||
className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows}
|
||||
data-testid="logRows"
|
||||
ref={logsContainerRef}
|
||||
>
|
||||
<InfiniteScroll
|
||||
loading={loading}
|
||||
loadMoreLogs={infiniteScrollAvailable ? loadMoreLogs : undefined}
|
||||
@@ -1022,41 +1022,41 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
renderPreview
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<LogsNavigation
|
||||
logsSortOrder={logsSortOrder}
|
||||
visibleRange={navigationRange ?? absoluteRange}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onChangeTime={onChangeTime}
|
||||
loading={loading}
|
||||
queries={logsQueries ?? []}
|
||||
scrollToTopLogs={scrollToTopLogs}
|
||||
addResultsToCache={addResultsToCache}
|
||||
clearCache={clearCache}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!loading && !hasData && !scanning && (
|
||||
<div className={styles.logRows}>
|
||||
<div className={styles.noDataWrapper}>
|
||||
<div className={styles.noData}>
|
||||
<Trans i18nKey="explore.logs.no-logs-found">No logs found.</Trans>
|
||||
<Button size="sm" variant="secondary" onClick={onClickScan}>
|
||||
<Button size="sm" variant="secondary" className={styles.scanButton} onClick={onClickScan}>
|
||||
<Trans i18nKey="explore.logs.scan-for-older-logs">Scan for older logs</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{scanning && (
|
||||
<div className={styles.logRows}>
|
||||
<div className={styles.noDataWrapper}>
|
||||
<div className={styles.noData}>
|
||||
<span>{scanText}</span>
|
||||
<Button size="sm" variant="secondary" onClick={onClickStopScan}>
|
||||
<Button size="sm" variant="secondary" className={styles.scanButton} onClick={onClickStopScan}>
|
||||
<Trans i18nKey="explore.logs.stop-scan">Stop scan</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<LogsNavigation
|
||||
logsSortOrder={logsSortOrder}
|
||||
visibleRange={navigationRange ?? absoluteRange}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onChangeTime={onChangeTime}
|
||||
loading={loading}
|
||||
queries={logsQueries ?? []}
|
||||
scrollToTopLogs={scrollToTopLogs}
|
||||
addResultsToCache={addResultsToCache}
|
||||
clearCache={clearCache}
|
||||
/>
|
||||
</div>
|
||||
</PanelChrome>
|
||||
</>
|
||||
@@ -1067,10 +1067,17 @@ export const Logs = withTheme2(UnthemedLogs);
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: number) => {
|
||||
return {
|
||||
noDataWrapper: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
noData: css({
|
||||
'& > *': {
|
||||
marginLeft: '0.5em',
|
||||
},
|
||||
display: 'inline-block',
|
||||
}),
|
||||
scanButton: css({
|
||||
marginLeft: theme.spacing(1),
|
||||
}),
|
||||
logOptions: css({
|
||||
display: 'flex',
|
||||
|
||||
@@ -323,7 +323,7 @@ describe('running queries', () => {
|
||||
cleanSupplementaryQueryAction({ exploreId, type: SupplementaryQueryType.LogsSample }),
|
||||
]);
|
||||
});
|
||||
it('should cancel running query when a new query is issued', async () => {
|
||||
it('should cancel running queries when a new query is issued', async () => {
|
||||
const initialState = {
|
||||
...makeExplorePaneState(),
|
||||
};
|
||||
@@ -333,6 +333,23 @@ describe('running queries', () => {
|
||||
|
||||
expect(dispatchedActions).toContainEqual(cancelQueriesAction({ exploreId }));
|
||||
});
|
||||
it('should not cancel running queries when scanning', async () => {
|
||||
const initialState = {
|
||||
...makeExplorePaneState(),
|
||||
explore: {
|
||||
panes: {
|
||||
[exploreId]: {
|
||||
scanning: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const dispatchedActions = await thunkTester(initialState)
|
||||
.givenThunk(runQueries)
|
||||
.whenThunkIsDispatched({ exploreId });
|
||||
|
||||
expect(dispatchedActions).not.toContainEqual(cancelQueriesAction({ exploreId }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeQueries', () => {
|
||||
|
||||
@@ -528,7 +528,10 @@ interface RunQueriesOptions {
|
||||
export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
|
||||
'explore/runQueries',
|
||||
async ({ exploreId, preserveCache }, { dispatch, getState }) => {
|
||||
dispatch(cancelQueries(exploreId));
|
||||
// Running cancel queries when Explore is scanning will cancel the scanning state
|
||||
if (!getState().explore?.panes[exploreId]?.scanning) {
|
||||
dispatch(cancelQueries(exploreId));
|
||||
}
|
||||
|
||||
const { defaultCorrelationEditorDatasource, scopedVars, showCorrelationEditorLinks } = await getCorrelationsData(
|
||||
getState(),
|
||||
@@ -658,11 +661,12 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
|
||||
|
||||
// Keep scanning for results if this was the last scanning transaction
|
||||
if (exploreState!.scanning) {
|
||||
console.log(data.series);
|
||||
if (data.state === LoadingState.Done && data.series.length === 0) {
|
||||
const range = getShiftedTimeRange(-1, exploreState!.range);
|
||||
dispatch(updateTime({ exploreId, absoluteRange: range }));
|
||||
dispatch(runQueries({ exploreId }));
|
||||
} else {
|
||||
} else if (data.series[0]?.length > 0 || data.state === LoadingState.Done) {
|
||||
// We can stop scanning if we have a result
|
||||
dispatch(scanStopAction({ exploreId }));
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { MetricScene } from './MetricScene';
|
||||
import { MetricSelectScene } from './MetricSelect/MetricSelectScene';
|
||||
import { MetricsHeader } from './MetricsHeader';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { NativeHistogramBanner } from './banners/NativeHistogramBanner';
|
||||
import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper';
|
||||
import { reportChangeInLabelFilters, reportExploreMetrics } from './interactions';
|
||||
import { migrateOtelDeploymentEnvironment } from './migrations/otelDeploymentEnvironment';
|
||||
@@ -93,10 +94,16 @@ export interface DataTrailState extends SceneObjectState {
|
||||
// Synced with url
|
||||
metric?: string;
|
||||
metricSearch?: string;
|
||||
|
||||
histogramsLoaded: boolean;
|
||||
nativeHistograms: string[];
|
||||
nativeHistogramMetric: string;
|
||||
}
|
||||
|
||||
export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneObjectWithUrlSync {
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['metric', 'metricSearch', 'showPreviews'] });
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, {
|
||||
keys: ['metric', 'metricSearch', 'showPreviews', 'nativeHistogramMetric'],
|
||||
});
|
||||
|
||||
public constructor(state: Partial<DataTrailState>) {
|
||||
super({
|
||||
@@ -120,6 +127,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
// preserve the otel join query
|
||||
otelJoinQuery: state.otelJoinQuery ?? '',
|
||||
showPreviews: state.showPreviews ?? true,
|
||||
nativeHistograms: state.nativeHistograms ?? [],
|
||||
histogramsLoaded: state.histogramsLoaded ?? false,
|
||||
nativeHistogramMetric: state.nativeHistogramMetric ?? '',
|
||||
...state,
|
||||
});
|
||||
|
||||
@@ -209,6 +219,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
if (name === VAR_DATASOURCE) {
|
||||
this.datasourceHelper.reset();
|
||||
|
||||
// reset native histograms
|
||||
this.resetNativeHistograms();
|
||||
|
||||
if (this.state.afterFirstOtelCheck) {
|
||||
// we need a new check for OTel
|
||||
this.setState({ initialOtelCheckComplete: false });
|
||||
@@ -262,6 +275,33 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
return this.datasourceHelper.getMetricMetadata(metric);
|
||||
}
|
||||
|
||||
public isNativeHistogram(metric: string) {
|
||||
return this.datasourceHelper.isNativeHistogram(metric);
|
||||
}
|
||||
|
||||
// use this to initialize histograms in all scenes
|
||||
public async initializeHistograms() {
|
||||
if (!this.state.histogramsLoaded) {
|
||||
await this.datasourceHelper.initializeHistograms();
|
||||
|
||||
this.setState({
|
||||
nativeHistograms: this.listNativeHistograms(),
|
||||
histogramsLoaded: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public listNativeHistograms() {
|
||||
return this.datasourceHelper.listNativeHistograms() ?? [];
|
||||
}
|
||||
|
||||
private resetNativeHistograms() {
|
||||
this.setState({
|
||||
histogramsLoaded: false,
|
||||
nativeHistograms: [],
|
||||
});
|
||||
}
|
||||
|
||||
public getCurrentMetricMetadata() {
|
||||
return this.getMetricMetadata(this.state.metric);
|
||||
}
|
||||
@@ -277,6 +317,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
history: this.state.history,
|
||||
metric: !state.metric ? undefined : state.metric,
|
||||
metricSearch: !state.metricSearch ? undefined : state.metricSearch,
|
||||
// store type because this requires an expensive api call to determine
|
||||
// when loading the metric scene
|
||||
nativeHistogramMetric: !state.nativeHistogramMetric ? undefined : state.nativeHistogramMetric,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -292,7 +335,13 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
await updateOtelJoinWithGroupLeft(this, metric);
|
||||
}
|
||||
|
||||
this.setState(this.getSceneUpdatesForNewMetricValue(metric));
|
||||
// from the metric preview panel we have the info loaded to determine that a metric is a native histogram
|
||||
let nativeHistogramMetric = false;
|
||||
if (this.isNativeHistogram(metric)) {
|
||||
nativeHistogramMetric = true;
|
||||
}
|
||||
|
||||
this.setState(this.getSceneUpdatesForNewMetricValue(metric, nativeHistogramMetric));
|
||||
|
||||
// Add metric to adhoc filters baseFilter
|
||||
const filterVar = sceneGraph.lookupVariable(VAR_FILTERS, this);
|
||||
@@ -303,19 +352,25 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
}
|
||||
}
|
||||
|
||||
private getSceneUpdatesForNewMetricValue(metric: string | undefined) {
|
||||
private getSceneUpdatesForNewMetricValue(metric: string | undefined, nativeHistogramMetric?: boolean) {
|
||||
const stateUpdate: Partial<DataTrailState> = {};
|
||||
stateUpdate.metric = metric;
|
||||
stateUpdate.topScene = getTopSceneFor(metric);
|
||||
// refactoring opportunity? Or do we pass metric knowledge all the way down?
|
||||
// must pass this native histogram prometheus knowledge deep into
|
||||
// the topscene set on the trail > MetricScene > getAutoQueriesForMetric() > createHistogramMetricQueryDefs();
|
||||
stateUpdate.nativeHistogramMetric = nativeHistogramMetric ? '1' : '';
|
||||
stateUpdate.topScene = getTopSceneFor(metric, nativeHistogramMetric);
|
||||
return stateUpdate;
|
||||
}
|
||||
|
||||
getUrlState(): SceneObjectUrlValues {
|
||||
const { metric, metricSearch, showPreviews } = this.state;
|
||||
const { metric, metricSearch, showPreviews, nativeHistogramMetric } = this.state;
|
||||
return {
|
||||
metric,
|
||||
metricSearch,
|
||||
...{ showPreviews: showPreviews === false ? 'false' : null },
|
||||
// store the native histogram knowledge in url for the metric scene
|
||||
nativeHistogramMetric,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,7 +379,14 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
|
||||
if (typeof values.metric === 'string') {
|
||||
if (this.state.metric !== values.metric) {
|
||||
Object.assign(stateUpdate, this.getSceneUpdatesForNewMetricValue(values.metric));
|
||||
// if we have a metric and we have stored in the url that it is a native histogram
|
||||
// we can pass that info into the metric scene to generate the appropriate queries
|
||||
let nativeHistogramMetric = false;
|
||||
if (values.nativeHistogramMetric === '1') {
|
||||
nativeHistogramMetric = true;
|
||||
}
|
||||
|
||||
Object.assign(stateUpdate, this.getSceneUpdatesForNewMetricValue(values.metric, nativeHistogramMetric));
|
||||
}
|
||||
} else if (values.metric == null) {
|
||||
stateUpdate.metric = undefined;
|
||||
@@ -489,11 +551,23 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
|
||||
const { controls, topScene, history, settings, useOtelExperience, hasOtelResources, embedded } = model.useState();
|
||||
const {
|
||||
controls,
|
||||
topScene,
|
||||
history,
|
||||
settings,
|
||||
useOtelExperience,
|
||||
hasOtelResources,
|
||||
embedded,
|
||||
histogramsLoaded,
|
||||
nativeHistograms,
|
||||
} = model.useState();
|
||||
|
||||
const chromeHeaderHeight = useChromeHeaderHeight();
|
||||
const styles = useStyles2(getStyles, embedded ? 0 : (chromeHeaderHeight ?? 0));
|
||||
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
|
||||
// need to initialize this here and not on activate because it requires the data source helper to be fully initialized first
|
||||
model.initializeHistograms();
|
||||
|
||||
useEffect(() => {
|
||||
if (model.state.addingLabelFromBreakdown) {
|
||||
@@ -526,6 +600,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{NativeHistogramBanner({ histogramsLoaded, nativeHistograms, trail: model })}
|
||||
{showHeaderForFirstTimeUsers && <MetricsHeader />}
|
||||
<history.Component model={history} />
|
||||
{controls && (
|
||||
@@ -546,9 +621,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
};
|
||||
}
|
||||
|
||||
export function getTopSceneFor(metric?: string) {
|
||||
export function getTopSceneFor(metric?: string, nativeHistogram?: boolean) {
|
||||
if (metric) {
|
||||
return new MetricScene({ metric: metric });
|
||||
return new MetricScene({ metric: metric, nativeHistogram: nativeHistogram ?? false });
|
||||
} else {
|
||||
return new MetricSelectScene({});
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const relatedLogsFeatureEnabled = config.featureToggles.exploreMetricsRelatedLog
|
||||
export interface MetricSceneState extends SceneObjectState {
|
||||
body: MetricGraphScene;
|
||||
metric: string;
|
||||
nativeHistogram?: boolean;
|
||||
actionView?: string;
|
||||
|
||||
autoQuery: AutoQueryInfo;
|
||||
@@ -54,7 +55,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['actionView'] });
|
||||
|
||||
public constructor(state: MakeOptional<MetricSceneState, 'body' | 'autoQuery'>) {
|
||||
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric);
|
||||
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric, state.nativeHistogram);
|
||||
super({
|
||||
$variables: state.$variables ?? getVariableSet(state.metric),
|
||||
body: state.body ?? new MetricGraphScene({}),
|
||||
|
||||
@@ -424,7 +424,9 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> i
|
||||
children.push(metric.itemRef.resolve());
|
||||
continue;
|
||||
}
|
||||
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description);
|
||||
// refactor this into the query generator in future
|
||||
const isNative = trail.isNativeHistogram(metric.name);
|
||||
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description, isNative);
|
||||
|
||||
metric.itemRef = panel.getRef();
|
||||
metric.isPanel = true;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObjectBase } from '@grafana/scenes';
|
||||
import { Badge, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from '@grafana/ui/src/utils/i18n';
|
||||
|
||||
export class NativeHistogramBadge extends SceneObjectBase {
|
||||
public static Component = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<Badge
|
||||
className={styles.badge}
|
||||
color="blue"
|
||||
text={<Trans i18nKey="trails.metric-select.native-histogram">Native Histogram</Trans>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
badge: css({
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
border: `1px solid ${theme.colors.info.text}`,
|
||||
background: theme.colors.info.transparent,
|
||||
cursor: 'auto',
|
||||
width: '112px',
|
||||
padding: '0rem 0.25rem 0 0.35rem',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -6,20 +6,32 @@ import { getVariablesWithMetricConstant, MDP_METRIC_PREVIEW, trailDS } from '../
|
||||
import { getColorByIndex } from '../utils';
|
||||
|
||||
import { AddToExplorationButton } from './AddToExplorationsButton';
|
||||
import { NativeHistogramBadge } from './NativeHistogramBadge';
|
||||
import { SelectMetricAction } from './SelectMetricAction';
|
||||
import { hideEmptyPreviews } from './hideEmptyPreviews';
|
||||
|
||||
export function getPreviewPanelFor(metric: string, index: number, currentFilterCount: number, description?: string) {
|
||||
const autoQuery = getAutoQueriesForMetric(metric);
|
||||
export function getPreviewPanelFor(
|
||||
metric: string,
|
||||
index: number,
|
||||
currentFilterCount: number,
|
||||
description?: string,
|
||||
nativeHistogram?: boolean
|
||||
) {
|
||||
const autoQuery = getAutoQueriesForMetric(metric, nativeHistogram);
|
||||
let actions: Array<SelectMetricAction | AddToExplorationButton | NativeHistogramBadge> = [
|
||||
new SelectMetricAction({ metric, title: 'Select' }),
|
||||
new AddToExplorationButton({ labelName: metric }),
|
||||
];
|
||||
|
||||
if (nativeHistogram) {
|
||||
actions.unshift(new NativeHistogramBadge({}));
|
||||
}
|
||||
|
||||
const vizPanel = autoQuery.preview
|
||||
.vizBuilder()
|
||||
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(index) })
|
||||
.setDescription(description)
|
||||
.setHeaderActions([
|
||||
new SelectMetricAction({ metric, title: 'Select' }),
|
||||
new AddToExplorationButton({ labelName: metric }),
|
||||
])
|
||||
.setHeaderActions(actions)
|
||||
.build();
|
||||
|
||||
const queries = autoQuery.preview.queries.map((query) =>
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
description: 'Test',
|
||||
@@ -80,6 +81,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
description: 'Test',
|
||||
@@ -228,6 +230,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'ds',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
description: 'Test',
|
||||
@@ -242,6 +245,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'ds',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
description: 'Test',
|
||||
@@ -256,6 +260,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'ds',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
description: 'Test',
|
||||
@@ -506,6 +511,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'bookmarked_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -603,6 +609,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
description: 'Test',
|
||||
@@ -616,6 +623,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'time',
|
||||
description: 'Test',
|
||||
@@ -655,6 +663,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'prom-mock',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
},
|
||||
@@ -666,6 +675,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'prom-mock',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'time',
|
||||
},
|
||||
@@ -677,6 +687,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'prom-mock',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
},
|
||||
@@ -713,6 +724,7 @@ describe('TrailStore', () => {
|
||||
history: [
|
||||
{
|
||||
urlValues: {
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -728,6 +740,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'bookmarked_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -743,6 +756,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'some_other_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -766,6 +780,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'bookmarked_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createSummaryMetricQueryDefs } from './queryGenerators/summary';
|
||||
import { AutoQueryContext, AutoQueryInfo } from './types';
|
||||
import { getUnit } from './units';
|
||||
|
||||
export function getAutoQueriesForMetric(metric: string): AutoQueryInfo {
|
||||
export function getAutoQueriesForMetric(metric: string, nativeHistogram?: boolean): AutoQueryInfo {
|
||||
const isUtf8Metric = false;
|
||||
const metricParts = metric.split('_');
|
||||
const suffix = metricParts.at(-1);
|
||||
@@ -28,7 +28,7 @@ export function getAutoQueriesForMetric(metric: string): AutoQueryInfo {
|
||||
return createSummaryMetricQueryDefs(ctx);
|
||||
}
|
||||
|
||||
if (suffix === 'bucket') {
|
||||
if (suffix === 'bucket' || nativeHistogram) {
|
||||
return createHistogramMetricQueryDefs(ctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { DataTrail } from '../DataTrail';
|
||||
import { MetricSelectedEvent } from '../shared';
|
||||
|
||||
import { NativeHistogramBanner } from './NativeHistogramBanner';
|
||||
|
||||
const mockTrail = {
|
||||
publishEvent: jest.fn(),
|
||||
} as unknown as DataTrail;
|
||||
|
||||
const mockProps = {
|
||||
histogramsLoaded: true,
|
||||
nativeHistograms: ['histogram1', 'histogram2'],
|
||||
trail: mockTrail,
|
||||
};
|
||||
|
||||
describe('NativeHistogramBanner', () => {
|
||||
test('renders correctly when histograms are loaded', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
expect(screen.getByText('Native Histogram Support')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and a way to combine and manipulate histograms in queries and in Grafana.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Has a learn more button works', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
const learnMoreButton = screen.getByText('Learn more');
|
||||
expect(learnMoreButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('See examples button works', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
const seeExamplesButton = screen.getByText('> See examples');
|
||||
expect(seeExamplesButton).toBeInTheDocument();
|
||||
fireEvent.click(seeExamplesButton);
|
||||
expect(screen.getByText('Native Histogram displayed as heatmap:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Native Histogram displayed as histogram:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Classic Histogram displayed as heatmap:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Classic Histogram displayed as histogram:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Native histograms buttons work', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
fireEvent.click(screen.getByText('> See examples'));
|
||||
const histogramButton = screen.getByText('histogram1');
|
||||
expect(histogramButton).toBeInTheDocument();
|
||||
fireEvent.click(histogramButton);
|
||||
expect(mockTrail.publishEvent).toHaveBeenCalledWith(new MetricSelectedEvent('histogram1'), true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useState, type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, useTheme2, Alert, Button } from '@grafana/ui';
|
||||
import { t, Trans } from '@grafana/ui/src/utils/i18n';
|
||||
|
||||
import { DataTrail } from '../DataTrail';
|
||||
import { reportExploreMetrics } from '../interactions';
|
||||
import { MetricSelectedEvent } from '../shared';
|
||||
|
||||
interface NativeHistogramInfoProps {
|
||||
histogramsLoaded: boolean;
|
||||
nativeHistograms: string[];
|
||||
trail: DataTrail;
|
||||
}
|
||||
|
||||
export function NativeHistogramBanner(props: NativeHistogramInfoProps) {
|
||||
const { histogramsLoaded, nativeHistograms, trail } = props;
|
||||
const [histogramMessage, setHistogramMessage] = useState(true);
|
||||
const [showHistogramExamples, setShowHistogramExamples] = useState(false);
|
||||
const styles = useStyles2(getStyles, 0);
|
||||
|
||||
if (!histogramsLoaded || nativeHistograms.length === 0 || !histogramMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<Alert
|
||||
title={'Native Histogram Support'}
|
||||
severity={'info'}
|
||||
onRemove={() => {
|
||||
setHistogramMessage(false);
|
||||
}}
|
||||
>
|
||||
<div className={styles.histogramRow}>
|
||||
<div className={styles.histogramSentence}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.sentence">
|
||||
Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and
|
||||
a way to combine and manipulate histograms in queries and in Grafana.
|
||||
</Trans>
|
||||
</div>
|
||||
<div className={styles.histogramLearnMore}>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open('https://grafana.com/docs/grafana-cloud/whats-new/native-histograms/', '_blank')
|
||||
}
|
||||
className={styles.button}
|
||||
>
|
||||
<Trans i18nKey="trails.native-histogram-banner.learn-more">Learn more</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NativeHistogramExamplesButton
|
||||
showHistogramExamples={showHistogramExamples}
|
||||
setShowHistogramExamples={setShowHistogramExamples}
|
||||
/>
|
||||
{showHistogramExamples && (
|
||||
<NativeHistogramExamples
|
||||
trail={trail}
|
||||
nativeHistograms={nativeHistograms}
|
||||
setHistogramMessage={setHistogramMessage}
|
||||
/>
|
||||
)}
|
||||
</Alert>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface NativeHistogramExamplesButtonProps {
|
||||
showHistogramExamples: boolean;
|
||||
setShowHistogramExamples: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const NativeHistogramExamplesButton = ({
|
||||
showHistogramExamples,
|
||||
setShowHistogramExamples,
|
||||
}: NativeHistogramExamplesButtonProps) => {
|
||||
const styles = useStyles2(getStyles, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className={`${styles.seeExamplesButton} native-histogram-examples-button`}
|
||||
type="button"
|
||||
fill="text"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (showHistogramExamples) {
|
||||
// hide the examples
|
||||
reportExploreMetrics('native_histogram_examples_closed', {});
|
||||
}
|
||||
setShowHistogramExamples(!showHistogramExamples);
|
||||
}}
|
||||
>
|
||||
{showHistogramExamples
|
||||
? t(`trails.native-histogram-banner.hide-examples`, `Hide examples`)
|
||||
: t(`trails.native-histogram-banner.see-examples`, `> See examples`)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type NativeHistogramExamplesProps = Pick<NativeHistogramInfoProps, 'trail' | 'nativeHistograms'> & {
|
||||
setHistogramMessage: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const NativeHistogramExamples = ({ trail, nativeHistograms, setHistogramMessage }: NativeHistogramExamplesProps) => {
|
||||
const styles = useStyles2(getStyles, 0);
|
||||
const isDark = useTheme2().isDark;
|
||||
const selectNativeHistogram = (metric: string) => {
|
||||
reportExploreMetrics('native_histogram_example_clicked', {
|
||||
metric,
|
||||
});
|
||||
trail.publishEvent(new MetricSelectedEvent(metric), true);
|
||||
};
|
||||
const images = {
|
||||
nativeHeatmap: isDark
|
||||
? 'public/img/native-histograms/DarkModeHeatmapNativeHistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHeatmapNativeHistogram.png',
|
||||
classicHeatmap: isDark
|
||||
? 'public/img/native-histograms/DarkModeHeatmapClassicHistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHeatmapClassicHistogram.png',
|
||||
nativeHistogram: isDark
|
||||
? 'public/img/native-histograms/DarkModeHistogramNativehistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHistogramClassicHistogram.png',
|
||||
classicHistogram: isDark
|
||||
? 'public/img/native-histograms/DarkModeHistogramClassicHistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHistogramClassicHistogram.png',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${styles.histogramRow} ${styles.seeExamplesRow}`}>
|
||||
<div className={styles.histogramImageCol}>
|
||||
<div>
|
||||
<Trans i18nKey="trails.native-histogram-banner.now">Now:</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.rightCol}`}>
|
||||
<div>
|
||||
<Trans i18nKey="trails.native-histogram-banner.previously">Previously:</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramRow} ${styles.seeExamplesRow}`}>
|
||||
<div className={styles.histogramImageCol}>
|
||||
<div className={styles.histogramRow}>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.nh-heatmap">
|
||||
Native Histogram displayed as heatmap:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.nativeHeatmap} alt="Native Histogram displayed as heatmap" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.nh-histogram">
|
||||
Native Histogram displayed as histogram:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.nativeHistogram} alt="Native Histogram displayed as histogram" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.rightImageCol} ${styles.rightCol}`}>
|
||||
<div className={styles.histogramRow}>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.ch-heatmap">
|
||||
Classic Histogram displayed as heatmap:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.classicHeatmap} alt="Classic Histogram displayed as heatmap" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.ch-histogram">
|
||||
Classic Histogram displayed as histogram:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.classicHistogram} alt="Classic Histogram displayed as histogram" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<Trans i18nKey="trails.native-histogram-banner.click-histogram">
|
||||
Click any of the native histograms below to explore them:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
{nativeHistograms.map((el) => {
|
||||
return (
|
||||
<div key={el}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
selectNativeHistogram(el);
|
||||
setHistogramMessage(false);
|
||||
}}
|
||||
key={el}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fill="text"
|
||||
className={`native-histogram-example-clicked`}
|
||||
>
|
||||
{t('trails.native-histogram-banner.metric-examples', el)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme2, _chromeHeaderHeight: number) {
|
||||
return {
|
||||
histogramRow: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
histogramSentence: css({
|
||||
width: '90%',
|
||||
}),
|
||||
histogramLearnMore: css({
|
||||
width: '10%',
|
||||
}),
|
||||
button: css({
|
||||
float: 'right',
|
||||
}),
|
||||
seeExamplesButton: css({
|
||||
paddingLeft: '0px',
|
||||
}),
|
||||
seeExamplesRow: css({
|
||||
paddingTop: '4px',
|
||||
}),
|
||||
histogramImageCol: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexBasis: '100%',
|
||||
flex: '1',
|
||||
}),
|
||||
fontSmall: css({
|
||||
fontSize: theme.typography.size.sm,
|
||||
}),
|
||||
imageText: css({
|
||||
paddingBottom: '4px',
|
||||
}),
|
||||
rightImageCol: css({
|
||||
borderLeft: `1px solid ${theme.colors.secondary.borderTransparent}`,
|
||||
}),
|
||||
rightCol: css({
|
||||
paddingLeft: '16px',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DataTrail } from '../DataTrail';
|
||||
|
||||
import { MetricDatasourceHelper } from './MetricDatasourceHelper';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
publicDashboardAccessToken: '123',
|
||||
},
|
||||
}));
|
||||
|
||||
const NATIVE_HISTOGRAM = 'test_metric';
|
||||
describe('MetricDatasourceHelper', () => {
|
||||
let metricDatasourceHelper: MetricDatasourceHelper;
|
||||
|
||||
beforeEach(() => {
|
||||
const trail = new DataTrail({});
|
||||
metricDatasourceHelper = new MetricDatasourceHelper(trail);
|
||||
metricDatasourceHelper['_classicHistograms'] = {
|
||||
test_metric_bucket: 1,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('isNativeHistogram', () => {
|
||||
it('should return false if metric is not provided', async () => {
|
||||
const result = await metricDatasourceHelper.isNativeHistogram('');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if metric is a native histogram', async () => {
|
||||
const result = await metricDatasourceHelper.isNativeHistogram(NATIVE_HISTOGRAM);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if metric is not a native histogram', async () => {
|
||||
const result = await metricDatasourceHelper.isNativeHistogram('non_histogram_metric');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,8 @@ export class MetricDatasourceHelper {
|
||||
public reset() {
|
||||
this._datasource = undefined;
|
||||
this._metricsMetadata = undefined;
|
||||
this._classicHistograms = {};
|
||||
this._nativeHistograms = [];
|
||||
}
|
||||
|
||||
private _trail: DataTrail;
|
||||
@@ -61,6 +63,67 @@ export class MetricDatasourceHelper {
|
||||
return metadata?.[metric];
|
||||
}
|
||||
|
||||
private _classicHistograms: Record<string, number> = {};
|
||||
private _nativeHistograms: string[] = [];
|
||||
|
||||
public listNativeHistograms() {
|
||||
return this._nativeHistograms;
|
||||
}
|
||||
/**
|
||||
* Identify native histograms by querying classic histograms and all metrics,
|
||||
* then comparing the results and build the collection of native histograms.
|
||||
*
|
||||
* classic histogram = test_metric_bucket
|
||||
* native histogram = test_metric
|
||||
*/
|
||||
public async initializeHistograms() {
|
||||
const ds = await this.getDatasource();
|
||||
if (Object.keys(this._classicHistograms).length === 0 && ds instanceof PrometheusDatasource) {
|
||||
const classicHistogramsCall = ds.metricFindQuery('metrics(.*_bucket)');
|
||||
const allMetricsCall = ds.metricFindQuery('metrics(.*)');
|
||||
|
||||
const [classicHistograms, allMetrics] = await Promise.all([classicHistogramsCall, allMetricsCall]);
|
||||
|
||||
classicHistograms.forEach((m) => {
|
||||
this._classicHistograms[m.text] = 1;
|
||||
});
|
||||
|
||||
allMetrics.forEach((m) => {
|
||||
if (this.isNativeHistogram(m.text)) {
|
||||
// Build the collection of native histograms.
|
||||
this.addNativeHistogram(m.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* If a metric name + _bucket exists in the classic histograms, then it is a native histogram
|
||||
*
|
||||
* classic histogram = test_metric_bucket
|
||||
* native histogram = test_metric
|
||||
* @param metric
|
||||
* @returns
|
||||
*/
|
||||
public isNativeHistogram(metric: string): boolean {
|
||||
if (!metric) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._classicHistograms[`${metric}_bucket`]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private addNativeHistogram(metric: string) {
|
||||
if (!this._nativeHistograms.includes(metric)) {
|
||||
this._nativeHistograms.push(metric);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for additional filtering for adhoc vars labels in Explore metrics.
|
||||
* @param options
|
||||
|
||||
@@ -131,7 +131,11 @@ type Interactions = {
|
||||
otel_experience_used: {},
|
||||
otel_experience_toggled: {
|
||||
value: ('on'| 'off')
|
||||
}
|
||||
},
|
||||
native_histogram_examples_closed: {},
|
||||
native_histogram_example_clicked: {
|
||||
metric: string;
|
||||
},
|
||||
};
|
||||
|
||||
const PREFIX = 'grafana_explore_metrics_';
|
||||
|
||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -3345,9 +3345,24 @@
|
||||
},
|
||||
"metric-select": {
|
||||
"filter-by": "Filter by",
|
||||
"native-histogram": "Native Histogram",
|
||||
"new-badge": "New",
|
||||
"otel-switch": "This switch enables filtering by OTel resources for OTel native data sources."
|
||||
},
|
||||
"native-histogram-banner": {
|
||||
"ch-heatmap": "Classic Histogram displayed as heatmap:",
|
||||
"ch-histogram": "Classic Histogram displayed as histogram:",
|
||||
"click-histogram": "Click any of the native histograms below to explore them:",
|
||||
"hide-examples": "Hide examples",
|
||||
"learn-more": "Learn more",
|
||||
"metric-examples": "",
|
||||
"nh-heatmap": "Native Histogram displayed as heatmap:",
|
||||
"nh-histogram": "Native Histogram displayed as histogram:",
|
||||
"now": "Now:",
|
||||
"previously": "Previously:",
|
||||
"see-examples": "> See examples",
|
||||
"sentence": "Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and a way to combine and manipulate histograms in queries and in Grafana."
|
||||
},
|
||||
"recent-metrics": {
|
||||
"or-view-a-recent-exploration": "Or view a recent exploration"
|
||||
},
|
||||
|
||||
@@ -3345,9 +3345,24 @@
|
||||
},
|
||||
"metric-select": {
|
||||
"filter-by": "Fįľŧęř þy",
|
||||
"native-histogram": "Ńäŧįvę Ħįşŧőģřäm",
|
||||
"new-badge": "Ńęŵ",
|
||||
"otel-switch": "Ŧĥįş şŵįŧčĥ ęʼnäþľęş ƒįľŧęřįʼnģ þy ØŦęľ řęşőūřčęş ƒőř ØŦęľ ʼnäŧįvę đäŧä şőūřčęş."
|
||||
},
|
||||
"native-histogram-banner": {
|
||||
"ch-heatmap": "Cľäşşįč Ħįşŧőģřäm đįşpľäyęđ äş ĥęäŧmäp:",
|
||||
"ch-histogram": "Cľäşşįč Ħįşŧőģřäm đįşpľäyęđ äş ĥįşŧőģřäm:",
|
||||
"click-histogram": "Cľįčĸ äʼny őƒ ŧĥę ʼnäŧįvę ĥįşŧőģřämş þęľőŵ ŧő ęχpľőřę ŧĥęm:",
|
||||
"hide-examples": "Ħįđę ęχämpľęş",
|
||||
"learn-more": "Ŀęäřʼn mőřę",
|
||||
"metric-examples": "",
|
||||
"nh-heatmap": "Ńäŧįvę Ħįşŧőģřäm đįşpľäyęđ äş ĥęäŧmäp:",
|
||||
"nh-histogram": "Ńäŧįvę Ħįşŧőģřäm đįşpľäyęđ äş ĥįşŧőģřäm:",
|
||||
"now": "Ńőŵ:",
|
||||
"previously": "Přęvįőūşľy:",
|
||||
"see-examples": "> Ŝęę ęχämpľęş",
|
||||
"sentence": "Přőmęŧĥęūş ʼnäŧįvę ĥįşŧőģřämş őƒƒęř ĥįģĥ řęşőľūŧįőʼn, ĥįģĥ přęčįşįőʼn, şįmpľę ūşäģę įʼn įʼnşŧřūmęʼnŧäŧįőʼn äʼnđ ä ŵäy ŧő čőmþįʼnę äʼnđ mäʼnįpūľäŧę ĥįşŧőģřämş įʼn qūęřįęş äʼnđ įʼn Ğřäƒäʼnä."
|
||||
},
|
||||
"recent-metrics": {
|
||||
"or-view-a-recent-exploration": "Øř vįęŵ ä řęčęʼnŧ ęχpľőřäŧįőʼn"
|
||||
},
|
||||
|
||||