Compare commits

...

33 Commits

Author SHA1 Message Date
joshhunt
cf233d68eb Add Oauth configuration to frontend service devenv 2026-01-14 15:45:34 +00:00
joshhunt
1aea178e23 FS: Fix oauth login error not displaying 2026-01-14 15:45:34 +00:00
Alyssa Joyner
6385b1f471 [Azure Monitor]: Preserve logs builder query when switching to KQL mode (#116161) 2026-01-14 08:17:21 -07:00
Isabel Matwawana
505fa869ee Docs: Dashboard schema v2 public preview updates (#115293) 2026-01-14 10:03:19 -05:00
Tania
399b3def4f Chore: Fix pluginsAutoUpdate flag evaluation (#116065)
* Experimental: Test flag evaluation

* Attempt to inject requester into the context

* fixup! Attempt to inject requester into the context
2026-01-14 15:57:12 +01:00
Paul Marbach
d6ac674f3e Gauge: Fix issue with gdev dashboard (#116235) 2026-01-14 09:55:34 -05:00
Paul Marbach
0e6651c729 Gauge: Re-introduce minVizHeight and minVizWidth (#116034) 2026-01-14 09:55:12 -05:00
Haris Rozajac
ea2a0936df Dashboard Conversion: Preserve repeat property when converting tabs to rows (#116180)
* preserve repeat property

* fix test

* preserve repeat when converting panels in tabs or rows with autogrid layout

* fix v1 serialization of autogrid
2026-01-14 07:29:51 -07:00
Ryan McKinley
d95c51b20e Chore: Deprecate experimental restore dashboard API (#116256) 2026-01-14 14:09:37 +00:00
Rodrigo Vasconcelos de Barros
d0df6b8de4 Alerting: Provisioning Status Differentiation for ALL resources (#115773)
* Show different badge for converted prometheus provisioned resource

* Update contact points and templates to populate provenance

* Update notification policies

* Handle non k8s contact point in ContactPointHeader

* Fix provenance check in enhanceContactPointsWithMetadata

* Update translations

* Fix unused import

* Refactor provenance enum

* Derive provisioned status from provenance in Route type

* Remove unused imports

* Treat PROVENANCE_NONE as no provenance in isRouteProvisione

* Rename KnownProvenance.None to .Empty to avoid confusion

* Change copy text for resources with converted_prometheus provenance

* Derive provisioned status from provenance in GrafanaManagedContactPoint

* Fix linter errors

* Extract helper method to check if contact point is provisioned

* Replace string literal with constant

* Refactor KnownProvenance enum values

Refactored the KnownProvenance enum to better reflect the known provenances defined by the backend.
Also refactored the methods where we assert if a resource is provisioned to better reflect the cases for which a provenance value reflects no provisioning.
A resource is considered not provisioned when the provenance is equal to '', 'none' or undefined.

* Use provenance to infer provenance status for Templates

Refactored useNotificationTemplateMetadata to use only provenance value, and extracted method used to assert if resource is provisioned or not to k8s/utils in order to be more resource agnostic.

* Replace empty string with 'none' for KnownProvenance enum

The empty string valye for provenance gets mapped to the string literal 'none' before being passed down in the api response, therefore we can use only 'none'

* Replace PROVENANCE_NONE with KnownProvenance.None

Replaced the constant PROVENANCE_NONE with the KnownProvenance.None enum value since the values where duplicated

* Fix JSDoc

* Change copy text for ProvisioningBadge

* Add missing tooltip in notification policy badge

* Add missing tooltip in TemplatesTable badge

* fix conflicts

---------

Co-authored-by: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>
Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
2026-01-14 08:58:03 -05:00
Alex Khomenko
9f44f868aa Stars: Fix infinite loading with no starred items (#116248) 2026-01-14 15:48:48 +02:00
Fabrizio
ba6a783997 Add Db2 plugin (#116190)
* Add new Db2 plugin

* Fix ID

* Fix ID

* Run `i18n-extract`

* Linting

* Fix ID

* Linting

* Rename plugin

* Fix i18n entry

* Run `yarn i18n-extract`
2026-01-14 14:20:24 +01:00
Andreas Christou
f704b8aa79 Cloud Monitoring: Add support for Google Cloud universe_domain (#115931)
* feat(cloud-monitoring): add support for Google Cloud universe_domain (#110083)

This change introduces support for Google Cloud's `universe_domain`, enabling connections to sovereign cloud environments with custom API endpoints.

- Adds an optional "Universe Domain" field in the Google Cloud Monitoring data source configuration (frontend and backend).
- Allows specifying a custom API domain (e.g., `s3nsapis.fr`) for use in sovereign environments.
- Defaults to `googleapis.com` to ensure backward compatibility for existing configurations.

Signed-off-by: Andreas Christou <andreas.christou@grafana.com>

* Minor docs update

* Doc updates

* Update editor

* Update docs/sources/datasources/google-cloud-monitoring/_index.md

Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com>

* Lint

* Review

---------

Signed-off-by: Andreas Christou <andreas.christou@grafana.com>
Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com>
2026-01-14 13:16:09 +00:00
Andreas Christou
c1a46fdcb5 Elasticsearch: Decoupling from core (#115900)
* Complete decoupling of backend

- Replace usage of featuremgmt
- Copy simplejson
- Add standalone logic

* Complete frontend decoupling

- Fix imports
- Copy store and reducer logic

* Add required files for full decoupling

* Regen cue

* Prettier

* Remove unneeded script

* Jest fix

* Add jest config

* Lint

* Lit

* Prune suppresions
2026-01-14 12:54:21 +00:00
Cauê Marcondes
7143324229 Elasticsearch: Add support for serverless connections (#114855)
* serverless connecction

* Adding api key

* fix

* addressing pr comments

* fixing tests

* refactoring

* changing to value semantic

* addressing pr comments

* minor changes

---------

Co-authored-by: Lucas Francisco Lopez <lucas.lopez@elastic.co>
2026-01-14 12:51:42 +00:00
Ryan McKinley
48625d67e5 Chore: update blevesearch dependencies (#116251) 2026-01-14 12:15:19 +00:00
Jack Westbrook
8bad33de4c Grafana/data: Fix theme types schema resolution (#116240)
* fix(grafana-data): copy theme schema json to types so declaration resolves

* refactor(grafana-data): move node scripts out of source code

* feat(grafana-data): generate types for theme schema

* chore(codeowners): update for grafana-data/scripts file move

* feat(grafana-data): put back copy plugin for theme json files

* revert(grafana-data): remove definition output

* feat(grafana-data): make builds great again

* minor tidy up

---------

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
2026-01-14 12:05:23 +00:00
Ryan McKinley
040854c8af Search: Allow query field selection (#116238) 2026-01-14 11:55:05 +00:00
Rafael Bortolon Paulovic
987c1fc6b6 feat(unified): add index scoring model config (#116210)
* feat(unified): add bm25 index scoring model

We want try BM25 scoring model since they have global scoring which we can probably re-use for fan-in/fan-out logic

32d98823c4/docs/scoring.md (global-scoring)

* fix(plugins): update plugin test data
2026-01-14 12:07:53 +01:00
Alejandro Fraenkel
170ac31c5a Alerting: Add alertingNavigationV2 feature toggle (#116215)
feat(alerting): add alertingNavigationV2 feature toggle

Introduces a new feature toggle to enable the improved Alerting navigation
structure with grouped menu items. This toggle will allow:
- Safe incremental rollout of navigation changes
- Quick rollback if issues arise
- Handling BE/FE deployment timing differences

Toggle details:
- Name: alertingNavigationV2
- Stage: Experimental
- Owner: @grafana/alerting-squad
- Default: false (disabled)
- Affects: Both backend (navtree) and frontend (navigation hooks)
2026-01-14 11:58:11 +01:00
Dominik Prokop
0d1e0bc21c PanelMenu: use openInNewTab links extensions API correctly (#116200)
* Extensons: Make links use openInNewTab API

* Use openInNewTab api correctly in the UI

* Bump scenes

* Fx circular dep

* test

* Revert "test"

This reverts commit 8784a7992c.
2026-01-14 11:29:43 +01:00
Natalia Bernarte Oses
afd84f0335 Datagrid: Deprecate panel (#116071)
* deprecate datagrid

* Update docs/sources/visualizations/panels-visualizations/visualizations/datagrid/index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

---------

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
2026-01-14 11:10:51 +01:00
Andres Martinez Gotor
d680537ea1 Advisor: Simplify interface used (#116191) 2026-01-14 11:05:16 +01:00
Bogdan Matei
78d507d285 Dynamic Dashboards: Change the stage of the feature toggle (#116189) 2026-01-14 09:50:37 +00:00
Tito Lins
9d1d0e72c2 Alerting: add sync timer support (#114602)
- add new feature flag to support enabling the dispatcher sync timer on the alertmanager
- this attempts to synchronize the flushes across HA nodes to decrease amount of duplicate notifications

---------

Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
2026-01-14 10:04:29 +01:00
Konrad Lalik
fd955f90ac Alerting: Enable server-side folder search for GMA rules (#116201)
* Alerting: Support backend filtering for folder search

Updates the Grafana managed rules API and filter logic to support
server-side filtering by folder (namespace).

Changes:
- Add `searchFolder` parameter to `getGrafanaGroups` API endpoint
- Map filter state `namespace` to `searchFolder` in backend filter
- Disable client-side namespace filtering when backend filtering is enabled
- Update tests to verify correct behavior for folder search with backend filters

* Add missing property in filter options

* Update tests
2026-01-14 09:48:07 +01:00
Sonia Aguilar
ccb032f376 Alerting: Single alertmanager contact points versions (#116076)
* POC ssingle AM

* wip

* add query param ?version=2

* wip2

* wip3

* Update logic

* update badges and tests

* remove unsused import

* fix: update NewReceiverView snapshots to include version field

* update translations

* fix: delegate version determination to backend for new integrations

- Remove hardcoded version: 'v1' from defaultChannelValues
- Reset version to undefined when integration type changes
- Backend uses GetCurrentVersion() when no version is provided
- Update snapshots to reflect version handling changes
- Remove unused getDefaultVersionForNotifier function

* update snapshot

* fix(alerting): fix contact point form issues

- Fix empty info alert showing when notifier.dto.info is undefined
- Fix options not loading for new contact points by using default creatable version

* fix(alerting): only show version badge for legacy integrations

* update tests for version badge and getOptionsForVersion changes

* docs: add comment explaining currentVersion field in NotifierDTO

* Show user-friendly 'Legacy' label for legacy integrations

- Replace technical version strings (v0mimir1, v0mimir2) with user-friendly labels
- v0mimir1 -> 'Legacy', v0mimir2 -> 'Legacy v2', etc.
- Technical version is still shown in tooltip for reference
- Add getLegacyVersionLabel() utility function
- Update tests for badge display and utility function

* Add v0mimir2 to test mock for Legacy v2 badge test

* hasLegacyIntegrations now uses isLegacyVersion

- Accept notifiers array to properly check canCreate: false
- No longer relies on version string comparison (v1 check)
- Uses isLegacyVersion for consistent legacy detection
- Update tests to pass notifiers and test correct behavior

* update translations
2026-01-14 08:31:13 +01:00
Alex Khomenko
cf452c167b Provisioning: Do not show the page when the toggle is off (#116206) 2026-01-14 07:41:10 +02:00
Hugo Häggmark
bd0140b6f0 GrafanaBootData: Deprecate config.apps (#115610)
* GrafanaBootData: decouple `config.apps` from boot data IV

* chore: changed to openfeature flags eval

* chore: updates after PR feedback

* chore: updates after PR feedback

* chore: copy types to runtime package

* chore: add code ownership

* chore: deprecate in interface too

* chore: add important notice to comments

* chore: deprecate the whole interface
2026-01-14 06:30:05 +01:00
grafana-pr-automation[bot]
215d25ef69 I18n: Download translations from Crowdin (#116232)
New Crowdin translations by GitHub Action

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-14 00:43:07 +00:00
sabithamuppuri
d3beed7dd2 Docs: add unified_alerting.state_history configuration section (fixes #114670) (#115607)
Co-authored-by: Pepe Cano <825430+ppcano@users.noreply.github.com>
Co-authored-by: Johnny Kartheiser <140559259+JohnnyK-Grafana@users.noreply.github.com>
2026-01-13 21:37:09 +00:00
Anton Chimrov
e2f2011d9e Restore Canvas element key simplification to prevent blinking icons (#113693)
* Simplify Canvas element key to prevent blinking icons

* Fix formatting with prettier
2026-01-13 13:01:22 -08:00
Paul Marbach
6db51cbdb9 Legends: Revert scrolled truncated legend for now (#116217)
* Revert "PieChart: Fix right-oriented legends (#116084)"

This reverts commit 0c8c886930.

* Revert "TimeSeries: Fix truncated label text in legend table mode (#115647)"

This reverts commit f91efcfe2c.
2026-01-13 19:54:42 +00:00
293 changed files with 19549 additions and 2880 deletions

2
.github/CODEOWNERS vendored
View File

@@ -543,6 +543,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
/packages/grafana-data/tsconfig.json @grafana/grafana-frontend-platform
/packages/grafana-data/test/ @grafana/grafana-frontend-platform
/packages/grafana-data/typings/ @grafana/grafana-frontend-platform
/packages/grafana-data/scripts/ @grafana/grafana-frontend-platform
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
/packages/grafana-data/src/context/plugins/ @grafana/plugins-platform-frontend
@@ -658,6 +659,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
/packages/grafana-runtime/src/services/LocationService.tsx @grafana/grafana-search-navigate-organise
/packages/grafana-runtime/src/services/LocationSrv.ts @grafana/grafana-search-navigate-organise
/packages/grafana-runtime/src/services/live.ts @grafana/dashboards-squad
/packages/grafana-runtime/src/services/pluginMeta @grafana/plugins-platform-frontend
/packages/grafana-runtime/src/utils/chromeHeaderHeight.ts @grafana/grafana-search-navigate-organise
/packages/grafana-runtime/src/utils/DataSourceWithBackend* @grafana/grafana-datasources-core-services
/packages/grafana-runtime/src/utils/licensing.ts @grafana/grafana-operator-experience-squad

View File

@@ -121,6 +121,8 @@ linters:
- '**/pkg/tsdb/zipkin/**/*'
- '**/pkg/tsdb/jaeger/*'
- '**/pkg/tsdb/jaeger/**/*'
- '**/pkg/tsdb/elasticsearch/*'
- '**/pkg/tsdb/elasticsearch/**/*'
deny:
- pkg: github.com/grafana/grafana/pkg/api
desc: Core plugins are not allowed to depend on Grafana core packages

View File

@@ -28,7 +28,7 @@ type check struct {
PluginStore pluginstore.Store
PluginContextProvider PluginContextProvider
PluginClient plugins.Client
PluginRepo repo.Service
PluginRepo checks.PluginInfoGetter
GrafanaVersion string
pluginCanBeInstalledCache map[string]bool
pluginExistsCacheMu sync.RWMutex
@@ -39,7 +39,7 @@ func New(
pluginStore pluginstore.Store,
pluginContextProvider PluginContextProvider,
pluginClient plugins.Client,
pluginRepo repo.Service,
pluginRepo checks.PluginInfoGetter,
grafanaVersion string,
) checks.Check {
return &check{

View File

@@ -15,7 +15,7 @@ import (
type missingPluginStep struct {
PluginStore pluginstore.Store
PluginRepo repo.Service
PluginRepo checks.PluginInfoGetter
GrafanaVersion string
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana-app-sdk/logging"
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/pkg/plugins/repo"
)
// Check returns metadata about the check being executed and the list of Steps
@@ -37,3 +38,10 @@ type Step interface {
// Run executes the step for an item and returns a report
Run(ctx context.Context, log logging.Logger, obj *advisorv0alpha1.CheckSpec, item any) ([]advisorv0alpha1.CheckReportFailure, error)
}
// PluginInfoGetter is a minimal interface for retrieving plugin information from a repository.
// It contains only the GetPluginsInfo method used by plugincheck and datasourcecheck.
type PluginInfoGetter interface {
// GetPluginsInfo will return a list of plugins from grafana.com/api/plugins.
GetPluginsInfo(ctx context.Context, options repo.GetPluginsInfoOptions, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error)
}

View File

@@ -17,7 +17,7 @@ const (
func New(
pluginStore pluginstore.Store,
pluginRepo repo.Service,
pluginRepo checks.PluginInfoGetter,
updateChecker pluginchecker.PluginUpdateChecker,
pluginErrorResolver plugins.ErrorResolver,
grafanaVersion string,
@@ -33,7 +33,7 @@ func New(
type check struct {
PluginStore pluginstore.Store
PluginRepo repo.Service
PluginRepo checks.PluginInfoGetter
updateChecker pluginchecker.PluginUpdateChecker
pluginErrorResolver plugins.ErrorResolver
GrafanaVersion string

View File

@@ -586,6 +586,7 @@
},
"id": -1,
"panels": [],
"repeat": "custom_var_tab",
"title": "Repeated Tab by \"$custom_var_tab\"",
"type": "row"
},
@@ -610,8 +611,11 @@
"y": 22
},
"id": 6,
"maxPerRow": 3,
"options": {},
"pluginVersion": "12.4.0-19736337744",
"repeat": "custom_var_panel",
"repeatDirection": "h",
"targets": [
{
"refId": "A"

View File

@@ -586,6 +586,7 @@
},
"id": -1,
"panels": [],
"repeat": "custom_var_tab",
"title": "Repeated Tab by \"$custom_var_tab\"",
"type": "row"
},
@@ -610,8 +611,11 @@
"y": 22
},
"id": 6,
"maxPerRow": 3,
"options": {},
"pluginVersion": "12.4.0-19736337744",
"repeat": "custom_var_panel",
"repeatDirection": "h",
"targets": [
{
"refId": "A"

View File

@@ -439,6 +439,11 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash
rowPanel["title"] = *tab.Spec.Title
}
if tab.Spec.Repeat != nil && tab.Spec.Repeat.Value != "" {
// We only use value here as V1 doesn't support mode
rowPanel["repeat"] = tab.Spec.Repeat.Value
}
rowPanel["gridPos"] = map[string]interface{}{
"x": 0,
"y": currentY,
@@ -819,6 +824,21 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da
},
}
// Convert AutoGridRepeatOptions to RepeatOptions if present
// AutoGridRepeatOptions only has mode and value; infer direction and maxPerRow from AutoGrid settings:
// - direction: always "h" (AutoGrid flows horizontally, left-to-right then wraps)
// - maxPerRow: from AutoGrid's maxColumnCount
if item.Spec.Repeat != nil {
directionH := dashv2alpha1.DashboardRepeatOptionsDirectionH
maxPerRow := int64(maxColumnCount)
gridItem.Spec.Repeat = &dashv2alpha1.DashboardRepeatOptions{
Mode: item.Spec.Repeat.Mode,
Value: item.Spec.Repeat.Value,
Direction: &directionH,
MaxPerRow: &maxPerRow,
}
}
panel, err := convertPanelFromElement(&element, &gridItem)
if err != nil {
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)

View File

@@ -2117,7 +2117,7 @@
}
],
"title": "Numeric, no series",
"type": "gauge"
"type": "radialbar"
},
{
"datasource": {
@@ -2183,7 +2183,7 @@
}
],
"title": "Non-numeric",
"type": "gauge"
"type": "radialbar"
}
],
"preload": false,
@@ -2201,4 +2201,4 @@
"title": "Panel tests - Gauge (new)",
"uid": "panel-tests-gauge-new",
"weekStart": ""
}
}

View File

@@ -290,7 +290,7 @@
],
"legend": {
"displayMode": "table",
"placement": "right",
"placement": "bottom",
"showLegend": true,
"values": [
"percent"
@@ -304,7 +304,7 @@
"fields": "",
"values": false
},
"showLegend": true,
"showLegend": false,
"strokeWidth": 1,
"text": {}
},
@@ -323,15 +323,6 @@
}
],
"title": "Percent",
"transformations": [
{
"id": "renameByRegex",
"options": {
"regex": "^Backend-(.*)$",
"renamePattern": "b-$1"
}
}
],
"type": "piechart"
},
{
@@ -375,7 +366,7 @@
],
"legend": {
"displayMode": "table",
"placement": "right",
"placement": "bottom",
"showLegend": true,
"values": [
"value"
@@ -389,7 +380,7 @@
"fields": "",
"values": false
},
"showLegend": true,
"showLegend": false,
"strokeWidth": 1,
"text": {}
},
@@ -408,15 +399,6 @@
}
],
"title": "Value",
"transformations": [
{
"id": "renameByRegex",
"options": {
"regex": "(.*)",
"renamePattern": "$1-how-much-wood-could-a-woodchuck-chuck-if-a-woodchuck-could-chuck-wood"
}
}
],
"type": "piechart"
},
{

View File

@@ -1,9 +1,16 @@
include ../sdk.mk
.PHONY: generate # Run Grafana App SDK code generation
generate: install-app-sdk update-app-sdk
.PHONY: internal-generate # Run Grafana App SDK code generation
internal-generate: install-app-sdk update-app-sdk
@$(APP_SDK_BIN) generate \
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--defencoding=none
--defencoding=none
.PHONY: generate
generate: internal-generate # copy files to packages/grafana-runtime/src/services/pluginMeta/types
rm -f ./packages/grafana-runtime/src/services/pluginMeta/types/*.ts
cp plugin/src/generated/meta/v0alpha1/meta_object_gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts
cp plugin/src/generated/meta/v0alpha1/types.spec.gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts
cp plugin/src/generated/meta/v0alpha1/types.status.gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts

View File

@@ -4,8 +4,7 @@ API documentation is available at http://localhost:3000/swagger?api=plugins.graf
## Codegen
- Go: `make generate`
- Frontend: Follow instructions in this [README](../..//packages/grafana-api-clients/README.md)
- Go and TypeScript: `make generate`
## Plugin sync

View File

@@ -11,7 +11,7 @@ manifest: {
v0alpha1Version: {
served: true
codegen: {
ts: {enabled: false}
ts: {enabled: true}
go: {enabled: true}
}
kinds: [

View File

@@ -0,0 +1,49 @@
/*
* This file was generated by grafana-app-sdk. DO NOT EDIT.
*/
import { Spec } from './types.spec.gen';
import { Status } from './types.status.gen';
export interface Metadata {
name: string;
namespace: string;
generateName?: string;
selfLink?: string;
uid?: string;
resourceVersion?: string;
generation?: number;
creationTimestamp?: string;
deletionTimestamp?: string;
deletionGracePeriodSeconds?: number;
labels?: Record<string, string>;
annotations?: Record<string, string>;
ownerReferences?: OwnerReference[];
finalizers?: string[];
managedFields?: ManagedFieldsEntry[];
}
export interface OwnerReference {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller?: boolean;
blockOwnerDeletion?: boolean;
}
export interface ManagedFieldsEntry {
manager?: string;
operation?: string;
apiVersion?: string;
time?: string;
fieldsType?: string;
subresource?: string;
}
export interface Meta {
kind: string;
apiVersion: string;
metadata: Metadata;
spec: Spec;
status: Status;
}

View File

@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
export interface Metadata {
updateTimestamp: string;
createdBy: string;
uid: string;
creationTimestamp: string;
deletionTimestamp?: string;
finalizers: string[];
resourceVersion: string;
generation: number;
updatedBy: string;
labels: Record<string, string>;
}
export const defaultMetadata = (): Metadata => ({
updateTimestamp: "",
createdBy: "",
uid: "",
creationTimestamp: "",
finalizers: [],
resourceVersion: "",
generation: 0,
updatedBy: "",
labels: {},
});

View File

@@ -0,0 +1,278 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// JSON configuration schema for Grafana plugins
// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json
export interface JSONData {
// Unique name of the plugin
id: string;
// Plugin type
type: "app" | "datasource" | "panel" | "renderer";
// Human-readable name of the plugin
name: string;
// Metadata for the plugin
info: Info;
// Dependency information
dependencies: Dependencies;
// Optional fields
alerting?: boolean;
annotations?: boolean;
autoEnabled?: boolean;
backend?: boolean;
buildMode?: string;
builtIn?: boolean;
category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other";
enterpriseFeatures?: EnterpriseFeatures;
executable?: string;
hideFromList?: boolean;
// +listType=atomic
includes?: Include[];
logs?: boolean;
metrics?: boolean;
multiValueFilterOperators?: boolean;
pascalName?: string;
preload?: boolean;
queryOptions?: QueryOptions;
// +listType=atomic
routes?: Route[];
skipDataQuery?: boolean;
state?: "alpha" | "beta";
streaming?: boolean;
suggestions?: boolean;
tracing?: boolean;
iam?: IAM;
// +listType=atomic
roles?: Role[];
extensions?: Extensions;
}
export const defaultJSONData = (): JSONData => ({
id: "",
type: "app",
name: "",
info: defaultInfo(),
dependencies: defaultDependencies(),
});
export interface Info {
// Required fields
// +listType=set
keywords: string[];
logos: {
small: string;
large: string;
};
updated: string;
version: string;
// Optional fields
author?: {
name?: string;
email?: string;
url?: string;
};
description?: string;
// +listType=atomic
links?: {
name?: string;
url?: string;
}[];
// +listType=atomic
screenshots?: {
name?: string;
path?: string;
}[];
}
export const defaultInfo = (): Info => ({
keywords: [],
logos: {
small: "",
large: "",
},
updated: "",
version: "",
});
export interface Dependencies {
// Required field
grafanaDependency: string;
// Optional fields
grafanaVersion?: string;
// +listType=set
// +listMapKey=id
plugins?: {
id: string;
type: "app" | "datasource" | "panel";
name: string;
}[];
extensions?: {
// +listType=set
exposedComponents?: string[];
};
}
export const defaultDependencies = (): Dependencies => ({
grafanaDependency: "",
});
export interface EnterpriseFeatures {
// Allow additional properties
healthDiagnosticsErrors?: boolean;
}
export const defaultEnterpriseFeatures = (): EnterpriseFeatures => ({
healthDiagnosticsErrors: false,
});
export interface Include {
uid?: string;
type?: "dashboard" | "page" | "panel" | "datasource";
name?: string;
component?: string;
role?: "Admin" | "Editor" | "Viewer" | "None";
action?: string;
path?: string;
addToNav?: boolean;
defaultNav?: boolean;
icon?: string;
}
export const defaultInclude = (): Include => ({
});
export interface QueryOptions {
maxDataPoints?: boolean;
minInterval?: boolean;
cacheTimeout?: boolean;
}
export const defaultQueryOptions = (): QueryOptions => ({
});
export interface Route {
path?: string;
method?: string;
url?: string;
reqSignedIn?: boolean;
reqRole?: string;
reqAction?: string;
// +listType=atomic
headers?: string[];
body?: Record<string, any>;
tokenAuth?: {
url?: string;
// +listType=set
scopes?: string[];
params?: Record<string, any>;
};
jwtTokenAuth?: {
url?: string;
// +listType=set
scopes?: string[];
params?: Record<string, any>;
};
// +listType=atomic
urlParams?: {
name?: string;
content?: string;
}[];
}
export const defaultRoute = (): Route => ({
});
export interface IAM {
// +listType=atomic
permissions?: {
action?: string;
scope?: string;
}[];
}
export const defaultIAM = (): IAM => ({
});
export interface Role {
role?: {
name?: string;
description?: string;
// +listType=atomic
permissions?: {
action?: string;
scope?: string;
}[];
};
// +listType=set
grants?: string[];
}
export const defaultRole = (): Role => ({
});
export interface Extensions {
// +listType=atomic
addedComponents?: {
// +listType=set
targets: string[];
title: string;
description?: string;
}[];
// +listType=atomic
addedLinks?: {
// +listType=set
targets: string[];
title: string;
description?: string;
}[];
// +listType=atomic
addedFunctions?: {
// +listType=set
targets: string[];
title: string;
description?: string;
}[];
// +listType=set
// +listMapKey=id
exposedComponents?: {
id: string;
title?: string;
description?: string;
}[];
// +listType=set
// +listMapKey=id
extensionPoints?: {
id: string;
title?: string;
description?: string;
}[];
}
export const defaultExtensions = (): Extensions => ({
});
export interface Spec {
pluginJson: JSONData;
class: "core" | "external";
module?: {
path: string;
hash?: string;
loadingStrategy?: "fetch" | "script";
};
baseURL?: string;
signature?: {
status: "internal" | "valid" | "invalid" | "modified" | "unsigned";
type?: "grafana" | "commercial" | "community" | "private" | "private-glob";
org?: string;
};
angular?: {
detected: boolean;
};
translations?: Record<string, string>;
// +listType=atomic
children?: string[];
}
export const defaultSpec = (): Spec => ({
pluginJson: defaultJSONData(),
class: "core",
});

View File

@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface OperatorState {
// lastEvaluation is the ResourceVersion last evaluated
lastEvaluation: string;
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
state: "success" | "in_progress" | "failed";
// descriptiveState is an optional more descriptive state field which has no requirements on format
descriptiveState?: string;
// details contains any extra information that is operator-specific
details?: Record<string, any>;
}
export const defaultOperatorState = (): OperatorState => ({
lastEvaluation: "",
state: "success",
});
export interface Status {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
operatorStates?: Record<string, OperatorState>;
// additionalFields is reserved for future use
additionalFields?: Record<string, any>;
}
export const defaultStatus = (): Status => ({
});

View File

@@ -0,0 +1,49 @@
/*
* This file was generated by grafana-app-sdk. DO NOT EDIT.
*/
import { Spec } from './types.spec.gen';
import { Status } from './types.status.gen';
export interface Metadata {
name: string;
namespace: string;
generateName?: string;
selfLink?: string;
uid?: string;
resourceVersion?: string;
generation?: number;
creationTimestamp?: string;
deletionTimestamp?: string;
deletionGracePeriodSeconds?: number;
labels?: Record<string, string>;
annotations?: Record<string, string>;
ownerReferences?: OwnerReference[];
finalizers?: string[];
managedFields?: ManagedFieldsEntry[];
}
export interface OwnerReference {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller?: boolean;
blockOwnerDeletion?: boolean;
}
export interface ManagedFieldsEntry {
manager?: string;
operation?: string;
apiVersion?: string;
time?: string;
fieldsType?: string;
subresource?: string;
}
export interface Plugin {
kind: string;
apiVersion: string;
metadata: Metadata;
spec: Spec;
status: Status;
}

View File

@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
export interface Metadata {
updateTimestamp: string;
createdBy: string;
uid: string;
creationTimestamp: string;
deletionTimestamp?: string;
finalizers: string[];
resourceVersion: string;
generation: number;
updatedBy: string;
labels: Record<string, string>;
}
export const defaultMetadata = (): Metadata => ({
updateTimestamp: "",
createdBy: "",
uid: "",
creationTimestamp: "",
finalizers: [],
resourceVersion: "",
generation: 0,
updatedBy: "",
labels: {},
});

View File

@@ -0,0 +1,13 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface Spec {
id: string;
version: string;
url?: string;
}
export const defaultSpec = (): Spec => ({
id: "",
version: "",
});

View File

@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface OperatorState {
// lastEvaluation is the ResourceVersion last evaluated
lastEvaluation: string;
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
state: "success" | "in_progress" | "failed";
// descriptiveState is an optional more descriptive state field which has no requirements on format
descriptiveState?: string;
// details contains any extra information that is operator-specific
details?: Record<string, any>;
}
export const defaultOperatorState = (): OperatorState => ({
lastEvaluation: "",
state: "success",
});
export interface Status {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
operatorStates?: Record<string, OperatorState>;
// additionalFields is reserved for future use
additionalFields?: Record<string, any>;
}
export const defaultStatus = (): Status => ({
});

View File

@@ -2067,7 +2067,7 @@
}
],
"title": "Numeric, no series",
"type": "gauge"
"type": "radialbar"
},
{
"datasource": {
@@ -2131,7 +2131,7 @@
}
],
"title": "Non-numeric",
"type": "gauge"
"type": "radialbar"
}
],
"preload": false,

View File

@@ -248,7 +248,7 @@
"legend": {
"values": ["percent"],
"displayMode": "table",
"placement": "right"
"placement": "bottom"
},
"pieType": "pie",
"reduceOptions": {
@@ -256,7 +256,7 @@
"fields": "",
"values": false
},
"showLegend": true,
"showLegend": false,
"strokeWidth": 1,
"text": {}
},
@@ -272,15 +272,6 @@
"timeFrom": null,
"timeShift": null,
"title": "Percent",
"transformations": [
{
"id": "renameByRegex",
"options": {
"regex": "^Backend-(.*)$",
"renamePattern": "b-$1"
}
}
],
"type": "piechart"
},
{
@@ -320,7 +311,7 @@
"legend": {
"values": ["value"],
"displayMode": "table",
"placement": "right"
"placement": "bottom"
},
"pieType": "pie",
"reduceOptions": {
@@ -328,7 +319,7 @@
"fields": "",
"values": false
},
"showLegend": true,
"showLegend": false,
"strokeWidth": 1,
"text": {}
},
@@ -344,15 +335,6 @@
"timeFrom": null,
"timeShift": null,
"title": "Value",
"transformations": [
{
"id": "renameByRegex",
"options": {
"regex": "(.*)",
"renamePattern": "$1-how-much-wood-could-a-woodchuck-chuck-if-a-woodchuck-could-chuck-wood"
}
}
],
"type": "piechart"
},
{

View File

@@ -71,6 +71,11 @@ dc_resource("tempo", labels=["observability"])
dc_resource("postgres", labels=["misc"])
dc_resource("tempo-init", labels=["misc"])
dc_resource("oauthkeycloakdb", labels=["auth"])
dc_resource("oauthkeycloak",
resource_deps=["oauthkeycloakdb"],
labels=["auth"]
)
# paths in tilt files are confusing....
# - if tilt is dealing with the path, it is relative to the Tiltfile
@@ -86,6 +91,7 @@ docker_build('grafana-fs-dev',
'devenv/frontend-service/provisioning',
'devenv/frontend-service/configs/grafana-api.local.ini',
'devenv/frontend-service/configs/frontend-service.local.ini',
'devenv/frontend-service/oauth',
'conf/defaults.ini',
'public/emails',
'public/views',
@@ -107,6 +113,7 @@ docker_build('grafana-fs-dev',
sync('../../public/dashboards', '/grafana/public/dashboards'),
sync('../../public/app/plugins', '/grafana/public/app/plugins'),
sync('../../public/build/assets-manifest.json', '/grafana/public/build/assets-manifest.json'),
sync('./oauth', '/grafana/devenv/frontend-service/oauth'),
sync('./provisioning', '/ignore/provisioning'), # Just to trigger a restart instead of rebuild
sync('./configs/grafana-api.local.ini', '/ignore/grafana-api.local.ini'), # Just to trigger a restart instead of rebuild
sync('./configs/frontend-service.local.ini', '/ignore/frontend-service.local.ini'), # Just to trigger a restart instead of rebuild

View File

@@ -73,7 +73,7 @@ server {
}
# API calls go to the backend
location ~ ^/(api|apis|avatar|bootdata|render|logout|public/plugins|public/openapi3|public/api-merged|goto|swagger|openapi/v3) {
location ~ ^/(api|apis|avatar|bootdata|render|logout|public/plugins|public/openapi3|public/api-merged|goto|swagger|openapi/v3|login/generic_oauth) {
# Add debug headers to the response
add_header Nginx-Trace-Id $otel_trace_id always;
add_header Nginx-Route "backend" always;

View File

@@ -150,6 +150,36 @@ services:
tempo-init:
condition: service_completed_successfully
oauthkeycloakdb:
image: docker.io/library/postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
volumes:
- ./oauth/cloak.sql:/docker-entrypoint-initdb.d/cloak.sql
- oauth-keycloak-data:/var/lib/postgresql/data
labels:
- 'alloy.logs=true'
oauthkeycloak:
image: quay.io/keycloak/keycloak:23.0
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://oauthkeycloakdb/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
PROXY_ADDRESS_FORWARDING: "true"
ports:
- '8087:8080'
depends_on:
- oauthkeycloakdb
labels:
- 'alloy.logs=true'
volumes:
backend-data:
postgres-data:
@@ -157,3 +187,4 @@ volumes:
loki-data:
tempo-data:
prometheus-data:
oauth-keycloak-data:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"keys": [
{
"kid": "On2FQuJ8Y-909uJGWQEDkbzG-GRNmMc43HslEgVv_VQ",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "qDmQHfTcOQOzmNJbVvtvuS8p_EgmiscP7vA_PZNyKx9O7utyGuoAmJH8e2w8gLIDDWHl5_x8aAIl_-TTPTSiyX8I68ryIdR28ZSe5u4pRdpXCVvJpOefKNIxQCTH7rs4KuRj0HZ2u1mu1Vz5_CeCCoKwKSmheD3u1xTJ8-VxQmdqfGxhuKtnkof7977HWOWy4GLDFqxyYHgihP_MmSeTmXUhVeZI6IOCqHMpF8eFWVGKM6V8rIKf8QO2K_vDJBM_3C933vMY8mqSQXbI3G54x-0myAaQXr4JkxjvUGKg5YC3ZXw7AjfZv_W_fQOG0GYp2hQ0akR4KNKT3XPNmpMVlQ",
"e": "AQAB",
"x5c": [
"MIICnTCCAYUCBgF+u1ir8jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdncmFmYW5hMB4XDTIyMDIwMjE2NDkxN1oXDTMyMDIwMjE2NTA1N1owEjEQMA4GA1UEAwwHZ3JhZmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKg5kB303DkDs5jSW1b7b7kvKfxIJorHD+7wPz2TcisfTu7rchrqAJiR/HtsPICyAw1h5ef8fGgCJf/k0z00osl/COvK8iHUdvGUnubuKUXaVwlbyaTnnyjSMUAkx+67OCrkY9B2drtZrtVc+fwnggqCsCkpoXg97tcUyfPlcUJnanxsYbirZ5KH+/e+x1jlsuBiwxascmB4IoT/zJknk5l1IVXmSOiDgqhzKRfHhVlRijOlfKyCn/EDtiv7wyQTP9wvd97zGPJqkkF2yNxueMftJsgGkF6+CZMY71BioOWAt2V8OwI32b/1v30DhtBmKdoUNGpEeCjSk91zzZqTFZUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABlW64QxuREB81VMGsyhj4Q5RykFaVuD5O8YlwUpmVfAVLzb0Drf54Kn4bnpnckKyYV+T+HsN4QXt81UE41xH0Aai2H3vrGH+PJf6aLPCDE+jpMqtN3n6IgImJXJPL8upMfhhWDv4nkM4uynEwWupzmrKi4oJuTETSMktJby4o6//XWnCzCVMoAGFJU4gtjBUzOMLW26zD+yc+BuUtfR3HzItVHSZKQSNSFO0kVS68RgrER8qJw07z3BOJ2bPpPM0PYyEngGMaowz/T6lI32ymGMWYMAnslthS1KAW9xcTBwnrW1nMhe5a0LPxIktys/wJtxIHZLc5sOddGT4xYklLg=="
],
"x5t": "prs-h1NBqOSJMH-tQWLTqguWets",
"x5t#S256": "YjK3HobZW8xbNL1IPDgFhCM41UC5c0hG2cxaF6v961Q"
}
]
}

View File

@@ -25,10 +25,6 @@ cards:
height: 24
href: ./foundation-sdk/
description: The Grafana Foundation SDK is a set of tools, types, and libraries that let you define Grafana dashboards and resources using familiar programming languages like Go, TypeScript, Python, Java, and PHP. Use it in conjunction with `grafanactl` to push your programmatically generated resources.
- title: JSON schema v2
height: 24
href: ./schema-v2/
description: Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings. Observability as Code works with all versions of the JSON model, and it's fully compatible with version 2.
- title: Git Sync (private preview)
height: 24
href: ./provision-resources/intro-git-sync/
@@ -68,7 +64,7 @@ Historically, managing Grafana as code involved various community and Grafana La
- This approach requires handling HTTP requests and responses but provides complete control over resource management.
- `grafanactl`, Git Sync, and the Foundation SDK are all built on top of these APIs.
- To understand Dashboard Schemas accepted by the APIs, refer to the [JSON models documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/).
- To understand Dashboard Schemas accepted by the APIs, refer to the [JSON models documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/visualizations/dashboards/build-dashboards/view-dashboard-json-model/index.md).
## Explore

View File

@@ -1,243 +0,0 @@
---
description: A reference for the JSON dashboard schemas used with Observability as Code, including the experimental V2 schema.
keywords:
- configuration
- as code
- dashboards
- git integration
- git sync
- github
labels:
products:
- cloud
- enterprise
- oss
title: JSON schema v2
weight: 500
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/
aliases:
- ../../observability-as-code/schema-v2/ # /docs/grafana/next/observability-as-code/schema-v2/
---
# Dashboard JSON schema v2
{{< admonition type="caution" >}}
Dashboard JSON schema v2 is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog).
**Do not enable this feature in production environments as it may result in the irreversible loss of data.**
{{< /admonition >}}
Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings.
Observability as Code works with all versions of the JSON model, and it's fully compatible with version 2.
## Before you begin
Schema v2 is automatically enabled with the Dynamic Dashboards feature toggle.
To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog).
It also requires the new dashboards API feature toggle, `kubernetesDashboards`, to be enabled as well.
For more information on how dashboards behave depending on your feature flag configuration, refer to [Notes and limitations](#notes-and-limitations).
## Accessing the JSON Model
To view the JSON representation of a dashboard:
1. Toggle on the edit mode switch in the top-right corner of the dashboard.
1. Click the gear icon in the top navigation bar to go to **Settings**.
1. Select the **JSON Model** tab.
1. Copy or edit the JSON structure as needed.
## JSON fields
```json
{
"annotations": [],
"cursorSync": "Off",
"editable": true,
"elements": {},
"layout": {
"kind": GridLayout, // Can also be AutoGridLayout, RowsLayout, or TabsLayout
"spec": {
"items": []
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [], // Tags associated with the dashboard.
"timeSettings": {
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"fiscalYearStartMonth": 0,
"from": "now-6h",
"hideTimepicker": false,
"timezone": "browser",
"to": "now"
},
"title": "",
"variables": []
},
```
The dashboard JSON sample shown uses the default `GridLayoutKind`.
The JSON in a new dashboard for the other three layout options, `AutoGridLayout`, `RowsLayout`, and `TabsLayout`, are as follows:
**`AutoGridLayout`**
```json
"layout": {
"kind": "AutoGridLayout",
"spec": {
"columnWidthMode": "standard",
"items": [],
"fillScreen": false,
"maxColumnCount": 3,
"rowHeightMode": "standard"
}
},
```
**`RowsLayout`**
```json
"layout": {
"kind": "RowsLayout",
"spec": {
"rows": []
},
```
**`TabsLayout`**
```json
"layout": {
"kind": "TabsLayout",
"spec": {
"tabs": []
},
```
### `DashboardSpec`
The following table explains the usage of the dashboard JSON fields.
The table includes default and other fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ------------ | ------------------------------------------------------------------------- |
| annotations | Contains the list of annotations that are associated with the dashboard. |
| cursorSync | Dashboard cursor sync behavior.<ul><li>`Off` - No shared crosshair or tooltip (default)</li><li>`Crosshair` - Shared crosshair</li><li>`Tooltip` - Shared crosshair and shared tooltip</li></ul> |
| editable | bool. Whether or not a dashboard is editable. |
| elements | Contains the list of elements included in the dashboard. Supported dashboard elements are: PanelKind and LibraryPanelKind. |
| layout | The dashboard layout. Supported layouts are:<ul><li>GridLayoutKind</li><li>AutoGridLayoutKind</li><li>RowsLayoutKind</li><li>TabsLayoutKind</li></ul> |
| links | Links with references to other dashboards or external websites. |
| liveNow | bool. When set to `true`, the dashboard redraws panels at an interval matching the pixel width. This keeps data "moving left" regardless of the query refresh rate. This setting helps avoid dashboards presenting stale live data. |
| preload | bool. When set to `true`, the dashboard loads all panels when the dashboard is loaded. |
| tags | Contains the list of tags associated with dashboard. |
| timeSettings | All time settings for the dashboard. |
| title | Title of the dashboard. |
| variables | Contains the list of configured template variables. |
<!-- prettier-ignore-end -->
### `annotations`
The configuration for the list of annotations that are associated with the dashboard.
For the JSON and field usage notes, refer to the [annotations schema documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/annotations-schema/).
### `elements`
Dashboards can contain the following elements:
- [PanelKind](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/panel-schema/)
- [LibraryPanelKind](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/librarypanel-schema/)
### `layout`
Dashboards can have four layout options:
- [GridLayoutKind](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/layout-schema/#gridlayoutkind)
- [AutoGridLayoutKind](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/layout-schema/#autogridlayoutkind)
- [RowsLayoutKind](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/layout-schema/#rowslayoutkind)
- [TabsLayoutKind](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/layout-schema/#tabslayoutkind)
For the JSON and field usage notes about each of these, refer to the [layout schema documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/layout-schema/).
### `links`
The configuration for links with references to other dashboards or external websites.
For the JSON and field usage notes, refer to the [links schema documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/links-schema/).
### `tags`
Tags associated with the dashboard. Each tag can be up to 50 characters long.
` [...string]`
### `timesettings`
The `TimeSettingsSpec` defines the default time configuration for the time picker and the refresh picker for the specific dashboard.
For the JSON and field usage notes about the `TimeSettingsSpec`, refer to the [timesettings schema documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/timesettings-schema/).
### `variables`
The `variables` schema defines which variables are used in the dashboard.
There are eight variables types:
- QueryVariableKind
- TextVariableKind
- ConstantVariableKind
- DatasourceVariableKind
- IntervalVariableKind
- CustomVariableKind
- GroupByVariableKind
- AdhocVariableKind
For the JSON and field usage notes about the `variables` spec, refer to the [variables schema documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/variables-schema/).
## Notes and limitations
### Existing dashboards
With schema v2 enabled, you can still open and view your pre-existing dashboards.
Upon saving, theyll be updated to the new schema where you can take advantage of the new features and functionalities.
### Dashboard behavior with disabled feature flags
If you disable the Dynamic dashboards or `kubernetesDashboards` feature flags, you should be aware of how dashboards will behave.
#### Disable Dynamic dashboards
If the Dynamic dashboards feature toggle is disabled, depending on how the dashboard was built, it will behave differently:
- Dashboards built on the new schema through the UI - View only
- Dashboards built on Schema v1 - View and edit
- Dashboards built on the new schema by way of Terraform or the CLI - View and edit
- Provisioned dashboards built on the new schema - View and edit, but the edit experience will be the old experience
#### Disable Dynamic dashboards and `kubernetesDashboards`
Youll be unable to view or edit dashboards created or updated in the new schema.
### Import and export
From the UI, dashboards created on schema v2 can be exported and imported like other dashboards.
When you export them to use in another instance, references of data sources are not persisted but data source types are.
Youll have the option to select the data source of your choice in the import UI.

View File

@@ -1,86 +0,0 @@
---
description: A reference for the JSON annotations schema used with Observability as Code.
keywords:
- configuration
- as code
- as-code
- dashboards
- git integration
- git sync
- github
- annotations
labels:
products:
- cloud
- enterprise
- oss
menuTitle: annotations schema
title: annotations
weight: 100
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/annotations-schema/
aliases:
- ../../../observability-as-code/schema-v2/annotations-schema/ # /docs/grafana/next/observability-as-code/schema-v2/annotations-schema/
---
# `annotations`
The configuration for the list of annotations that are associated with the dashboard.
```json
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"builtIn": false,
"datasource": {
"type": "",
"uid": ""
},
"enable": false,
"hide": false,
"iconColor": "",
"name": ""
}
}
],
```
`AnnotationsQueryKind` consists of:
- kind: "AnnotationQuery"
- spec: [AnnotationQuerySpec](#annotationqueryspec)
## `AnnotationQuerySpec`
| Name | Type/Definition |
| ---------- | ----------------------------------------------------------------- |
| datasource | [`DataSourceRef`](#datasourceref) |
| query | [`DataQueryKind`](#dataquerykind) |
| enable | bool |
| hide | bool |
| iconColor | string |
| name | string |
| builtIn | bool. Default is `false`. |
| filter | [`AnnotationPanelFilter`](#annotationpanelfilter) |
| options | `[string]`: A catch-all field for datasource-specific properties. |
### `DataSourceRef`
| Name | Usage |
| ----- | ---------------------------------- |
| type? | string. The plugin type-id. |
| uid? | The specific data source instance. |
### `DataQueryKind`
| Name | Type |
| ---- | ------ |
| kind | string |
| spec | string |
### `AnnotationPanelFilter`
| Name | Type/Definition |
| -------- | ------------------------------------------------------------------------------ |
| exclude? | bool. Should the specified panels be included or excluded. Default is `false`. |
| ids | `[...uint8]`. Panel IDs that should be included or excluded. |

View File

@@ -1,339 +0,0 @@
---
description: A reference for the JSON layout schema used with Observability as Code.
keywords:
- configuration
- as code
- as-code
- dashboards
- git integration
- git sync
- github
- layout
labels:
products:
- cloud
- enterprise
- oss
menuTitle: layout schema
title: layout
weight: 400
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/layout-schema/
aliases:
- ../../../observability-as-code/schema-v2/layout-schema/ # /docs/grafana/next/observability-as-code/schema-v2/layout-schema/
---
# `layout`
There are four layout options offering two types of panel control:
**Panel layout options**
These options control the size and position of panels:
- [GridLayoutKind](#gridlayoutkind) - Corresponds to the **Custom** option in the UI. You define panel size and panel positions using x- and y- settings.
- [AutoGridLayoutKind](#autogridlayoutkind) - Corresponds to the **Auto grid** option in the UI. Panel size and position are automatically set based on column and row parameters.
**Panel grouping options**
These options control the grouping of panels:
- [RowsLayoutKind](#rowslayoutkind) - Groups panels into rows.
- [TabsLayoutKind](#tabslayoutkind) - Groups panels into tabs.
## `GridLayoutKind`
The grid layout allows you to manually size and position grid items by setting the height, width, x, and y of each item.
This layout corresponds to the **Custom** option in the UI.
Following is the JSON for a default grid layout, a grid layout item, and a grid layout row:
```json
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"element": {...},
"height": 0,
"width": 0,
"x": 0,
"y": 0
}
},
{
"kind": "GridLayoutRow",
"spec": {
"collapsed": false,
"elements": [],
"title": "",
"y": 0
}
},
]
}
```
`GridLayoutKind` consists of:
- kind: "GridLayout"
- spec: GridLayoutSpec
- items: GridLayoutItemKind` or GridLayoutRowKind`
- GridLayoutItemKind
- kind: "GridLayoutItem"
- spec: [GridLayoutItemSpec](#gridlayoutitemspec)
- GridLayoutRowKind
- kind: "GridLayoutRow"
- spec: [GridLayoutRowSpec](#gridlayoutrowspec)
### `GridLayoutItemSpec`
The following table explains the usage of the grid layout item JSON fields:
| Name | Usage |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| x | integer. Position of the item x-axis. |
| y | integer. Position of the item y-axis. |
| width | Width of the item in pixels. |
| height | Height of the item in pixels. |
| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. |
| repeat? | [RepeatOptions](#repeatoptions). Configured repeat options, if any |
#### `RepeatOptions`
The following table explains the usage of the repeat option JSON fields:
| Name | Usage |
| ---------- | ---------------------------------------------------- |
| mode | `RepeatMode` - "variable" |
| value | string |
| direction? | Options are `h` for horizontal and `v` for vertical. |
| maxPerRow? | integer |
### `GridLayoutRowSpec`
The following table explains the usage of the grid layout row JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ---- | ----- |
| y | integer. Position of the row y-axis |
| collapsed | bool. Whether or not the row is collapsed |
| title | Row title |
| elements | [`[...GridLayoutItemKind]`](#gridlayoutitemspec). Grid items in the row will have their y value be relative to the row's y value. This means a panel positioned at `y: 0` in a row with `y: 10` will be positioned at `y: 11` (row header has a height of 1) in the dashboard. |
| repeat? | [RowRepeatOptions](#rowrepeatoptions) Configured row repeat options, if any</p> |
<!-- prettier-ignore-end -->
#### `RowRepeatOptions`
| Name | Usage |
| ----- | ------------------------- |
| mode | `RepeatMode` - "variable" |
| value | string |
## `AutoGridLayoutKind`
With an auto grid, Grafana sizes and positions your panels for the best fit based on the column and row constraints that you set.
This layout corresponds to the **Auto grid** option in the UI.
Following is the JSON for a default auto grid layout and a grid layout item:
<!-- prettier-ignore-end -->
```json
"kind": "AutoGridLayout",
"spec": {
"columnWidthMode": "standard",
"fillScreen": false,
"items": [
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {...},
}
}
],
"maxColumnCount": 3,
"rowHeightMode": "standard"
}
```
`AutoGridLayoutKind` consists of:
- kind: "AutoGridLayout"
- spec: [AutoGridLayoutSpec](#autogridlayoutspec)
### `AutoGridLayoutSpec`
The following table explains the usage of the auto grid layout JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ---- | ----- |
| maxColumnCount? | number. Default is `3`. |
| columnWidthMode | Options are: `narrow`, `standard`, `wide`, and `custom`. Default is `standard`. |
| columnWidth? | number |
| rowHeightMode | Options are: `short`, `standard`, `tall`, and `custom`. Default is `standard`. |
| rowHeight? | number |
| fillScreen? | bool. Default is `false`. |
| items | `AutoGridLayoutItemKind`. Consists of:<ul><li>kind: "AutoGridLayoutItem"</li><li>spec: [AutoGridLayoutItemSpec](#autogridlayoutitemspec)</li></ul> |
<!-- prettier-ignore-end -->
#### `AutoGridLayoutItemSpec`
The following table explains the usage of the auto grid layout item JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ---- | ----- |
| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. |
| repeat? | [AutoGridRepeatOptions](#autogridrepeatoptions). Configured repeat options, if any. |
| conditionalRendering? | `ConditionalRenderingGroupKind`. Rules for hiding or showing panels, if any. Consists of:<ul><li>kind: "ConditionalRenderingGroup"</li><li>spec: [ConditionalRenderingGroupSpec](#conditionalrenderinggroupspec)</li></ul> |
<!-- prettier-ignore-end -->
##### `AutoGridRepeatOptions`
The following table explains the usage of the auto grid repeat option JSON fields:
| Name | Usage |
| ----- | ------------------------- |
| mode | `RepeatMode` - "variable" |
| value | String |
##### `ConditionalRenderingGroupSpec`
<!-- prettier-ignore-start -->
| Name | Usage |
| ---- | ----- |
| visibility | Options are `show` and `hide` |
| condition | Options are `and` and `or` |
| items | Options are:<ul><li>ConditionalRenderingVariableKind<ul><li>kind: "ConditionalRenderingVariable"</li><li>spec: [ConditionalRenderingVariableSpec](#conditionalrenderingvariablespec)</li></ul></li><li>ConditionalRenderingDataKind<ul><li>kind: "ConditionalRenderingData"</li><li>spec: [ConditionalRenderingDataSpec](#conditionalrenderingdataspec)</li></ul></li><li>ConditionalRenderingTimeRangeSizeKind<ul><li>kind: "ConditionalRenderingTimeRangeSize"</li><li>spec: [ConditionalRenderingTimeRangeSizeSpec](#conditionalrenderingtimerangesizespec)</li></ul></li></ul> |
<!-- prettier-ignore-end -->
###### `ConditionalRenderingVariableSpec`
| Name | Usage |
| -------- | ------------------------------------ |
| variable | string |
| operator | Options are `equals` and `notEquals` |
| value | string |
###### `ConditionalRenderingDataSpec`
| Name | Type |
| ----- | ---- |
| value | bool |
###### `ConditionalRenderingTimeRangeSizeSpec`
| Name | Type |
| ----- | ------ |
| value | string |
## `RowsLayoutKind`
The `RowsLayoutKind` is one of two options that you can use to group panels.
You can nest any other kind of layout inside a layout row.
Rows can also be nested in auto grids or tabs.
Following is the JSON for a default rows layout row:
```json
"kind": "RowsLayout",
"spec": {
"rows": [
{
"kind": "RowsLayoutRow",
"spec": {
"layout": {
"kind": "GridLayout", // Can also be AutoGridLayout or TabsLayout
"spec": {...}
},
"title": ""
}
}
]
}
```
`RowsLayoutKind` consists of:
- kind: RowsLayout
- spec: RowsLayoutSpec
- rows: RowsLayoutRowKind
- kind: RowsLayoutRow
- spec: [RowsLayoutRowSpec](#rowslayoutrowspec)
### `RowsLayoutRowSpec`
The following table explains the usage of the rows layout row JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ---- | ----- |
| title? | Title of the row. |
| collapse | bool. Whether or not the row is collapsed. |
| hideHeader? | bool. Whether the row header is hidden or shown. |
| fullScreen? | bool. Whether or not the row takes up the full screen. |
| conditionalRendering? | `ConditionalRenderingGroupKind`. Rules for hiding or showing rows, if any. Consists of:<ul><li>kind: "ConditionalRenderingGroup"</li><li>spec: [ConditionalRenderingGroupSpec](#conditionalrenderinggroupspec)</li></ul> |
| repeat? | [RowRepeatOptions](#rowrepeatoptions). Configured repeat options, if any. |
| layout | Supported layouts are:<ul><li>[GridLayoutKind](#gridlayoutkind)</li><li>[RowsLayoutKind](#rowslayoutkind)</li><li>[AutoGridLayoutKind](#autogridlayoutkind)</li><li>[TabsLayoutKind](#tabslayoutkind)</li></ul> |
<!-- prettier-ignore-end -->
## `TabsLayoutKind`
The `TabsLayoutKind` is one of two options that you can use to group panels.
You can nest any other kind of layout inside a tab.
Tabs can also be nested in auto grids or rows.
Following is the JSON for a default tabs layout tab and a tab:
```json
"kind": "TabsLayout",
"spec": {
"tabs": [
{
"kind": "TabsLayoutTab",
"spec": {
"layout": {
"kind": "GridLayout", // Can also be AutoGridLayout or RowsLayout
"spec": {...}
},
"title": "New tab"
}
}
]
}
```
`TabsLayoutKind` consists of:
- kind: TabsLayout
- spec: TabsLayoutSpec
- tabs: TabsLayoutTabKind
- kind: TabsLayoutTab
- spec: [TabsLayoutTabSpec](#tabslayouttabspec)
### `TabsLayoutTabSpec`
The following table explains the usage of the tabs layout tab JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ---- | ----- |
| title? | The title of the tab. |
| layout | Supported layouts are:<ul><li>[GridLayoutKind](#gridlayoutkind)</li><li>[RowsLayoutKind](#rowslayoutkind)</li><li>[AutoGridLayoutKind](#autogridlayoutkind)</li><li>[TabsLayoutKind](#tabslayoutkind)</li></ul> |
| conditionalRendering? | `ConditionalRenderingGroupKind`. Rules for hiding or showing panels, if any. Consists of:<ul><li>kind: "ConditionalRenderingGroup"</li><li>spec: [ConditionalRenderingGroupSpec](#conditionalrenderinggroupspec)</li></ul> |
<!-- prettier-ignore-end -->

View File

@@ -1,68 +0,0 @@
---
description: A reference for the JSON library panel schema used with Observability as Code.
keywords:
- configuration
- as code
- as-code
- dashboards
- git integration
- git sync
- github
- library panel
labels:
products:
- cloud
- enterprise
- oss
menuTitle: LibraryPanelKind schema
title: LibraryPanelKind
weight: 300
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/librarypanel-schema/
aliases:
- ../../../observability-as-code/schema-v2/librarypanel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/librarypanel-schema/
---
# `LibraryPanelKind`
A library panel is a reusable panel that you can use in any dashboard.
When you make a change to a library panel, that change propagates to all instances of where the panel is used.
Library panels streamline reuse of panels across multiple dashboards.
Following is the default library panel element JSON:
```json
"kind": "LibraryPanel",
"spec": {
"id": 0,
"libraryPanel": {
name: "",
uid: "",
}
"title": ""
}
```
The `LibraryPanelKind` consists of:
- kind: "LibraryPanel"
- spec: [LibraryPanelKindSpec](#librarypanelkindspec)
- libraryPanel: [LibraryPanelRef](#librarypanelref)
## `LibraryPanelKindSpec`
The following table explains the usage of the library panel element JSON fields:
| Name | Usage |
| ------------ | ------------------------------------------------ |
| id | Panel ID for the library panel in the dashboard. |
| libraryPanel | [`LibraryPanelRef`](#librarypanelref) |
| title | Title for the library panel in the dashboard. |
### `LibraryPanelRef`
The following table explains the usage of the library panel reference JSON fields:
| Name | Usage |
| ---- | ------------------ |
| name | Library panel name |
| uid | Library panel uid |

View File

@@ -1,67 +0,0 @@
---
description: A reference for the JSON links schema used with Observability as Code.
keywords:
- configuration
- as code
- as-code
- dashboards
- git integration
- git sync
- github
- links
labels:
products:
- cloud
- enterprise
- oss
menuTitle: links schema
title: links
weight: 500
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/links-schema/
aliases:
- ../../../observability-as-code/schema-v2/links-schema/ # /docs/grafana/next/observability-as-code/schema-v2/links-schema/
---
# `links`
The `links` schema is the configuration for links with references to other dashboards or external websites.
Following are the default JSON fields:
```json
"links": [
{
"asDropdown": false,
"icon": "",
"includeVars": false,
"keepTime": false,
"tags": [],
"targetBlank": false,
"title": "",
"tooltip": "",
"type": "link",
},
],
```
## `DashboardLink`
The following table explains the usage of the dashboard link JSON fields.
The table includes default and other fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ----------- | --------------------------------------- |
| title | string. Title to display with the link. |
| type | `DashboardLinkType`. Link type. Accepted values are:<ul><li>dashboards - To refer to another dashboard</li><li>link - To refer to an external resource</li></ul> |
| icon | string. Icon name to be displayed with the link. |
| tooltip | string. Tooltip to display when the user hovers their mouse over it. |
| url? | string. Link URL. Only required/valid if the type is link. |
| tags | string. List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards. |
| asDropdown | bool. If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards. Default is `false`. |
| targetBlank | bool. If true, the link will be opened in a new tab. Default is `false`. |
| includeVars | bool. If true, includes current template variables values in the link as query params. Default is `false`. |
| keepTime | bool. If true, includes current time range in the link as query params. Default is `false`. |
| placement? | string. Use placement to display the link somewhere else on the dashboard other than above the visualizations. Use the `inControlsMenu` parameter to render the link in the dashboard controls dropdown menu. |
<!-- prettier-ignore-end -->

View File

@@ -1,305 +0,0 @@
---
description: A reference for the JSON panel schema used with Observability as Code.
keywords:
- configuration
- as code
- as-code
- dashboards
- git integration
- git sync
- github
- panels
labels:
products:
- cloud
- enterprise
- oss
menuTitle: PanelKind schema
title: PanelKind
weight: 200
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/panel-schema/
aliases:
- ../../../observability-as-code/schema-v2/panel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/panel-schema/
---
# `PanelKind`
The panel element contains all the information about the panel including the visualization type, panel and visualization configuration, queries, and transformations.
There's a panel element for each panel contained in the dashboard.
Following is the default panel element JSON:
```json
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {...},
"description": "",
"id": 0,
"links": [],
"title": "",
"vizConfig": {
"kind": "",
"spec": {...},
}
}
```
The `PanelKind` consists of:
- kind: "Panel"
- spec: [PanelSpec](#panelspec)
## `PanelSpec`
The following table explains the usage of the panel element JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ------------ | --------------------------------------------------------------------- |
| data | `QueryGroupKind`, which includes queries and transformations. Consists of:<ul><li>kind: "QueryGroup"</li><li>spec: [QueryGroupSpec](#querygroupspec)</li></ul> |
| description | The panel description. |
| id | The panel ID. |
| links | Links with references to other dashboards or external websites. |
| title | The panel title. |
| vizConfig | `VizConfigKind`. Includes visualization type, field configuration options, and all other visualization options. Consists of:<ul><li>kind: string. Plugin ID.</li><li>spec: [VizConfigSpec](#vizconfigspec)</li></ul> |
| transparent? | bool. Controls whether or not the panel background is transparent. |
<!-- prettier-ignore-end -->
### `QueryGroupSpec`
<!-- prettier-ignore-start -->
| Name | Usage |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| queries | `PanelQueryKind`. Consists of:<ul><li>kind: PanelQuery</li><li>spec: [PanelQuerySpec](#panelqueryspec)</li></ul> |
| transformations | `TransformationKind`. Consists of:<ul><li>kind: string. The transformation ID.</li><li>spec: [DataTransformerConfig](#datatransformerconfig)</li></ul> |
| queryOptions | [`QueryOptionsSpec`](#queryoptionsspec) |
<!-- prettier-ignore-end -->
#### `PanelQuerySpec`
| Name | Usage |
| ----------- | --------------------------------- |
| query | [`DataQueryKind`](#dataquerykind) |
| datasource? | [`DataSourceRef`](#datasourceref) |
##### `DataQueryKind`
| Name | Type |
| ---- | ------ |
| kind | string |
| spec | string |
##### `DataSourceRef`
| Name | Usage |
| ----- | ---------------------------------- |
| type? | string. The plugin type-id. |
| uid? | The specific data source instance. |
#### `DataTransformerConfig`
Transformations allow you to manipulate data returned by a query before the system applies a visualization.
Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, or use the output of one transformation as the input to another transformation.
<!-- prettier-ignore-start -->
| Name | Usage |
| --------- | ------------------------------------------- |
| id | string. Unique identifier of transformer. |
| disabled? | bool. Disabled transformations are skipped. |
| filter? | [`MatcherConfig`](#matcherconfig). Optional frame matcher. When missing it will be applied to all results. |
| topic? | `DataTopic`. Where to pull `DataFrames` from as input to transformation. Options are: `series`, `annotations`, and `alertStates`. |
| options | Options to be passed to the transformer. Valid options depend on the transformer id. |
<!-- prettier-ignore-end -->
##### `MatcherConfig`
Matcher is a predicate configuration.
Based on the configuration a set of field or values, it's filtered to apply an override or transformation.
It comes with in id (to resolve implementation from registry) and a configuration thats specific to a particular matcher type.
| Name | Usage |
| -------- | -------------------------------------------------------------------------------------- |
| id | string. The matcher id. This is used to find the matcher implementation from registry. |
| options? | The matcher options. This is specific to the matcher implementation. |
#### `QueryOptionsSpec`
| Name | Type |
| ----------------- | ------- |
| timeFrom? | string |
| maxDataPoints? | integer |
| timeShift? | string |
| queryCachingTTL? | integer |
| interval? | string |
| cacheTimeout? | string |
| hideTimeOverride? | bool |
### `VizConfigSpec`
| Name | Type/Definition |
| ------------- | --------------------------------------- |
| pluginVersion | string |
| options | string |
| fieldConfig | [FieldConfigSource](#fieldconfigsource) |
#### `FieldConfigSource`
The data model used in Grafana, namely the _data frame_, is a columnar-oriented table structure that unifies both time series and table query results.
Each column within this structure is called a field.
A field can represent a single time series or table column.
Field options allow you to change how the data is displayed in your visualizations.
<!-- prettier-ignore-start -->
| Name | Type/Definition |
| ---------- | ------------------------------------- |
| defaults | [`FieldConfig`](#fieldconfig). Defaults are the options applied to all fields. |
| overrides | The options applied to specific fields overriding the defaults. |
| matcher | [`MatcherConfig`](#matcherconfig). Optional frame matcher. When missing it will be applied to all results. |
| properties | `DynamicConfigValue`. Consists of:<ul><li>`id` - string</li><li>value?</li></ul> |
<!-- prettier-ignore-end -->
##### `FieldConfig`
<!-- prettier-ignore-start -->
| Name | Type/Definition |
| ------------------ | --------------------------------------- |
| displayName? | string. The display value for this field. This supports template variables where empty is auto. |
| displayNameFromDS? | string. This can be used by data sources that return an explicit naming structure for values and labels. When this property is configured, this value is used rather than the default naming strategy. |
| description? | string. Human readable field metadata. |
| path? | string. An explicit path to the field in the data source. 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 data source scope, and may be used to update the results. |
| writeable? | bool. True if the data source can write a value to the path. Auth/authz are supported separately. |
| filterable? | bool. True if the data source field supports ad-hoc filters. |
| unit? | string. Unit a field should use. The unit you select is applied to all fields except time. You can use the unit's ID available in Grafana or a custom unit. [Available units in Grafana](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts). As custom units, you can use the following formats:<ul><li>`suffix:<suffix>` for custom unit that should go after value.</li><li>`prefix:<prefix>` for custom unit that should go before value.</li><li> `time:<format>` for custom date time formats type for example</li><li>`time:YYYY-MM-DD`</li><li>`si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. You can specify both a unit and the source data scale, so if your source data is represented as milli (thousands of) something, prefix the unit with that SI scale character.</li><li>`count:<unit>` for a custom count unit.</li><li>`currency:<unit>` for custom a currency unit.</li></ul> |
| decimals? | number. Specify the number of decimals Grafana includes in the rendered value. If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. For example 1.1234 will display as 1.12 and 100.456 will display as 100. To display all decimals, set the unit to `string`. |
| min? | number. The minimum value used in percentage threshold calculations. Leave empty for auto calculation based on all series and fields. |
| max? | number. The maximum value used in percentage threshold calculations. Leave empty for auto calculation based on all series and fields. |
| mappings? | `[...ValueMapping]`. Convert input values into a display string. Options are: [`ValueMap`](#valuemap), [`RangeMap`](#rangemap), [`RegexMap`](#rangemap), [`SpecialValueMap`](#specialvaluemap). |
| thresholds? | `ThresholdsConfig`. Map numeric values to states. Consists of:<ul><li>`mode` - `ThresholdsMode`. Options are: `absolute` and `percentage`.</li><li>`steps` - `[...Threshold]`</li></ul> |
| color? | [`FieldColor`](#fieldcolor). Panel color configuration. |
| links? | `[...]`. The behavior when clicking a result. |
| noValue? | string. Alternative to an empty string. |
| custom? | `{...}`. Specified by the `FieldConfig` field in panel plugin schemas. |
<!-- prettier-ignore-end -->
###### `ValueMap`
Maps text values to a color or different display text and color.
For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
<!-- prettier-ignore-start -->
| Name | Usage |
| ------- | -------- |
| type | `MappingType` & "value". `MappingType` options are: `value`, `range`, `regex`, and `special`. |
| options | string. [`ValueMappingResult`](#valuemappingresult). Map with `<value_to_match>`: `ValueMappingResult`. For example: `{ "10": { text: "Perfection!", color: "green" } }`. |
<!-- prettier-ignore-end -->
###### `RangeMap`
Maps numerical ranges to a display text and color.
For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
<!-- prettier-ignore-start -->
| Name | Usage |
| ------- | ---------------------------------------------------------------------------------------------------- |
| type | `MappingType` & "range". `MappingType` options are: `value`, `range`, `regex`, and `special`. |
| options | Range to match against and the result to apply when the value is within the range. Spec:<ul><li>`from` - `float64` or `null`. Min value of the range. It can be null which means `-Infinity`.</li><li>`to` - `float64` or `null`. Max value of the range. It can be null which means `+Infinity`.</li><li>`result` - [`ValueMappingResult`](#valuemappingresult) |
<!-- prettier-ignore-end -->
###### `RegexMap`
Maps regular expressions to replacement text and a color.
For example, if a value is `www.example.com`, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
<!-- prettier-ignore-start -->
| Name | Usage |
| ------- | --------------------------------------------------------------------------------------------- |
| type | `MappingType` & "regex". `MappingType` options are: `value`, `range`, `regex`, and `special`. |
| options | Regular expression to match against and the result to apply when the value matches the regex. Spec:<ul><li>`pattern` - string. Regular expression to match against.</li><li>`result` - [`ValueMappingResult`](#valuemappingresult) |
<!-- prettier-ignore-end -->
###### `SpecialValueMap`
Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.
See `SpecialValueMatch` in the following table to see the list of special values.
For example, you can configure a special value mapping so that null values appear as N/A.
<!-- prettier-ignore-start -->
| Name | Usage |
| ------- | ----------------------------------------------------------------------------------------------- |
| type | `MappingType` & "special". `MappingType` options are: `value`, `range`, `regex`, and `special`. |
| options | Spec:<ul><li>`match` - `SpecialValueMatch`. Special value to match against. Types are:<ul><li>true</li><li>false</li><li>null</li><li>nan</li><li>empty</li></ul> </li><li>`result` - [`ValueMappingResult`](#valuemappingresult) |
<!-- prettier-ignore-end -->
###### `ValueMappingResult`
Result used as replacement with text and color when the value matches.
| Name | Usage |
| ----- | ----------------------------------------------------------------------------- |
| text | string. Text to display when the value matches. |
| color | string. Color to use when the value matches. |
| icon | string. Icon to display when the value matches. Only specific visualizations. |
| index | int32. Position in the mapping array. Only used internally. |
###### `FieldColor`
Map a field to a color.
<!-- prettier-ignore-start -->
| Name | Usage |
| ----------- | -------------------------------------------------------------------- |
| mode | [`FieldColorModeId`](#fieldcolormodeid). The main color scheme mode. |
| FixedColor? | string. The fixed color value for fixed or shades color modes. |
| seriesBy? | `FieldColorSeriesByMode`. Some visualizations need to know how to assign a series color from by value color schemes. 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. Options are: `min`, `max`, and `last`. |
<!-- prettier-ignore-end -->
###### `FieldColorModeId`
Color mode for a field.
You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
Continuous color interpolates a color using the percentage of a value relative to min and max.
Accepted values are:
<!-- prettier-ignore-start -->
| Name | Description |
| --- | ---- |
| 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 | 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 |
| continuous-BlPu | Continuous Blue-Purple palette mode |
| continuous-YlBl | Continuous Yellow-Blue palette mode |
| continuous-blues | Continuous Blue palette mode |
| continuous-reds | Continuous Red palette mode |
| continuous-greens | Continuous Green palette mode |
| 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. |
<!-- prettier-ignore-end -->

View File

@@ -1,87 +0,0 @@
---
description: A reference for the JSON timesettings schema used with Observability as Code.
keywords:
- configuration
- as code
- as-code
- dashboards
- git integration
- git sync
- github
- time settings
labels:
products:
- cloud
- enterprise
- oss
menuTitle: timesettings schema
title: timesettings
weight: 600
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/timesettings-schema/
aliases:
- ../../../observability-as-code/schema-v2/timesettings-schema/ # /docs/grafana/next/observability-as-code/schema-v2/timesettings-schema/
---
# `timeSettings`
The `TimeSettingsSpec` defines the default time configuration for the time picker and the refresh picker for the specific dashboard.
Following is the JSON for default time settings:
```json
"timeSettings": {
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"fiscalYearStartMonth": 0,
"from": "now-6h",
"hideTimepicker": false,
"timezone": "browser",
"to": "now"
},
```
`timeSettings` consists of:
- [TimeSettingsSpec](#timesettingsspec)
## `TimeSettingsSpec`
The following table explains the usage of the time settings JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ---- | ----- |
| timezone? | string. Timezone of dashboard. Accepted values are IANA TZDB zone ID, `browser`, or `utc`. Default is `browser`. |
| from | string. Start time range for dashboard. Accepted values are relative time strings like `now-6h` or absolute time strings like `2020-07-10T08:00:00.000Z`. Default is `now-6h`. |
| to | string. End time range for dashboard. Accepted values are relative time strings like `now-6h` or absolute time strings like `2020-07-10T08:00:00.000Z`. Default is `now`. |
| autoRefresh | string. Refresh rate of dashboard. Represented by interval string. For example: `5s`, `1m`, `1h`, `1d`. No default. In schema v1: `refresh`. |
| autoRefreshIntervals | string. Interval options available in the refresh picker drop-down menu. The default array is `["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]`. |
|quickRanges? | Selectable options available in the time picker drop-down menu. Has no effect on provisioned dashboard. Defined in the [`TimeRangeOption`](#timerangeoption) spec. In schema v1: `timepicker.quick_ranges`, not exposed in the UI. |
| hideTimepicker | bool. Whether or not the time picker is visible. Default is `false`. In schema v1: `timepicker.hidden`. |
| weekStart? | Day when the week starts. Expressed by the name of the day in lowercase. For example: `monday`. Options are `saturday`, `monday`, and `sunday`. |
| fiscalYearStartMonth | The month that the fiscal year starts on. `0` = January, `11` = December |
| nowDelay? | string. Override the "now" time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. In schema v1: `timepicker.nowDelay`. |
<!-- prettier-ignore-end -->
### `TimeRangeOption`
The following table explains the usage of the time range option JSON fields:
| Name | Usage |
| ------- | ---------------------------------- |
| display | string. Default is `Last 6 hours`. |
| from | string. Default is `now-6h`. |
| to | string. Default is `now`. |

View File

@@ -1,501 +0,0 @@
---
description: A reference for the JSON variables schema used with Observability as Code.
keywords:
- configuration
- as code
- as-code
- dashboards
- git integration
- git sync
- github
- variables
labels:
products:
- cloud
- enterprise
- oss
menuTitle: variables schema
title: variables
weight: 700
canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/variables-schema/
aliases:
- ../../../observability-as-code/schema-v2/variables-schema/ # /docs/grafana/next/observability-as-code/schema-v2/variables-schema/
---
# `variables`
The available variable types described in the following sections:
- [QueryVariableKind](#queryvariablekind)
- [TextVariableKind](#textvariablekind)
- [ConstantVariableKind](#constantvariablekind)
- [DatasourceVariableKind](#datasourcevariablekind)
- [IntervalVariableKind](#intervalvariablekind)
- [CustomVariableKind](#customvariablekind)
- [SwitchVariableKind](#switchvariablekind)
- [GroupByVariableKind](#groupbyvariablekind)
- [AdhocVariableKind](#adhocvariablekind)
## `QueryVariableKind`
Following is the JSON for a default query variable:
```json
"variables": [
{
"kind": "QueryVariable",
"spec": {
"current": {
"text": "",
"value": ""
},
"hide": "dontHide",
"includeAll": false,
"multi": false,
"name": "",
"options": [],
"query": defaultDataQueryKind(),
"refresh": "never",
"regex": "",
"skipUrlSync": false,
"sort": "disabled"
}
}
]
```
`QueryVariableKind` consists of:
- kind: "QueryVariable"
- spec: [QueryVariableSpec](#queryvariablespec)
### `QueryVariableSpec`
The following table explains the usage of the query variable JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| ------------ | ---------------------------------------------- |
| name | string. Name of the variable. |
| current | "Text" and a "value" or [`VariableOption`](#variableoption) |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| refresh | `VariableRefresh`. Options are `never`, `onDashboardLoad`, and `onTimeChanged`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string |
| datasource? | [`DataSourceRef`](#datasourceref) |
| query | `DataQueryKind`. Consists of:<ul><li>kind: string</li><li>spec: string</li></ul> |
| regex | string |
| sort | `VariableSort`. Options are:<ul><li>disabled</li><li>alphabeticalAsc</li><li>alphabeticalDesc</li><li>numericalAsc</li><li>numericalDesc</li><li>alphabeticalCaseInsensitiveAsc</li><li>alphabeticalCaseInsensitiveDesc</li><li>naturalAsc</li><li>naturalDesc</li></ul> |
| definition? | string |
| options | [`VariableOption`](#variableoption) |
| multi | bool. Default is `false`. |
| includeAll | bool. Default is `false`. |
| allValue? | string |
| placeholder? | string |
<!-- prettier-ignore-end -->
#### `VariableOption`
| Name | Usage |
| -------- | -------------------------------------------- |
| selected | bool. Whether or not the option is selected. |
| text | string. Text to be displayed for the option. |
| value | string. Value of the option. |
#### `DataSourceRef`
| Name | Usage |
| ----- | ---------------------------------- |
| type? | string. The plugin type-id. |
| uid? | The specific data source instance. |
## `TextVariableKind`
Following is the JSON for a default text variable:
```json
"variables": [
{
"kind": "TextVariable",
"spec": {
"current": {
"text": "",
"value": ""
},
"hide": "dontHide",
"name": "",
"query": "",
"skipUrlSync": false
}
}
]
```
`TextVariableKind` consists of:
- kind: TextVariableKind
- spec: [TextVariableSpec](#textvariablespec)
### `TextVariableSpec`
The following table explains the usage of the query variable JSON fields:
| Name | Usage |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable. |
| current | "Text" and a "value" or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| query | string |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string |
## `ConstantVariableKind`
Following is the JSON for a default constant variable:
```json
"variables": [
{
"kind": "ConstantVariable",
"spec": {
"current": {
"text": "",
"value": ""
},
"hide": "hideVariable",
"name": "",
"query": "",
"skipUrlSync": true
}
}
]
```
`ConstantVariableKind` consists of:
- kind: "ConstantVariable"
- spec: [ConstantVariableSpec](#constantvariablespec)
### `ConstantVariableSpec`
The following table explains the usage of the constant variable JSON fields:
| Name | Usage |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable. |
| query | string |
| current | "Text" and a "value" or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string |
## `DatasourceVariableKind`
Following is the JSON for a default data source variable:
```json
"variables": [
{
"kind": "DatasourceVariable",
"spec": {
"current": {
"text": "",
"value": ""
},
"hide": "dontHide",
"includeAll": false,
"multi": false,
"name": "",
"options": [],
"pluginId": "",
"refresh": "never",
"regex": "",
"skipUrlSync": false
}
}
]
```
`DatasourceVariableKind` consists of:
- kind: "DatasourceVariable"
- spec: [DatasourceVariableSpec](#datasourcevariablespec)
### `DatasourceVariableSpec`
The following table explains the usage of the data source variable JSON fields:
| Name | Usage |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable. |
| pluginId | string |
| refresh | `VariableRefresh`. Options are `never`, `onDashboardLoad`, and `onTimeChanged`. |
| regex | string |
| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| multi | bool. Default is `false`. |
| includeAll | bool. Default is `false`. |
| allValue? | string |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string |
## `IntervalVariableKind`
Following is the JSON for a default interval variable:
```json
"variables": [
{
"kind": "IntervalVariable",
"spec": {
"auto": false,
"auto_count": 0,
"auto_min": "",
"current": {
"text": "",
"value": ""
},
"hide": "dontHide",
"name": "",
"options": [],
"query": "",
"refresh": "never",
"skipUrlSync": false
}
}
]
```
`IntervalVariableKind` consists of:
- kind: "IntervalVariable"
- spec: [IntervalVariableSpec](#intervalvariablespec)
### `IntervalVariableSpec`
The following table explains the usage of the interval variable JSON fields:
| Name | Usage |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable. |
| query | string |
| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| auto | bool. Default is `false`. |
| auto_count | integer. Default is `0`. |
| refresh | `VariableRefresh`. Options are `never`, `onDashboardLoad`, and `onTimeChanged`. |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false` |
| description? | string |
## `CustomVariableKind`
Following is the JSON for a default custom variable:
```json
"variables": [
{
"kind": "CustomVariable",
"spec": {
"current": defaultVariableOption(),
"hide": "dontHide",
"includeAll": false,
"multi": false,
"name": "",
"options": [],
"query": "",
"skipUrlSync": false
}
}
]
```
`CustomVariableKind` consists of:
- kind: "CustomVariable"
- spec: [CustomVariableSpec](#customvariablespec)
### `CustomVariableSpec`
The following table explains the usage of the custom variable JSON fields:
| Name | Usage |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable. |
| query | string |
| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| multi | bool. Default is `false`. |
| includeAll | bool. Default is `false`. |
| allValue? | string |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string |
## `SwitchVariableKind`
Following is the JSON for a default switch variable:
```json
"variables": [
{
"kind": "SwitchVariable",
"spec": {
"current": "false",
"enabledValue": "true",
"disabledValue": "false",
"hide": "dontHide",
"name": "",
"skipUrlSync": false
}
}
]
```
`SwitchVariableKind` consists of:
- kind: "SwitchVariable"
- spec: [SwitchVariableSpec](#switchvariablespec)
### `SwitchVariableSpec`
The following table explains the usage of the switch variable JSON fields:
<!-- prettier-ignore-start -->
| Name | Usage |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable. |
| current | string. Current value of the switch variable (either `enabledValue` or `disabledValue`). |
| enabledValue | string. Value when the switch is in the enabled state. |
| disabledValue | string. Value when the switch is in the disabled state. |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string |
<!-- prettier-ignore-end -->
## `GroupByVariableKind`
Following is the JSON for a default group by variable:
```json
"variables": [
{
"kind": "GroupByVariable",
"spec": {
"current": {
"text": [
""
],
"value": [
""
]
},
"datasource": {},
"hide": "dontHide",
"multi": false,
"name": "",
"options": [],
"skipUrlSync": false
}
}
]
```
`GroupByVariableKind` consists of:
- kind: "GroupByVariable"
- spec: [GroupByVariableSpec](#groupbyvariablespec)
### `GroupByVariableSpec`
The following table explains the usage of the group by variable JSON fields:
| Name | Usage |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable |
| datasource? | `DataSourceRef`. Refer to the [`DataSourceRef` definition](#datasourceref) under `QueryVariableKind`. |
| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. |
| multi | bool. Default is `false`. |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string. |
## `AdhocVariableKind`
Following is the JSON for a default ad hoc variable:
```json
"variables": [
{
"kind": "AdhocVariable",
"spec": {
"baseFilters": [],
"defaultKeys": [],
"filters": [],
"hide": "dontHide",
"name": "",
"skipUrlSync": false
}
}
]
```
`AdhocVariableKind` consists of:
- kind: "AdhocVariable"
- spec: [AdhocVariableSpec](#adhocvariablespec)
### `AdhocVariableSpec`
The following table explains the usage of the ad hoc variable JSON fields:
| Name | Usage |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string. Name of the variable. |
| datasource? | `DataSourceRef`. Consists of:<ul><li>type? - string. The plugin type-id.</li><li>uid? - string. The specific data source instance.</li></ul> |
| baseFilters | [AdHocFilterWithLabels](#adhocfilterswithlabels) |
| filters | [AdHocFilterWithLabels](#adhocfilterswithlabels) |
| defaultKeys | [MetricFindValue](#metricfindvalue) |
| label? | string |
| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. |
| skipUrlSync | bool. Default is `false`. |
| description? | string |
#### `AdHocFiltersWithLabels`
The following table explains the usage of the ad hoc variable with labels JSON fields:
| Name | Type |
| ------------ | ------------- |
| key | string |
| operator | string |
| value | string |
| values? | `[...string]` |
| keyLabel | string |
| valueLabels? | `[...string]` |
| forceEdit? | bool |
#### `MetricFindValue`
The following table explains the usage of the metric find value JSON fields:
| Name | Type |
| ----------- | ---------------- |
| text | string |
| value? | string or number |
| group? | string |
| expandable? | bool |

View File

@@ -103,10 +103,11 @@ To configure basic settings for the data source, complete the following steps:
1. Set the data source's basic configuration options:
| Name | Description |
| ----------- | ------------------------------------------------------------------------ |
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
| **Default** | Sets whether the data source is pre-selected for new panels. |
| Name | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
| **Default** | Sets whether the data source is pre-selected for new panels. |
| **Universe Domain** | The universe domain to connect to. For more information, refer to [Documentation on universe domains](https://docs.cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.service_monitoring_service.ServiceMonitoringServiceAsyncClient#google_cloud_monitoring_v3_services_service_monitoring_service_ServiceMonitoringServiceAsyncClient_universe_domain). Defaults to `googleapis.com`. |
### Provision the data source
@@ -129,6 +130,7 @@ datasources:
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
authenticationType: jwt
defaultProject: my-project-name
universeDomain: googleapis.com
secureJsonData:
privateKey: |
-----BEGIN PRIVATE KEY-----
@@ -152,6 +154,7 @@ datasources:
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
authenticationType: jwt
defaultProject: my-project-name
universeDomain: googleapis.com
privateKeyPath: /etc/secrets/gce.pem
```
@@ -166,6 +169,7 @@ datasources:
access: proxy
jsonData:
authenticationType: gce
universeDomain: googleapis.com
```
## Import pre-configured dashboards

View File

@@ -171,146 +171,3 @@ Status Codes:
- **200** - Ok
- **401** - Unauthorized
- **404** - Dashboard version not found
```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
```
The response is a textual representation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab.
Status Codes:
- **200** - Ok
- **400** - Bad request (invalid JSON sent)
- **401** - Unauthorized
- **404** - Not found
**Example response (basic diff)**:
```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
```
The response here is a summary of the changes, derived from the diff between the two JSON objects.
Status Codes:
- **200** - OK
- **400** - Bad request (invalid JSON sent)
- **401** - Unauthorized
- **404** - Not found
{
"id": 70,
"slug": "my-dashboard",
"status": "success",
"uid": "QA7wKklGz",
"url": "/d/QA7wKklGz/my-dashboard",
"version": 3
}
```
JSON response body schema:
- **slug** - the URL friendly slug of the dashboard's title
- **status** - whether the restoration was successful or not
- **version** - the new dashboard version, following the restoration
Status codes:
- **200** - OK
- **400** - Bad request (specified version has the same content as the current dashboard)
- **401** - Unauthorized
- **404** - Not found (dashboard not found or dashboard version not found)
- **500** - Internal server error (indicates issue retrieving dashboard tags from database)
**Example error response**
```http
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=UTF-8
Content-Length: 46
{
"message": "Dashboard version not found"
}
```
JSON response body schema:
- **message** - Message explaining the reason for the request failure.
## Compare dashboard versions
`POST /api/dashboards/calculate-diff`
Compares two dashboard versions by calculating the JSON diff of them.
**Example request**:
```http
POST /api/dashboards/calculate-diff HTTP/1.1
Accept: text/html
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"base": {
"dashboardId": 1,
"version": 1
},
"new": {
"dashboardId": 1,
"version": 2
},
"diffType": "json"
}
```
JSON body schema:
- **base** - an object representing the base dashboard version
- **new** - an object representing the new dashboard version
- **diffType** - the type of diff to return. Can be "json" or "basic".
**Example response (JSON diff)**:
```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
<p id="l1" class="diff-line diff-json-same">
<!-- Diff omitted -->
</p>
```
The response is a textual representation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab.
Status Codes:
- **200** - Ok
- **400** - Bad request (invalid JSON sent)
- **401** - Unauthorized
- **404** - Not found
**Example response (basic diff)**:
```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
<div class="diff-group">
<!-- Diff omitted -->
</div>
```
The response here is a summary of the changes, derived from the diff between the two JSON objects.
Status Codes:
- **200** - OK
- **400** - Bad request (invalid JSON sent)
- **401** - Unauthorized
- **404** - Not found

View File

@@ -87,6 +87,7 @@ With a Grafana Enterprise license, you also get access to premium data sources,
- [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource)
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
- [IBM Db2](/grafana/plugins/grafana-ibmdb2-datasource)
- [Drone](/grafana/plugins/grafana-drone-datasource)
- [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/)
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)

View File

@@ -2030,6 +2030,44 @@ For example: `disabled_labels=grafana_folder`
<hr>
### `[unified_alerting.state_history]`
This section configures where Grafana Alerting writes alert state history. Refer to [Configure alert state history](/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-alert-state-history/) for end-to-end setup and examples.
#### `enabled `
Enables recording alert state history. Default is `false`.
#### `backend `
Select the backend used to store alert state history. Supported values: `loki`, `prometheus`, `multiple`.
#### `loki_remote_url `
The URL of the Loki server used when `backend = loki` (or when `backend = multiple` and Loki is a primary/secondary).
#### `prometheus_target_datasource_uid `
Target Prometheus data source UID used for writing alert state changes when `backend = prometheus` (or when `backend = multiple` and Prometheus is a secondary).
#### `prometheus_metric_name `
Optional. Metric name for the alert state metric. Default is `GRAFANA_ALERTS`.
#### `prometheus_write_timeout `
Optional. Timeout for writing alert state data to the target data source. Default is `10s`.
#### `primary `
Used only when `backend = multiple`. Selects the primary backend (for example `loki`).
#### `secondaries `
Used only when `backend = multiple`. Comma-separated list of secondary backends (for example `prometheus`).
<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)

View File

@@ -83,6 +83,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `reportingRetries` | Enables rendering retries for the reporting feature |
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
| `dashboardNewLayouts` | Enables new dashboard layouts |
| `pdfTables` | Enables generating table data as PDF in reporting |
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. |

View File

@@ -3,45 +3,75 @@ aliases:
- ../../../reference/dashboard/ # /docs/grafana/next/reference/dashboard/
- ../../../dashboards/json-model/ # /docs/grafana/next/dashboards/json-model/
- ../../../dashboards/build-dashboards/view-dashboard-json-model/ # /docs/grafana/next/dashboards/build-dashboards/view-dashboard-json-model/
- ../../../as-code/observability-as-code/schema-v2/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/
- ../../../as-code/observability-as-code/schema-v2/annotations-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/annotations-schema/
- ../../../as-code/observability-as-code/schema-v2/panel-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/panel-schema/
- ../../../as-code/observability-as-code/schema-v2/librarypanel-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/librarypanel-schema/
- ../../../as-code/observability-as-code/schema-v2/layout-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/layout-schema/
- ../../../as-code/observability-as-code/schema-v2/links-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/links-schema/
- ../../../as-code/observability-as-code/schema-v2/timesettings-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/timesettings-schema/
- ../../../as-code/observability-as-code/schema-v2/variables-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/variables-schema/
- ../../../observability-as-code/schema-v2/ # /docs/grafana/latest/observability-as-code/schema-v2/
- ../../../../next/observability-as-code/schema-v2/annotations-schema/ # /docs/grafana/next/observability-as-code/schema-v2/annotations-schema/
- ../../../../next/observability-as-code/schema-v2/panel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/panel-schema/
- ../../../../next/observability-as-code/schema-v2/librarypanel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/librarypanel-schema/
- ../../../../next/observability-as-code/schema-v2/layout-schema/ # /docs/grafana/next/observability-as-code/schema-v2/layout-schema/
- ../../../../next/observability-as-code/schema-v2/links-schema/ # /docs/grafana/next/observability-as-code/schema-v2/links-schema/
- ../../../../next/observability-as-code/schema-v2/timesettings-schema/ # /docs/grafana/next/observability-as-code/schema-v2/timesettings-schema/
- ../../../../next/observability-as-code/schema-v2/variables-schema/ # /docs/grafana/next/observability-as-code/schema-v2/variables-schema/
keywords:
- grafana
- dashboard
- documentation
- json
- model
- schema v2
- v1 resource
- v2 resource
- classic
labels:
products:
- cloud
- enterprise
- oss
title: JSON model
description: View your Grafana dashboard JSON object
description: View and update your Grafana dashboard JSON object
weight: 700
refs:
annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
---
# Dashboard JSON model
A dashboard in Grafana is represented by a JSON object, which stores metadata of its dashboard. Dashboard metadata includes dashboard properties, metadata from panels, template variables, panel queries, etc.
Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings.
To view the JSON of a dashboard:
## Different dashboard schema models
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
1. Go to the **JSON Model** tab.
1. When you've finished viewing the JSON, click **Back to dashboard** and **Exit edit**.
There are currently three dashboard JSON schema models:
## JSON fields
When a user creates a new dashboard, a new dashboard JSON object is initialized with the following fields:
- [Classic](#classic-model) - A non-Kubernetes resource used before the adoption of the Kubernetes API by Grafana in v12.2.0. It's been widely used for exporting, importing, and sharing dashboards in the Grafana dashboards collection at [grafana.com/dashboards](https://grafana.com/grafana/dashboards/).
- [V1 Resource](#v1-resource-model) - The Classic dashboard schema formatted as a Kubernetes-style resource. Its `spec` property contains the Classic model of the schema. This is the default format for API communication after Grafana v12.2.0, which enabled the Kubernetes Platform API as default backend for Grafana dashboards. Dashboards created using the Classic model can be exported using either the Classic or the V1 Resource format.
- [V2 Resource](#v2-resource-model) - The latest format, supporting new features such as advanced layouts and conditional rendering. It models all dashboard elements as Kubernetes kinds, following Kubernetes conventions for declaring dashboard components. This format is future-proof and represents the evolving standard for dashboards.
{{< admonition type="note" >}}
In the following JSON, id is shown as null which is the default value assigned to it until a dashboard is saved. Once a dashboard is saved, an integer value is assigned to the `id` field.
[Observability as Code](https://grafana.com/docs/grafana/latest/as-code/observability-as-code/) works with all versions of the JSON model, and it's fully compatible with version 2.
{{< /admonition >}}
## Access and update the JSON model (#view-json)
To access the JSON representation of a dashboard:
1. Click **Edit** in the top-right corner of the dashboard.
1. Click the gear icon in the right sidebar and click **Settings** in the secondary sidebar.
1. Select the **JSON Model** tab.
1. Update the JSON structure as needed.
1. Click **Save changes**.
## Classic model
When you create a new dashboard in self-managed Grafana, a new dashboard JSON object was initialized with the following fields:
{{< admonition type="note" >}}
In the following JSON, id is shown as null which is the default value assigned to it until a dashboard is saved.
After a dashboard is saved, an integer value is assigned to the `id` field.
{{< /admonition >}}
```json
@@ -76,26 +106,30 @@ In the following JSON, id is shown as null which is the default value assigned t
Each field in the dashboard JSON is explained below with its usage:
| Name | Usage |
| ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| **id** | unique numeric identifier for the dashboard. (generated by the db) |
| **uid** | unique dashboard identifier that can be generated by anyone. string (8-40) |
| **title** | current title of dashboard |
| **tags** | tags associated with dashboard, an array of strings |
| **style** | theme of dashboard, i.e. `dark` or `light` |
| **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
| **editable** | whether a dashboard is editable or not |
| **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip |
| **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc |
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
| **templating** | templating metadata, see [templating section](#templating) for details |
| **annotations** | annotations metadata, see [annotations](ref:annotations) for how to add them |
| **refresh** | auto-refresh interval |
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
| **panels** | panels array, see below for detail. |
<!--prettier-ignore-start -->
## Panels
| Name | Usage |
| ----------------- | ------------------------------------------------------------------------------------------ |
| **id** | unique numeric identifier for the dashboard. (generated by the db) |
| **uid** | unique dashboard identifier that can be generated by anyone. string (8-40) |
| **title** | current title of dashboard |
| **tags** | tags associated with dashboard, an array of strings |
| **style** | theme of dashboard, i.e. `dark` or `light` |
| **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
| **editable** | whether a dashboard is editable or not |
| **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip |
| **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc |
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
| **templating** | templating metadata, see [templating section](#templating) for details |
| **annotations** | annotations metadata, see [annotations](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/) for how to add them |
| **refresh** | auto-refresh interval|
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
| **panels** | panels array, see below for detail. |
<!--prettier-ignore-end -->
### Panels
Panels are the building blocks of a dashboard. It consists of data source queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depend on the panel type. Following is an example of panel JSON of a text panel.
@@ -168,18 +202,22 @@ The grid has a negative gravity that moves panels up if there is empty space abo
Usage of the fields is explained below:
| Name | Usage |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **collapse** | whether timepicker is collapsed or not |
| **enable** | whether timepicker is enabled or not |
| **notice** | |
| **now** | |
| **hidden** | whether timepicker is hidden or not |
| **nowDelay** | override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. |
| **quick_ranges** | custom quick ranges |
| **refresh_intervals** | interval options available in the refresh picker dropdown |
| **status** | |
| **type** | |
<!--prettier-ignore-start -->
| Name | Usage |
| --------------------- | --------------------------------------------------------- |
| **collapse** | whether timepicker is collapsed or not |
| **enable** | whether timepicker is enabled or not |
| **notice** | |
| **now** | |
| **hidden** | whether timepicker is hidden or not |
| **nowDelay** | override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. |
| **quick_ranges** | custom quick ranges |
| **refresh_intervals** | interval options available in the refresh picker dropdown |
| **status** | |
| **type** | |
<!--prettier-ignore-end -->
### templating
@@ -270,3 +308,82 @@ Usage of the above mentioned fields in the templating section is explained below
| **refresh** | configures when to refresh a variable |
| **regex** | extracts part of a series name or metric node segment |
| **type** | type of variable, i.e. `custom`, `query` or `interval` |
## V1 Resource model
The V1 Resource schema model formats the [Classic JSON model](#classic-model) schema as a Kubernetes-style resource.
The `spec` property of the schema contains the Classic-style model of the schema.
Dashboards created using the Classic model can be exported using either this model or the Classic one.
The following code snippet shows the fields included in the V1 Resource model.
```json
{
"apiVersion": "dashboard.grafana.app/v1beta1",
"kind": "Dashboard",
"metadata": {
"name": "isnt5ss",
"namespace": "stacks-521104",
"uid": "92674c0e-0360-4bb4-99ab-fb150581376d",
"resourceVersion": "1764705030717045",
"generation": 1,
"creationTimestamp": "2025-12-02T19:50:30Z",
"labels": {
"grafana.app/deprecatedInternalID": "1329"
},
"annotations": {
"grafana.app/createdBy": "user:u000000002",
"grafana.app/folder": "",
"grafana.app/saved-from-ui": "Grafana Cloud (instant)"
}
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 1329,
"links": [],
"panels": [],
"preload": false,
"schemaVersion": 42,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "Africa/Abidjan",
"title": "Graphite suggestions",
"uid": "isnt5ss",
"version": 1,
"weekStart": ""
},
"status": {}
}
```
## V2 Resource model
{{< docs/public-preview product="Dashboard JSON schema v2" >}}
For the detailed V2 Resource model schema, refer to the [Swagger documentation](https://play.grafana.org/swagger?api=dashboard.grafana.app-v2beta1).

View File

@@ -30,7 +30,9 @@ refs:
# Datagrid
{{< docs/experimental product="The datagrid visualization" featureFlag="`enableDatagridEditing`" >}}
{{< admonition type="caution" >}}
Starting with Grafana 12.4, Datagrid is deprecated. It will be removed in version 13.0.
{{< /admonition >}}
Datagrids offer you the ability to create, edit, and fine-tune data within Grafana. As such, this panel can act as a data source for other panels
inside a dashboard.

View File

@@ -1337,6 +1337,11 @@
"count": 2
}
},
"public/app/features/alerting/unified/api/onCallApi.test.ts": {
"no-restricted-syntax": {
"count": 2
}
},
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
@@ -1377,6 +1382,11 @@
"count": 1
}
},
"public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.test.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/alerting/unified/components/import-to-gma/NamespaceAndGroupFilter.tsx": {
"no-restricted-syntax": {
"count": 2
@@ -1617,11 +1627,31 @@
"count": 1
}
},
"public/app/features/alerting/unified/mocks/server/configure.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/alerting/unified/mocks/server/handlers/plugins.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx": {
"no-restricted-syntax": {
"count": 2
}
},
"public/app/features/alerting/unified/rule-editor/formDefaults.ts": {
"no-restricted-syntax": {
"count": 6
}
},
"public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/alerting/unified/types/alerting.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 5
@@ -1632,6 +1662,16 @@
"count": 1
}
},
"public/app/features/alerting/unified/utils/config.test.ts": {
"no-restricted-syntax": {
"count": 6
}
},
"public/app/features/alerting/unified/utils/config.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/alerting/unified/utils/datasource.ts": {
"no-restricted-syntax": {
"count": 2
@@ -1663,12 +1703,20 @@
"count": 1
}
},
"public/app/features/alerting/unified/utils/rules.test.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/alerting/unified/utils/rules.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 3
},
"@typescript-eslint/no-explicit-any": {
"count": 1
},
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx": {
@@ -1724,6 +1772,16 @@
"count": 1
}
},
"public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx": {
"no-restricted-syntax": {
"count": 2
}
},
"public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/connections/tabs/ConnectData/ConnectData.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
@@ -2063,6 +2121,11 @@
"count": 1
}
},
"public/app/features/dashboard/components/GenAI/utils.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx": {
"no-restricted-syntax": {
"count": 3
@@ -2889,6 +2952,71 @@
"count": 1
}
},
"public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts": {
"no-restricted-syntax": {
"count": 6
}
},
"public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts": {
"no-restricted-syntax": {
"count": 6
}
},
"public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts": {
"no-restricted-syntax": {
"count": 6
}
},
"public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts": {
"no-restricted-syntax": {
"count": 6
}
},
"public/app/features/plugins/extensions/usePluginComponent.test.tsx": {
"no-restricted-syntax": {
"count": 3
}
},
"public/app/features/plugins/extensions/usePluginComponents.test.tsx": {
"no-restricted-syntax": {
"count": 2
}
},
"public/app/features/plugins/extensions/usePluginFunctions.test.tsx": {
"no-restricted-syntax": {
"count": 2
}
},
"public/app/features/plugins/extensions/usePluginLinks.test.tsx": {
"no-restricted-syntax": {
"count": 2
}
},
"public/app/features/plugins/extensions/utils.test.tsx": {
"no-restricted-syntax": {
"count": 27
}
},
"public/app/features/plugins/extensions/utils.tsx": {
"no-restricted-syntax": {
"count": 7
}
},
"public/app/features/plugins/extensions/validators.test.tsx": {
"no-restricted-syntax": {
"count": 30
}
},
"public/app/features/plugins/extensions/validators.ts": {
"no-restricted-syntax": {
"count": 4
}
},
"public/app/features/plugins/sandbox/codeLoader.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/plugins/sandbox/distortions.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
@@ -3615,46 +3743,21 @@
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 2
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": {
"no-restricted-syntax": {
"count": 1

View File

@@ -117,6 +117,8 @@ module.exports = [
'scripts/grafana-server/tmp',
'packages/grafana-ui/src/graveyard', // deprecated UI components slated for removal
'public/build-swagger', // swagger build output
'apps/plugins/plugin/src/generated/meta/v0alpha1',
'apps/plugins/plugin/src/generated/plugin/v0alpha1',
],
},
...grafanaConfig,
@@ -575,6 +577,42 @@ module.exports = [
"Property[key.name='a11y'][value.type='ObjectExpression'] Property[key.name='test'][value.value='off']",
message: 'Skipping a11y tests is not allowed. Please fix the component or story instead.',
},
{
selector: 'MemberExpression[object.name="config"][property.name="apps"]',
message:
'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead',
},
],
},
},
{
files: [...commonTestIgnores],
ignores: [
// FIXME: Remove once all enterprise issues are fixed -
// we don't have a suppressions file/approach for enterprise code yet
...enterpriseIgnores,
],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'MemberExpression[object.name="config"][property.name="apps"]',
message:
'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead',
},
],
},
},
{
files: [...enterpriseIgnores],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'MemberExpression[object.name="config"][property.name="apps"]',
message:
'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead',
},
],
},
},

23
go.mod
View File

@@ -44,8 +44,8 @@ require (
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad
github.com/blevesearch/bleve/v2 v2.5.0 // @grafana/grafana-search-and-storage
github.com/blevesearch/bleve_index_api v1.2.7 // @grafana/grafana-search-and-storage
github.com/blevesearch/bleve/v2 v2.5.7 // @grafana/grafana-search-and-storage
github.com/blevesearch/bleve_index_api v1.3.0 // @grafana/grafana-search-and-storage
github.com/blugelabs/bluge v0.2.2 // @grafana/grafana-backend-group
github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // @grafana/grafana-backend-group
@@ -365,22 +365,22 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blevesearch/geo v0.1.20 // indirect
github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.26 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.9 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.1.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.1 // indirect
github.com/blevesearch/zapx/v12 v12.4.1 // indirect
github.com/blevesearch/zapx/v13 v13.4.1 // indirect
github.com/blevesearch/zapx/v14 v14.4.1 // indirect
github.com/blevesearch/zapx/v15 v15.4.1 // indirect
github.com/blevesearch/zapx/v16 v16.2.2 // indirect
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/blugelabs/ice v1.0.0 // indirect
github.com/blugelabs/ice/v2 v2.0.1 // indirect
@@ -443,7 +443,6 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.1 // indirect

46
go.sum
View File

@@ -931,14 +931,14 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4=
github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk=
github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY=
github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E=
github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
@@ -947,8 +947,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+
github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o=
github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
@@ -960,18 +960,18 @@ github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+
github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE=
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA=
github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs=
github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ=
github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II=
github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE=
github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8=
github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE=
github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms=
github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g=
github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y=
github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk=
github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk=
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/blugelabs/bluge v0.2.2 h1:gat8CqE6P6tOgeX30XGLOVNTC26cpM2RWVcreXWtYcM=
@@ -1442,8 +1442,6 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=

View File

@@ -520,14 +520,40 @@ github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC
github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E=
github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA=
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI=
github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ=
github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A=
github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg=
github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc=
github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc=
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
@@ -998,8 +1024,6 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ=
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
@@ -1092,6 +1116,7 @@ github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU=
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY=
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y=
github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE=

View File

@@ -82,6 +82,7 @@ module.exports = {
// Decoupled plugins run their own tests so ignoring them here.
'<rootDir>/public/app/plugins/datasource/azuremonitor',
'<rootDir>/public/app/plugins/datasource/cloud-monitoring',
'<rootDir>/public/app/plugins/datasource/elasticsearch',
'<rootDir>/public/app/plugins/datasource/grafana-postgresql-datasource',
'<rootDir>/public/app/plugins/datasource/grafana-pyroscope-datasource',
'<rootDir>/public/app/plugins/datasource/grafana-testdata-datasource',

View File

@@ -293,8 +293,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "v6.52.1",
"@grafana/scenes-react": "v6.52.1",
"@grafana/scenes": "6.52.2",
"@grafana/scenes-react": "6.52.2",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",

View File

@@ -727,17 +727,6 @@ const injectedRtkApi = api
}),
invalidatesTags: ['dashboards', 'permissions'],
}),
restoreDashboardVersionByUid: build.mutation<
RestoreDashboardVersionByUidApiResponse,
RestoreDashboardVersionByUidApiArg
>({
query: (queryArg) => ({
url: `/dashboards/uid/${queryArg.uid}/restore`,
method: 'POST',
body: queryArg.restoreDashboardVersionCommand,
}),
invalidatesTags: ['dashboards', 'versions'],
}),
getDashboardVersionsByUid: build.query<GetDashboardVersionsByUidApiResponse, GetDashboardVersionsByUidApiArg>({
query: (queryArg) => ({
url: `/dashboards/uid/${queryArg.uid}/versions`,
@@ -2628,26 +2617,6 @@ export type UpdateDashboardPermissionsByUidApiArg = {
uid: string;
updateDashboardAclCommand: UpdateDashboardAclCommand;
};
export type RestoreDashboardVersionByUidApiResponse = /** status 200 (empty) */ {
/** FolderUID The unique identifier (uid) of the folder the dashboard belongs to. */
folderUid?: string;
/** ID The unique identifier (id) of the created/updated dashboard. */
id: number;
/** Status status of the response. */
status: string;
/** Slug The slug of the dashboard. */
title: string;
/** UID The unique identifier (uid) of the created/updated dashboard. */
uid: string;
/** URL The relative URL for accessing the created/updated dashboard. */
url: string;
/** Version The version of the dashboard. */
version: number;
};
export type RestoreDashboardVersionByUidApiArg = {
uid: string;
restoreDashboardVersionCommand: RestoreDashboardVersionCommand;
};
export type GetDashboardVersionsByUidApiResponse = /** status 200 (empty) */ DashboardVersionResponseMeta;
export type GetDashboardVersionsByUidApiArg = {
uid: string;
@@ -4568,9 +4537,6 @@ export type DashboardAclUpdateItem = {
export type UpdateDashboardAclCommand = {
items?: DashboardAclUpdateItem[];
};
export type RestoreDashboardVersionCommand = {
version?: number;
};
export type DashboardVersionMeta = {
created?: string;
createdBy?: string;
@@ -6633,7 +6599,6 @@ export const {
useGetDashboardPermissionsListByUidQuery,
useLazyGetDashboardPermissionsListByUidQuery,
useUpdateDashboardPermissionsByUidMutation,
useRestoreDashboardVersionByUidMutation,
useGetDashboardVersionsByUidQuery,
useLazyGetDashboardVersionsByUidQuery,
useGetDashboardVersionByUidQuery,

View File

@@ -35,6 +35,14 @@
},
"./test": {
"@grafana-app/source": "./test/index.ts"
},
"./themes/schema.generated.json": {
"@grafana-app/source": "./src/themes/schema.generated.json",
"default": "./dist/esm/themes/schema.generated.json"
},
"./themes/definitions/*.json": {
"@grafana-app/source": "./src/themes/themeDefinitions/*.json",
"default": "./dist/esm/themes/themeDefinitions/*.json"
}
},
"publishConfig": {
@@ -52,7 +60,7 @@
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json",
"themes-schema": "tsx ./src/themes/scripts/generateSchema.ts"
"themes-schema": "tsx ./scripts/generateSchema.ts"
},
"dependencies": {
"@braintree/sanitize-url": "7.0.1",
@@ -102,6 +110,7 @@
"react-dom": "18.3.1",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
"rollup-plugin-copy": "3.5.0",
"rollup-plugin-esbuild": "6.2.1",
"rollup-plugin-node-externals": "^8.0.0",
"tsx": "^4.21.0",

View File

@@ -1,21 +1,40 @@
import json from '@rollup/plugin-json';
import { createRequire } from 'node:module';
import copy from 'rollup-plugin-copy';
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
const rq = createRequire(import.meta.url);
const pkg = rq('./package.json');
const grafanaDataPlugins = [
...plugins,
copy({
targets: [
{
src: 'src/themes/schema.generated.json',
dest: 'dist/esm/',
},
{
src: 'src/themes/themeDefinitions/*.json',
dest: 'dist/esm/',
},
],
flatten: false,
}),
json(),
];
export default [
{
input: entryPoint,
plugins: [...plugins, json()],
plugins: grafanaDataPlugins,
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
treeshake: false,
},
{
input: 'src/unstable.ts',
plugins: [...plugins, json()],
plugins: grafanaDataPlugins,
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
treeshake: false,
},

View File

@@ -0,0 +1,22 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { NewThemeOptionsSchema } from '../src/themes/createTheme';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const jsonOut = path.join(__dirname, '..', 'src', 'themes', 'schema.generated.json');
fs.writeFileSync(
jsonOut,
JSON.stringify(
NewThemeOptionsSchema.toJSONSchema({
target: 'draft-07',
}),
undefined,
2
)
);
console.log('Successfully generated theme schema');

View File

@@ -844,7 +844,6 @@ export {
DataLinkConfigOrigin,
SupportedTransformationType,
type InternalDataLink,
type LinkTarget,
type LinkModel,
type LinkModelSupplier,
VariableOrigin,
@@ -852,6 +851,7 @@ export {
VariableSuggestionsScope,
OneClickMode,
} from './types/dataLink';
export { type LinkTarget } from './types/linkTarget';
export {
type Action,
type ActionModel,

View File

@@ -93,7 +93,6 @@ export { DataTransformerID } from '../transformations/transformers/ids';
export { mergeTransformer } from '../transformations/transformers/merge';
export { getThemeById } from '../themes/registry';
export * as experimentalThemeDefinitions from '../themes/themeDefinitions';
export { GrafanaEdition } from '../types/config';
export { SIPrefix } from '../valueFormats/symbolFormatters';

View File

@@ -1,7 +1,18 @@
import { Registry, RegistryItem } from '../utils/Registry';
import { createTheme, NewThemeOptionsSchema } from './createTheme';
import * as extraThemes from './themeDefinitions';
import aubergine from './themeDefinitions/aubergine.json';
import debug from './themeDefinitions/debug.json';
import desertbloom from './themeDefinitions/desertbloom.json';
import gildedgrove from './themeDefinitions/gildedgrove.json';
import gloom from './themeDefinitions/gloom.json';
import mars from './themeDefinitions/mars.json';
import matrix from './themeDefinitions/matrix.json';
import sapphiredusk from './themeDefinitions/sapphiredusk.json';
import synthwave from './themeDefinitions/synthwave.json';
import tron from './themeDefinitions/tron.json';
import victorian from './themeDefinitions/victorian.json';
import zen from './themeDefinitions/zen.json';
import { GrafanaTheme2 } from './types';
export interface ThemeRegistryItem extends RegistryItem {
@@ -9,6 +20,21 @@ export interface ThemeRegistryItem extends RegistryItem {
build: () => GrafanaTheme2;
}
const extraThemes: { [key: string]: unknown } = {
aubergine,
debug,
desertbloom,
gildedgrove,
gloom,
mars,
matrix,
sapphiredusk,
synthwave,
tron,
victorian,
zen,
};
/**
* @internal
* Only for internal use, never use this from a plugin

View File

@@ -1,19 +0,0 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { NewThemeOptionsSchema } from '../createTheme';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
fs.writeFileSync(
path.join(__dirname, '../schema.generated.json'),
JSON.stringify(
NewThemeOptionsSchema.toJSONSchema({
target: 'draft-07',
}),
undefined,
2
)
);

View File

@@ -1,12 +0,0 @@
export { default as aubergine } from './aubergine.json';
export { default as debug } from './debug.json';
export { default as desertbloom } from './desertbloom.json';
export { default as gildedgrove } from './gildedgrove.json';
export { default as mars } from './mars.json';
export { default as matrix } from './matrix.json';
export { default as sapphiredusk } from './sapphiredusk.json';
export { default as synthwave } from './synthwave.json';
export { default as tron } from './tron.json';
export { default as victorian } from './victorian.json';
export { default as zen } from './zen.json';
export { default as gloom } from './gloom.json';

View File

@@ -32,6 +32,7 @@ export type AppPluginConfig = {
path: string;
version: string;
preload: boolean;
/** @deprecated it will be removed in a future release */
angular: AngularMeta;
loadingStrategy: PluginLoadingStrategy;
dependencies: PluginDependencies;
@@ -219,6 +220,7 @@ export interface GrafanaConfig {
snapshotEnabled: boolean;
datasources: { [str: string]: DataSourceInstanceSettings };
panels: { [key: string]: PanelPluginMeta };
/** @deprecated it will be removed in a future release */
apps: Record<string, AppPluginConfig>;
auth: AuthSettings;
minRefreshInterval: string;

View File

@@ -1,5 +1,6 @@
import { ScopedVars } from './ScopedVars';
import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore';
import { LinkTarget } from './linkTarget';
import { InterpolateFunction } from './panel';
import { DataQuery } from './query';
import { TimeRange } from './time';
@@ -88,8 +89,6 @@ export interface InternalDataLink<T extends DataQuery = any> {
range?: TimeRange;
}
export type LinkTarget = '_blank' | '_self' | undefined;
/**
* Processed Link Model. The values are ready to use
*/

View File

@@ -356,7 +356,7 @@ export interface FeatureToggles {
*/
dashboardScene?: boolean;
/**
* Enables experimental new dashboard layouts
* Enables new dashboard layouts
*/
dashboardNewLayouts?: boolean;
/**
@@ -531,6 +531,10 @@ export interface FeatureToggles {
*/
alertingListViewV2?: boolean;
/**
* Enables the new Alerting navigation structure with improved menu grouping
*/
alertingNavigationV2?: boolean;
/**
* Enables saved searches for alert rules list
*/
alertingSavedSearches?: boolean;
@@ -1251,4 +1255,8 @@ export interface FeatureToggles {
* Enables profiles exemplars support in profiles drilldown
*/
profilesExemplars?: boolean;
/**
* Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
*/
alertingSyncDispatchTimer?: boolean;
}

View File

@@ -0,0 +1,4 @@
/**
* Target for links - controls whether link opens in new tab or same tab
*/
export type LinkTarget = '_blank' | '_self' | undefined;

View File

@@ -1,7 +1,7 @@
import { ComponentType } from 'react';
import { LinkTarget } from './dataLink';
import { IconName } from './icon';
import { LinkTarget } from './linkTarget';
export interface NavLinkDTO {
id?: string;

View File

@@ -11,6 +11,7 @@ import { DataFrame } from './dataFrame';
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
import { FieldConfigSource } from './fieldOverrides';
import { IconName } from './icon';
import { LinkTarget } from './linkTarget';
import { OptionEditorConfig } from './options';
import { PluginMeta } from './plugin';
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
@@ -191,6 +192,7 @@ export interface PanelMenuItem {
onClick?: (event: React.MouseEvent) => void;
shortcut?: string;
href?: string;
target?: LinkTarget;
subMenu?: PanelMenuItem[];
}

View File

@@ -53,6 +53,7 @@ export interface PluginError {
pluginType?: PluginType;
}
/** @deprecated it will be removed in a future release */
export interface AngularMeta {
detected: boolean;
hideDeprecation: boolean;

View File

@@ -9,4 +9,4 @@
* and be subject to the standard policies
*/
export { default as themeJsonSchema } from './themes/schema.generated.json';
export {};

View File

@@ -8,7 +8,8 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."],
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"resolveJsonModule": true
},
"exclude": ["dist/**/*"],
"include": [

View File

@@ -86,6 +86,7 @@ export class GrafanaBootConfig {
snapshotEnabled = true;
datasources: { [str: string]: DataSourceInstanceSettings } = {};
panels: { [key: string]: PanelPluginMeta } = {};
/** @deprecated it will be removed in a future release, use isAppPluginInstalled or getAppPluginVersion instead */
apps: Record<string, AppPluginConfigGrafanaData> = {};
auth: AuthSettings = {};
minRefreshInterval = '';

View File

@@ -77,3 +77,5 @@ export {
getCorrelationsService,
setCorrelationsService,
} from './services/CorrelationsService';
export { getAppPluginVersion, isAppPluginInstalled } from './services/pluginMeta/apps';
export { useAppPluginInstalled, useAppPluginVersion } from './services/pluginMeta/hooks';

View File

@@ -29,3 +29,5 @@ export {
export { UserStorage } from '../utils/userStorage';
export { initOpenFeature, evaluateBooleanFlag } from './openFeature';
export { getAppPluginMeta, getAppPluginMetas, setAppPluginMetas } from '../services/pluginMeta/apps';
export { useAppPluginMeta, useAppPluginMetas } from '../services/pluginMeta/hooks';

View File

@@ -0,0 +1,258 @@
import { evaluateBooleanFlag } from '../../internal/openFeature';
import {
getAppPluginMeta,
getAppPluginMetas,
getAppPluginVersion,
isAppPluginInstalled,
setAppPluginMetas,
} from './apps';
import { initPluginMetas } from './plugins';
import { app } from './test-fixtures/config.apps';
jest.mock('./plugins', () => ({ ...jest.requireActual('./plugins'), initPluginMetas: jest.fn() }));
jest.mock('../../internal/openFeature', () => ({
...jest.requireActual('../../internal/openFeature'),
evaluateBooleanFlag: jest.fn(),
}));
const initPluginMetasMock = jest.mocked(initPluginMetas);
const evaluateBooleanFlagMock = jest.mocked(evaluateBooleanFlag);
describe('when useMTPlugins flag is enabled and apps is not initialized', () => {
beforeEach(() => {
setAppPluginMetas({});
jest.resetAllMocks();
initPluginMetasMock.mockResolvedValue({ items: [] });
evaluateBooleanFlagMock.mockReturnValue(true);
});
it('getAppPluginMetas should call initPluginMetas and return correct result', async () => {
const apps = await getAppPluginMetas();
expect(apps).toEqual([]);
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
});
it('getAppPluginMeta should call initPluginMetas and return correct result', async () => {
const result = await getAppPluginMeta('myorg-someplugin-app');
expect(result).toEqual(null);
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
});
it('isAppPluginInstalled should call initPluginMetas and return false', async () => {
const installed = await isAppPluginInstalled('myorg-someplugin-app');
expect(installed).toEqual(false);
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
});
it('getAppPluginVersion should call initPluginMetas and return null', async () => {
const result = await getAppPluginVersion('myorg-someplugin-app');
expect(result).toEqual(null);
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
});
});
describe('when useMTPlugins flag is enabled and apps is initialized', () => {
beforeEach(() => {
setAppPluginMetas({ 'myorg-someplugin-app': app });
jest.resetAllMocks();
evaluateBooleanFlagMock.mockReturnValue(true);
});
it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => {
const apps = await getAppPluginMetas();
expect(apps).toEqual([app]);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => {
const result = await getAppPluginMeta('myorg-someplugin-app');
expect(result).toEqual(app);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginMeta should return null if the pluginId is not found', async () => {
const result = await getAppPluginMeta('otherorg-otherplugin-app');
expect(result).toEqual(null);
});
it('isAppPluginInstalled should not call initPluginMetas and return true', async () => {
const installed = await isAppPluginInstalled('myorg-someplugin-app');
expect(installed).toEqual(true);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('isAppPluginInstalled should return false if the pluginId is not found', async () => {
const result = await isAppPluginInstalled('otherorg-otherplugin-app');
expect(result).toEqual(false);
});
it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => {
const result = await getAppPluginVersion('myorg-someplugin-app');
expect(result).toEqual('1.0.0');
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginVersion should return null if the pluginId is not found', async () => {
const result = await getAppPluginVersion('otherorg-otherplugin-app');
expect(result).toEqual(null);
});
});
describe('when useMTPlugins flag is disabled and apps is not initialized', () => {
beforeEach(() => {
setAppPluginMetas({});
jest.resetAllMocks();
evaluateBooleanFlagMock.mockReturnValue(false);
});
it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => {
const apps = await getAppPluginMetas();
expect(apps).toEqual([]);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => {
const result = await getAppPluginMeta('myorg-someplugin-app');
expect(result).toEqual(null);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('isAppPluginInstalled should not call initPluginMetas and return false', async () => {
const result = await isAppPluginInstalled('myorg-someplugin-app');
expect(result).toEqual(false);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => {
const result = await getAppPluginVersion('myorg-someplugin-app');
expect(result).toEqual(null);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
});
describe('when useMTPlugins flag is disabled and apps is initialized', () => {
beforeEach(() => {
setAppPluginMetas({ 'myorg-someplugin-app': app });
jest.resetAllMocks();
evaluateBooleanFlagMock.mockReturnValue(false);
});
it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => {
const apps = await getAppPluginMetas();
expect(apps).toEqual([app]);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => {
const result = await getAppPluginMeta('myorg-someplugin-app');
expect(result).toEqual(app);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginMeta should return null if the pluginId is not found', async () => {
const result = await getAppPluginMeta('otherorg-otherplugin-app');
expect(result).toEqual(null);
});
it('isAppPluginInstalled should not call initPluginMetas and return true', async () => {
const result = await isAppPluginInstalled('myorg-someplugin-app');
expect(result).toEqual(true);
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('isAppPluginInstalled should return false if the pluginId is not found', async () => {
const result = await isAppPluginInstalled('otherorg-otherplugin-app');
expect(result).toEqual(false);
});
it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => {
const result = await getAppPluginVersion('myorg-someplugin-app');
expect(result).toEqual('1.0.0');
expect(initPluginMetasMock).not.toHaveBeenCalled();
});
it('getAppPluginVersion should return null if the pluginId is not found', async () => {
const result = await getAppPluginVersion('otherorg-otherplugin-app');
expect(result).toEqual(null);
});
});
describe('immutability', () => {
beforeEach(() => {
setAppPluginMetas({ 'myorg-someplugin-app': app });
jest.resetAllMocks();
evaluateBooleanFlagMock.mockReturnValue(false);
});
it('getAppPluginMetas should return a deep clone', async () => {
const mutatedApps = await getAppPluginMetas();
// assert we have correct props
expect(mutatedApps).toHaveLength(1);
expect(mutatedApps[0].dependencies.grafanaDependency).toEqual('>=10.4.0');
expect(mutatedApps[0].extensions.addedLinks).toHaveLength(0);
// mutate deep props
mutatedApps[0].dependencies.grafanaDependency = '';
mutatedApps[0].extensions.addedLinks.push({ targets: [], title: '', description: '' });
// assert we have mutated props
expect(mutatedApps[0].dependencies.grafanaDependency).toEqual('');
expect(mutatedApps[0].extensions.addedLinks).toHaveLength(1);
expect(mutatedApps[0].extensions.addedLinks[0]).toEqual({ targets: [], title: '', description: '' });
const apps = await getAppPluginMetas();
// assert that we have not mutated the source
expect(apps[0].dependencies.grafanaDependency).toEqual('>=10.4.0');
expect(apps[0].extensions.addedLinks).toHaveLength(0);
});
it('getAppPluginMeta should return a deep clone', async () => {
const mutatedApp = await getAppPluginMeta('myorg-someplugin-app');
// assert we have correct props
expect(mutatedApp).toBeDefined();
expect(mutatedApp!.dependencies.grafanaDependency).toEqual('>=10.4.0');
expect(mutatedApp!.extensions.addedLinks).toHaveLength(0);
// mutate deep props
mutatedApp!.dependencies.grafanaDependency = '';
mutatedApp!.extensions.addedLinks.push({ targets: [], title: '', description: '' });
// assert we have mutated props
expect(mutatedApp!.dependencies.grafanaDependency).toEqual('');
expect(mutatedApp!.extensions.addedLinks).toHaveLength(1);
expect(mutatedApp!.extensions.addedLinks[0]).toEqual({ targets: [], title: '', description: '' });
const result = await getAppPluginMeta('myorg-someplugin-app');
// assert that we have not mutated the source
expect(result).toBeDefined();
expect(result!.dependencies.grafanaDependency).toEqual('>=10.4.0');
expect(result!.extensions.addedLinks).toHaveLength(0);
});
});

View File

@@ -0,0 +1,71 @@
import type { AppPluginConfig } from '@grafana/data';
import { config } from '../../config';
import { evaluateBooleanFlag } from '../../internal/openFeature';
import { getAppPluginMapper } from './mappers/mappers';
import { initPluginMetas } from './plugins';
import type { AppPluginMetas } from './types';
let apps: AppPluginMetas = {};
function initialized(): boolean {
return Boolean(Object.keys(apps).length);
}
async function initAppPluginMetas(): Promise<void> {
if (!evaluateBooleanFlag('useMTPlugins', false)) {
// eslint-disable-next-line no-restricted-syntax
apps = config.apps;
return;
}
const metas = await initPluginMetas();
const mapper = getAppPluginMapper();
apps = mapper(metas);
}
export async function getAppPluginMetas(): Promise<AppPluginConfig[]> {
if (!initialized()) {
await initAppPluginMetas();
}
return Object.values(structuredClone(apps));
}
export async function getAppPluginMeta(pluginId: string): Promise<AppPluginConfig | null> {
if (!initialized()) {
await initAppPluginMetas();
}
const app = apps[pluginId];
return app ? structuredClone(app) : null;
}
/**
* Check if an app plugin is installed. The function does not check if the app plugin is enabled.
* @param pluginId - The id of the app plugin.
* @returns True if the app plugin is installed, false otherwise.
*/
export async function isAppPluginInstalled(pluginId: string): Promise<boolean> {
const app = await getAppPluginMeta(pluginId);
return Boolean(app);
}
/**
* Get the version of an app plugin.
* @param pluginId - The id of the app plugin.
* @returns The version of the app plugin, or null if the plugin is not installed.
*/
export async function getAppPluginVersion(pluginId: string): Promise<string | null> {
const app = await getAppPluginMeta(pluginId);
return app?.version ?? null;
}
export function setAppPluginMetas(override: AppPluginMetas): void {
if (process.env.NODE_ENV !== 'test') {
throw new Error('setAppPluginMetas() function can only be called from tests.');
}
apps = structuredClone(override);
}

View File

@@ -0,0 +1,214 @@
import { renderHook, waitFor } from '@testing-library/react';
import {
getAppPluginMeta,
getAppPluginMetas,
getAppPluginVersion,
isAppPluginInstalled,
setAppPluginMetas,
} from './apps';
import { useAppPluginMeta, useAppPluginMetas, useAppPluginInstalled, useAppPluginVersion } from './hooks';
import { apps } from './test-fixtures/config.apps';
const actualApps = jest.requireActual<typeof import('./apps')>('./apps');
jest.mock('./apps', () => ({
...jest.requireActual('./apps'),
getAppPluginMetas: jest.fn(),
getAppPluginMeta: jest.fn(),
isAppPluginInstalled: jest.fn(),
getAppPluginVersion: jest.fn(),
}));
const getAppPluginMetaMock = jest.mocked(getAppPluginMeta);
const getAppPluginMetasMock = jest.mocked(getAppPluginMetas);
const isAppPluginInstalledMock = jest.mocked(isAppPluginInstalled);
const getAppPluginVersionMock = jest.mocked(getAppPluginVersion);
describe('useAppPluginMeta', () => {
beforeEach(() => {
setAppPluginMetas(apps);
jest.resetAllMocks();
getAppPluginMetaMock.mockImplementation(actualApps.getAppPluginMeta);
});
it('should return correct default values', async () => {
const { result } = renderHook(() => useAppPluginMeta('grafana-exploretraces-app'));
expect(result.current.loading).toEqual(true);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toBeUndefined();
await waitFor(() => expect(result.current.loading).toEqual(true));
});
it('should return correct values after loading', async () => {
const { result } = renderHook(() => useAppPluginMeta('grafana-exploretraces-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toEqual(apps['grafana-exploretraces-app']);
});
it('should return correct values if the pluginId does not exist', async () => {
const { result } = renderHook(() => useAppPluginMeta('otherorg-otherplugin-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toEqual(null);
});
it('should return correct values if useAppPluginMeta throws', async () => {
getAppPluginMetaMock.mockRejectedValue(new Error('Some error'));
const { result } = renderHook(() => useAppPluginMeta('otherorg-otherplugin-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toEqual(new Error('Some error'));
expect(result.current.value).toBeUndefined();
});
});
describe('useAppPluginMetas', () => {
beforeEach(() => {
setAppPluginMetas(apps);
jest.resetAllMocks();
getAppPluginMetasMock.mockImplementation(actualApps.getAppPluginMetas);
});
it('should return correct default values', async () => {
const { result } = renderHook(() => useAppPluginMetas());
expect(result.current.loading).toEqual(true);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toBeUndefined();
await waitFor(() => expect(result.current.loading).toEqual(true));
});
it('should return correct values after loading', async () => {
const { result } = renderHook(() => useAppPluginMetas());
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toEqual(Object.values(apps));
});
it('should return correct values if useAppPluginMetas throws', async () => {
getAppPluginMetasMock.mockRejectedValue(new Error('Some error'));
const { result } = renderHook(() => useAppPluginMetas());
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toEqual(new Error('Some error'));
expect(result.current.value).toBeUndefined();
});
});
describe('useAppPluginInstalled', () => {
beforeEach(() => {
setAppPluginMetas(apps);
jest.resetAllMocks();
isAppPluginInstalledMock.mockImplementation(actualApps.isAppPluginInstalled);
});
it('should return correct default values', async () => {
const { result } = renderHook(() => useAppPluginInstalled('grafana-exploretraces-app'));
expect(result.current.loading).toEqual(true);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toBeUndefined();
await waitFor(() => expect(result.current.loading).toEqual(true));
});
it('should return correct values after loading', async () => {
const { result } = renderHook(() => useAppPluginInstalled('grafana-exploretraces-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toEqual(true);
});
it('should return correct values if the pluginId does not exist', async () => {
const { result } = renderHook(() => useAppPluginInstalled('otherorg-otherplugin-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toEqual(false);
});
it('should return correct values if isAppPluginInstalled throws', async () => {
isAppPluginInstalledMock.mockRejectedValue(new Error('Some error'));
const { result } = renderHook(() => useAppPluginInstalled('otherorg-otherplugin-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toEqual(new Error('Some error'));
expect(result.current.value).toBeUndefined();
});
});
describe('useAppPluginVersion', () => {
beforeEach(() => {
setAppPluginMetas(apps);
jest.resetAllMocks();
getAppPluginVersionMock.mockImplementation(actualApps.getAppPluginVersion);
});
it('should return correct default values', async () => {
const { result } = renderHook(() => useAppPluginVersion('grafana-exploretraces-app'));
expect(result.current.loading).toEqual(true);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toBeUndefined();
await waitFor(() => expect(result.current.loading).toEqual(true));
});
it('should return correct values after loading', async () => {
const { result } = renderHook(() => useAppPluginVersion('grafana-exploretraces-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toEqual('1.2.2');
});
it('should return correct values if the pluginId does not exist', async () => {
const { result } = renderHook(() => useAppPluginVersion('otherorg-otherplugin-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toBeUndefined();
expect(result.current.value).toEqual(null);
});
it('should return correct values if getAppPluginVersion throws', async () => {
getAppPluginVersionMock.mockRejectedValue(new Error('Some error'));
const { result } = renderHook(() => useAppPluginVersion('otherorg-otherplugin-app'));
await waitFor(() => expect(result.current.loading).toEqual(false));
expect(result.current.loading).toEqual(false);
expect(result.current.error).toEqual(new Error('Some error'));
expect(result.current.value).toBeUndefined();
});
});

View File

@@ -0,0 +1,35 @@
import { useAsync } from 'react-use';
import { getAppPluginMeta, getAppPluginMetas, getAppPluginVersion, isAppPluginInstalled } from './apps';
export function useAppPluginMetas() {
const { loading, error, value } = useAsync(async () => getAppPluginMetas());
return { loading, error, value };
}
export function useAppPluginMeta(pluginId: string) {
const { loading, error, value } = useAsync(async () => getAppPluginMeta(pluginId));
return { loading, error, value };
}
/**
* Hook that checks if an app plugin is installed. The hook does not check if the app plugin is enabled.
* @param pluginId - The ID of the app plugin.
* @returns loading, error, value of the app plugin installed status.
* The value is true if the app plugin is installed, false otherwise.
*/
export function useAppPluginInstalled(pluginId: string) {
const { loading, error, value } = useAsync(async () => isAppPluginInstalled(pluginId));
return { loading, error, value };
}
/**
* Hook that gets the version of an app plugin.
* @param pluginId - The ID of the app plugin.
* @returns loading, error, value of the app plugin version.
* The value is the version of the app plugin, or null if the plugin is not installed.
*/
export function useAppPluginVersion(pluginId: string) {
const { loading, error, value } = useAsync(async () => getAppPluginVersion(pluginId));
return { loading, error, value };
}

View File

@@ -0,0 +1,7 @@
import { AppPluginMetasMapper, PluginMetasResponse } from '../types';
import { v0alpha1AppMapper } from './v0alpha1AppMapper';
export function getAppPluginMapper(): AppPluginMetasMapper<PluginMetasResponse> {
return v0alpha1AppMapper;
}

View File

@@ -0,0 +1,84 @@
import { apps } from '../test-fixtures/config.apps';
import { v0alpha1Response } from '../test-fixtures/v0alpha1Response';
import { v0alpha1AppMapper } from './v0alpha1AppMapper';
const PLUGIN_IDS = v0alpha1Response.items
.filter((i) => i.spec.pluginJson.type === 'app')
.map((i) => ({ pluginId: i.spec.pluginJson.id }));
describe('v0alpha1AppMapper', () => {
describe.each(PLUGIN_IDS)('when called for pluginId:$pluginId', ({ pluginId }) => {
it('should map id property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].id).toEqual(apps[pluginId].id);
});
it('should map path property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].path).toEqual(apps[pluginId].path);
});
it('should map version property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].version).toEqual(apps[pluginId].version);
});
it('should map preload property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].preload).toEqual(apps[pluginId].preload);
});
it('should map angular property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].angular).toEqual({});
});
it('should map loadingStrategy property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].loadingStrategy).toEqual(apps[pluginId].loadingStrategy);
});
it('should map dependencies property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].dependencies).toEqual(apps[pluginId].dependencies);
});
it('should map extensions property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].extensions.addedComponents).toEqual(apps[pluginId].extensions.addedComponents);
expect(result[pluginId].extensions.addedFunctions).toEqual(apps[pluginId].extensions.addedFunctions);
expect(result[pluginId].extensions.addedLinks).toEqual(apps[pluginId].extensions.addedLinks);
expect(result[pluginId].extensions.exposedComponents).toEqual(apps[pluginId].extensions.exposedComponents);
expect(result[pluginId].extensions.extensionPoints).toEqual(apps[pluginId].extensions.extensionPoints);
});
it('should map moduleHash property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].moduleHash).toEqual(apps[pluginId].moduleHash);
});
it('should map buildMode property correctly', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(result[pluginId].buildMode).toEqual(apps[pluginId].buildMode);
});
});
it('should only map specs with type app', () => {
const result = v0alpha1AppMapper(v0alpha1Response);
expect(v0alpha1Response.items).toHaveLength(58);
expect(Object.keys(result)).toHaveLength(5);
expect(Object.keys(result)).toEqual(Object.keys(apps));
});
});

View File

@@ -0,0 +1,111 @@
import {
type AngularMeta,
type AppPluginConfig,
type PluginDependencies,
type PluginExtensions,
PluginLoadingStrategy,
type PluginType,
} from '@grafana/data';
import type { AppPluginMetas, AppPluginMetasMapper, PluginMetasResponse } from '../types';
import type { Spec as v0alpha1Spec } from '../types/types.spec.gen';
function angularyMapper(spec: v0alpha1Spec): AngularMeta {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return {} as AngularMeta;
}
function dependenciesMapper(spec: v0alpha1Spec): PluginDependencies {
const plugins = (spec.pluginJson.dependencies?.plugins ?? []).map((v) => ({
...v,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
type: v.type as PluginType,
version: '',
}));
const dependencies: PluginDependencies = {
...spec.pluginJson.dependencies,
extensions: {
exposedComponents: spec.pluginJson.dependencies.extensions?.exposedComponents ?? [],
},
grafanaDependency: spec.pluginJson.dependencies.grafanaDependency,
grafanaVersion: spec.pluginJson.dependencies.grafanaVersion ?? '',
plugins,
};
return dependencies;
}
function extensionsMapper(spec: v0alpha1Spec): PluginExtensions {
const addedComponents = spec.pluginJson.extensions?.addedComponents ?? [];
const addedFunctions = spec.pluginJson.extensions?.addedFunctions ?? [];
const addedLinks = spec.pluginJson.extensions?.addedLinks ?? [];
const exposedComponents = (spec.pluginJson.extensions?.exposedComponents ?? []).map((v) => ({
...v,
description: v.description ?? '',
title: v.title ?? '',
}));
const extensionPoints = (spec.pluginJson.extensions?.extensionPoints ?? []).map((v) => ({
...v,
description: v.description ?? '',
title: v.title ?? '',
}));
const extensions: PluginExtensions = {
addedComponents,
addedFunctions,
addedLinks,
exposedComponents,
extensionPoints,
};
return extensions;
}
function loadingStrategyMapper(spec: v0alpha1Spec): PluginLoadingStrategy {
const loadingStrategy = spec.module?.loadingStrategy ?? PluginLoadingStrategy.fetch;
if (loadingStrategy === PluginLoadingStrategy.script) {
return PluginLoadingStrategy.script;
}
return PluginLoadingStrategy.fetch;
}
function specMapper(spec: v0alpha1Spec): AppPluginConfig {
const { id, info, preload = false } = spec.pluginJson;
const angular = angularyMapper(spec);
const dependencies = dependenciesMapper(spec);
const extensions = extensionsMapper(spec);
const loadingStrategy = loadingStrategyMapper(spec);
const path = spec.module?.path ?? '';
const version = info.version;
const buildMode = spec.pluginJson.buildMode ?? 'production';
const moduleHash = spec.module?.hash;
return {
id,
angular,
dependencies,
extensions,
loadingStrategy,
path,
preload,
version,
buildMode,
moduleHash,
};
}
export const v0alpha1AppMapper: AppPluginMetasMapper<PluginMetasResponse> = (response) => {
const result: AppPluginMetas = {};
return response.items.reduce((acc, curr) => {
if (curr.spec.pluginJson.type !== 'app') {
return acc;
}
const config = specMapper(curr.spec);
acc[config.id] = config;
return acc;
}, result);
};

View File

@@ -0,0 +1,153 @@
import { evaluateBooleanFlag } from '../../internal/openFeature';
import { clearCache, initPluginMetas } from './plugins';
import { v0alpha1Meta } from './test-fixtures/v0alpha1Response';
jest.mock('../../internal/openFeature', () => ({
...jest.requireActual('../../internal/openFeature'),
evaluateBooleanFlag: jest.fn(),
}));
const evaluateBooleanFlagMock = jest.mocked(evaluateBooleanFlag);
describe('when useMTPlugins toggle is enabled and cache is not initialized', () => {
const originalFetch = global.fetch;
beforeEach(() => {
jest.resetAllMocks();
clearCache();
evaluateBooleanFlagMock.mockReturnValue(true);
});
afterEach(() => {
global.fetch = originalFetch;
});
it('initPluginMetas should call loadPluginMetas and return correct result if response is ok', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ items: [v0alpha1Meta] }),
});
const response = await initPluginMetas();
expect(response.items).toHaveLength(1);
expect(response.items[0]).toEqual(v0alpha1Meta);
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith('/apis/plugins.grafana.app/v0alpha1/namespaces/default/metas');
});
it('initPluginMetas should call loadPluginMetas and return correct result if response is not ok', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not found',
});
await expect(initPluginMetas()).rejects.toThrow(new Error(`Failed to load plugin metas 404:Not found`));
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith('/apis/plugins.grafana.app/v0alpha1/namespaces/default/metas');
});
});
describe('when useMTPlugins toggle is enabled and cache is initialized', () => {
const originalFetch = global.fetch;
beforeEach(() => {
jest.resetAllMocks();
clearCache();
evaluateBooleanFlagMock.mockReturnValue(true);
});
afterEach(() => {
global.fetch = originalFetch;
});
it('initPluginMetas should return cache', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ items: [v0alpha1Meta] }),
});
const original = await initPluginMetas();
const cached = await initPluginMetas();
expect(original).toEqual(cached);
expect(global.fetch).toHaveBeenCalledTimes(1);
});
it('initPluginMetas should return inflight promise', async () => {
jest.useFakeTimers();
global.fetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ items: [v0alpha1Meta] }),
});
const original = initPluginMetas();
const cached = initPluginMetas();
await jest.runAllTimersAsync();
expect(original).toEqual(cached);
expect(global.fetch).toHaveBeenCalledTimes(1);
});
});
describe('when useMTPlugins toggle is disabled and cache is not initialized', () => {
const originalFetch = global.fetch;
beforeEach(() => {
jest.resetAllMocks();
clearCache();
global.fetch = jest.fn();
evaluateBooleanFlagMock.mockReturnValue(false);
});
afterEach(() => {
global.fetch = originalFetch;
});
it('initPluginMetas should call loadPluginMetas and return correct result if response is ok', async () => {
const response = await initPluginMetas();
expect(response.items).toHaveLength(0);
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe('when useMTPlugins toggle is disabled and cache is initialized', () => {
const originalFetch = global.fetch;
beforeEach(() => {
jest.resetAllMocks();
clearCache();
global.fetch = jest.fn();
evaluateBooleanFlagMock.mockReturnValue(false);
});
afterEach(() => {
global.fetch = originalFetch;
});
it('initPluginMetas should return cache', async () => {
const original = await initPluginMetas();
const cached = await initPluginMetas();
expect(original).toEqual(cached);
expect(global.fetch).not.toHaveBeenCalled();
});
it('initPluginMetas should return inflight promise', async () => {
jest.useFakeTimers();
const original = initPluginMetas();
const cached = initPluginMetas();
await jest.runAllTimersAsync();
expect(original).toEqual(cached);
expect(global.fetch).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,41 @@
import { config } from '../../config';
import { evaluateBooleanFlag } from '../../internal/openFeature';
import type { PluginMetasResponse } from './types';
let initPromise: Promise<PluginMetasResponse> | null = null;
function getApiVersion(): string {
return 'v0alpha1';
}
async function loadPluginMetas(): Promise<PluginMetasResponse> {
if (!evaluateBooleanFlag('useMTPlugins', false)) {
const result = { items: [] };
return result;
}
const metas = await fetch(`/apis/plugins.grafana.app/${getApiVersion()}/namespaces/${config.namespace}/metas`);
if (!metas.ok) {
throw new Error(`Failed to load plugin metas ${metas.status}:${metas.statusText}`);
}
const result = await metas.json();
return result;
}
export function initPluginMetas(): Promise<PluginMetasResponse> {
if (!initPromise) {
initPromise = loadPluginMetas();
}
return initPromise;
}
export function clearCache() {
if (process.env.NODE_ENV !== 'test') {
throw new Error('clearCache() function can only be called from tests.');
}
initPromise = null;
}

View File

@@ -0,0 +1,303 @@
import { cloneDeep } from 'lodash';
import { AngularMeta, AppPluginConfig, PluginLoadingStrategy } from '@grafana/data';
import { AppPluginMetas } from '../types';
export const app: AppPluginConfig = cloneDeep({
id: 'myorg-someplugin-app',
path: 'public/plugins/myorg-someplugin-app/module.js',
version: '1.0.0',
preload: false,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
angular: { detected: false } as AngularMeta,
loadingStrategy: PluginLoadingStrategy.script,
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [],
extensionPoints: [],
addedFunctions: [],
},
dependencies: {
grafanaDependency: '>=10.4.0',
grafanaVersion: '*',
plugins: [],
extensions: {
exposedComponents: [],
},
},
buildMode: 'production',
});
export const apps: AppPluginMetas = cloneDeep({
'grafana-exploretraces-app': {
id: 'grafana-exploretraces-app',
path: 'public/plugins/grafana-exploretraces-app/module.js',
version: '1.2.2',
preload: true,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
angular: { detected: false } as AngularMeta,
loadingStrategy: PluginLoadingStrategy.script,
extensions: {
addedLinks: [
{
targets: ['grafana/dashboard/panel/menu'],
title: 'Open in Traces Drilldown',
description: 'Open current query in the Traces Drilldown app',
},
{
targets: ['grafana/explore/toolbar/action'],
title: 'Open in Grafana Traces Drilldown',
description: 'Try our new queryless experience for traces',
},
],
addedComponents: [
{
targets: ['grafana-asserts-app/entity-assertions-widget/v1'],
title: 'Asserts widget',
description: 'A block with assertions for a given service',
},
{
targets: ['grafana-asserts-app/insights-timeline-widget/v1'],
title: 'Insights Timeline Widget',
description: 'Widget for displaying insights timeline in other apps',
},
],
exposedComponents: [
{
id: 'grafana-exploretraces-app/open-in-explore-traces-button/v1',
title: 'Open in Traces Drilldown button',
description: 'A button that opens a traces view in the Traces Drilldown app.',
},
{
id: 'grafana-exploretraces-app/embedded-trace-exploration/v1',
title: 'Embedded Trace Exploration',
description:
'A component that renders a trace exploration view that can be embedded in other parts of Grafana.',
},
],
extensionPoints: [
{
id: 'grafana-exploretraces-app/investigation/v1',
title: '',
description: '',
},
{
id: 'grafana-exploretraces-app/get-logs-drilldown-link/v1',
title: '',
description: '',
},
],
addedFunctions: [],
},
dependencies: {
grafanaDependency: '>=11.5.0',
grafanaVersion: '*',
plugins: [],
extensions: {
exposedComponents: [
'grafana-asserts-app/entity-assertions-widget/v1',
'grafana-asserts-app/insights-timeline-widget/v1',
],
},
},
buildMode: 'production',
},
'grafana-lokiexplore-app': {
id: 'grafana-lokiexplore-app',
path: 'public/plugins/grafana-lokiexplore-app/module.js',
version: '1.0.32',
preload: true,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
angular: { detected: false } as AngularMeta,
loadingStrategy: PluginLoadingStrategy.script,
extensions: {
addedLinks: [
{
targets: [
'grafana/dashboard/panel/menu',
'grafana/explore/toolbar/action',
'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1',
'grafana-assistant-app/navigateToDrilldown/v1',
],
title: 'Open in Grafana Logs Drilldown',
description: 'Open current query in the Grafana Logs Drilldown view',
},
],
addedComponents: [
{
targets: ['grafana-asserts-app/insights-timeline-widget/v1'],
title: 'Insights Timeline Widget',
description: 'Widget for displaying insights timeline in other apps',
},
],
exposedComponents: [
{
id: 'grafana-lokiexplore-app/open-in-explore-logs-button/v1',
title: 'Open in Logs Drilldown button',
description: 'A button that opens a logs view in the Logs Drilldown app.',
},
{
id: 'grafana-lokiexplore-app/embedded-logs-exploration/v1',
title: 'Embedded Logs Exploration',
description:
'A component that renders a logs exploration view that can be embedded in other parts of Grafana.',
},
],
extensionPoints: [
{
id: 'grafana-lokiexplore-app/investigation/v1',
title: '',
description: '',
},
],
addedFunctions: [
{
targets: ['grafana-exploretraces-app/get-logs-drilldown-link/v1'],
title: 'Open Logs Drilldown',
description: 'Returns url to logs drilldown app',
},
],
},
dependencies: {
grafanaDependency: '>=11.6.0',
grafanaVersion: '*',
plugins: [],
extensions: {
exposedComponents: [
'grafana-adaptivelogs-app/temporary-exemptions/v1',
'grafana-lokiexplore-app/embedded-logs-exploration/v1',
'grafana-asserts-app/insights-timeline-widget/v1',
'grafana/add-to-dashboard-form/v1',
],
},
},
buildMode: 'production',
},
'grafana-metricsdrilldown-app': {
id: 'grafana-metricsdrilldown-app',
path: 'public/plugins/grafana-metricsdrilldown-app/module.js',
version: '1.0.26',
preload: true,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
angular: { detected: false } as AngularMeta,
loadingStrategy: PluginLoadingStrategy.script,
extensions: {
addedLinks: [
{
targets: [
'grafana/dashboard/panel/menu',
'grafana/explore/toolbar/action',
'grafana-assistant-app/navigateToDrilldown/v1',
'grafana/alerting/alertingrule/queryeditor',
],
title: 'Open in Grafana Metrics Drilldown',
description: 'Open current query in the Grafana Metrics Drilldown view',
},
{
targets: ['grafana-metricsdrilldown-app/grafana-assistant-app/navigateToDrilldown/v0-alpha'],
title: 'Navigate to metrics drilldown',
description: 'Build a url path to the metrics drilldown',
},
{
targets: ['grafana/datasources/config/actions', 'grafana/datasources/config/status'],
title: 'Open in Metrics Drilldown',
description: 'Browse metrics in Grafana Metrics Drilldown',
},
],
addedComponents: [],
exposedComponents: [
{
id: 'grafana-metricsdrilldown-app/label-breakdown-component/v1',
title: 'Label Breakdown',
description: 'A metrics label breakdown view from the Metrics Drilldown app.',
},
{
id: 'grafana-metricsdrilldown-app/knowledge-graph-insight-metrics/v1',
title: 'Knowledge Graph Source Metrics',
description: 'Explore the underlying metrics related to a Knowledge Graph insight',
},
],
extensionPoints: [
{
id: 'grafana-exploremetrics-app/investigation/v1',
title: '',
description: '',
},
{
id: 'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1',
title: '',
description: '',
},
],
addedFunctions: [],
},
dependencies: {
grafanaDependency: '>=11.6.0',
grafanaVersion: '*',
plugins: [],
extensions: {
exposedComponents: ['grafana/add-to-dashboard-form/v1'],
},
},
buildMode: 'production',
},
'grafana-pyroscope-app': {
id: 'grafana-pyroscope-app',
path: 'public/plugins/grafana-pyroscope-app/module.js',
version: '1.14.2',
preload: true,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
angular: { detected: false } as AngularMeta,
loadingStrategy: PluginLoadingStrategy.script,
extensions: {
addedLinks: [
{
targets: [
'grafana/explore/toolbar/action',
'grafana/traceview/details',
'grafana-assistant-app/navigateToDrilldown/v1',
],
title: 'Open in Grafana Profiles Drilldown',
description: 'Try our new queryless experience for profiles',
},
],
addedComponents: [],
exposedComponents: [
{
id: 'grafana-pyroscope-app/embedded-profiles-exploration/v1',
title: 'Embedded Profiles Exploration',
description:
'A component that renders a profiles exploration view that can be embedded in other parts of Grafana.',
},
],
extensionPoints: [
{
id: 'grafana-pyroscope-app/investigation/v1',
title: '',
description: '',
},
{
id: 'grafana-pyroscope-app/settings/v1',
title: '',
description: '',
},
],
addedFunctions: [],
},
dependencies: {
grafanaDependency: '>=11.5.0',
grafanaVersion: '*',
plugins: [],
extensions: {
exposedComponents: [
'grafana-o11yinsights-app/insights-launcher/v1',
'grafana-adaptiveprofiles-app/resolution-boost/v1',
],
},
},
buildMode: 'production',
},
[app.id]: app,
});

View File

@@ -0,0 +1,10 @@
import type { AppPluginConfig } from '@grafana/data';
import type { Meta } from './types/meta_object_gen';
export type AppPluginMetas = Record<string, AppPluginConfig>;
export type AppPluginMetasMapper<T> = (response: T) => AppPluginMetas;
export interface PluginMetasResponse {
items: Meta[];
}

View File

@@ -0,0 +1,49 @@
/*
* This file was generated by grafana-app-sdk. DO NOT EDIT.
*/
import { Spec } from './types.spec.gen';
import { Status } from './types.status.gen';
export interface Metadata {
name: string;
namespace: string;
generateName?: string;
selfLink?: string;
uid?: string;
resourceVersion?: string;
generation?: number;
creationTimestamp?: string;
deletionTimestamp?: string;
deletionGracePeriodSeconds?: number;
labels?: Record<string, string>;
annotations?: Record<string, string>;
ownerReferences?: OwnerReference[];
finalizers?: string[];
managedFields?: ManagedFieldsEntry[];
}
export interface OwnerReference {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller?: boolean;
blockOwnerDeletion?: boolean;
}
export interface ManagedFieldsEntry {
manager?: string;
operation?: string;
apiVersion?: string;
time?: string;
fieldsType?: string;
subresource?: string;
}
export interface Meta {
kind: string;
apiVersion: string;
metadata: Metadata;
spec: Spec;
status: Status;
}

View File

@@ -0,0 +1,278 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// JSON configuration schema for Grafana plugins
// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json
export interface JSONData {
// Unique name of the plugin
id: string;
// Plugin type
type: "app" | "datasource" | "panel" | "renderer";
// Human-readable name of the plugin
name: string;
// Metadata for the plugin
info: Info;
// Dependency information
dependencies: Dependencies;
// Optional fields
alerting?: boolean;
annotations?: boolean;
autoEnabled?: boolean;
backend?: boolean;
buildMode?: string;
builtIn?: boolean;
category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other";
enterpriseFeatures?: EnterpriseFeatures;
executable?: string;
hideFromList?: boolean;
// +listType=atomic
includes?: Include[];
logs?: boolean;
metrics?: boolean;
multiValueFilterOperators?: boolean;
pascalName?: string;
preload?: boolean;
queryOptions?: QueryOptions;
// +listType=atomic
routes?: Route[];
skipDataQuery?: boolean;
state?: "alpha" | "beta";
streaming?: boolean;
suggestions?: boolean;
tracing?: boolean;
iam?: IAM;
// +listType=atomic
roles?: Role[];
extensions?: Extensions;
}
export const defaultJSONData = (): JSONData => ({
id: "",
type: "app",
name: "",
info: defaultInfo(),
dependencies: defaultDependencies(),
});
export interface Info {
// Required fields
// +listType=set
keywords: string[];
logos: {
small: string;
large: string;
};
updated: string;
version: string;
// Optional fields
author?: {
name?: string;
email?: string;
url?: string;
};
description?: string;
// +listType=atomic
links?: {
name?: string;
url?: string;
}[];
// +listType=atomic
screenshots?: {
name?: string;
path?: string;
}[];
}
export const defaultInfo = (): Info => ({
keywords: [],
logos: {
small: "",
large: "",
},
updated: "",
version: "",
});
export interface Dependencies {
// Required field
grafanaDependency: string;
// Optional fields
grafanaVersion?: string;
// +listType=set
// +listMapKey=id
plugins?: {
id: string;
type: "app" | "datasource" | "panel";
name: string;
}[];
extensions?: {
// +listType=set
exposedComponents?: string[];
};
}
export const defaultDependencies = (): Dependencies => ({
grafanaDependency: "",
});
export interface EnterpriseFeatures {
// Allow additional properties
healthDiagnosticsErrors?: boolean;
}
export const defaultEnterpriseFeatures = (): EnterpriseFeatures => ({
healthDiagnosticsErrors: false,
});
export interface Include {
uid?: string;
type?: "dashboard" | "page" | "panel" | "datasource";
name?: string;
component?: string;
role?: "Admin" | "Editor" | "Viewer" | "None";
action?: string;
path?: string;
addToNav?: boolean;
defaultNav?: boolean;
icon?: string;
}
export const defaultInclude = (): Include => ({
});
export interface QueryOptions {
maxDataPoints?: boolean;
minInterval?: boolean;
cacheTimeout?: boolean;
}
export const defaultQueryOptions = (): QueryOptions => ({
});
export interface Route {
path?: string;
method?: string;
url?: string;
reqSignedIn?: boolean;
reqRole?: string;
reqAction?: string;
// +listType=atomic
headers?: string[];
body?: Record<string, any>;
tokenAuth?: {
url?: string;
// +listType=set
scopes?: string[];
params?: Record<string, any>;
};
jwtTokenAuth?: {
url?: string;
// +listType=set
scopes?: string[];
params?: Record<string, any>;
};
// +listType=atomic
urlParams?: {
name?: string;
content?: string;
}[];
}
export const defaultRoute = (): Route => ({
});
export interface IAM {
// +listType=atomic
permissions?: {
action?: string;
scope?: string;
}[];
}
export const defaultIAM = (): IAM => ({
});
export interface Role {
role?: {
name?: string;
description?: string;
// +listType=atomic
permissions?: {
action?: string;
scope?: string;
}[];
};
// +listType=set
grants?: string[];
}
export const defaultRole = (): Role => ({
});
export interface Extensions {
// +listType=atomic
addedComponents?: {
// +listType=set
targets: string[];
title: string;
description?: string;
}[];
// +listType=atomic
addedLinks?: {
// +listType=set
targets: string[];
title: string;
description?: string;
}[];
// +listType=atomic
addedFunctions?: {
// +listType=set
targets: string[];
title: string;
description?: string;
}[];
// +listType=set
// +listMapKey=id
exposedComponents?: {
id: string;
title?: string;
description?: string;
}[];
// +listType=set
// +listMapKey=id
extensionPoints?: {
id: string;
title?: string;
description?: string;
}[];
}
export const defaultExtensions = (): Extensions => ({
});
export interface Spec {
pluginJson: JSONData;
class: "core" | "external";
module?: {
path: string;
hash?: string;
loadingStrategy?: "fetch" | "script";
};
baseURL?: string;
signature?: {
status: "internal" | "valid" | "invalid" | "modified" | "unsigned";
type?: "grafana" | "commercial" | "community" | "private" | "private-glob";
org?: string;
};
angular?: {
detected: boolean;
};
translations?: Record<string, string>;
// +listType=atomic
children?: string[];
}
export const defaultSpec = (): Spec => ({
pluginJson: defaultJSONData(),
class: "core",
});

View File

@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface OperatorState {
// lastEvaluation is the ResourceVersion last evaluated
lastEvaluation: string;
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
state: "success" | "in_progress" | "failed";
// descriptiveState is an optional more descriptive state field which has no requirements on format
descriptiveState?: string;
// details contains any extra information that is operator-specific
details?: Record<string, any>;
}
export const defaultOperatorState = (): OperatorState => ({
lastEvaluation: "",
state: "success",
});
export interface Status {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
operatorStates?: Record<string, OperatorState>;
// additionalFields is reserved for future use
additionalFields?: Record<string, any>;
}
export const defaultStatus = (): Status => ({
});

View File

@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "12.4.0-pre";
export const pluginVersion = "%VERSION%";
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);

View File

@@ -29,11 +29,14 @@ export interface Options extends common.SingleStatBaseOptions {
barWidthFactor: number;
effects: GaugePanelEffects;
endpointMarker?: ('point' | 'glow' | 'none');
minVizHeight: number;
minVizWidth: number;
segmentCount: number;
segmentSpacing: number;
shape: ('circle' | 'gauge');
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
sizing: common.BarGaugeSizing;
sparkline?: boolean;
textMode?: ('auto' | 'value_and_name' | 'value' | 'name' | 'none');
}
@@ -43,11 +46,14 @@ export const defaultOptions: Partial<Options> = {
barWidthFactor: 0.5,
effects: {},
endpointMarker: 'point',
minVizHeight: 75,
minVizWidth: 75,
segmentCount: 1,
segmentSpacing: 0.3,
shape: 'gauge',
showThresholdLabels: false,
showThresholdMarkers: true,
sizing: common.BarGaugeSizing.Auto,
sparkline: true,
textMode: 'auto',
};

View File

@@ -1,78 +0,0 @@
import { render, screen } from '@testing-library/react';
import { VizLegendTable } from './VizLegendTable';
import { VizLegendItem } from './types';
describe('VizLegendTable', () => {
const mockItems: VizLegendItem[] = [
{ label: 'Series 1', color: 'red', yAxis: 1 },
{ label: 'Series 2', color: 'blue', yAxis: 1 },
{ label: 'Series 3', color: 'green', yAxis: 1 },
];
it('renders without crashing', () => {
const { container } = render(<VizLegendTable items={mockItems} placement="bottom" />);
expect(container.querySelector('table')).toBeInTheDocument();
});
it('renders all items', () => {
render(<VizLegendTable items={mockItems} placement="bottom" />);
expect(screen.getByText('Series 1')).toBeInTheDocument();
expect(screen.getByText('Series 2')).toBeInTheDocument();
expect(screen.getByText('Series 3')).toBeInTheDocument();
});
it('renders table headers when items have display values', () => {
const itemsWithStats: VizLegendItem[] = [
{
label: 'Series 1',
color: 'red',
yAxis: 1,
getDisplayValues: () => [
{ numeric: 100, text: '100', title: 'Max' },
{ numeric: 50, text: '50', title: 'Min' },
],
},
];
render(<VizLegendTable items={itemsWithStats} placement="bottom" />);
expect(screen.getByText('Max')).toBeInTheDocument();
expect(screen.getByText('Min')).toBeInTheDocument();
});
it('renders sort icon when sorted', () => {
const { container } = render(
<VizLegendTable items={mockItems} placement="bottom" sortBy="Name" sortDesc={false} />
);
expect(container.querySelector('svg')).toBeInTheDocument();
});
it('calls onToggleSort when header is clicked', () => {
const onToggleSort = jest.fn();
render(<VizLegendTable items={mockItems} placement="bottom" onToggleSort={onToggleSort} isSortable={true} />);
const header = screen.getByText('Name');
header.click();
expect(onToggleSort).toHaveBeenCalledWith('Name');
});
it('does not call onToggleSort when not sortable', () => {
const onToggleSort = jest.fn();
render(<VizLegendTable items={mockItems} placement="bottom" onToggleSort={onToggleSort} isSortable={false} />);
const header = screen.getByText('Name');
header.click();
expect(onToggleSort).not.toHaveBeenCalled();
});
it('renders with long labels', () => {
const itemsWithLongLabels: VizLegendItem[] = [
{
label: 'This is a very long series name that should be scrollable within its table cell',
color: 'red',
yAxis: 1,
},
];
render(<VizLegendTable items={itemsWithLongLabels} placement="bottom" />);
expect(
screen.getByText('This is a very long series name that should be scrollable within its table cell')
).toBeInTheDocument();
});
});

View File

@@ -1,112 +0,0 @@
import { render, screen } from '@testing-library/react';
import { LegendTableItem } from './VizLegendTableItem';
import { VizLegendItem } from './types';
describe('LegendTableItem', () => {
const mockItem: VizLegendItem = {
label: 'Series 1',
color: 'red',
yAxis: 1,
};
it('renders without crashing', () => {
const { container } = render(
<table>
<tbody>
<LegendTableItem item={mockItem} />
</tbody>
</table>
);
expect(container.querySelector('tr')).toBeInTheDocument();
});
it('renders label text', () => {
render(
<table>
<tbody>
<LegendTableItem item={mockItem} />
</tbody>
</table>
);
expect(screen.getByText('Series 1')).toBeInTheDocument();
});
it('renders with long label text', () => {
const longLabelItem: VizLegendItem = {
...mockItem,
label: 'This is a very long series name that should be scrollable in the table cell',
};
render(
<table>
<tbody>
<LegendTableItem item={longLabelItem} />
</tbody>
</table>
);
expect(
screen.getByText('This is a very long series name that should be scrollable in the table cell')
).toBeInTheDocument();
});
it('renders stat values when provided', () => {
const itemWithStats: VizLegendItem = {
...mockItem,
getDisplayValues: () => [
{ numeric: 100, text: '100', title: 'Max' },
{ numeric: 50, text: '50', title: 'Min' },
],
};
render(
<table>
<tbody>
<LegendTableItem item={itemWithStats} />
</tbody>
</table>
);
expect(screen.getByText('100')).toBeInTheDocument();
expect(screen.getByText('50')).toBeInTheDocument();
});
it('renders right y-axis indicator when yAxis is 2', () => {
const rightAxisItem: VizLegendItem = {
...mockItem,
yAxis: 2,
};
render(
<table>
<tbody>
<LegendTableItem item={rightAxisItem} />
</tbody>
</table>
);
expect(screen.getByText('(right y-axis)')).toBeInTheDocument();
});
it('calls onLabelClick when label is clicked', () => {
const onLabelClick = jest.fn();
render(
<table>
<tbody>
<LegendTableItem item={mockItem} onLabelClick={onLabelClick} />
</tbody>
</table>
);
const button = screen.getByRole('button');
button.click();
expect(onLabelClick).toHaveBeenCalledWith(mockItem, expect.any(Object));
});
it('does not call onClick when readonly', () => {
const onLabelClick = jest.fn();
render(
<table>
<tbody>
<LegendTableItem item={mockItem} onLabelClick={onLabelClick} readonly={true} />
</tbody>
</table>
);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
});

View File

@@ -69,7 +69,7 @@ export const LegendTableItem = ({
return (
<tr className={cx(styles.row, className)}>
<td className={styles.labelCell}>
<td>
<span className={styles.itemWrapper}>
<VizLegendSeriesIcon
color={item.color}
@@ -77,26 +77,24 @@ export const LegendTableItem = ({
readonly={readonly}
lineStyle={item.lineStyle}
/>
<div className={styles.labelCellInner}>
<button
disabled={readonly}
type="button"
title={item.label}
onBlur={onMouseOut}
onFocus={onMouseOver}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
onClick={!readonly ? onClick : undefined}
className={cx(styles.label, item.disabled && styles.labelDisabled)}
>
{item.label}{' '}
{item.yAxis === 2 && (
<span className={styles.yAxisLabel}>
<Trans i18nKey="grafana-ui.viz-legend.right-axis-indicator">(right y-axis)</Trans>
</span>
)}
</button>
</div>
<button
disabled={readonly}
type="button"
title={item.label}
onBlur={onMouseOut}
onFocus={onMouseOver}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
onClick={!readonly ? onClick : undefined}
className={cx(styles.label, item.disabled && styles.labelDisabled)}
>
{item.label}{' '}
{item.yAxis === 2 && (
<span className={styles.yAxisLabel}>
<Trans i18nKey="grafana-ui.viz-legend.right-axis-indicator">(right y-axis)</Trans>
</span>
)}
</button>
</span>
</td>
{item.getDisplayValues &&
@@ -130,28 +128,6 @@ const getStyles = (theme: GrafanaTheme2) => {
background: rowHoverBg,
},
}),
labelCell: css({
label: 'LegendLabelCell',
maxWidth: 0,
width: '100%',
minWidth: theme.spacing(16),
}),
labelCellInner: css({
label: 'LegendLabelCellInner',
display: 'block',
flex: 1,
minWidth: 0,
overflowX: 'auto',
overflowY: 'hidden',
paddingRight: theme.spacing(3),
scrollbarWidth: 'none',
msOverflowStyle: 'none',
maskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`,
WebkitMaskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`,
'&::-webkit-scrollbar': {
display: 'none',
},
}),
label: css({
label: 'LegendLabel',
whiteSpace: 'nowrap',
@@ -159,6 +135,9 @@ const getStyles = (theme: GrafanaTheme2) => {
border: 'none',
fontSize: 'inherit',
padding: 0,
maxWidth: '600px',
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'text',
}),
labelDisabled: css({

View File

@@ -795,6 +795,10 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
// swagger:route POST /dashboards/uid/{uid}/restore dashboards versions restoreDashboardVersionByUID
//
// Restore a dashboard to a given dashboard version using UID.
// This API will be removed when /apis/dashboards.grafana.app/v1 is released.
// You can restore a dashboard by reading it from history, then creating it again.
//
// Deprecated: true
//
// Responses:
// 200: postDashboardResponse

View File

@@ -574,8 +574,8 @@ var (
},
{
Name: "dashboardNewLayouts",
Description: "Enables experimental new dashboard layouts",
Stage: FeatureStageExperimental,
Description: "Enables new dashboard layouts",
Stage: FeatureStagePublicPreview,
FrontendOnly: false, // The restore backend feature changes behavior based on this flag
Owner: grafanaDashboardsSquad,
},
@@ -879,6 +879,13 @@ var (
Owner: grafanaAlertingSquad,
FrontendOnly: true,
},
{
Name: "alertingNavigationV2",
Description: "Enables the new Alerting navigation structure with improved menu grouping",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
FrontendOnly: false,
},
{
Name: "alertingSavedSearches",
Description: "Enables saved searches for alert rules list",
@@ -981,7 +988,8 @@ var (
Stage: FeatureStageDeprecated,
Owner: grafanaPartnerPluginsSquad,
Expression: "true", // Enabled by default for now
}, {
},
{
Name: "alertingFilterV2",
Description: "Enable the new alerting search experience",
Stage: FeatureStageExperimental,
@@ -2069,6 +2077,14 @@ var (
Owner: grafanaObservabilityTracesAndProfilingSquad,
FrontendOnly: false,
},
{
Name: "alertingSyncDispatchTimer",
Description: "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
RequiresRestart: true,
HideFromDocs: true,
},
}
)

View File

@@ -79,7 +79,7 @@ annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true
dashboardScene,GA,@grafana/dashboards-squad,false,false,true
dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false
dashboardNewLayouts,preview,@grafana/dashboards-squad,false,false,false
dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true
unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true
drilldownRecommendations,experimental,@grafana/dashboards-squad,false,false,true
@@ -121,6 +121,7 @@ dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false
suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false
dashboardTemplates,preview,@grafana/sharing-squad,false,false,false
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
alertingNavigationV2,experimental,@grafana/alerting-squad,false,false,false
alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
@@ -280,3 +281,4 @@ multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true
smoothingTransformation,experimental,@grafana/datapro,false,false,true
secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false
alertingSyncDispatchTimer,experimental,@grafana/alerting-squad,false,true,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
79 dashboardSceneForViewers GA @grafana/dashboards-squad false false true
80 dashboardSceneSolo GA @grafana/dashboards-squad false false true
81 dashboardScene GA @grafana/dashboards-squad false false true
82 dashboardNewLayouts experimental preview @grafana/dashboards-squad false false false
83 dashboardUndoRedo experimental @grafana/dashboards-squad false false true
84 unlimitedLayoutsNesting experimental @grafana/dashboards-squad false false true
85 drilldownRecommendations experimental @grafana/dashboards-squad false false true
121 suggestedDashboards experimental @grafana/sharing-squad false false false
122 dashboardTemplates preview @grafana/sharing-squad false false false
123 alertingListViewV2 privatePreview @grafana/alerting-squad false false true
124 alertingNavigationV2 experimental @grafana/alerting-squad false false false
125 alertingSavedSearches experimental @grafana/alerting-squad false false true
126 alertingDisableSendAlertsExternal experimental @grafana/alerting-squad false false false
127 preserveDashboardStateWhenNavigating experimental @grafana/dashboards-squad false false false
281 smoothingTransformation experimental @grafana/datapro false false true
282 secretsManagementAppPlatformAwsKeeper experimental @grafana/grafana-operator-experience-squad false false false
283 profilesExemplars experimental @grafana/observability-traces-and-profiling false false false
284 alertingSyncDispatchTimer experimental @grafana/alerting-squad false true false

View File

@@ -260,7 +260,7 @@ const (
FlagAnnotationPermissionUpdate = "annotationPermissionUpdate"
// FlagDashboardNewLayouts
// Enables experimental new dashboard layouts
// Enables new dashboard layouts
FlagDashboardNewLayouts = "dashboardNewLayouts"
// FlagPdfTables
@@ -371,6 +371,10 @@ const (
// Enables a flow to get started with a new dashboard from a template
FlagDashboardTemplates = "dashboardTemplates"
// FlagAlertingNavigationV2
// Enables the new Alerting navigation structure with improved menu grouping
FlagAlertingNavigationV2 = "alertingNavigationV2"
// FlagAlertingDisableSendAlertsExternal
// Disables the ability to send alerts to an external Alertmanager datasource.
FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal"
@@ -789,4 +793,8 @@ const (
// FlagProfilesExemplars
// Enables profiles exemplars support in profiles drilldown
FlagProfilesExemplars = "profilesExemplars"
// FlagAlertingSyncDispatchTimer
// Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
FlagAlertingSyncDispatchTimer = "alertingSyncDispatchTimer"
)

View File

@@ -348,6 +348,18 @@
"expression": "true"
}
},
{
"metadata": {
"name": "alertingNavigationV2",
"resourceVersion": "1768320918269",
"creationTimestamp": "2026-01-13T16:15:18Z"
},
"spec": {
"description": "Enables the new Alerting navigation structure with improved menu grouping",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad"
}
},
{
"metadata": {
"name": "alertingNotificationHistory",
@@ -511,6 +523,20 @@
"frontend": true
}
},
{
"metadata": {
"name": "alertingSyncDispatchTimer",
"resourceVersion": "1766161788928",
"creationTimestamp": "2025-12-19T16:29:48Z"
},
"spec": {
"description": "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"requiresRestart": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "alertingTriage",
@@ -662,7 +688,8 @@
"metadata": {
"name": "auditLoggingAppPlatform",
"resourceVersion": "1767013056996",
"creationTimestamp": "2025-12-29T12:57:36Z"
"creationTimestamp": "2025-12-29T12:57:36Z",
"deletionTimestamp": "2026-01-06T09:18:36Z"
},
"spec": {
"description": "Enable audit logging with Kubernetes under app platform",
@@ -1015,12 +1042,15 @@
{
"metadata": {
"name": "dashboardNewLayouts",
"resourceVersion": "1764664939750",
"creationTimestamp": "2024-10-23T08:55:45Z"
"resourceVersion": "1768382835527",
"creationTimestamp": "2024-10-23T08:55:45Z",
"annotations": {
"grafana.app/updatedTimestamp": "2026-01-14 09:27:15.527103 +0000 UTC"
}
},
"spec": {
"description": "Enables experimental new dashboard layouts",
"stage": "experimental",
"description": "Enables new dashboard layouts",
"stage": "preview",
"codeowner": "@grafana/dashboards-squad"
}
},

View File

@@ -187,6 +187,112 @@ func TestFrontendService_Middleware(t *testing.T) {
})
}
func TestFrontendService_LoginErrorCookie(t *testing.T) {
publicDir := setupTestWebAssets(t)
cfg := &setting.Cfg{
HTTPPort: "3000",
StaticRootPath: publicDir,
BuildVersion: "10.3.0",
OAuthLoginErrorMessage: "oauth.login.error",
CookieSecure: false,
CookieSameSiteDisabled: false,
CookieSameSiteMode: http.SameSiteLaxMode,
}
t.Run("should detect login_error cookie and set generic error message", func(t *testing.T) {
service := createTestService(t, cfg)
mux := web.New()
service.addMiddlewares(mux)
service.registerRoutes(mux)
req := httptest.NewRequest("GET", "/", nil)
// Set the login_error cookie (with some encrypted-looking value)
req.AddCookie(&http.Cookie{
Name: "login_error",
Value: "abc123encryptedvalue",
})
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
body := recorder.Body.String()
// Check that the generic error message is in the response
assert.Contains(t, body, "loginError", "Should contain loginError when cookie is present")
assert.Contains(t, body, "oauth.login.error", "Should contain the generic OAuth error message")
// Check that the cookie was deleted (MaxAge=-1)
cookies := recorder.Result().Cookies()
var foundDeletedCookie bool
for _, cookie := range cookies {
if cookie.Name == "login_error" {
assert.Equal(t, -1, cookie.MaxAge, "Cookie should be deleted (MaxAge=-1)")
assert.Equal(t, "", cookie.Value, "Cookie value should be empty")
foundDeletedCookie = true
break
}
}
assert.True(t, foundDeletedCookie, "Should have set a cookie deletion header")
})
t.Run("should not set error when login_error cookie is absent", func(t *testing.T) {
service := createTestService(t, cfg)
mux := web.New()
service.addMiddlewares(mux)
service.registerRoutes(mux)
req := httptest.NewRequest("GET", "/", nil)
// No login_error cookie
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
body := recorder.Body.String()
// The page should render but without the login error
assert.Contains(t, body, "window.grafanaBootData")
// Check that loginError is not set (or is empty/omitted in JSON)
// Since it's omitempty, it shouldn't appear at all
assert.NotContains(t, body, "loginError", "Should not contain loginError when cookie is absent")
})
t.Run("should handle custom OAuth error message from config", func(t *testing.T) {
customCfg := &setting.Cfg{
HTTPPort: "3000",
StaticRootPath: publicDir,
BuildVersion: "10.3.0",
OAuthLoginErrorMessage: "Oh no a boo-boo happened!",
CookieSecure: false,
CookieSameSiteDisabled: false,
CookieSameSiteMode: http.SameSiteLaxMode,
}
service := createTestService(t, customCfg)
mux := web.New()
service.addMiddlewares(mux)
service.registerRoutes(mux)
req := httptest.NewRequest("GET", "/", nil)
req.AddCookie(&http.Cookie{
Name: "login_error",
Value: "abc123encryptedvalue",
})
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
body := recorder.Body.String()
// Check that the custom error message is used
assert.Contains(t, body, "Oh no a boo-boo happened!", "Should use custom OAuth error message from config")
})
}
func TestFrontendService_IndexHooks(t *testing.T) {
publicDir := setupTestWebAssets(t)
cfg := &setting.Cfg{

View File

@@ -47,4 +47,6 @@ type FSFrontendSettings struct {
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled,omitempty"`
Http2Enabled bool `json:"http2Enabled,omitempty"`
ReportingStaticContext map[string]string `json:"reportingStaticContext,omitempty"`
LoginError string `json:"loginError,omitempty"`
}

Some files were not shown because too many files have changed in this diff Show More