Compare commits

...

15 Commits

Author SHA1 Message Date
jtvdez e003210566 adjusitng some plaement and adding hr elements 2025-12-02 12:08:38 -06:00
jtvdez e055bf28e1 adding more missing sections 2025-12-02 12:07:11 -06:00
jtvdez d20a4ae4cc additional alerting configs 2025-12-02 10:23:50 -06:00
jtvdez 91f48d51e6 docs: syncing config docs with defaults.ini 2025-12-01 15:55:09 -06:00
Sergej-Vlasov 780a64e771 DashboardControls: Adjust dashboard controls UI shift (#114639)
adjust controls layout
2025-12-01 16:08:26 +00:00
Dave Thompson 156a6f1375 fix(operator): unify service center capitalization (#113720)
Change "Service Center" to "Service center" in navigation menu to follow
sentence case capitalization style consistently across the application.

Fixes grafana/slo#3818

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 17:20:29 +02:00
Isabel Matwawana 5fd4fb5fb8 Docs: Add missing layout options and rework Grid view section (#113007)
Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
Co-authored-by: Joey <90795735+joey-grafana@users.noreply.github.com>
2025-12-01 09:19:00 -05:00
Torkel Ödegaard 0275939762 Sidebar: Persist state to local storage (#114627)
* fix merge issues

* unsaved file
2025-12-01 12:54:18 +01:00
Serge Zaitsev c15b1b6f10 Chore: Annotation store interface (#114100)
* annotation legacy store with api server, read only

* annotations are not addressable by ID for read operations

* add ownership for an app

* typo, of course

* fix go workspace

* update workspace

* copy annotation app in dockerfile

* experimenting with store interface

* finalising interfaces

* add tags as custom handler

* implement tags handler

* add missing config file

* mute linter

* update generated files

* update workspace
2025-12-01 12:28:46 +01:00
Josh Hunt 32b9bebc75 Swagger: Load embedded icons from CDN (#114632) 2025-12-01 11:17:28 +00:00
Rafael Bortolon Paulovic 7fce2d9516 fix(unified): set SQLite path in cfg for reusing shared DB (#114580) 2025-12-01 11:30:22 +01:00
Ashley Harrison 2da171595a FieldColor: Add accessible color palettes (#114424)
* add viridis and others, allow passing interpolator directly to FieldColorSchemeMode

* fix bug

* be more defensive when getting the fieldcolor

* backend changes

* add d3-scale-chromatic to list of esModules
2025-12-01 09:51:11 +00:00
Tom Ratcliffe dd77107ed4 Folders: Add additional query param to legacy API for tracking purposes (#114505) 2025-12-01 09:25:57 +00:00
Stephanie Hingtgen aaa5d02a3e AuthZ: Set span errors (#114460) 2025-12-01 09:29:04 +01:00
Stephanie Hingtgen db9afe31e4 Provisioning: Fix panic on watcher when channel is closed (#114439) 2025-12-01 08:24:03 +03:00
54 changed files with 1498 additions and 242 deletions
+14
View File
@@ -10,6 +10,20 @@ manifest: {
v0alpha1: {
kinds: [annotationv0alpha1]
routes: {
namespaced: {
"/tags": {
"GET": {
response: {
tags: [...{
tag: string
count: number
}]
}
}
}
}
}
codegen: {
ts: {
enabled: true
@@ -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{}
}
@@ -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
View File
@@ -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.
+17
View File
@@ -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
+16
View File
@@ -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"
@@ -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"`.
@@ -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&mdash;or _nodes_&mdash;for each element you want to visualize, connected by lines&mdash;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&mdash;or _nodes_&mdash;for each element you want to visualize, connected by lines&mdash;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.
![Node graph exploration](/media/docs/grafana/panels-visualizations/node-graph-exploration-8.0-2.png 'Node graph exploration')
### 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.
![Node graph grid](/media/docs/grafana/panels-visualizations/screenshot-node-graph-grid-v11.3.png 'Node graph grid')
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.
![Node graph legend](/media/docs/grafana/panels-visualizations/screenshot-node-graph-legend-v11.3.png 'Node graph legend')
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.
![Node graph grid to default](/media/docs/grafana/panels-visualizations/screenshot-node-graph-view-v11.3.png 'Node graph grid to default')
## 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
View File
@@ -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=
+11 -6
View File
@@ -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")
+1
View File
@@ -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",
+65 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+49 -122
View File
@@ -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")
}
+255
View File
@@ -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
}
+47
View File
@@ -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)
}
}
+2 -2
View File
@@ -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")
}
+1 -1
View File
@@ -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,
+3
View File
@@ -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)
+13 -4
View File
@@ -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),
+2 -3
View File
@@ -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 */}
+7 -3
View File
@@ -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>
+1
View File
@@ -3122,6 +3122,7 @@ __metadata:
"@types/systemjs": "npm:6.15.3"
"@types/tinycolor2": "npm:1.4.6"
d3-interpolate: "npm:3.0.1"
d3-scale-chromatic: "npm:3.1.0"
date-fns: "npm:4.1.0"
dompurify: "npm:3.3.0"
esbuild: "npm:0.25.8"