Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e003210566 | |||
| e055bf28e1 | |||
| d20a4ae4cc | |||
| 91f48d51e6 | |||
| 780a64e771 | |||
| 156a6f1375 | |||
| 5fd4fb5fb8 | |||
| 0275939762 | |||
| c15b1b6f10 | |||
| 32b9bebc75 | |||
| 7fce2d9516 | |||
| 2da171595a | |||
| dd77107ed4 | |||
| aaa5d02a3e | |||
| db9afe31e4 |
@@ -10,6 +10,20 @@ manifest: {
|
||||
|
||||
v0alpha1: {
|
||||
kinds: [annotationv0alpha1]
|
||||
routes: {
|
||||
namespaced: {
|
||||
"/tags": {
|
||||
"GET": {
|
||||
response: {
|
||||
tags: [...{
|
||||
tag: string
|
||||
count: number
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
codegen: {
|
||||
ts: {
|
||||
enabled: true
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetTagsBody struct {
|
||||
Tags []V0alpha1GetTagsBodyTags `json:"tags"`
|
||||
}
|
||||
|
||||
// NewGetTagsBody creates a new GetTagsBody object.
|
||||
func NewGetTagsBody() *GetTagsBody {
|
||||
return &GetTagsBody{
|
||||
Tags: []V0alpha1GetTagsBodyTags{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1GetTagsBodyTags struct {
|
||||
Tag string `json:"tag"`
|
||||
Count float64 `json:"count"`
|
||||
}
|
||||
|
||||
// NewV0alpha1GetTagsBodyTags creates a new V0alpha1GetTagsBodyTags object.
|
||||
func NewV0alpha1GetTagsBodyTags() *V0alpha1GetTagsBodyTags {
|
||||
return &V0alpha1GetTagsBodyTags{}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetTags struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
GetTagsBody `json:",inline"`
|
||||
}
|
||||
|
||||
func NewGetTags() *GetTags {
|
||||
return &GetTags{}
|
||||
}
|
||||
|
||||
func (t *GetTagsBody) DeepCopyInto(dst *GetTagsBody) {
|
||||
_ = resource.CopyObjectInto(dst, t)
|
||||
}
|
||||
|
||||
func (o *GetTags) DeepCopyObject() runtime.Object {
|
||||
dst := NewGetTags()
|
||||
o.DeepCopyInto(dst)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (o *GetTags) DeepCopyInto(dst *GetTags) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.GetTagsBody.DeepCopyInto(&dst.GetTagsBody)
|
||||
}
|
||||
|
||||
var _ runtime.Object = NewGetTags()
|
||||
+57
-4
@@ -43,9 +43,60 @@ var appManifestData = app.ManifestData{
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
Namespaced: map[string]spec3.PathProps{},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{},
|
||||
Namespaced: map[string]spec3.PathProps{
|
||||
"/tags": {
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
|
||||
OperationId: "getTags",
|
||||
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
Default: &spec3.Response{
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Description: "Default OK response",
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
},
|
||||
},
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
},
|
||||
},
|
||||
"tags": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"tags",
|
||||
"apiVersion",
|
||||
"kind",
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -70,7 +121,9 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoResponseType = map[string]any{}
|
||||
var customRouteToGoResponseType = map[string]any{
|
||||
"v0alpha1||<namespace>/tags|GET": v0alpha1.GetTags{},
|
||||
}
|
||||
|
||||
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
|
||||
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
|
||||
|
||||
@@ -30,6 +30,23 @@ func New(cfg app.Config) (app.App, error) {
|
||||
},
|
||||
}
|
||||
|
||||
// Add custom route handlers if a TagHandler is provided in SpecificConfig.
|
||||
// The handler is created/owned by the registry layer and passed in via
|
||||
// SpecificConfig to avoid the apps package depending on the registry.
|
||||
if cfg.SpecificConfig != nil {
|
||||
if annotationConfig, ok := cfg.SpecificConfig.(*AnnotationConfig); ok && annotationConfig.TagHandler != nil {
|
||||
simpleConfig.VersionedCustomRoutes = map[string]simple.AppVersionRouteHandlers{
|
||||
"v0alpha1": {
|
||||
{
|
||||
Namespaced: true,
|
||||
Path: "tags",
|
||||
Method: "GET",
|
||||
}: annotationConfig.TagHandler,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a, err := simple.NewApp(simpleConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
// AnnotationConfig is the app-specific config for the annotation app. The
|
||||
// registry can pass a TagHandler implementation here to wire the /tags
|
||||
// resource route into the app without importing registry types.
|
||||
type AnnotationConfig struct {
|
||||
// TagHandler is the handler function for the GET /tags custom route.
|
||||
// The function signature matches app.CustomRouteHandler from the app-sdk.
|
||||
TagHandler func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error
|
||||
}
|
||||
@@ -340,7 +340,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -352,7 +357,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -377,7 +382,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -387,7 +392,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
|
||||
@@ -113,7 +113,7 @@ DashboardLink: {
|
||||
placement?: DashboardLinkPlacement
|
||||
}
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
DashboardLinkPlacement: "inControlsMenu"
|
||||
|
||||
@@ -342,7 +342,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -354,7 +359,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -379,7 +384,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -389,7 +394,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
|
||||
@@ -301,8 +301,8 @@ lineage: schemas: [{
|
||||
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
|
||||
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
#DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type")
|
||||
|
||||
// Annotation Query placement. Defines where the annotation query should be displayed.
|
||||
@@ -318,7 +318,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -330,7 +330,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -381,7 +381,12 @@ lineage: schemas: [{
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -393,7 +398,7 @@ lineage: schemas: [{
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
|
||||
|
||||
@@ -301,8 +301,8 @@ lineage: schemas: [{
|
||||
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
|
||||
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
#DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type")
|
||||
|
||||
// Annotation Query placement. Defines where the annotation query should be displayed.
|
||||
@@ -318,7 +318,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -330,7 +330,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -381,7 +381,12 @@ lineage: schemas: [{
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -393,7 +398,7 @@ lineage: schemas: [{
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
|
||||
|
||||
@@ -344,7 +344,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -356,7 +361,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -381,7 +386,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -391,7 +396,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
|
||||
@@ -583,7 +583,12 @@ func NewDashboardFieldColor() *DashboardFieldColor {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -602,6 +607,11 @@ const (
|
||||
DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds"
|
||||
DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic"
|
||||
DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name"
|
||||
DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis"
|
||||
DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma"
|
||||
DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma"
|
||||
DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno"
|
||||
DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis"
|
||||
DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd"
|
||||
DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr"
|
||||
DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd"
|
||||
|
||||
@@ -117,7 +117,7 @@ DashboardLink: {
|
||||
placement?: DashboardLinkPlacement
|
||||
}
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
DashboardLinkPlacement: "inControlsMenu"
|
||||
|
||||
@@ -346,7 +346,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -358,7 +363,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -383,7 +388,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -393,7 +398,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
|
||||
+11
-1
@@ -587,7 +587,12 @@ func NewDashboardFieldColor() *DashboardFieldColor {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -606,6 +611,11 @@ const (
|
||||
DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds"
|
||||
DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic"
|
||||
DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name"
|
||||
DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis"
|
||||
DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma"
|
||||
DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma"
|
||||
DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno"
|
||||
DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis"
|
||||
DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd"
|
||||
DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr"
|
||||
DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd"
|
||||
|
||||
@@ -28,6 +28,7 @@ type fileWatcher struct {
|
||||
timers map[string]*time.Timer
|
||||
watcher *fsnotify.Watcher
|
||||
logger logging.Logger
|
||||
closed bool
|
||||
}
|
||||
|
||||
// File watcher that buffers events for 100ms before actually firing them
|
||||
@@ -77,22 +78,21 @@ func NewFileWatcher(path string, accept func(string) bool) (FileWatcher, error)
|
||||
|
||||
// Keep watching for changes until the context is done
|
||||
func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) {
|
||||
defer f.cleanup(events)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(events)
|
||||
return
|
||||
|
||||
case _, ok := <-f.watcher.Errors:
|
||||
if !ok { // Channel was closed (i.e. Watcher.Close() was called).
|
||||
close(events)
|
||||
return
|
||||
}
|
||||
|
||||
// Read from Events.
|
||||
case e, ok := <-f.watcher.Events:
|
||||
if !ok { // Channel was closed (i.e. Watcher.Close() was called).
|
||||
close(events)
|
||||
return
|
||||
}
|
||||
name := filepath.Base(e.Name)
|
||||
@@ -114,6 +114,11 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) {
|
||||
if !ok {
|
||||
nameCopy := e.Name
|
||||
t = time.AfterFunc(math.MaxInt64, func() {
|
||||
// before sending the event, check if the watcher has been closed
|
||||
if f.closed {
|
||||
return
|
||||
}
|
||||
|
||||
path, _ := strings.CutPrefix(nameCopy, f.prefix)
|
||||
events <- path
|
||||
|
||||
@@ -128,3 +133,17 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stop all pending timers and close the event channel
|
||||
func (f *fileWatcher) cleanup(events chan<- string) {
|
||||
f.timersMu.Lock()
|
||||
defer f.timersMu.Unlock()
|
||||
|
||||
for _, timer := range f.timers {
|
||||
timer.Stop()
|
||||
}
|
||||
f.timers = make(map[string]*time.Timer)
|
||||
|
||||
close(events)
|
||||
f.closed = true
|
||||
}
|
||||
|
||||
@@ -226,8 +226,8 @@ Dashboards are reloaded when the JSON files change.
|
||||
|
||||
#### `min_tls_version`
|
||||
|
||||
The TLS Handshake requires a minimum TLS version. The available options are TLS1.2 and TLS1.3.
|
||||
If you do not specify a version, the system uses TLS1.2.
|
||||
The TLS handshake requires a minimum TLS version. Available options are `TLS1.2` and `TLS1.3`.
|
||||
If you don't specify a version, Grafana uses `TLS1.2`.
|
||||
|
||||
#### `http_addr`
|
||||
|
||||
@@ -286,9 +286,9 @@ Set to `true` for Grafana to log all HTTP requests (not just errors). These are
|
||||
|
||||
#### `static_root_path`
|
||||
|
||||
The path to the directory where the frontend files (HTML, JS, and CSS
|
||||
files). Defaults to `public` which is why the Grafana binary needs to be
|
||||
executed with working directory set to the installation path.
|
||||
The path to the directory containing the frontend files (HTML, JS, and CSS).
|
||||
Defaults to `public`, which is why you must run the Grafana binary with the
|
||||
working directory set to the installation path.
|
||||
|
||||
#### `enable_gzip`
|
||||
|
||||
@@ -312,8 +312,8 @@ Optional. Password to decrypt encrypted certificates.
|
||||
#### `certs_watch_interval`
|
||||
|
||||
Controls whether `cert_key` and `cert_file` are periodically watched for changes.
|
||||
Disabled, by default. When enabled, `cert_key` and `cert_file`
|
||||
are watched for changes. If there is change, the new certificates are loaded automatically.
|
||||
Disabled by default. When enabled, `cert_key` and `cert_file` are watched for
|
||||
changes. If there is a change, Grafana loads the new certificates automatically.
|
||||
|
||||
{{< admonition type="warning" >}}
|
||||
After the new certificates are loaded, connections with old certificates don't work.
|
||||
@@ -393,6 +393,10 @@ The database user's password (not applicable for `sqlite3`). If the password con
|
||||
Use either URL or the previous fields to configure the database
|
||||
Example: `type://user:password@host:port/name`
|
||||
|
||||
#### `high_availability`
|
||||
|
||||
Enable or disable high availability mode. When disabled, some functions run in-process instead of relying on the database. Default is `true`.
|
||||
|
||||
#### `max_idle_conn`
|
||||
|
||||
The maximum number of connections in the idle connection pool.
|
||||
@@ -475,6 +479,10 @@ This setting applies to `sqlite` only and controls the number of times the syste
|
||||
|
||||
Set to `true` to add metrics and tracing for database queries. The default value is `false`.
|
||||
|
||||
#### `delete_auto_gen_ids`
|
||||
|
||||
Delete auto-generated primary keys during migrations. Useful if the database has auto-generated primary keys enabled. Default is `false`.
|
||||
|
||||
#### `skip_dashboard_uid_migration_on_startup`
|
||||
|
||||
Set to true to skip dashboard UID migrations on startup. Improves startup performance for instances with large numbers of annotations who do not plan to downgrade Grafana. The default value is `false`.
|
||||
@@ -516,6 +524,14 @@ Example connection string: `addr=127.0.0.1:6379,pool_size=100,db=0,username=graf
|
||||
|
||||
Example connection string: `127.0.0.1:11211`
|
||||
|
||||
#### `prefix`
|
||||
|
||||
Prefix prepended to all keys in the remote cache.
|
||||
|
||||
#### `encryption`
|
||||
|
||||
Enable encryption of values stored in the remote cache.
|
||||
|
||||
<hr />
|
||||
|
||||
### `[dataproxy]`
|
||||
@@ -530,6 +546,10 @@ How long the data proxy should wait before timing out. Default is 30 seconds.
|
||||
|
||||
This setting also applies to core backend HTTP data sources where query requests use an HTTP client with timeout set.
|
||||
|
||||
#### `dialTimeout`
|
||||
|
||||
How long the data proxy waits to establish a TCP connection before timing out. Default is `10` seconds.
|
||||
|
||||
#### `keep_alive_seconds`
|
||||
|
||||
Interval between keep-alive probes. Default is `30` seconds. For more details, refer to the [`Dialer.KeepAlive`](https://golang.org/pkg/net/#Dialer.KeepAlive) documentation.
|
||||
@@ -818,6 +838,22 @@ Set to `true` to execute the CSRF check even if the login cookie is not in a req
|
||||
|
||||
Comma-separated list of plugins IDs to load inside the frontend sandbox.
|
||||
|
||||
### `[security.encryption]`
|
||||
|
||||
Configure encryption-related cache settings for data encryption keys used by Grafana.
|
||||
|
||||
#### `data_keys_cache_ttl`
|
||||
|
||||
Defines the time-to-live (TTL) for decrypted data encryption keys stored in memory. Default: `15m`.
|
||||
|
||||
#### `data_keys_cache_cleanup_interval`
|
||||
|
||||
Sets how often Grafana cleans up the encryption key cache, removing entries that reached the TTL. Default: `1m`.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Small TTL values can impact performance due to frequent decryption operations.
|
||||
{{< /admonition >}}
|
||||
|
||||
### `[snapshots]`
|
||||
|
||||
#### `enabled`
|
||||
@@ -1650,6 +1686,10 @@ Custom HTTP endpoint to send events captured by the Grafana Faro agent to. Defau
|
||||
|
||||
If `custom_endpoint` required authentication, you can set the API key here. Only relevant for Grafana JavaScript Agent provider.
|
||||
|
||||
#### `internal_logger_level`
|
||||
|
||||
Sets the internal logging level for the Grafana JavaScript agent. Allowed values are `0` (OFF), `1` (ERROR), `2` (WARN), `3` (INFO), and `4` (VERBOSE). Default is `0`.
|
||||
|
||||
#### `instrumentations_console_enabled`
|
||||
|
||||
Enables the [Console instrumentation](https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/instrument/console-instrumentation/) for Grafana Faro, defaults to `true`.
|
||||
@@ -1970,7 +2010,29 @@ This setting has precedence over each individual rule frequency.
|
||||
If a rule frequency is lower than this value, then this value is enforced.
|
||||
{{< /admonition >}}
|
||||
|
||||
<hr>
|
||||
#### `state_periodic_save_interval`
|
||||
|
||||
If the `alertingSaveStatePeriodic` feature flag is enabled, sets the interval used to persist alerting instances to the database. Specify a duration (for example, `5m`). Default is `5m`.
|
||||
|
||||
#### `state_periodic_save_batch_size`
|
||||
|
||||
If the `alertingSaveStatePeriodic` feature flag is enabled, sets the size of the batch that is saved to the database at once. Default is `1`.
|
||||
|
||||
#### `state_periodic_save_jitter_enabled`
|
||||
|
||||
Enables jitter for periodic state saving to distribute database load over time. When enabled, batches are spread across the save interval to prevent load spikes. Default is `false`.
|
||||
|
||||
#### `disable_jitter`
|
||||
|
||||
Disables smoothing of alert evaluations across their evaluation window. When set to `true`, rules evaluate in sync. Default is `false`.
|
||||
|
||||
#### `notification_log_retention`
|
||||
|
||||
Retention period for Alertmanager notification log entries. Specify a duration (for example, `5d`). Default is `5d`.
|
||||
|
||||
#### `resolved_alert_retention`
|
||||
|
||||
Duration for which a resolved alert state transition continues to be sent to the Alertmanager. Specify a duration (for example, `15m`). Default is `15m`.
|
||||
|
||||
#### `rule_version_record_limit`
|
||||
|
||||
@@ -1978,6 +2040,12 @@ Defines the limits for how many alert rule versions are stored in the database p
|
||||
|
||||
The default `0` value means there's no limit.
|
||||
|
||||
#### `deleted_rule_retention`
|
||||
|
||||
Retention period for deleted alerting rules before permanent removal. Specify a duration (for example, `30d`). Default is `30d`. Setting `0` deletes rules immediately.
|
||||
|
||||
<hr>
|
||||
|
||||
### `[unified_alerting.screenshots]`
|
||||
|
||||
For more information about screenshots, refer to [Images in notifications](../../alerting/configure-notifications/template-notifications/images-in-notifications/).
|
||||
@@ -2017,6 +2085,86 @@ For example: `disabled_labels=grafana_folder`
|
||||
|
||||
<hr>
|
||||
|
||||
### `[unified_alerting.state_history]`
|
||||
|
||||
Configure state history for Unified Alerting. Previous alert rule states can be queried in panels and viewed in the UI.
|
||||
|
||||
#### `enabled`
|
||||
|
||||
Enable or disable the state history functionality. Default: `true`.
|
||||
|
||||
#### `backend`
|
||||
|
||||
Select the backend for state history. Options: `annotations`, `loki`, `prometheus`, `multiple`. Default: `annotations`.
|
||||
|
||||
The backends provide different storage and query characteristics:
|
||||
|
||||
- **annotations:** Stores alert state transitions as Grafana annotations in the local database.
|
||||
- **loki:** Writes alert state history to an external Loki instance. Requires Loki connection configuration in this section.
|
||||
- **prometheus:** Emits alert state as `GRAFANA_ALERTS` metrics to a Prometheus-compatible data source. Requires Prometheus target configuration in this section.
|
||||
- **multiple:** Writes state history to more than one backend at the same time. Use `primary` to select which backend serves queries, and `secondaries` for additional write targets.
|
||||
|
||||
Backend-specific configuration requirements:
|
||||
|
||||
- When `backend = annotations`, no additional keys in this section are required.
|
||||
- When a Loki backend is used in any capacity (for example, `backend = loki`, or `backend = multiple` with Loki as `primary` or present in `secondaries`) you must set either `loki_remote_url` or both `loki_remote_read_url` and `loki_remote_write_url`.
|
||||
- When a Prometheus backend is used in any capacity (for example, `backend = prometheus`, or `backend = multiple` with Prometheus present in `secondaries`) you must set `prometheus_target_datasource_uid`.
|
||||
- When `backend = multiple`, set `primary` and `secondaries`.
|
||||
|
||||
#### `primary`
|
||||
|
||||
For `multiple` backend only. Sets the primary backend used to serve queries. Options: `annotations`, `loki`.
|
||||
|
||||
#### `secondaries`
|
||||
|
||||
For `multiple` backend only. Comma-separated list of additional backends to write state history to.
|
||||
|
||||
#### `loki_remote_url`
|
||||
|
||||
For `loki` backend. URL of the external Loki instance.
|
||||
|
||||
#### `loki_remote_read_url`
|
||||
|
||||
For `loki` backend. Read URL when Loki read/write endpoints are separated.
|
||||
|
||||
#### `loki_remote_write_url`
|
||||
|
||||
For `loki` backend. Write URL when Loki read/write endpoints are separated.
|
||||
|
||||
#### `loki_tenant_id`
|
||||
|
||||
For `loki` backend. Optional tenant ID to attach to requests.
|
||||
|
||||
#### `loki_basic_auth_username`
|
||||
|
||||
For `loki` backend. Optional username for basic authentication.
|
||||
|
||||
#### `loki_basic_auth_password`
|
||||
|
||||
For `loki` backend. Optional password for basic authentication.
|
||||
|
||||
#### `loki_max_query_length`
|
||||
|
||||
For `loki` backend. Maximum query length duration. Default: `721h`.
|
||||
|
||||
#### `loki_max_query_size`
|
||||
|
||||
For `loki` backend. Maximum query size in bytes. Default: `65536`.
|
||||
|
||||
#### `prometheus_target_datasource_uid`
|
||||
|
||||
For `prometheus` backend. Target datasource UID for writing `GRAFANA_ALERTS` metrics.
|
||||
|
||||
#### `prometheus_metric_name`
|
||||
|
||||
For `prometheus` backend. Metric name for `GRAFANA_ALERTS`. Default: `GRAFANA_ALERTS`.
|
||||
|
||||
#### `prometheus_write_timeout`
|
||||
|
||||
For `prometheus` backend. Timeout for writing `GRAFANA_ALERTS` metrics. Default: `10s`.
|
||||
|
||||
<hr>
|
||||
|
||||
### `[unified_alerting.state_history.annotations]`
|
||||
|
||||
This section controls retention of annotations automatically created while evaluating alert rules when alerting state history backend is configured to be annotations (see setting [unified_alerting.state_history].backend)
|
||||
@@ -2031,6 +2179,36 @@ Configures max number of alert annotations that Grafana stores. Default value is
|
||||
|
||||
<hr>
|
||||
|
||||
### `[unified_alerting.notification_history]`
|
||||
|
||||
Enable storage of Alertmanager notification logs in Loki.
|
||||
|
||||
#### `enabled`
|
||||
|
||||
Enable or disable the notification history functionality. Default: `false`.
|
||||
|
||||
#### `loki_remote_url`
|
||||
|
||||
URL of the Loki instance used to store logs.
|
||||
|
||||
#### `loki_tenant_id`
|
||||
|
||||
Optional tenant ID to attach to requests sent to Loki.
|
||||
|
||||
#### `loki_basic_auth_username`
|
||||
|
||||
Optional username for basic authentication to Loki.
|
||||
|
||||
#### `loki_basic_auth_password`
|
||||
|
||||
Optional password for basic authentication to Loki.
|
||||
|
||||
### `[unified_alerting.notification_history.external_labels]`
|
||||
|
||||
Optional extra labels to attach to outbound notification history records or log streams. Provide any number of label key-value pairs.
|
||||
|
||||
<hr>
|
||||
|
||||
### `[unified_alerting.prometheus_conversion]`
|
||||
|
||||
This section applies only to rules imported as Grafana-managed rules. For more information about the import process, refer to [Import data source-managed rules to Grafana-managed rules](/docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/alerting-migration/).
|
||||
@@ -2041,6 +2219,52 @@ Set the query offset to imported Grafana-managed rules when `query_offset` is no
|
||||
|
||||
<hr>
|
||||
|
||||
### `[recording_rules]`
|
||||
|
||||
Configure recording rules.
|
||||
|
||||
#### `enabled`
|
||||
|
||||
Enable recording rules. Default: `true`.
|
||||
|
||||
#### `timeout`
|
||||
|
||||
Request timeout for recording rule writes. Default: `10s`.
|
||||
|
||||
#### `default_datasource_uid`
|
||||
|
||||
Default data source UID to write to if not specified in the rule definition.
|
||||
|
||||
### `[recording_rules.custom_headers]`
|
||||
|
||||
Optional custom headers to include in recording rule write requests.
|
||||
|
||||
### `[remote.alertmanager]`
|
||||
|
||||
Configure a remote Alertmanager to replace the internal one.
|
||||
|
||||
#### `url`
|
||||
|
||||
Root URL of the remote Alertmanager. Grafana automatically appends `/alertmanager` for certain HTTP calls.
|
||||
|
||||
#### `tenant`
|
||||
|
||||
Tenant ID used in requests. Also used as basic auth username if a password is configured.
|
||||
|
||||
#### `password`
|
||||
|
||||
Optional password for basic authentication. If not present, the tenant ID will be set in the X-Scope-OrgID header.
|
||||
|
||||
#### `sync_interval`
|
||||
|
||||
Interval for syncing with the Alertmanager. Default: `5m`.
|
||||
|
||||
#### `timeout`
|
||||
|
||||
Timeout for the HTTP client. Default: `30s`.
|
||||
|
||||
<hr>
|
||||
|
||||
### `[annotations]`
|
||||
|
||||
#### `cleanupjob_batchsize`
|
||||
@@ -2951,6 +3175,8 @@ Move an app plugin (referenced by its id), including all its pages, to a specifi
|
||||
Move an individual app plugin page (referenced by its `path` field) to a specific navigation section.
|
||||
Format: `<pageUrl> = <sectionId> <sortWeight>`
|
||||
|
||||
<hr>
|
||||
|
||||
### `[public_dashboards]`
|
||||
|
||||
This section configures the [shared dashboards](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/dashboards/share-dashboards-panels/shared-dashboards/) feature.
|
||||
@@ -2958,3 +3184,81 @@ This section configures the [shared dashboards](https://grafana.com/docs/grafana
|
||||
#### `enabled`
|
||||
|
||||
Set this to `false` to disable the shared dashboards feature. This prevents users from creating new shared dashboards and disables existing ones.
|
||||
|
||||
<hr>
|
||||
|
||||
### `[cloud_migration]`
|
||||
|
||||
Configure the Grafana Cloud Migration Assistant.
|
||||
|
||||
#### `enabled`
|
||||
|
||||
Enable or disable the Cloud Migration Assistant. Default is `true`.
|
||||
|
||||
#### `is_target`
|
||||
|
||||
Enable the target-side migration UI. Default is `false`.
|
||||
|
||||
#### `gcom_api_token`
|
||||
|
||||
Token used to send requests to Grafana com. Default is empty.
|
||||
|
||||
#### `start_snapshot_timeout`
|
||||
|
||||
Timeout for requests to start a snapshot. Default is `5s`.
|
||||
|
||||
#### `validate_key_timeout`
|
||||
|
||||
Timeout for requests to validate a key. Default is `5s`.
|
||||
|
||||
#### `get_snapshot_status_timeout`
|
||||
|
||||
Timeout for requests to get snapshot status. Default is `5s`.
|
||||
|
||||
#### `create_upload_url_timeout`
|
||||
|
||||
Timeout for requests to create a presigned upload URL. Default is `5s`.
|
||||
|
||||
#### `report_event_timeout`
|
||||
|
||||
Timeout for requests to report an event. Default is `5s`.
|
||||
|
||||
#### `fetch_instance_timeout`
|
||||
|
||||
Timeout for requests to fetch an instance. Default is `5s`.
|
||||
|
||||
#### `create_access_policy_timeout`
|
||||
|
||||
Timeout for requests to create an access policy. Default is `5s`.
|
||||
|
||||
#### `fetch_access_policy_timeout`
|
||||
|
||||
Timeout for requests to fetch an access policy. Default is `5s`.
|
||||
|
||||
#### `delete_access_policy_timeout`
|
||||
|
||||
Timeout for requests to delete an access policy. Default is `5s`.
|
||||
|
||||
#### `domain`
|
||||
|
||||
Domain name used to access the cloud migration service. Default is `grafana.net`.
|
||||
|
||||
#### `snapshot_folder`
|
||||
|
||||
Folder used to store snapshot files. Default is empty (home dir).
|
||||
|
||||
#### `frontend_poll_interval`
|
||||
|
||||
Polling interval for the frontend UI while resources are migrating. Default is `2s`.
|
||||
|
||||
#### `alert_rules_state`
|
||||
|
||||
Controls how alert rules are migrated. Options are `paused` or `unchanged`. Default is `"paused"`.
|
||||
|
||||
{{< adomition type="note" >}}
|
||||
For more information, refer to the [Prevent duplicated alert notifications](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/migration-guide/cloud-migration-assistant/#prevent-duplicated-alert-notifications) documentation.
|
||||
{{< /admonition >}}
|
||||
|
||||
#### `resource_storage_type`
|
||||
|
||||
Resource snapshot storage type. Options are `db` (database) or `fs` (file system). Default is `"db"`.
|
||||
|
||||
+40
-17
@@ -22,7 +22,7 @@ weight: 100
|
||||
|
||||
# Node graph
|
||||
|
||||
Node graphs are useful when you need to visualize elements that are related to each other. This is done by displaying circles—or _nodes_—for each element you want to visualize, connected by lines—or _edges_. The visualization uses a directed force layout that positions the nodes into a network of connected circles.
|
||||
Node graphs are useful when you need to visualize elements that are related to each other. This is done by displaying circles—or _nodes_—for each element you want to visualize, connected by lines—or _edges_. By default, the visualization uses a [layered layout](#layout-algorithm) that positions the nodes into a network of connected circles.
|
||||
|
||||
Node graphs display useful information about each node, as well as the relationships between them, allowing you to visualize complex infrastructure maps, hierarchies, or execution diagrams.
|
||||
|
||||
@@ -123,26 +123,32 @@ You can pan the view by clicking outside any node or edge and dragging your mous
|
||||
|
||||
Use the buttons in the lower right corner to zoom in or out. You can also use the mouse wheel or touchpad scroll, together with either Ctrl or Cmd key to do so.
|
||||
|
||||
### Switch layouts
|
||||
|
||||
Switch quickly between displaying the visualization in graph or grid [layout](#layout-algorithm).
|
||||
|
||||
Click a node and select either **Show in Grid layout** or **Show in Graph layout**, depending on the current layout of the visualization:
|
||||
|
||||
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-node-graph-grid-menu.png" max-width="750px" alt="Node graph in grid layout with node menu open" >}}
|
||||
|
||||
In grid layout, you can sort nodes by clicking on the stats inside the legend.
|
||||
The marker next to the stat name shows which stat is currently used for sorting and the sorting direction:
|
||||
|
||||
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-node-graph-legend-sort.png" max-width="550px" alt="Node graph legend sorting" >}}
|
||||
|
||||
Switching between grid and other layouts this way only changes the layout temporarily.
|
||||
The visualization maintains the layout algorithm selected in the panel editor, and reverts to it when the dashboard refreshes.
|
||||
|
||||
For more information about layouts, refer to [Layout algorithm](#layout-algorithm).
|
||||
|
||||
<!-- if you have the panel in grid layout and switch it to graph, is it switching to layered? -->
|
||||
|
||||
### Hidden nodes
|
||||
|
||||
The number of nodes shown at a given time is limited to maintain a reasonable visualization performance. Nodes that are not currently visible are hidden behind clickable markers that show an approximate number of hidden nodes that are connected by a particular edge. You can click on the marker to expand the graph around that node.
|
||||
|
||||

|
||||
|
||||
### Grid view
|
||||
|
||||
You can switch to the grid view to have a better overview of the most interesting nodes in the graph. Grid view shows nodes in a grid without edges and can be sorted by stats shown inside the node or by stats represented by the a colored border of the nodes.
|
||||
|
||||

|
||||
|
||||
To sort the nodes, click on the stats inside the legend. The marker next to the stat name shows which stat is currently used for sorting and sorting direction.
|
||||
|
||||

|
||||
|
||||
Click on the node and select "Show in Graph layout" option to switch back to graph layout and focus on the selected node, to show it in context of the full graph.
|
||||
|
||||

|
||||
|
||||
## Configuration options
|
||||
|
||||
{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
@@ -155,7 +161,24 @@ Click on the node and select "Show in Graph layout" option to switch back to gra
|
||||
|
||||
Use the following options to refine your node graph visualization.
|
||||
|
||||
- **Zoom mode** - Choose how the node graph should handle zoom and scroll events.
|
||||
#### Zoom mode
|
||||
|
||||
Choose how the node graph should handle zoom and scroll events:
|
||||
|
||||
- **Cooperative** - Allows you to scroll the visualization normally.
|
||||
- **Greedy** - Reacts to all zoom gestures.
|
||||
|
||||
#### Layout algorithm
|
||||
|
||||
Choose how the visualization layout is generated:
|
||||
|
||||
- **Layered** - Default. Creates a predictable and orderly layout, especially useful for service graphs.
|
||||
- **Force** - Uses a physics-based force layout algorithm that's useful with a large number of nodes (500+).
|
||||
- **Grid** - Arranges nodes into a grid format to provide a better overview of the most interesting nodes in the graph. This layout shows nodes in a grid without edges and can be sorted by the stats shown inside the node or by the ones represented by the a colored border of the nodes.
|
||||
|
||||
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-node-graph-grid.png" max-width="650px" alt="Node graph in grid layout" >}}
|
||||
|
||||
For more information about using the graph in grid layout, refer to [Switch layouts](#switch-layouts).
|
||||
|
||||
### Nodes options
|
||||
|
||||
@@ -239,6 +262,6 @@ Optional fields:
|
||||
| arc\_\_\* | number | Any field prefixed with `arc__` will be used to create the color circle around the node. All values in these fields should add up to 1. You can specify color using `config.color.fixedColor`. |
|
||||
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. |
|
||||
| color | string/number | Can be used to specify a single color instead of using the `arc__` fields to specify color sections. It can be either a string which should then be an acceptable HTML color string or it can be a number in which case the behavior depends on `field.config.color.mode` setting. This can be for example used to create gradient colors controlled by the field value. |
|
||||
| icon | string | Name of the icon to show inside the node instead of the default stats. Only Grafana [built in icons](https://developers.grafana.com/ui/latest/index.html?path=/story/iconography-icon--icons-overview)) are allowed. |
|
||||
| icon | string | Name of the icon to show inside the node instead of the default stats. Only Grafana [built in icons](https://developers.grafana.com/ui/latest/index.html?path=/story/iconography-icon--icons-overview) are allowed. |
|
||||
| nodeRadius | number | Radius value in pixels. Used to manage node size. |
|
||||
| highlighted | boolean | Sets whether the node should be highlighted. Useful for example to represent a specific path in the graph by highlighting several nodes and edges. Default: `false` |
|
||||
|
||||
+11
@@ -870,6 +870,17 @@ github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVn
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||
github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM=
|
||||
github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0UrQVTqtYBU=
|
||||
github.com/grafana/grafana-app-sdk v0.41.0/go.mod h1:Wg/3vEZfok1hhIWiHaaJm+FwkosfO98o8KbeLFEnZpY=
|
||||
github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc=
|
||||
github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.43.0/go.mod h1:0xrjKSGY5z+NLGuGsXQpxiCHR4Smu79i/CbAfdkaB1M=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.43.1/go.mod h1:0xrjKSGY5z+NLGuGsXQpxiCHR4Smu79i/CbAfdkaB1M=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.43.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
|
||||
@@ -297,8 +297,8 @@ lineage: schemas: [{
|
||||
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
|
||||
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
#DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type")
|
||||
|
||||
// Annotation Query placement. Defines where the annotation query should be displayed.
|
||||
@@ -314,7 +314,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -326,7 +326,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -377,7 +377,12 @@ lineage: schemas: [{
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -389,7 +394,7 @@ lineage: schemas: [{
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"@types/string-hash": "1.1.3",
|
||||
"@types/systemjs": "6.15.3",
|
||||
"d3-interpolate": "3.0.1",
|
||||
"d3-scale-chromatic": "3.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"dompurify": "3.3.0",
|
||||
"eventemitter3": "5.0.1",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { interpolateRgbBasis } from 'd3-interpolate';
|
||||
import {
|
||||
interpolateViridis,
|
||||
interpolateMagma,
|
||||
interpolatePlasma,
|
||||
interpolateInferno,
|
||||
interpolateCividis,
|
||||
} from 'd3-scale-chromatic';
|
||||
import stringHash from 'string-hash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
@@ -75,6 +82,41 @@ export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
|
||||
);
|
||||
},
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousViridis,
|
||||
name: 'Viridis',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateViridis,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousMagma,
|
||||
name: 'Magma',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateMagma,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousPlasma,
|
||||
name: 'Plasma',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolatePlasma,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousInferno,
|
||||
name: 'Inferno',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateInferno,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousCividis,
|
||||
name: 'Cividis',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateCividis,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousGrYlRd,
|
||||
name: 'Green-Yellow-Red',
|
||||
@@ -148,16 +190,27 @@ export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
interface FieldColorSchemeModeOptions {
|
||||
interface BaseFieldColorSchemeModeOptions {
|
||||
id: FieldColorModeId;
|
||||
name: string;
|
||||
description?: string;
|
||||
getColors: (theme: GrafanaTheme2) => string[];
|
||||
isContinuous: boolean;
|
||||
isByValue: boolean;
|
||||
useSeriesName?: boolean;
|
||||
}
|
||||
|
||||
interface FieldColorSchemeModeInterpolator extends BaseFieldColorSchemeModeOptions {
|
||||
interpolator: (value: number) => string;
|
||||
getColors?: never;
|
||||
}
|
||||
|
||||
interface FieldColorSchemeModeGetColors extends BaseFieldColorSchemeModeOptions {
|
||||
getColors: (theme: GrafanaTheme2) => string[];
|
||||
interpolator?: never;
|
||||
}
|
||||
|
||||
type FieldColorSchemeModeOptions = FieldColorSchemeModeGetColors | FieldColorSchemeModeInterpolator;
|
||||
|
||||
export class FieldColorSchemeMode implements FieldColorMode {
|
||||
id: FieldColorModeId;
|
||||
name: string;
|
||||
@@ -178,11 +231,15 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
||||
this.isContinuous = options.isContinuous;
|
||||
this.isByValue = options.isByValue;
|
||||
this.useSeriesName = options.useSeriesName;
|
||||
this.interpolator = options.interpolator;
|
||||
}
|
||||
|
||||
getColors(theme: GrafanaTheme2): string[] {
|
||||
if (!this.getNamedColors) {
|
||||
return [];
|
||||
if (!this.interpolator) {
|
||||
return [];
|
||||
}
|
||||
this.getNamedColors = () => new Array(9).fill(0).map((_, i) => this.getInterpolator()(i / 8));
|
||||
}
|
||||
|
||||
if (this.colorCache && this.colorCacheTheme === theme) {
|
||||
@@ -231,12 +288,15 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
||||
|
||||
/** @beta */
|
||||
export function getFieldColorModeForField(field: Field): FieldColorMode {
|
||||
return fieldColorModeRegistry.get(field.config.color?.mode ?? FieldColorModeId.Thresholds);
|
||||
return (
|
||||
fieldColorModeRegistry.getIfExists(field.config.color?.mode) ??
|
||||
fieldColorModeRegistry.get(FieldColorModeId.Thresholds)
|
||||
);
|
||||
}
|
||||
|
||||
/** @beta */
|
||||
export function getFieldColorMode(mode?: FieldColorModeId | string): FieldColorMode {
|
||||
return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds);
|
||||
return fieldColorModeRegistry.getIfExists(mode) ?? fieldColorModeRegistry.get(FieldColorModeId.Thresholds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,11 @@ export enum FieldColorModeId {
|
||||
ContinuousReds = 'continuous-reds',
|
||||
ContinuousGreens = 'continuous-greens',
|
||||
ContinuousPurples = 'continuous-purples',
|
||||
ContinuousViridis = 'continuous-viridis',
|
||||
ContinuousMagma = 'continuous-magma',
|
||||
ContinuousPlasma = 'continuous-plasma',
|
||||
ContinuousInferno = 'continuous-inferno',
|
||||
ContinuousCividis = 'continuous-cividis',
|
||||
Fixed = 'fixed',
|
||||
Shades = 'shades',
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const grafanaESModules = [
|
||||
'd3',
|
||||
'd3-color',
|
||||
'd3-interpolate',
|
||||
'd3-scale-chromatic',
|
||||
'delaunator',
|
||||
'get-user-locale',
|
||||
'internmap',
|
||||
|
||||
@@ -475,7 +475,12 @@ export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'dataso
|
||||
* `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
* `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
* `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
* `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
* `continuous-viridis`: Continuous Viridis palette mode
|
||||
* `continuous-magma`: Continuous Magma palette mode
|
||||
* `continuous-plasma`: Continuous Plasma palette mode
|
||||
* `continuous-inferno`: Continuous Inferno palette mode
|
||||
* `continuous-cividis`: Continuous Cividis palette mode
|
||||
* `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
* `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
* `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
* `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -492,11 +497,16 @@ export enum FieldColorModeId {
|
||||
ContinuousBlPu = 'continuous-BlPu',
|
||||
ContinuousBlYlRd = 'continuous-BlYlRd',
|
||||
ContinuousBlues = 'continuous-blues',
|
||||
ContinuousCividis = 'continuous-cividis',
|
||||
ContinuousGrYlRd = 'continuous-GrYlRd',
|
||||
ContinuousGreens = 'continuous-greens',
|
||||
ContinuousInferno = 'continuous-inferno',
|
||||
ContinuousMagma = 'continuous-magma',
|
||||
ContinuousPlasma = 'continuous-plasma',
|
||||
ContinuousPurples = 'continuous-purples',
|
||||
ContinuousRdYlGr = 'continuous-RdYlGr',
|
||||
ContinuousReds = 'continuous-reds',
|
||||
ContinuousViridis = 'continuous-viridis',
|
||||
ContinuousYlBl = 'continuous-YlBl',
|
||||
ContinuousYlRd = 'continuous-YlRd',
|
||||
Fixed = 'fixed',
|
||||
|
||||
@@ -334,7 +334,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -346,7 +351,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
|
||||
@@ -327,7 +327,7 @@ export interface FieldConfig {
|
||||
description?: string;
|
||||
// An explicit path to the field in the datasource. When the frame meta includes a path,
|
||||
// This will default to `${frame.meta.path}/${field.name}
|
||||
//
|
||||
//
|
||||
// When defined, this value can be used as an identifier within the datasource scope, and
|
||||
// may be used to update the results
|
||||
path?: string;
|
||||
@@ -529,7 +529,12 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -541,7 +546,7 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
|
||||
export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds");
|
||||
|
||||
|
||||
@@ -489,7 +489,12 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -501,7 +506,7 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
|
||||
export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds");
|
||||
|
||||
|
||||
@@ -496,7 +496,12 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -508,7 +513,7 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
|
||||
export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds");
|
||||
|
||||
|
||||
@@ -23,13 +23,32 @@ describe('Sidebar', () => {
|
||||
// Verify pane is closed
|
||||
expect(screen.queryByTestId('sidebar-pane-header-title')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Can persist docked state', async () => {
|
||||
const { unmount } = render(<TestSetup persistanceKey="test" />);
|
||||
|
||||
act(() => screen.getByLabelText('Settings').click());
|
||||
act(() => screen.getByLabelText('Dock').click());
|
||||
|
||||
unmount();
|
||||
|
||||
render(<TestSetup persistanceKey="test" />);
|
||||
|
||||
act(() => screen.getByLabelText('Settings').click());
|
||||
expect(screen.getByLabelText('Undock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function TestSetup() {
|
||||
interface TestSetupProps {
|
||||
persistanceKey?: string;
|
||||
}
|
||||
|
||||
function TestSetup({ persistanceKey }: TestSetupProps) {
|
||||
const [openPane, setOpenPane] = React.useState('');
|
||||
const contextValue = useSidebar({
|
||||
position: 'right',
|
||||
hasOpenPane: openPane !== '',
|
||||
persistanceKey,
|
||||
onClosePane: () => setOpenPane(''),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { clamp } from 'lodash';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { store } from '@grafana/data';
|
||||
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
|
||||
export type SidebarPosition = 'left' | 'right';
|
||||
@@ -30,7 +32,10 @@ export interface UseSideBarOptions {
|
||||
hasOpenPane?: boolean;
|
||||
position?: SidebarPosition;
|
||||
tabsMode?: boolean;
|
||||
compactDefault?: boolean;
|
||||
/** Initial state for compact mode */
|
||||
defaultToCompact?: boolean;
|
||||
/** Initial state for docked mode */
|
||||
defaultToDocked?: boolean;
|
||||
/** defaults to 2 grid units (16px) */
|
||||
bottomMargin?: number;
|
||||
/** defaults to 2 grid units (16px) */
|
||||
@@ -39,6 +44,11 @@ export interface UseSideBarOptions {
|
||||
contentMargin?: number;
|
||||
/** Called when pane is closed or clicked outside of (in undocked mode) */
|
||||
onClosePane?: () => void;
|
||||
/**
|
||||
* Optional key to use for persisting sidebar state (docked / compact / size)
|
||||
* Can only be app name as the final local storag key will be `grafana.ui.sidebar.{persistanceKey}.{docked|compact|size}`
|
||||
*/
|
||||
persistanceKey?: string;
|
||||
}
|
||||
|
||||
export const SIDE_BAR_WIDTH_ICON_ONLY = 5;
|
||||
@@ -48,21 +58,30 @@ export function useSidebar({
|
||||
hasOpenPane,
|
||||
position = 'right',
|
||||
tabsMode,
|
||||
compactDefault = true,
|
||||
defaultToCompact = true,
|
||||
defaultToDocked = false,
|
||||
bottomMargin = 2,
|
||||
edgeMargin = 2,
|
||||
contentMargin = 2,
|
||||
persistanceKey,
|
||||
onClosePane,
|
||||
}: UseSideBarOptions): SidebarContextValue {
|
||||
const theme = useTheme2();
|
||||
const [isDocked, setIsDocked] = React.useState(false);
|
||||
const [paneWidth, setPaneWidth] = React.useState(280);
|
||||
const [compact, setCompact] = React.useState(compactDefault);
|
||||
|
||||
const [isDocked, setIsDocked] = useSidebarSavedState(persistanceKey, 'docked', defaultToDocked);
|
||||
const [compact, setCompact] = useSidebarSavedState(persistanceKey, 'compact', defaultToCompact);
|
||||
const [paneWidth, setPaneWidth] = useSidebarSavedState(persistanceKey, 'size', 280);
|
||||
|
||||
// Used to accumulate drag distance to know when to change compact mode
|
||||
const [_, setCompactDrag] = React.useState(0);
|
||||
|
||||
const onToggleDock = useCallback(() => setIsDocked((prev) => !prev), []);
|
||||
const onToggleDock = useCallback(() => {
|
||||
setIsDocked((prev) => {
|
||||
return !prev;
|
||||
});
|
||||
}, [setIsDocked]);
|
||||
|
||||
// Calculate how much space the outer wrapper needs to reserve for the sidebar toolbar + pane (if docked)
|
||||
const prop = position === 'right' ? 'paddingRight' : 'paddingLeft';
|
||||
const toolbarWidth =
|
||||
((compact ? SIDE_BAR_WIDTH_ICON_ONLY : SIDE_BAR_WIDTH_WITH_TEXT) + edgeMargin + contentMargin) *
|
||||
@@ -82,10 +101,10 @@ export function useSidebar({
|
||||
setCompactDrag((prevDrag) => {
|
||||
const newDrag = prevDrag + diff;
|
||||
if (newDrag < -20 && !compact) {
|
||||
setCompact(true);
|
||||
setCompact(() => true);
|
||||
return 0;
|
||||
} else if (newDrag > 20 && compact) {
|
||||
setCompact(false);
|
||||
setCompact(() => false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -98,7 +117,7 @@ export function useSidebar({
|
||||
return clamp(prevWidth + diff, 100, 500);
|
||||
});
|
||||
},
|
||||
[hasOpenPane, compact]
|
||||
[hasOpenPane, setCompact, setPaneWidth, compact]
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -117,3 +136,53 @@ export function useSidebar({
|
||||
onClosePane,
|
||||
};
|
||||
}
|
||||
|
||||
function useSidebarSavedState<T = number | boolean>(
|
||||
persistanceKey: string | undefined,
|
||||
subKey: string,
|
||||
defaultValue: T
|
||||
) {
|
||||
const [state, setState] = React.useState<T>(() => {
|
||||
if (!persistanceKey) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return store.getBool(`grafana.ui.sidebar.${persistanceKey}.${subKey}`, defaultValue) as T;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'number') {
|
||||
const value = Number.parseInt(store.get(`grafana.ui.sidebar.${persistanceKey}.${subKey}`), 10);
|
||||
if (Number.isNaN(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return value as T;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const setPersisted = useCallback(
|
||||
(cb: (prevState: T) => T) => {
|
||||
setState((prevState) => {
|
||||
const newState = cb(prevState);
|
||||
|
||||
if (!persistanceKey) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
if (persistanceKey) {
|
||||
store.set(`grafana.ui.sidebar.${persistanceKey}.${subKey}`, String(newState));
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
[persistanceKey, subKey]
|
||||
);
|
||||
|
||||
return [state, setPersisted] as const;
|
||||
}
|
||||
|
||||
+2
-4
@@ -30,10 +30,8 @@ func (hs *HTTPServer) registerSwaggerUI(r routing.RouteRegister) {
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"Nonce": c.RequestNonce,
|
||||
"Assets": assets,
|
||||
"FavIcon": "public/img/fav32.png",
|
||||
"AppleTouchIcon": "public/img/apple-touch-icon.png",
|
||||
"Nonce": c.RequestNonce,
|
||||
"Assets": assets,
|
||||
}
|
||||
if hs.Cfg.CSPEnabled {
|
||||
data["CSPEnabled"] = true
|
||||
|
||||
+11
-1
@@ -619,7 +619,12 @@ func NewFieldColor() *FieldColor {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -637,6 +642,11 @@ const (
|
||||
FieldColorModeIdThresholds FieldColorModeId = "thresholds"
|
||||
FieldColorModeIdPaletteClassic FieldColorModeId = "palette-classic"
|
||||
FieldColorModeIdPaletteClassicByName FieldColorModeId = "palette-classic-by-name"
|
||||
FieldColorModeIdContinuousViridis FieldColorModeId = "continuous-viridis"
|
||||
FieldColorModeIdContinuousMagma FieldColorModeId = "continuous-magma"
|
||||
FieldColorModeIdContinuousPlasma FieldColorModeId = "continuous-plasma"
|
||||
FieldColorModeIdContinuousInferno FieldColorModeId = "continuous-inferno"
|
||||
FieldColorModeIdContinuousCividis FieldColorModeId = "continuous-cividis"
|
||||
FieldColorModeIdContinuousGrYlRd FieldColorModeId = "continuous-GrYlRd"
|
||||
FieldColorModeIdContinuousRdYlGr FieldColorModeId = "continuous-RdYlGr"
|
||||
FieldColorModeIdContinuousBlYlRd FieldColorModeId = "continuous-BlYlRd"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
)
|
||||
|
||||
type memoryStore struct {
|
||||
mu sync.RWMutex
|
||||
data map[string]*annotationV0.Annotation
|
||||
}
|
||||
|
||||
func NewMemoryStore() Store {
|
||||
return &memoryStore{
|
||||
data: make(map[string]*annotationV0.Annotation),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memoryStore) Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
key := namespace + "/" + name
|
||||
anno, ok := m.data[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
return anno.DeepCopy(), nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
//nolint:prealloc
|
||||
var result []annotationV0.Annotation // no, we can't pre-alloc it, we don't know the size yet
|
||||
|
||||
for _, anno := range m.data {
|
||||
if anno.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.DashboardUID != "" && (anno.Spec.DashboardUID == nil || *anno.Spec.DashboardUID != opts.DashboardUID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.PanelID != 0 && (anno.Spec.PanelID == nil || *anno.Spec.PanelID != opts.PanelID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.From > 0 && anno.Spec.Time < opts.From {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.To > 0 && anno.Spec.Time > opts.To {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, *anno.DeepCopy())
|
||||
|
||||
if opts.Limit > 0 && int64(len(result)) >= opts.Limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &AnnotationList{Items: result}, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Create(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if anno.Name == "" {
|
||||
anno.Name = uuid.New().String()
|
||||
}
|
||||
|
||||
key := anno.Namespace + "/" + anno.Name
|
||||
|
||||
if _, exists := m.data[key]; exists {
|
||||
return nil, fmt.Errorf("annotation already exists")
|
||||
}
|
||||
|
||||
created := anno.DeepCopy()
|
||||
if created.CreationTimestamp.IsZero() {
|
||||
created.CreationTimestamp = metav1.Now()
|
||||
}
|
||||
|
||||
m.data[key] = created
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Update(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := anno.Namespace + "/" + anno.Name
|
||||
|
||||
if _, exists := m.data[key]; !exists {
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
updated := anno.DeepCopy()
|
||||
m.data[key] = updated
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Delete(ctx context.Context, namespace, name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := namespace + "/" + name
|
||||
|
||||
if _, exists := m.data[key]; !exists {
|
||||
return fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
delete(m.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
tagCounts := make(map[string]int64)
|
||||
|
||||
for _, anno := range m.data {
|
||||
if anno.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
for _, tag := range anno.Spec.Tags {
|
||||
if opts.Prefix == "" || strings.HasPrefix(tag, opts.Prefix) {
|
||||
tagCounts[tag]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags := make([]Tag, 0, len(tagCounts))
|
||||
for name, count := range tagCounts {
|
||||
tags = append(tags, Tag{Name: name, Count: count})
|
||||
}
|
||||
|
||||
if opts.Limit > 0 && len(tags) > opts.Limit {
|
||||
tags = tags[:opts.Limit]
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
@@ -21,12 +22,11 @@ import (
|
||||
"github.com/grafana/grafana/apps/annotation/pkg/apis"
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
annotationapp "github.com/grafana/grafana/apps/annotation/pkg/app"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
grafrequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -46,15 +46,32 @@ func RegisterAppInstaller(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
service annotations.Repository,
|
||||
cleaner annotations.Cleaner,
|
||||
) (*AnnotationAppInstaller, error) {
|
||||
installer := &AnnotationAppInstaller{
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
var tagHandler func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error
|
||||
if service != nil {
|
||||
mapper := grafrequest.GetNamespaceMapper(cfg)
|
||||
sqlAdapter := NewSQLAdapter(service, cleaner, mapper, cfg)
|
||||
installer.legacy = &legacyStorage{
|
||||
store: sqlAdapter,
|
||||
mapper: mapper,
|
||||
}
|
||||
// Create the tags handler using the sqlAdapter as TagProvider
|
||||
tagHandler = newTagsHandler(sqlAdapter)
|
||||
}
|
||||
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), nil, annotationapp.New)
|
||||
|
||||
appConfig := app.Config{
|
||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||
KubeConfig: restclient.Config{},
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
SpecificConfig: &annotationapp.AnnotationConfig{
|
||||
TagHandler: tagHandler,
|
||||
},
|
||||
}
|
||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator())
|
||||
if err != nil {
|
||||
@@ -62,13 +79,6 @@ func RegisterAppInstaller(
|
||||
}
|
||||
installer.AppInstaller = i
|
||||
|
||||
if service != nil {
|
||||
installer.legacy = &legacyStorage{
|
||||
service: service,
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
return installer, nil
|
||||
}
|
||||
|
||||
@@ -79,9 +89,11 @@ func (a *AnnotationAppInstaller) GetLegacyStorage(requested schema.GroupVersionR
|
||||
Version: kind.Version(),
|
||||
Resource: kind.Plural(),
|
||||
}
|
||||
|
||||
if requested.String() != gvr.String() {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.legacy.tableConverter = utils.NewTableConverter(
|
||||
gvr.GroupResource(),
|
||||
utils.TableColumns{
|
||||
@@ -114,8 +126,8 @@ var (
|
||||
)
|
||||
|
||||
type legacyStorage struct {
|
||||
service annotations.Repository
|
||||
namespacer request.NamespaceMapper
|
||||
store Store
|
||||
mapper grafrequest.NamespaceMapper
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
@@ -142,21 +154,15 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
orgID, err := request.OrgIDForList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := &annotations.ItemQuery{OrgID: orgID, SignedInUser: user, AlertID: -1}
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
|
||||
opts := ListOptions{}
|
||||
if options.FieldSelector != nil {
|
||||
for _, r := range options.FieldSelector.Requirements() {
|
||||
switch r.Field {
|
||||
case "spec.dashboardUID":
|
||||
if r.Operator == selection.Equals || r.Operator == selection.DoubleEquals {
|
||||
query.DashboardUID = r.Value
|
||||
opts.DashboardUID = r.Value
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.dashboardUID (only = supported)", r.Operator)
|
||||
}
|
||||
@@ -167,7 +173,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid panelID value %q: %w", r.Value, err)
|
||||
}
|
||||
query.PanelID = panelID
|
||||
opts.PanelID = panelID
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.panelID (only = supported)", r.Operator)
|
||||
}
|
||||
@@ -178,13 +184,13 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err)
|
||||
}
|
||||
query.From = from
|
||||
opts.From = from
|
||||
case selection.LessThan:
|
||||
to, err := strconv.ParseInt(r.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err)
|
||||
}
|
||||
query.To = to
|
||||
opts.To = to
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.time (only >, < supported for ranges)", r.Operator)
|
||||
}
|
||||
@@ -196,13 +202,13 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err)
|
||||
}
|
||||
query.From = from
|
||||
opts.From = from
|
||||
case selection.LessThan:
|
||||
to, err := strconv.ParseInt(r.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err)
|
||||
}
|
||||
query.To = to
|
||||
opts.To = to
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.timeEnd (only >, < supported for ranges)", r.Operator)
|
||||
}
|
||||
@@ -213,31 +219,22 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
}
|
||||
}
|
||||
|
||||
query.Limit = 100
|
||||
opts.Limit = 100
|
||||
if options.Limit > 0 {
|
||||
query.Limit = options.Limit
|
||||
opts.Limit = options.Limit
|
||||
}
|
||||
items, err := s.service.Find(ctx, query)
|
||||
|
||||
result, err := s.store.List(ctx, namespace, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := &annotationV0.AnnotationList{
|
||||
Items: make([]annotationV0.Annotation, len(items)),
|
||||
}
|
||||
for i, item := range items {
|
||||
c, err := toK8sResource(orgID, item, s.namespacer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list.Items[i] = *c
|
||||
}
|
||||
|
||||
// TODO: pagination?
|
||||
return list, nil
|
||||
return &annotationV0.AnnotationList{Items: result.Items}, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
return nil, errors.New("fetching single annotations not supported by legacy storage")
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
return s.store.Get(ctx, namespace, name)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context,
|
||||
@@ -245,22 +242,11 @@ func (s *legacyStorage) Create(ctx context.Context,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
options *metav1.CreateOptions,
|
||||
) (runtime.Object, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
// resource, ok := obj.(*correlationsV0.Correlation)
|
||||
// if !ok {
|
||||
// return nil, fmt.Errorf("expected correlation")
|
||||
// }
|
||||
//
|
||||
// cmd, err := correlations.ToCreateCorrelationCommand(resource)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// out, err := s.service.CreateCorrelation(ctx, *cmd)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
resource, ok := obj.(*annotationV0.Annotation)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected annotation")
|
||||
}
|
||||
return s.store.Create(ctx, resource)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Update(ctx context.Context,
|
||||
@@ -272,73 +258,14 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
options *metav1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
return nil, false, errors.New("not implemented")
|
||||
// before, err := s.Get(ctx, name, &metav1.GetOptions{})
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// obj, err := objInfo.UpdatedObject(ctx, before)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
//
|
||||
// resource, ok := obj.(*correlationsV0.Correlation)
|
||||
// if !ok {
|
||||
// return nil, false, fmt.Errorf("expected correlation")
|
||||
// }
|
||||
//
|
||||
// cmd, err := correlations.ToUpdateCorrelationCommand(resource)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
//
|
||||
// out, err := s.service.UpdateCorrelation(ctx, *cmd)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// obj, err = s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
// return obj, false, err
|
||||
}
|
||||
|
||||
// GracefulDeleter
|
||||
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, errors.New("not implemented")
|
||||
// orgID, err := request.OrgIDForList(ctx)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// err = s.service.DeleteCorrelation(ctx, correlations.DeleteCorrelationCommand{
|
||||
// OrgId: orgID,
|
||||
// UID: name,
|
||||
// })
|
||||
// return nil, (err == nil), err
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
err := s.store.Delete(ctx, namespace, name)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// CollectionDeleter
|
||||
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return nil, fmt.Errorf("DeleteCollection for annotation not implemented")
|
||||
}
|
||||
|
||||
func toK8sResource(orgID int64, item *annotations.ItemDTO, namespacer request.NamespaceMapper) (*annotationV0.Annotation, error) {
|
||||
annotation := &annotationV0.Annotation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("a-%d", item.ID), // FIXME
|
||||
Namespace: namespacer(orgID),
|
||||
},
|
||||
Spec: annotationV0.AnnotationSpec{
|
||||
Text: item.Text,
|
||||
Time: item.Time,
|
||||
Tags: item.Tags,
|
||||
},
|
||||
}
|
||||
|
||||
if item.DashboardUID != nil && *item.DashboardUID != "" {
|
||||
annotation.Spec.DashboardUID = item.DashboardUID
|
||||
}
|
||||
if item.PanelID != 0 {
|
||||
annotation.Spec.PanelID = &item.PanelID
|
||||
}
|
||||
if item.TimeEnd != 0 {
|
||||
annotation.Spec.TimeEnd = &item.TimeEnd
|
||||
}
|
||||
return annotation, nil
|
||||
return nil, fmt.Errorf("DeleteCollection for annotation is not available")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type sqlAdapter struct {
|
||||
repo annotations.Repository
|
||||
cleaner annotations.Cleaner
|
||||
nsMapper request.NamespaceMapper
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func NewSQLAdapter(repo annotations.Repository, cleaner annotations.Cleaner, nsMapper request.NamespaceMapper, cfg *setting.Cfg) *sqlAdapter {
|
||||
return &sqlAdapter{
|
||||
repo: repo,
|
||||
cleaner: cleaner,
|
||||
nsMapper: nsMapper,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error) {
|
||||
id, err := parseAnnotationID(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
SignedInUser: user,
|
||||
OrgID: orgID,
|
||||
Limit: 1000,
|
||||
AlertID: -1,
|
||||
}
|
||||
|
||||
items, err := a.repo.Find(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item.ID == id {
|
||||
return a.toK8sResource(item, namespace), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
SignedInUser: user,
|
||||
OrgID: orgID,
|
||||
DashboardUID: opts.DashboardUID,
|
||||
PanelID: opts.PanelID,
|
||||
From: opts.From,
|
||||
To: opts.To,
|
||||
Limit: opts.Limit,
|
||||
AlertID: -1,
|
||||
}
|
||||
|
||||
items, err := a.repo.Find(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]annotationV0.Annotation, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, *a.toK8sResource(item, namespace))
|
||||
}
|
||||
|
||||
return &AnnotationList{
|
||||
Items: result,
|
||||
Continue: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Create(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, anno.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := a.fromK8sResource(anno)
|
||||
item.OrgID = orgID
|
||||
|
||||
if err := a.repo.Save(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created := anno.DeepCopy()
|
||||
created.Name = fmt.Sprintf("a-%d", item.ID)
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Update(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, anno.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := a.fromK8sResource(anno)
|
||||
item.OrgID = orgID
|
||||
|
||||
if err := a.repo.Update(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return anno, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Delete(ctx context.Context, namespace, name string) error {
|
||||
id, err := parseAnnotationID(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.repo.Delete(ctx, &annotations.DeleteParams{
|
||||
ID: id,
|
||||
OrgID: orgID,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Cleanup(ctx context.Context) (int64, error) {
|
||||
if a.cleaner == nil {
|
||||
return 0, nil
|
||||
}
|
||||
deleted, _, err := a.cleaner.Run(ctx, a.cfg)
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.TagsQuery{
|
||||
OrgID: orgID,
|
||||
Limit: int64(opts.Limit),
|
||||
Tag: opts.Prefix,
|
||||
}
|
||||
|
||||
result, err := a.repo.FindTags(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags := make([]Tag, len(result.Tags))
|
||||
for i, t := range result.Tags {
|
||||
tags[i] = Tag{Name: t.Tag, Count: t.Count}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) toK8sResource(item *annotations.ItemDTO, namespace string) *annotationV0.Annotation {
|
||||
anno := &annotationV0.Annotation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("a-%d", item.ID),
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: annotationV0.AnnotationSpec{
|
||||
Text: item.Text,
|
||||
Time: item.Time,
|
||||
Tags: item.Tags,
|
||||
},
|
||||
}
|
||||
|
||||
if item.DashboardUID != nil && *item.DashboardUID != "" {
|
||||
anno.Spec.DashboardUID = item.DashboardUID
|
||||
}
|
||||
if item.PanelID != 0 {
|
||||
anno.Spec.PanelID = &item.PanelID
|
||||
}
|
||||
if item.TimeEnd != 0 {
|
||||
anno.Spec.TimeEnd = &item.TimeEnd
|
||||
}
|
||||
|
||||
return anno
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) fromK8sResource(anno *annotationV0.Annotation) *annotations.Item {
|
||||
item := &annotations.Item{
|
||||
Text: anno.Spec.Text,
|
||||
Epoch: anno.Spec.Time,
|
||||
Tags: anno.Spec.Tags,
|
||||
}
|
||||
|
||||
if anno.Name != "" {
|
||||
if id, err := parseAnnotationID(anno.Name); err == nil {
|
||||
item.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
if anno.Spec.DashboardUID != nil {
|
||||
item.DashboardUID = *anno.Spec.DashboardUID
|
||||
}
|
||||
if anno.Spec.PanelID != nil {
|
||||
item.PanelID = *anno.Spec.PanelID
|
||||
}
|
||||
if anno.Spec.TimeEnd != nil {
|
||||
item.EpochEnd = *anno.Spec.TimeEnd
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
func parseAnnotationID(name string) (int64, error) {
|
||||
if len(name) < 3 || name[:2] != "a-" {
|
||||
return 0, fmt.Errorf("invalid annotation name format: %s", name)
|
||||
}
|
||||
return strconv.ParseInt(name[2:], 10, 64)
|
||||
}
|
||||
|
||||
func namespaceToOrgID(ctx context.Context, namespace string) (int64, error) {
|
||||
info, err := claims.ParseNamespace(namespace)
|
||||
return info.OrgID, err
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error)
|
||||
List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error)
|
||||
Create(ctx context.Context, annotation *annotationV0.Annotation) (*annotationV0.Annotation, error)
|
||||
Update(ctx context.Context, annotation *annotationV0.Annotation) (*annotationV0.Annotation, error)
|
||||
Delete(ctx context.Context, namespace, name string) error
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
DashboardUID string
|
||||
PanelID int64
|
||||
From int64
|
||||
To int64
|
||||
Limit int64
|
||||
Continue string
|
||||
}
|
||||
|
||||
type AnnotationList struct {
|
||||
Items []annotationV0.Annotation
|
||||
Continue string
|
||||
}
|
||||
|
||||
type LifecycleManager interface {
|
||||
Cleanup(ctx context.Context) (int64, error)
|
||||
}
|
||||
|
||||
type TagProvider interface {
|
||||
ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error)
|
||||
}
|
||||
|
||||
type TagListOptions struct {
|
||||
Prefix string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
Count int64
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
type tagResponse struct {
|
||||
Tags []tagItem `json:"tags"`
|
||||
}
|
||||
|
||||
type tagItem struct {
|
||||
Tag string `json:"tag"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func newTagsHandler(tagProvider TagProvider) func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
|
||||
return func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
|
||||
fmt.Println("Handling /tags request")
|
||||
namespace := request.ResourceIdentifier.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
tags, err := tagProvider.ListTags(ctx, namespace, TagListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]tagItem, len(tags))
|
||||
for i, tag := range tags {
|
||||
items[i] = tagItem{
|
||||
Tag: tag.Name,
|
||||
Count: tag.Count,
|
||||
}
|
||||
}
|
||||
|
||||
response := tagResponse{
|
||||
Tags: items,
|
||||
}
|
||||
|
||||
return json.NewEncoder(writer).Encode(response)
|
||||
}
|
||||
}
|
||||
Generated
+2
-2
@@ -813,7 +813,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl)
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl, cleanupServiceImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1467,7 +1467,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl)
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl, cleanupServiceImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -15,6 +16,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
|
||||
defer span.End()
|
||||
|
||||
if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -24,11 +27,15 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
|
||||
|
||||
store, err := s.getStoreInfo(ctx, r.GetNamespace())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contextuals, err := s.getContextuals(r.GetSubject())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -37,6 +44,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
|
||||
for _, item := range r.GetItems() {
|
||||
res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -25,6 +26,8 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
|
||||
|
||||
res, err := s.check(ctx, r)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform check request", "error", err, "namespace", r.GetNamespace())
|
||||
return nil, errors.New("failed to perform check request")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
@@ -28,6 +29,8 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis
|
||||
|
||||
res, err := s.list(ctx, r)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform list request", "error", err, "namespace", r.GetNamespace())
|
||||
return nil, errors.New("failed to perform list request")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
type OperationGroup string
|
||||
@@ -30,6 +31,8 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au
|
||||
|
||||
res, err := s.mutate(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform mutate request")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) {
|
||||
@@ -20,6 +21,8 @@ func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*auth
|
||||
|
||||
res, err := s.query(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform query request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform query request")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -22,6 +23,8 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze
|
||||
|
||||
res, err := s.read(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform read request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform read request")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -22,6 +23,8 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth
|
||||
|
||||
res, err := s.write(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform write request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform write request")
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ func (s *ServiceImpl) processAppPlugin(plugin pluginstore.Plugin, c *contextmode
|
||||
// Add Service Center as a standalone nav item under Alerts & IRM
|
||||
if alertsSection := treeRoot.FindById(navtree.NavIDAlertsAndIncidents); alertsSection != nil {
|
||||
serviceLink := &navtree.NavLink{
|
||||
Text: "Service Center",
|
||||
Text: "Service center",
|
||||
Id: "standalone-plugin-page-slo-services",
|
||||
Url: s.cfg.AppSubURL + "/a/grafana-slo-app/services",
|
||||
SortWeight: 1,
|
||||
|
||||
@@ -107,6 +107,9 @@ func StartGrafanaEnvWithDB(t *testing.T, grafDir, cfgPath string) (string, *serv
|
||||
dbCfg.Key("user").SetValue(testDB.User)
|
||||
dbCfg.Key("password").SetValue(testDB.Password)
|
||||
dbCfg.Key("name").SetValue(testDB.Database)
|
||||
if testDB.Path != "" {
|
||||
dbCfg.Key("path").SetValue(testDB.Path)
|
||||
}
|
||||
|
||||
t.Log("Using test database", "type", testDB.DriverName, "host", testDB.Host, "port", testDB.Port, "user", testDB.User, "name", testDB.Database, "path", testDB.Path)
|
||||
|
||||
|
||||
@@ -103,10 +103,17 @@ const combineFolderResponses = (
|
||||
|
||||
export async function getFolderByUidFacade(uid: string): Promise<FolderDTO> {
|
||||
const isVirtualFolder = uid && [GENERAL_FOLDER_UID, config.sharedWithMeFolderUID].includes(uid);
|
||||
// We need the legacy API call regardless, for now
|
||||
const legacyApiCall = dispatch(browseDashboardsAPI.endpoints.getFolder.initiate(uid));
|
||||
|
||||
const shouldUseAppPlatformAPI = Boolean(config.featureToggles.foldersAppPlatformAPI);
|
||||
|
||||
// We need the legacy API call regardless, for now
|
||||
const legacyApiCall = dispatch(
|
||||
browseDashboardsAPI.endpoints.getFolder.initiate({
|
||||
folderUID: uid,
|
||||
accesscontrol: true,
|
||||
isLegacyCall: shouldUseAppPlatformAPI,
|
||||
})
|
||||
);
|
||||
|
||||
if (shouldUseAppPlatformAPI) {
|
||||
let virtualFolderResponse;
|
||||
if (isVirtualFolder) {
|
||||
@@ -165,7 +172,9 @@ export function useGetFolderQueryFacade(uid?: string) {
|
||||
// This may look weird that we call the legacy folder anyway all the time, but the issue is we don't have good API
|
||||
// for the access control metadata yet, and so we still take it from the old api.
|
||||
// see https://github.com/grafana/identity-access-team/issues/1103
|
||||
const legacyFolderResult = useGetFolderQueryLegacy(uid || skipToken);
|
||||
const legacyFolderResult = useGetFolderQueryLegacy(
|
||||
uid ? { folderUID: uid, accesscontrol: true, isLegacyCall: true } : skipToken
|
||||
);
|
||||
let resultFolder = useGetFolderQuery(shouldUseAppPlatformAPI && !isVirtualFolder ? params : skipToken);
|
||||
// We get parents and folders for virtual folders too. Parents should just return empty array but it's easier to
|
||||
// stitch the responses this way and access can actually return different response based on the grafana setup.
|
||||
|
||||
@@ -94,9 +94,17 @@ export const browseDashboardsAPI = createApi({
|
||||
}),
|
||||
|
||||
// get folder info (e.g. title, parents) but *not* children
|
||||
getFolder: builder.query<FolderDTO, string>({
|
||||
providesTags: (_result, _error, folderUID) => [{ type: 'getFolder', id: folderUID }],
|
||||
query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }),
|
||||
getFolder: builder.query<FolderDTO, { folderUID: string; accesscontrol: boolean; isLegacyCall: boolean }>({
|
||||
providesTags: (_result, _error, { folderUID }) => [{ type: 'getFolder', id: folderUID }],
|
||||
query: ({ folderUID, accesscontrol, isLegacyCall }) => ({
|
||||
url: `/folders/${folderUID}`,
|
||||
params: {
|
||||
accesscontrol,
|
||||
// Add additional query param so we can tell when
|
||||
// this was called for app platform compatibility purposes vs. actually needing to use the legacy API
|
||||
isLegacyCall,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
// create a new folder
|
||||
|
||||
@@ -69,6 +69,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
hasOpenPane: Boolean(openPane),
|
||||
contentMargin: 1,
|
||||
position: 'right',
|
||||
persistanceKey: 'dashboard',
|
||||
onClosePane: () => editPane.closePane(),
|
||||
});
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
|
||||
>
|
||||
<div className={cx(styles.rightControls, editPanel && styles.rightControlsWrap)}>
|
||||
{!hideTimeControls && (
|
||||
<div className={styles.timeControls}>
|
||||
<div className={styles.fixedControls}>
|
||||
<timePicker.Component model={timePicker} />
|
||||
<refreshPicker.Component model={refreshPicker} />
|
||||
</div>
|
||||
@@ -181,7 +181,11 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
|
||||
<DashboardControlsButton dashboard={dashboard} />
|
||||
</div>
|
||||
)}
|
||||
{config.featureToggles.dashboardNewLayouts && <DashboardControlActions dashboard={dashboard} />}
|
||||
{config.featureToggles.dashboardNewLayouts && (
|
||||
<div className={styles.fixedControls}>
|
||||
<DashboardControlActions dashboard={dashboard} />
|
||||
</div>
|
||||
)}
|
||||
{!hideLinksControls && !editPanel && <DashboardLinksControls links={links} dashboard={dashboard} />}
|
||||
</div>
|
||||
{!hideVariableControls && (
|
||||
@@ -274,12 +278,12 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
float: 'right',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
}),
|
||||
timeControls: css({
|
||||
fixedControls: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: theme.spacing(1),
|
||||
|
||||
@@ -5,10 +5,9 @@ import SwaggerUI from 'swagger-ui-react';
|
||||
|
||||
import { createTheme, monacoLanguageRegistry, SelectableValue } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Stack, Select, UserIcon, UserView, Button } from '@grafana/ui';
|
||||
import { Icon, Stack, Select, UserIcon, UserView, Button } from '@grafana/ui';
|
||||
import { setMonacoEnv } from 'app/core/monacoEnv';
|
||||
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
|
||||
import grafanaIconSvg from 'img/grafana_icon.svg';
|
||||
|
||||
import { NamespaceContext, WrappedPlugins } from './plugins';
|
||||
|
||||
@@ -85,7 +84,7 @@ export const Page = () => {
|
||||
<NamespaceContext.Provider value={namespace.value}>
|
||||
<div style={{ backgroundColor: '#000', padding: '10px' }}>
|
||||
<Stack justifyContent={'space-between'}>
|
||||
<img height="40" src={grafanaIconSvg} alt="Grafana" />
|
||||
<Icon name="grafana" size="xxl" />
|
||||
<Select
|
||||
options={urls.value}
|
||||
isClearable={false /* TODO -- when we allow a landing page, this can be true */}
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
|
||||
<link rel="stylesheet" href="[[.Assets.Light]]" />
|
||||
|
||||
<link rel="icon" type="image/png" href="[[.FavIcon]]" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
|
||||
<link rel="mask-icon" href="[[.Assets.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
<link rel="icon" type="image/png" href="[[.Assets.ContentDeliveryURL]]public/build/img/fav32.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="[[.Assets.ContentDeliveryURL]]public/build/img/apple-touch-icon.png"
|
||||
/>
|
||||
<link rel="mask-icon" href="[[.Assets.ContentDeliveryURL]]public/build/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user