From bd0140b6f0e62b019c182be768fd370be5a20563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 14 Jan 2026 06:30:05 +0100 Subject: [PATCH] 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 --- .github/CODEOWNERS | 1 + apps/plugins/Makefile | 13 +- apps/plugins/README.md | 3 +- apps/plugins/kinds/manifest.cue | 2 +- .../meta/v0alpha1/meta_object_gen.ts | 49 + .../meta/v0alpha1/types.metadata.gen.ts | 30 + .../generated/meta/v0alpha1/types.spec.gen.ts | 278 ++ .../meta/v0alpha1/types.status.gen.ts | 30 + .../plugin/v0alpha1/plugin_object_gen.ts | 49 + .../plugin/v0alpha1/types.metadata.gen.ts | 30 + .../plugin/v0alpha1/types.spec.gen.ts | 13 + .../plugin/v0alpha1/types.status.gen.ts | 30 + eslint-suppressions.json | 128 + eslint.config.js | 38 + packages/grafana-data/src/types/config.ts | 2 + packages/grafana-data/src/types/plugin.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + packages/grafana-runtime/src/index.ts | 2 + .../grafana-runtime/src/internal/index.ts | 2 + .../src/services/pluginMeta/apps.test.ts | 258 + .../src/services/pluginMeta/apps.ts | 71 + .../src/services/pluginMeta/hooks.test.tsx | 214 + .../src/services/pluginMeta/hooks.tsx | 35 + .../services/pluginMeta/mappers/mappers.ts | 7 + .../mappers/v0alpha1AppMapper.test.ts | 84 + .../pluginMeta/mappers/v0alpha1AppMapper.ts | 111 + .../src/services/pluginMeta/plugins.test.ts | 153 + .../src/services/pluginMeta/plugins.ts | 41 + .../pluginMeta/test-fixtures/config.apps.ts | 303 ++ .../test-fixtures/v0alpha1Response.ts | 4378 +++++++++++++++++ .../src/services/pluginMeta/types.ts | 10 + .../pluginMeta/types/meta_object_gen.ts | 49 + .../pluginMeta/types/types.spec.gen.ts | 278 ++ .../pluginMeta/types/types.status.gen.ts | 30 + 34 files changed, 6718 insertions(+), 6 deletions(-) create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/apps.test.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/apps.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx create mode 100644 packages/grafana-runtime/src/services/pluginMeta/hooks.tsx create mode 100644 packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/plugins.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ca9de90ba02..0dda8519ef6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -658,6 +658,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 diff --git a/apps/plugins/Makefile b/apps/plugins/Makefile index 230bfd4149a..2db266ef19b 100644 --- a/apps/plugins/Makefile +++ b/apps/plugins/Makefile @@ -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 \ No newline at end of file + --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 \ No newline at end of file diff --git a/apps/plugins/README.md b/apps/plugins/README.md index 7f91dd6ea12..f21fa6701b5 100644 --- a/apps/plugins/README.md +++ b/apps/plugins/README.md @@ -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 diff --git a/apps/plugins/kinds/manifest.cue b/apps/plugins/kinds/manifest.cue index f624dc117bc..680a0f7565d 100644 --- a/apps/plugins/kinds/manifest.cue +++ b/apps/plugins/kinds/manifest.cue @@ -11,7 +11,7 @@ manifest: { v0alpha1Version: { served: true codegen: { - ts: {enabled: false} + ts: {enabled: true} go: {enabled: true} } kinds: [ diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts new file mode 100644 index 00000000000..044ec1f4cd8 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts @@ -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; + annotations?: Record; + 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; +} diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts @@ -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; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..51845e98454 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts @@ -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; + tokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + jwtTokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + // +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; + // +listType=atomic + children?: string[]; +} + +export const defaultSpec = (): Spec => ({ + pluginJson: defaultJSONData(), + class: "core", +}); + diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts @@ -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; +} + +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; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts new file mode 100644 index 00000000000..c4e625fc418 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts @@ -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; + annotations?: Record; + 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; +} diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts @@ -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; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..6b7824b8941 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts @@ -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: "", +}); + diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts @@ -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; +} + +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; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f633ed5b4eb..70df9a82829 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/eslint.config.js b/eslint.config.js index 5e44ffebaf4..479f11aac66 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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', + }, ], }, }, diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index b2d3c16a3b1..922d9273699 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -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; auth: AuthSettings; minRefreshInterval: string; diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index 045dfdcee0b..8b96ac8f70f 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -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; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 18cce14f236..99809235cab 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -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 = {}; auth: AuthSettings = {}; minRefreshInterval = ''; diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 58b30be8542..380b87fee7d 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -77,3 +77,5 @@ export { getCorrelationsService, setCorrelationsService, } from './services/CorrelationsService'; +export { getAppPluginVersion, isAppPluginInstalled } from './services/pluginMeta/apps'; +export { useAppPluginInstalled, useAppPluginVersion } from './services/pluginMeta/hooks'; diff --git a/packages/grafana-runtime/src/internal/index.ts b/packages/grafana-runtime/src/internal/index.ts index aed6b86ebfb..fa13873c094 100644 --- a/packages/grafana-runtime/src/internal/index.ts +++ b/packages/grafana-runtime/src/internal/index.ts @@ -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'; diff --git a/packages/grafana-runtime/src/services/pluginMeta/apps.test.ts b/packages/grafana-runtime/src/services/pluginMeta/apps.test.ts new file mode 100644 index 00000000000..554917041cc --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/apps.test.ts @@ -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); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/apps.ts b/packages/grafana-runtime/src/services/pluginMeta/apps.ts new file mode 100644 index 00000000000..7db359b5a4b --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/apps.ts @@ -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 { + 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 { + if (!initialized()) { + await initAppPluginMetas(); + } + + return Object.values(structuredClone(apps)); +} + +export async function getAppPluginMeta(pluginId: string): Promise { + 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 { + 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 { + 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); +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx b/packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx new file mode 100644 index 00000000000..1e3c7311118 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx @@ -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('./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(); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/hooks.tsx b/packages/grafana-runtime/src/services/pluginMeta/hooks.tsx new file mode 100644 index 00000000000..58ac42bbdd2 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/hooks.tsx @@ -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 }; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts b/packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts new file mode 100644 index 00000000000..15505b2edc0 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts @@ -0,0 +1,7 @@ +import { AppPluginMetasMapper, PluginMetasResponse } from '../types'; + +import { v0alpha1AppMapper } from './v0alpha1AppMapper'; + +export function getAppPluginMapper(): AppPluginMetasMapper { + return v0alpha1AppMapper; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts new file mode 100644 index 00000000000..dfc82d41b3e --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts @@ -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)); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts new file mode 100644 index 00000000000..aa5ca6e2ce0 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts @@ -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 = (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); +}; diff --git a/packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts b/packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts new file mode 100644 index 00000000000..9a5077d1b2b --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts @@ -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(); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/plugins.ts b/packages/grafana-runtime/src/services/pluginMeta/plugins.ts new file mode 100644 index 00000000000..ec2fa4a9d11 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/plugins.ts @@ -0,0 +1,41 @@ +import { config } from '../../config'; +import { evaluateBooleanFlag } from '../../internal/openFeature'; + +import type { PluginMetasResponse } from './types'; + +let initPromise: Promise | null = null; + +function getApiVersion(): string { + return 'v0alpha1'; +} + +async function loadPluginMetas(): Promise { + 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 { + 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; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts new file mode 100644 index 00000000000..365308bd76c --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts @@ -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, +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts new file mode 100644 index 00000000000..7bd4c38d9fa --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts @@ -0,0 +1,4378 @@ +import { cloneDeep } from 'lodash'; + +import type { PluginMetasResponse } from '../types'; +import type { Meta } from '../types/meta_object_gen'; + +export const v0alpha1Meta: Meta = cloneDeep({ + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'myorg-someplugin-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'myorg-someplugin-app', + type: 'app', + name: 'Some-Plugin', + info: { + keywords: ['app'], + logos: { + small: 'public/plugins/myorg-someplugin-app/img/logo.svg', + large: 'public/plugins/myorg-someplugin-app/img/logo.svg', + }, + updated: '2025-12-15', + version: '1.0.0', + author: { + name: 'Myorg', + }, + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + includes: [ + { + type: 'page', + name: 'Page One', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/one', + addToNav: true, + defaultNav: true, + }, + { + type: 'page', + name: 'Page Two', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/two', + addToNav: true, + }, + { + type: 'page', + name: 'Page Three', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/three', + addToNav: true, + }, + { + type: 'page', + name: 'Page Four', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/four', + addToNav: true, + }, + { + type: 'page', + name: 'Configuration', + role: 'Admin', + path: '/plugins/myorg-someplugin-app', + addToNav: true, + icon: 'cog', + }, + ], + }, + class: 'external', + module: { + path: 'public/plugins/myorg-someplugin-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/myorg-someplugin-app', + signature: { + status: 'unsigned', + }, + angular: { + detected: false, + }, + }, + status: {}, +}); + +export const v0alpha1Response: PluginMetasResponse = cloneDeep({ + items: [ + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'alertlist', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'alertlist', + type: 'panel', + name: 'Alert list', + info: { + keywords: [], + logos: { + small: 'public/plugins/alertlist/img/icn-singlestat-panel.svg', + large: 'public/plugins/alertlist/img/icn-singlestat-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Shows list of alerts and their current status', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/alert-list/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/alertlist', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/alertlist', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'alertmanager', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'alertmanager', + type: 'datasource', + name: 'Alertmanager', + info: { + keywords: ['alerts', 'alerting', 'prometheus', 'alertmanager', 'mimir', 'cortex'], + logos: { + small: 'public/plugins/alertmanager/img/logo.svg', + large: 'public/plugins/alertmanager/img/logo.svg', + }, + updated: '', + version: '', + author: { + name: 'Prometheus alertmanager', + url: 'https://grafana.com', + }, + description: + 'Add external Alertmanagers (supports Prometheus and Mimir implementations) so you can use the Grafana Alerting UI to manage silences, contact points, and notification policies.', + links: [ + { + name: 'Learn more', + url: 'https://prometheus.io/docs/alerting/latest/alertmanager/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/alertmanager/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + routes: [ + { + path: 'alertmanager/api/v2/silences', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/silences', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/silences', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'api/v2/silences', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'alertmanager/api/v2/silence/', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/silence/', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/silence/', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'api/v2/silence/', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'alertmanager/api/v2/alerts/groups', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/alerts/groups', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/alerts', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/alerts', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/alerts', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'api/v2/alerts', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'alertmanager/api/v2/status', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.notifications.external:read', + }, + { + path: 'api/v2/status', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.notifications.external:read', + }, + { + path: 'alertmanager/api/v2/receivers', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/receivers', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v1/alerts', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.notifications.external:read', + }, + { + path: 'api/v1/alerts', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.notifications.external:write', + }, + { + path: 'api/v1/alerts', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.notifications.external:write', + }, + { + method: 'POST', + reqRole: 'Admin', + }, + { + method: 'PUT', + reqRole: 'Admin', + }, + { + method: 'DELETE', + reqRole: 'Admin', + }, + { + method: 'GET', + reqRole: 'Admin', + }, + ], + }, + class: 'core', + module: { + path: 'core:plugin/alertmanager', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/alertmanager', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'annolist', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'annolist', + type: 'panel', + name: 'Annotations list', + info: { + keywords: [], + logos: { + small: 'public/plugins/annolist/img/icn-annolist-panel.svg', + large: 'public/plugins/annolist/img/icn-annolist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'List annotations', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/annotations/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/annolist', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/annolist', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'barchart', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'barchart', + type: 'panel', + name: 'Bar chart', + info: { + keywords: [], + logos: { + small: 'public/plugins/barchart/img/barchart.svg', + large: 'public/plugins/barchart/img/barchart.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Categorical charts with group support', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/bar-chart/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/barchart', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/barchart', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'bargauge', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'bargauge', + type: 'panel', + name: 'Bar gauge', + info: { + keywords: [], + logos: { + small: 'public/plugins/bargauge/img/icon_bar_gauge.svg', + large: 'public/plugins/bargauge/img/icon_bar_gauge.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Horizontal and vertical gauges', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/bar-gauge/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/bargauge', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/bargauge', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'candlestick', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'candlestick', + type: 'panel', + name: 'Candlestick', + info: { + keywords: ['financial', 'price', 'currency', 'k-line'], + logos: { + small: 'public/plugins/candlestick/img/candlestick.svg', + large: 'public/plugins/candlestick/img/candlestick.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Graphical representation of price movements of a security, derivative, or currency.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/candlestick/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/candlestick', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/candlestick', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'canvas', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'canvas', + type: 'panel', + name: 'Canvas', + info: { + keywords: [], + logos: { + small: 'public/plugins/canvas/img/icn-canvas.svg', + large: 'public/plugins/canvas/img/icn-canvas.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Explicit element placement', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/canvas/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/canvas', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/canvas', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'cloudwatch', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'cloudwatch', + type: 'datasource', + name: 'CloudWatch', + info: { + keywords: ['aws', 'amazon'], + logos: { + small: 'public/plugins/cloudwatch/img/amazon-web-services.png', + large: 'public/plugins/cloudwatch/img/amazon-web-services.png', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for Amazon AWS monitoring service', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'cloud', + includes: [ + { + type: 'dashboard', + name: 'EC2', + role: 'Viewer', + path: 'dashboards/ec2.json', + }, + { + type: 'dashboard', + name: 'EBS', + role: 'Viewer', + path: 'dashboards/EBS.json', + }, + { + type: 'dashboard', + name: 'Lambda', + role: 'Viewer', + path: 'dashboards/Lambda.json', + }, + { + type: 'dashboard', + name: 'Logs', + role: 'Viewer', + path: 'dashboards/Logs.json', + }, + { + type: 'dashboard', + name: 'RDS', + role: 'Viewer', + path: 'dashboards/RDS.json', + }, + ], + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/cloudwatch', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/cloudwatch', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'dashboard', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'dashboard', + type: 'datasource', + name: '-- Dashboard --', + info: { + keywords: [], + logos: { + small: 'public/plugins/dashboard/img/icn-reusequeries.svg', + large: 'public/plugins/dashboard/img/icn-reusequeries.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Uses the result set from another panel in the same dashboard', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + builtIn: true, + metrics: true, + }, + class: 'core', + module: { + path: 'core:plugin/dashboard', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/dashboard', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'dashlist', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'dashlist', + type: 'panel', + name: 'Dashboard list', + info: { + keywords: [], + logos: { + small: 'public/plugins/dashlist/img/icn-dashlist-panel.svg', + large: 'public/plugins/dashlist/img/icn-dashlist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'List of dynamic links to other dashboards', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/dashboard-list/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/dashlist', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/dashlist', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'datagrid', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'datagrid', + type: 'panel', + name: 'Datagrid', + info: { + keywords: [], + logos: { + small: 'public/plugins/datagrid/img/icn-table-panel.svg', + large: 'public/plugins/datagrid/img/icn-table-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/datagrid/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'beta', + }, + class: 'core', + module: { + path: 'core:plugin/datagrid', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/datagrid', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'debug', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'debug', + type: 'panel', + name: 'Debug', + info: { + keywords: [], + logos: { + small: 'public/plugins/debug/img/icn-debug.svg', + large: 'public/plugins/debug/img/icn-debug.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Debug Panel for Grafana', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'alpha', + }, + class: 'core', + module: { + path: 'core:plugin/debug', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/debug', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'elasticsearch', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'elasticsearch', + type: 'datasource', + name: 'Elasticsearch', + info: { + keywords: ['elasticsearch', 'datasource', 'database', 'logs', 'nosql', 'traces'], + logos: { + small: 'public/plugins/elasticsearch/img/elasticsearch.svg', + large: 'public/plugins/elasticsearch/img/elasticsearch.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source logging & analytics database', + links: [ + { + name: 'Learn more', + url: 'https://grafana.com/docs/features/datasources/elasticsearch/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/elasticsearch/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'logging', + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/elasticsearch', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/elasticsearch', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'flamegraph', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'flamegraph', + type: 'panel', + name: 'Flame Graph', + info: { + keywords: [], + logos: { + small: 'public/plugins/flamegraph/img/icn-flamegraph.svg', + large: 'public/plugins/flamegraph/img/icn-flamegraph.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/flame-graph/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/flamegraph', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/flamegraph', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'gauge', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'gauge', + type: 'panel', + name: 'Gauge', + info: { + keywords: [], + logos: { + small: 'public/plugins/gauge/img/icon_gauge.svg', + large: 'public/plugins/gauge/img/icon_gauge.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Standard gauge visualization', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/gauge/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/gauge', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/gauge', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'geomap', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'geomap', + type: 'panel', + name: 'Geomap', + info: { + keywords: [], + logos: { + small: 'public/plugins/geomap/img/icn-geomap.svg', + large: 'public/plugins/geomap/img/icn-geomap.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Geomap panel', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/geomap', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/geomap', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'gettingstarted', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'gettingstarted', + type: 'panel', + name: 'Getting Started', + info: { + keywords: [], + logos: { + small: 'public/plugins/gettingstarted/img/icn-dashlist-panel.svg', + large: 'public/plugins/gettingstarted/img/icn-dashlist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + hideFromList: true, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/gettingstarted', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/gettingstarted', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana', + type: 'datasource', + name: '-- Grafana --', + info: { + keywords: [], + logos: { + small: 'public/plugins/grafana/img/icn-grafanadb.svg', + large: 'public/plugins/grafana/img/icn-grafanadb.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: + 'A built-in data source that generates random walk data and can poll the Testdata data source. This helps you test visualizations and run experiments.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + annotations: true, + backend: true, + builtIn: true, + metrics: true, + }, + class: 'core', + module: { + path: 'core:plugin/grafana', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-azure-monitor-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-azure-monitor-datasource', + type: 'datasource', + name: 'Azure Monitor', + info: { + keywords: ['azure', 'monitor', 'Application Insights', 'Log Analytics', 'App Insights'], + logos: { + small: 'public/plugins/grafana-azure-monitor-datasource/img/logo.jpg', + large: 'public/plugins/grafana-azure-monitor-datasource/img/logo.jpg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for Microsoft Azure Monitor & Application Insights', + links: [ + { + name: 'Learn more', + url: 'https://grafana.com/docs/grafana/latest/datasources/azuremonitor/', + }, + { + name: 'License', + url: 'https://github.com/grafana/grafana/blob/HEAD/LICENSE', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/azure-monitor/', + }, + ], + screenshots: [ + { + name: 'Azure Contoso Loans', + path: 'public/plugins/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png', + }, + { + name: 'Azure Monitor Network', + path: 'public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_network.png', + }, + { + name: 'Azure Monitor CPU', + path: 'public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'cloud', + executable: 'gpx_azuremonitor', + includes: [ + { + type: 'dashboard', + name: 'Azure / Alert Consumption', + role: 'Viewer', + path: 'dashboards/v1Alerts.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Apps Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraApps.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Compute Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraCompute.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Data Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraData.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Network Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraNetwork.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Storage and Key Vaults Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraStorageVaults.json', + }, + { + type: 'dashboard', + name: 'Azure / Azure PostgreSQL / Flexible Server Monitoring', + role: 'Viewer', + path: 'dashboards/postgresFlexibleServer.json', + }, + { + type: 'dashboard', + name: 'Azure Monitor / Container Insights / Syslog', + role: 'Viewer', + path: 'dashboards/containerInsightsSyslog.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications', + role: 'Viewer', + path: 'dashboards/appInsights.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Performance / Operations', + role: 'Viewer', + path: 'dashboards/appInsightsPerfOperations.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Performance / Dependencies', + role: 'Viewer', + path: 'dashboards/appInsightsPerfDependencies.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Failures / Operations', + role: 'Viewer', + path: 'dashboards/appInsightsFailureOperations.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Failures / Dependencies', + role: 'Viewer', + path: 'dashboards/appInsightsFailureDependencies.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Failures / Exceptions', + role: 'Viewer', + path: 'dashboards/appInsightsFailureExceptions.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications Test Availability Geo Map', + role: 'Viewer', + path: 'dashboards/appInsightsGeoMap.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / CosmosDB', + role: 'Viewer', + path: 'dashboards/cosmosdb.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Data Explorer Clusters', + role: 'Viewer', + path: 'dashboards/adx.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Key Vaults', + role: 'Viewer', + path: 'dashboards/keyvault.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Networks', + role: 'Viewer', + path: 'dashboards/networkInsightsDashboard.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / SQL Database', + role: 'Viewer', + path: 'dashboards/sqldb.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Storage Accounts', + role: 'Viewer', + path: 'dashboards/storage.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Virtual Machines by Resource Group', + role: 'Viewer', + path: 'dashboards/vMInsightsRG.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Virtual Machines by Workspace', + role: 'Viewer', + path: 'dashboards/vMInsightsWorkspace.json', + }, + { + type: 'dashboard', + name: 'Azure / Resources Overview', + role: 'Viewer', + path: 'dashboards/arg.json', + }, + ], + logs: true, + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-azure-monitor-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-azure-monitor-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + translations: { + 'cs-CZ': + 'public/plugins/grafana-azure-monitor-datasource/locales/cs-CZ/grafana-azure-monitor-datasource.json', + 'de-DE': + 'public/plugins/grafana-azure-monitor-datasource/locales/de-DE/grafana-azure-monitor-datasource.json', + 'en-US': + 'public/plugins/grafana-azure-monitor-datasource/locales/en-US/grafana-azure-monitor-datasource.json', + 'es-ES': + 'public/plugins/grafana-azure-monitor-datasource/locales/es-ES/grafana-azure-monitor-datasource.json', + 'fr-FR': + 'public/plugins/grafana-azure-monitor-datasource/locales/fr-FR/grafana-azure-monitor-datasource.json', + 'hu-HU': + 'public/plugins/grafana-azure-monitor-datasource/locales/hu-HU/grafana-azure-monitor-datasource.json', + 'id-ID': + 'public/plugins/grafana-azure-monitor-datasource/locales/id-ID/grafana-azure-monitor-datasource.json', + 'it-IT': + 'public/plugins/grafana-azure-monitor-datasource/locales/it-IT/grafana-azure-monitor-datasource.json', + 'ja-JP': + 'public/plugins/grafana-azure-monitor-datasource/locales/ja-JP/grafana-azure-monitor-datasource.json', + 'ko-KR': + 'public/plugins/grafana-azure-monitor-datasource/locales/ko-KR/grafana-azure-monitor-datasource.json', + 'nl-NL': + 'public/plugins/grafana-azure-monitor-datasource/locales/nl-NL/grafana-azure-monitor-datasource.json', + 'pl-PL': + 'public/plugins/grafana-azure-monitor-datasource/locales/pl-PL/grafana-azure-monitor-datasource.json', + 'pt-BR': + 'public/plugins/grafana-azure-monitor-datasource/locales/pt-BR/grafana-azure-monitor-datasource.json', + 'pt-PT': + 'public/plugins/grafana-azure-monitor-datasource/locales/pt-PT/grafana-azure-monitor-datasource.json', + 'ru-RU': + 'public/plugins/grafana-azure-monitor-datasource/locales/ru-RU/grafana-azure-monitor-datasource.json', + 'sv-SE': + 'public/plugins/grafana-azure-monitor-datasource/locales/sv-SE/grafana-azure-monitor-datasource.json', + 'tr-TR': + 'public/plugins/grafana-azure-monitor-datasource/locales/tr-TR/grafana-azure-monitor-datasource.json', + 'zh-Hans': + 'public/plugins/grafana-azure-monitor-datasource/locales/zh-Hans/grafana-azure-monitor-datasource.json', + 'zh-Hant': + 'public/plugins/grafana-azure-monitor-datasource/locales/zh-Hant/grafana-azure-monitor-datasource.json', + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-exploretraces-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-exploretraces-app', + type: 'app', + name: 'Grafana Traces Drilldown', + info: { + keywords: ['app', 'tempo', 'traces', 'explore'], + logos: { + small: 'public/plugins/grafana-exploretraces-app/img/logo.svg', + large: 'public/plugins/grafana-exploretraces-app/img/logo.svg', + }, + updated: '2025-12-04', + version: '1.2.2', + author: { + name: 'Grafana', + }, + description: + 'Use Rate, Errors, and Duration (RED) metrics derived from traces to investigate errors within complex distributed systems.', + links: [ + { + name: 'Github', + url: 'https://github.com/grafana/explore-traces', + }, + { + name: 'Report bug', + url: 'https://github.com/grafana/explore-traces/issues/new', + }, + ], + screenshots: [ + { + name: 'histogram-breakdown', + path: 'public/plugins/grafana-exploretraces-app/img/histogram-breakdown.png', + }, + { + name: 'errors-metric-flow', + path: 'public/plugins/grafana-exploretraces-app/img/errors-metric-flow.png', + }, + { + name: 'errors-root-cause', + path: 'public/plugins/grafana-exploretraces-app/img/errors-root-cause.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.5.0', + grafanaVersion: '*', + extensions: { + exposedComponents: [ + 'grafana-asserts-app/entity-assertions-widget/v1', + 'grafana-asserts-app/insights-timeline-widget/v1', + ], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Explore', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-exploretraces-app/', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + extensions: { + 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', + }, + ], + 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', + }, + ], + 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', + }, + { + id: 'grafana-exploretraces-app/get-logs-drilldown-link/v1', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-exploretraces-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-exploretraces-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-lokiexplore-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-lokiexplore-app', + type: 'app', + name: 'Grafana Logs Drilldown', + info: { + keywords: ['app', 'loki', 'explore', 'logs', 'drilldown', 'drill', 'down', 'drill-down'], + logos: { + small: 'public/plugins/grafana-lokiexplore-app/img/logo.svg', + large: 'public/plugins/grafana-lokiexplore-app/img/logo.svg', + }, + updated: '2025-12-09', + version: '1.0.32', + author: { + name: 'Grafana', + }, + description: + 'Visualize log volumes to easily detect anomalies or significant changes over time, without needing to compose LogQL queries.', + links: [ + { + name: 'Github', + url: 'https://github.com/grafana/explore-logs', + }, + { + name: 'Report bug', + url: 'https://github.com/grafana/explore-logs/issues/new', + }, + ], + screenshots: [ + { + name: 'patterns', + path: 'public/plugins/grafana-lokiexplore-app/img/patterns.png', + }, + { + name: 'fields', + path: 'public/plugins/grafana-lokiexplore-app/img/fields.png', + }, + { + name: 'table', + path: 'public/plugins/grafana-lokiexplore-app/img/table.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + 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', + ], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Grafana Logs Drilldown', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-lokiexplore-app/explore', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + extensions: { + addedComponents: [ + { + targets: ['grafana-asserts-app/insights-timeline-widget/v1'], + title: 'Insights Timeline Widget', + description: 'Widget for displaying insights timeline in other apps', + }, + ], + 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', + }, + ], + addedFunctions: [ + { + targets: ['grafana-exploretraces-app/get-logs-drilldown-link/v1'], + title: 'Open Logs Drilldown', + description: 'Returns url to logs drilldown app', + }, + ], + 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', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-lokiexplore-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-lokiexplore-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-metricsdrilldown-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-metricsdrilldown-app', + type: 'app', + name: 'Grafana Metrics Drilldown', + info: { + keywords: ['drilldown', 'metrics', 'app', 'prometheus', 'mimir'], + logos: { + small: 'public/plugins/grafana-metricsdrilldown-app/img/logo.svg', + large: 'public/plugins/grafana-metricsdrilldown-app/img/logo.svg', + }, + updated: '2025-12-17', + version: '1.0.26', + author: { + name: 'Grafana', + }, + description: + 'Quickly find related metrics with a few clicks, without needing to write PromQL queries to retrieve metrics.', + links: [ + { + name: 'GitHub', + url: 'https://github.com/grafana/metrics-drilldown', + }, + { + name: 'Report a bug', + url: 'https://github.com/grafana/metrics-drilldown/issues/new', + }, + ], + screenshots: [ + { + name: 'metricselect', + path: 'public/plugins/grafana-metricsdrilldown-app/img/metrics-drilldown.png', + }, + { + name: 'breakdown', + path: 'public/plugins/grafana-metricsdrilldown-app/img/breakdown.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + extensions: { + exposedComponents: ['grafana/add-to-dashboard-form/v1'], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Grafana Metrics Drilldown', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-metricsdrilldown-app/drilldown', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + 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', + }, + ], + 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', + }, + { + id: 'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-metricsdrilldown-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-metricsdrilldown-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-postgresql-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-postgresql-datasource', + type: 'datasource', + name: 'PostgreSQL', + info: { + keywords: [], + logos: { + small: 'public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg', + large: 'public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for PostgreSQL and compatible databases', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/postgres/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'sql', + executable: 'gpx_grafana-postgresql-datasource', + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-postgresql-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-postgresql-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-pyroscope-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-pyroscope-app', + type: 'app', + name: 'Grafana Profiles Drilldown', + info: { + keywords: ['app', 'pyroscope', 'profiling', 'explore', 'profiles', 'performance', 'drilldown'], + logos: { + small: 'public/plugins/grafana-pyroscope-app/img/logo.svg', + large: 'public/plugins/grafana-pyroscope-app/img/logo.svg', + }, + updated: '2025-12-18', + version: '1.14.2', + author: { + name: 'Grafana', + }, + description: + 'View and analyze high-level service performance, identify problem processes for optimization, and diagnose issues to determine root causes.', + links: [ + { + name: 'GitHub', + url: 'https://github.com/grafana/profiles-drilldown', + }, + { + name: 'Report bug', + url: 'https://github.com/grafana/profiles-drilldown/issues/new', + }, + ], + screenshots: [ + { + name: 'Hero Image', + path: 'public/plugins/grafana-pyroscope-app/img/hero-image.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.5.0', + grafanaVersion: '*', + extensions: { + exposedComponents: [ + 'grafana-o11yinsights-app/insights-launcher/v1', + 'grafana-adaptiveprofiles-app/resolution-boost/v1', + ], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Profiles', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-pyroscope-app/explore', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + 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', + }, + ], + 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', + }, + { + id: 'grafana-pyroscope-app/settings/v1', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-pyroscope-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-pyroscope-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-pyroscope-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-pyroscope-datasource', + type: 'datasource', + name: 'Grafana Pyroscope', + info: { + keywords: [ + 'grafana', + 'datasource', + 'phlare', + 'flamegraph', + 'profiling', + 'continuous profiling', + 'pyroscope', + ], + logos: { + small: 'public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg', + large: 'public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://www.grafana.com', + }, + description: + 'Data source for Grafana Pyroscope, horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation system.', + links: [ + { + name: 'GitHub Project', + url: 'https://github.com/grafana/pyroscope', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/pyroscope/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/pyroscope/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'profiling', + executable: 'gpx_grafana-pyroscope-datasource', + metrics: true, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-pyroscope-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-pyroscope-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-testdata-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-testdata-datasource', + type: 'datasource', + name: 'TestData', + info: { + keywords: [], + logos: { + small: 'public/plugins/grafana-testdata-datasource/img/testdata.svg', + large: 'public/plugins/grafana-testdata-datasource/img/testdata.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Generates test data in different forms', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/testdata/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + executable: 'gpx_testdata', + includes: [ + { + type: 'dashboard', + name: 'Streaming Example', + role: 'Viewer', + path: 'dashboards/streaming.json', + }, + ], + logs: true, + metrics: true, + queryOptions: { + maxDataPoints: true, + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-testdata-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-testdata-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'graphite', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'graphite', + type: 'datasource', + name: 'Graphite', + info: { + keywords: [], + logos: { + small: 'public/plugins/graphite/img/graphite_logo.png', + large: 'public/plugins/graphite/img/graphite_logo.png', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database', + links: [ + { + name: 'Learn more', + url: 'https://graphiteapp.org/', + }, + { + name: 'Graphite 1.1 Release', + url: 'https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/graphite/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + executable: 'gpx_graphite', + includes: [ + { + type: 'dashboard', + name: 'Graphite Carbon Metrics', + role: 'Viewer', + path: 'dashboards/carbon_metrics.json', + }, + { + type: 'dashboard', + name: 'Metrictank (Graphite alternative)', + role: 'Viewer', + path: 'dashboards/metrictank.json', + }, + ], + metrics: true, + queryOptions: { + maxDataPoints: true, + cacheTimeout: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/graphite/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/graphite', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'heatmap', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'heatmap', + type: 'panel', + name: 'Heatmap', + info: { + keywords: [], + logos: { + small: 'public/plugins/heatmap/img/icn-heatmap-panel.svg', + large: 'public/plugins/heatmap/img/icn-heatmap-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Like a histogram over time', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/heatmap/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/heatmap', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/heatmap', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'histogram', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'histogram', + type: 'panel', + name: 'Histogram', + info: { + keywords: ['distribution', 'bar chart', 'frequency', 'proportional'], + logos: { + small: 'public/plugins/histogram/img/histogram.svg', + large: 'public/plugins/histogram/img/histogram.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Distribution of values presented as a bar chart.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/histogram/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/histogram', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/histogram', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'influxdb', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'influxdb', + type: 'datasource', + name: 'InfluxDB', + info: { + keywords: [], + logos: { + small: 'public/plugins/influxdb/img/influxdb_logo.svg', + large: 'public/plugins/influxdb/img/influxdb_logo.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/influxdb/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/influxdb', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/influxdb', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'jaeger', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'jaeger', + type: 'datasource', + name: 'Jaeger', + info: { + keywords: [], + logos: { + small: 'public/plugins/jaeger/img/jaeger_logo.svg', + large: 'public/plugins/jaeger/img/jaeger_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source, end-to-end distributed tracing', + links: [ + { + name: 'Learn more', + url: 'https://www.jaegertracing.io', + }, + { + name: 'Jaeger GitHub Project', + url: 'https://github.com/jaegertracing/jaeger', + }, + { + name: 'Repository', + url: 'https://github.com/grafana/grafana', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/jaeger/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'tracing', + executable: 'gpx_jaeger', + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/jaeger/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/jaeger', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'live', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'live', + type: 'panel', + name: 'Live', + info: { + keywords: [], + logos: { + small: 'public/plugins/live/img/live.svg', + large: 'public/plugins/live/img/live.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + state: 'alpha', + }, + class: 'core', + module: { + path: 'core:plugin/live', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/live', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'logs', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'logs', + type: 'panel', + name: 'Logs', + info: { + keywords: [], + logos: { + small: 'public/plugins/logs/img/icn-logs-panel.svg', + large: 'public/plugins/logs/img/icn-logs-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/logs/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/logs', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/logs', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'loki', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'loki', + type: 'datasource', + name: 'Loki', + info: { + keywords: [], + logos: { + small: 'public/plugins/loki/img/loki_icon.svg', + large: 'public/plugins/loki/img/loki_icon.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Like Prometheus but for logs. OSS logging solution from Grafana Labs', + links: [ + { + name: 'Learn more', + url: 'https://grafana.com/loki', + }, + { + name: 'GitHub Project', + url: 'https://github.com/grafana/loki', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/loki/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'logging', + executable: 'gpx_loki', + logs: true, + metrics: true, + queryOptions: { + maxDataPoints: true, + }, + streaming: true, + }, + class: 'core', + module: { + path: 'public/plugins/loki/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/loki', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'mixed', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'mixed', + type: 'datasource', + name: '-- Mixed --', + info: { + keywords: [], + logos: { + small: 'public/plugins/mixed/img/icn-mixeddatasources.svg', + large: 'public/plugins/mixed/img/icn-mixeddatasources.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Lets you query multiple data sources in the same panel.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/#special-data-sources', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + builtIn: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/mixed', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/mixed', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'mssql', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'mssql', + type: 'datasource', + name: 'Microsoft SQL Server', + info: { + keywords: [], + logos: { + small: 'public/plugins/mssql/img/sql_server_logo.svg', + large: 'public/plugins/mssql/img/sql_server_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for Microsoft SQL Server compatible databases', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/mssql/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'sql', + executable: 'gpx_mssql', + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/mssql/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/mssql', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + translations: { + 'cs-CZ': 'public/plugins/mssql/locales/cs-CZ/mssql.json', + 'de-DE': 'public/plugins/mssql/locales/de-DE/mssql.json', + 'en-US': 'public/plugins/mssql/locales/en-US/mssql.json', + 'es-ES': 'public/plugins/mssql/locales/es-ES/mssql.json', + 'fr-FR': 'public/plugins/mssql/locales/fr-FR/mssql.json', + 'hu-HU': 'public/plugins/mssql/locales/hu-HU/mssql.json', + 'id-ID': 'public/plugins/mssql/locales/id-ID/mssql.json', + 'it-IT': 'public/plugins/mssql/locales/it-IT/mssql.json', + 'ja-JP': 'public/plugins/mssql/locales/ja-JP/mssql.json', + 'ko-KR': 'public/plugins/mssql/locales/ko-KR/mssql.json', + 'nl-NL': 'public/plugins/mssql/locales/nl-NL/mssql.json', + 'pl-PL': 'public/plugins/mssql/locales/pl-PL/mssql.json', + 'pt-BR': 'public/plugins/mssql/locales/pt-BR/mssql.json', + 'pt-PT': 'public/plugins/mssql/locales/pt-PT/mssql.json', + 'ru-RU': 'public/plugins/mssql/locales/ru-RU/mssql.json', + 'sv-SE': 'public/plugins/mssql/locales/sv-SE/mssql.json', + 'tr-TR': 'public/plugins/mssql/locales/tr-TR/mssql.json', + 'zh-Hans': 'public/plugins/mssql/locales/zh-Hans/mssql.json', + 'zh-Hant': 'public/plugins/mssql/locales/zh-Hant/mssql.json', + }, + }, + status: {}, + }, + v0alpha1Meta, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'mysql', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'mysql', + type: 'datasource', + name: 'MySQL', + info: { + keywords: [], + logos: { + small: 'public/plugins/mysql/img/mysql_logo.svg', + large: 'public/plugins/mysql/img/mysql_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for MySQL databases', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/mysql/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'sql', + executable: 'gpx_mysql', + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/mysql/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/mysql', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'news', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'news', + type: 'panel', + name: 'News', + info: { + keywords: [], + logos: { + small: 'public/plugins/news/img/news.svg', + large: 'public/plugins/news/img/news.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'RSS feed reader', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/news/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + state: 'beta', + }, + class: 'core', + module: { + path: 'core:plugin/news', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/news', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'nodeGraph', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'nodeGraph', + type: 'panel', + name: 'Node Graph', + info: { + keywords: [], + logos: { + small: 'public/plugins/nodeGraph/img/icn-node-graph.svg', + large: 'public/plugins/nodeGraph/img/icn-node-graph.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/node-graph/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/nodeGraph', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/nodeGraph', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'opentsdb', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'opentsdb', + type: 'datasource', + name: 'OpenTSDB', + info: { + keywords: [], + logos: { + small: 'public/plugins/opentsdb/img/opentsdb_logo.png', + large: 'public/plugins/opentsdb/img/opentsdb_logo.png', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/opentsdb/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + executable: 'gpx_opentsdb', + metrics: true, + }, + class: 'core', + module: { + path: 'public/plugins/opentsdb/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/opentsdb', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'parca', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'parca', + type: 'datasource', + name: 'Parca', + info: { + keywords: ['grafana', 'datasource', 'parca', 'profiling'], + logos: { + small: 'public/plugins/parca/img/logo-small.svg', + large: 'public/plugins/parca/img/logo-small.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://www.grafana.com', + }, + description: + 'Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.', + links: [ + { + name: 'GitHub Project', + url: 'https://github.com/parca-dev/parca', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/parca/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'profiling', + executable: 'gpx_parca', + metrics: true, + }, + class: 'core', + module: { + path: 'public/plugins/parca/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/parca', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'piechart', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'piechart', + type: 'panel', + name: 'Pie chart', + info: { + keywords: [], + logos: { + small: 'public/plugins/piechart/img/icon_piechart.svg', + large: 'public/plugins/piechart/img/icon_piechart.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'The new core pie chart visualization', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/pie-chart/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/piechart', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/piechart', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'prometheus', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'prometheus', + type: 'datasource', + name: 'Prometheus', + info: { + keywords: [], + logos: { + small: 'public/plugins/prometheus/img/prometheus_logo.svg', + large: 'public/plugins/prometheus/img/prometheus_logo.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database & alerting', + links: [ + { + name: 'Learn more', + url: 'https://prometheus.io/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/prometheus/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + includes: [ + { + type: 'dashboard', + name: 'Prometheus Stats', + role: 'Viewer', + path: 'dashboards/prometheus_stats.json', + }, + { + type: 'dashboard', + name: 'Prometheus 2.0 Stats', + role: 'Viewer', + path: 'dashboards/prometheus_2_stats.json', + }, + { + type: 'dashboard', + name: 'Grafana Stats', + role: 'Viewer', + path: 'dashboards/grafana_stats.json', + }, + ], + metrics: true, + multiValueFilterOperators: true, + queryOptions: { + minInterval: true, + }, + routes: [ + { + path: 'api/v1/query', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/query_range', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/series', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/labels', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/query_exemplars', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: '/rules', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.rules.external:read', + }, + { + path: '/rules', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + { + path: '/rules', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + { + path: '/config/v1/rules', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + { + path: '/config/v1/rules', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + ], + }, + class: 'core', + module: { + path: 'core:plugin/prometheus', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/prometheus', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'radialbar', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'radialbar', + type: 'panel', + name: 'New Gauge', + info: { + keywords: [], + logos: { + small: 'public/plugins/radialbar/img/icon_gauge.svg', + large: 'public/plugins/radialbar/img/icon_gauge.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Standard gauge visualization', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/gauge/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'alpha', + }, + class: 'core', + module: { + path: 'core:plugin/radialbar', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/radialbar', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'stackdriver', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'stackdriver', + type: 'datasource', + name: 'Google Cloud Monitoring', + info: { + keywords: [], + logos: { + small: 'public/plugins/stackdriver/img/cloud_monitoring_logo.svg', + large: 'public/plugins/stackdriver/img/cloud_monitoring_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: "Data source for Google's monitoring service (formerly named Stackdriver)", + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/google-cloud-monitoring/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'cloud', + executable: 'gpx_cloudmonitoring', + includes: [ + { + type: 'dashboard', + name: 'Data Processing Monitoring', + role: 'Viewer', + path: 'dashboards/dataprocessing-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud Functions Monitoring', + role: 'Viewer', + path: 'dashboards/cloudfunctions-monitoring.json', + }, + { + type: 'dashboard', + name: 'GCE VM Instance Monitoring', + role: 'Viewer', + path: 'dashboards/gce-vm-instance-monitoring.json', + }, + { + type: 'dashboard', + name: 'GKE Prometheus Pod/Node Monitoring', + role: 'Viewer', + path: 'dashboards/gke-prometheus-pod-node-monitoring.json', + }, + { + type: 'dashboard', + name: 'Firewall Insights Monitoring', + role: 'Viewer', + path: 'dashboards/firewall-insight-monitoring.json', + }, + { + type: 'dashboard', + name: 'GCE Network Monitoring', + role: 'Viewer', + path: 'dashboards/gce-network-monitoring.json', + }, + { + type: 'dashboard', + name: 'HTTP/S LB Backend Services', + role: 'Viewer', + path: 'dashboards/https-lb-backend-services-monitoring.json', + }, + { + type: 'dashboard', + name: 'HTTP/S Load Balancer Monitoring', + role: 'Viewer', + path: 'dashboards/https-loadbalancer-monitoring.json', + }, + { + type: 'dashboard', + name: 'Network TCP Load Balancer Monitoring', + role: 'Viewer', + path: 'dashboards/network-tcp-loadbalancer-monitoring.json', + }, + { + type: 'dashboard', + name: 'MicroService Monitoring', + role: 'Viewer', + path: 'dashboards/micro-service-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud Storage Monitoring', + role: 'Viewer', + path: 'dashboards/cloud-storage-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud SQL Monitoring', + role: 'Viewer', + path: 'dashboards/cloudsql-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud SQL(MySQL) Monitoring', + role: 'Viewer', + path: 'dashboards/cloudsql-mysql-monitoring.json', + }, + ], + logs: true, + metrics: true, + queryOptions: { + maxDataPoints: true, + cacheTimeout: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/stackdriver/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/stackdriver', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'stat', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'stat', + type: 'panel', + name: 'Stat', + info: { + keywords: [], + logos: { + small: 'public/plugins/stat/img/icn-singlestat-panel.svg', + large: 'public/plugins/stat/img/icn-singlestat-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Big stat values & sparklines', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/stat/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/stat', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/stat', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'state-timeline', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'state-timeline', + type: 'panel', + name: 'State timeline', + info: { + keywords: [], + logos: { + small: 'public/plugins/state-timeline/img/timeline.svg', + large: 'public/plugins/state-timeline/img/timeline.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'State changes and durations', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/state-timeline/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/state-timeline', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/state-timeline', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'status-history', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'status-history', + type: 'panel', + name: 'Status history', + info: { + keywords: [], + logos: { + small: 'public/plugins/status-history/img/status.svg', + large: 'public/plugins/status-history/img/status.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Periodic status history', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/status-history/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/status-history', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/status-history', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'table', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'table', + type: 'panel', + name: 'Table', + info: { + keywords: [], + logos: { + small: 'public/plugins/table/img/icn-table-panel.svg', + large: 'public/plugins/table/img/icn-table-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Supports many column styles', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/table/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/table', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/table', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'tempo', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'tempo', + type: 'datasource', + name: 'Tempo', + info: { + keywords: [], + logos: { + small: 'public/plugins/tempo/img/tempo_logo.svg', + large: 'public/plugins/tempo/img/tempo_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.', + links: [ + { + name: 'GitHub Project', + url: 'https://github.com/grafana/tempo', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/tempo/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'tracing', + executable: 'gpx_tempo', + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/tempo/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/tempo', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'text', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'text', + type: 'panel', + name: 'Text', + info: { + keywords: [], + logos: { + small: 'public/plugins/text/img/icn-text-panel.svg', + large: 'public/plugins/text/img/icn-text-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Supports markdown and html content', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/text/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/text', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/text', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'timeseries', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'timeseries', + type: 'panel', + name: 'Time series', + info: { + keywords: [], + logos: { + small: 'public/plugins/timeseries/img/icn-timeseries-panel.svg', + large: 'public/plugins/timeseries/img/icn-timeseries-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Time based line, area and bar charts', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/time-series/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/timeseries', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/timeseries', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'traces', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'traces', + type: 'panel', + name: 'Traces', + info: { + keywords: [], + logos: { + small: 'public/plugins/traces/img/traces-panel.svg', + large: 'public/plugins/traces/img/traces-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/traces/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/traces', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/traces', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'trend', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'trend', + type: 'panel', + name: 'Trend', + info: { + keywords: [], + logos: { + small: 'public/plugins/trend/img/trend.svg', + large: 'public/plugins/trend/img/trend.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Like timeseries, but when x != time', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/trend/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'beta', + }, + class: 'core', + module: { + path: 'core:plugin/trend', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/trend', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'welcome', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'welcome', + type: 'panel', + name: 'Welcome', + info: { + keywords: [], + logos: { + small: 'public/plugins/welcome/img/icn-dashlist-panel.svg', + large: 'public/plugins/welcome/img/icn-dashlist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + hideFromList: true, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/welcome', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/welcome', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'xychart', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'xychart', + type: 'panel', + name: 'XY Chart', + info: { + keywords: ['scatter', 'plot'], + logos: { + small: 'public/plugins/xychart/img/icn-xychart.svg', + large: 'public/plugins/xychart/img/icn-xychart.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Supports arbitrary X vs Y in a graph to visualize the relationship between two variables.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/xy-chart/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/xychart', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/xychart', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'zipkin', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'zipkin', + type: 'datasource', + name: 'Zipkin', + info: { + keywords: [], + logos: { + small: 'public/plugins/zipkin/img/zipkin-logo.svg', + large: 'public/plugins/zipkin/img/zipkin-logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Placeholder for the distributed tracing system.', + links: [ + { + name: 'Learn more', + url: 'https://zipkin.io', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/zipkin/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'tracing', + executable: 'gpx_zipkin', + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/zipkin/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/zipkin', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + ], +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/types.ts b/packages/grafana-runtime/src/services/pluginMeta/types.ts new file mode 100644 index 00000000000..81efe0df7b3 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types.ts @@ -0,0 +1,10 @@ +import type { AppPluginConfig } from '@grafana/data'; + +import type { Meta } from './types/meta_object_gen'; + +export type AppPluginMetas = Record; + +export type AppPluginMetasMapper = (response: T) => AppPluginMetas; +export interface PluginMetasResponse { + items: Meta[]; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts b/packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts new file mode 100644 index 00000000000..044ec1f4cd8 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts @@ -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; + annotations?: Record; + 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; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts b/packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts new file mode 100644 index 00000000000..51845e98454 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts @@ -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; + tokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + jwtTokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + // +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; + // +listType=atomic + children?: string[]; +} + +export const defaultSpec = (): Spec => ({ + pluginJson: defaultJSONData(), + class: "core", +}); + diff --git a/packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts b/packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts @@ -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; +} + +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; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); +