diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0dda8519ef6..6b40b814064 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -543,6 +543,7 @@ i18next.config.ts @grafana/grafana-frontend-platform /packages/grafana-data/tsconfig.json @grafana/grafana-frontend-platform /packages/grafana-data/test/ @grafana/grafana-frontend-platform /packages/grafana-data/typings/ @grafana/grafana-frontend-platform +/packages/grafana-data/scripts/ @grafana/grafana-frontend-platform /packages/grafana-data/src/**/*logs* @grafana/observability-logs /packages/grafana-data/src/context/plugins/ @grafana/plugins-platform-frontend diff --git a/.golangci.yml b/.golangci.yml index 069e88632ff..d7037bf6fac 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -121,6 +121,8 @@ linters: - '**/pkg/tsdb/zipkin/**/*' - '**/pkg/tsdb/jaeger/*' - '**/pkg/tsdb/jaeger/**/*' + - '**/pkg/tsdb/elasticsearch/*' + - '**/pkg/tsdb/elasticsearch/**/*' deny: - pkg: github.com/grafana/grafana/pkg/api desc: Core plugins are not allowed to depend on Grafana core packages diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v0alpha1.json index 716b476f825..40b4fff030e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v0alpha1.json @@ -586,6 +586,7 @@ }, "id": -1, "panels": [], + "repeat": "custom_var_tab", "title": "Repeated Tab by \"$custom_var_tab\"", "type": "row" }, @@ -610,8 +611,11 @@ "y": 22 }, "id": 6, + "maxPerRow": 3, "options": {}, "pluginVersion": "12.4.0-19736337744", + "repeat": "custom_var_panel", + "repeatDirection": "h", "targets": [ { "refId": "A" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v1beta1.json index f915142fd14..ff1e2d42e20 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tabs-and-rows-repeated.v1beta1.json @@ -586,6 +586,7 @@ }, "id": -1, "panels": [], + "repeat": "custom_var_tab", "title": "Repeated Tab by \"$custom_var_tab\"", "type": "row" }, @@ -610,8 +611,11 @@ "y": 22 }, "id": 6, + "maxPerRow": 3, "options": {}, "pluginVersion": "12.4.0-19736337744", + "repeat": "custom_var_panel", + "repeatDirection": "h", "targets": [ { "refId": "A" diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index 857af7ca866..f9f953f965d 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -439,6 +439,11 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash rowPanel["title"] = *tab.Spec.Title } + if tab.Spec.Repeat != nil && tab.Spec.Repeat.Value != "" { + // We only use value here as V1 doesn't support mode + rowPanel["repeat"] = tab.Spec.Repeat.Value + } + rowPanel["gridPos"] = map[string]interface{}{ "x": 0, "y": currentY, @@ -819,6 +824,21 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da }, } + // Convert AutoGridRepeatOptions to RepeatOptions if present + // AutoGridRepeatOptions only has mode and value; infer direction and maxPerRow from AutoGrid settings: + // - direction: always "h" (AutoGrid flows horizontally, left-to-right then wraps) + // - maxPerRow: from AutoGrid's maxColumnCount + if item.Spec.Repeat != nil { + directionH := dashv2alpha1.DashboardRepeatOptionsDirectionH + maxPerRow := int64(maxColumnCount) + gridItem.Spec.Repeat = &dashv2alpha1.DashboardRepeatOptions{ + Mode: item.Spec.Repeat.Mode, + Value: item.Spec.Repeat.Value, + Direction: &directionH, + MaxPerRow: &maxPerRow, + } + } + panel, err := convertPanelFromElement(&element, &gridItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index 61f092d491e..7fdd474df87 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -2117,7 +2117,7 @@ } ], "title": "Numeric, no series", - "type": "gauge" + "type": "radialbar" }, { "datasource": { @@ -2183,7 +2183,7 @@ } ], "title": "Non-numeric", - "type": "gauge" + "type": "radialbar" } ], "preload": false, @@ -2201,4 +2201,4 @@ "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index ff69226fdf5..c44353b7df2 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -2067,7 +2067,7 @@ } ], "title": "Numeric, no series", - "type": "gauge" + "type": "radialbar" }, { "datasource": { @@ -2131,7 +2131,7 @@ } ], "title": "Non-numeric", - "type": "gauge" + "type": "radialbar" } ], "preload": false, diff --git a/docs/sources/as-code/observability-as-code/_index.md b/docs/sources/as-code/observability-as-code/_index.md index 338d9235255..92441bd5110 100644 --- a/docs/sources/as-code/observability-as-code/_index.md +++ b/docs/sources/as-code/observability-as-code/_index.md @@ -25,10 +25,6 @@ cards: height: 24 href: ./foundation-sdk/ description: The Grafana Foundation SDK is a set of tools, types, and libraries that let you define Grafana dashboards and resources using familiar programming languages like Go, TypeScript, Python, Java, and PHP. Use it in conjunction with `grafanactl` to push your programmatically generated resources. - - title: JSON schema v2 - height: 24 - href: ./schema-v2/ - description: Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings. Observability as Code works with all versions of the JSON model, and it's fully compatible with version 2. - title: Git Sync (private preview) height: 24 href: ./provision-resources/intro-git-sync/ @@ -68,7 +64,7 @@ Historically, managing Grafana as code involved various community and Grafana La - This approach requires handling HTTP requests and responses but provides complete control over resource management. - `grafanactl`, Git Sync, and the Foundation SDK are all built on top of these APIs. -- To understand Dashboard Schemas accepted by the APIs, refer to the [JSON models documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/). +- To understand Dashboard Schemas accepted by the APIs, refer to the [JSON models documentation](https://grafana.com/docs/grafana//visualizations/dashboards/build-dashboards/view-dashboard-json-model/index.md). ## Explore diff --git a/docs/sources/as-code/observability-as-code/schema-v2/_index.md b/docs/sources/as-code/observability-as-code/schema-v2/_index.md deleted file mode 100644 index 65c73a49cbe..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/_index.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -description: A reference for the JSON dashboard schemas used with Observability as Code, including the experimental V2 schema. -keywords: - - configuration - - as code - - dashboards - - git integration - - git sync - - github -labels: - products: - - cloud - - enterprise - - oss -title: JSON schema v2 -weight: 500 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/ -aliases: - - ../../observability-as-code/schema-v2/ # /docs/grafana/next/observability-as-code/schema-v2/ ---- - -# Dashboard JSON schema v2 - -{{< admonition type="caution" >}} - -Dashboard JSON schema v2 is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog). - -**Do not enable this feature in production environments as it may result in the irreversible loss of data.** - -{{< /admonition >}} - -Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings. - -Observability as Code works with all versions of the JSON model, and it's fully compatible with version 2. - -## Before you begin - -Schema v2 is automatically enabled with the Dynamic Dashboards feature toggle. -To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog). -It also requires the new dashboards API feature toggle, `kubernetesDashboards`, to be enabled as well. - -For more information on how dashboards behave depending on your feature flag configuration, refer to [Notes and limitations](#notes-and-limitations). - -## Accessing the JSON Model - -To view the JSON representation of a dashboard: - -1. Toggle on the edit mode switch in the top-right corner of the dashboard. -1. Click the gear icon in the top navigation bar to go to **Settings**. -1. Select the **JSON Model** tab. -1. Copy or edit the JSON structure as needed. - -## JSON fields - -```json -{ - "annotations": [], - "cursorSync": "Off", - "editable": true, - "elements": {}, - "layout": { - "kind": GridLayout, // Can also be AutoGridLayout, RowsLayout, or TabsLayout - "spec": { - "items": [] - } - }, - "links": [], - "liveNow": false, - "preload": false, - "tags": [], // Tags associated with the dashboard. - "timeSettings": { - "autoRefresh": "", - "autoRefreshIntervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "fiscalYearStartMonth": 0, - "from": "now-6h", - "hideTimepicker": false, - "timezone": "browser", - "to": "now" - }, - "title": "", - "variables": [] -}, -``` - -The dashboard JSON sample shown uses the default `GridLayoutKind`. -The JSON in a new dashboard for the other three layout options, `AutoGridLayout`, `RowsLayout`, and `TabsLayout`, are as follows: - -**`AutoGridLayout`** - -```json - "layout": { - "kind": "AutoGridLayout", - "spec": { - "columnWidthMode": "standard", - "items": [], - "fillScreen": false, - "maxColumnCount": 3, - "rowHeightMode": "standard" - } - }, -``` - -**`RowsLayout`** - -```json - "layout": { - "kind": "RowsLayout", - "spec": { - "rows": [] - }, -``` - -**`TabsLayout`** - -```json - "layout": { - "kind": "TabsLayout", - "spec": { - "tabs": [] - }, -``` - -### `DashboardSpec` - -The following table explains the usage of the dashboard JSON fields. -The table includes default and other fields: - - - -| Name | Usage | -| ------------ | ------------------------------------------------------------------------- | -| annotations | Contains the list of annotations that are associated with the dashboard. | -| cursorSync | Dashboard cursor sync behavior. | -| editable | bool. Whether or not a dashboard is editable. | -| elements | Contains the list of elements included in the dashboard. Supported dashboard elements are: PanelKind and LibraryPanelKind. | -| layout | The dashboard layout. Supported layouts are: | -| links | Links with references to other dashboards or external websites. | -| liveNow | bool. When set to `true`, the dashboard redraws panels at an interval matching the pixel width. This keeps data "moving left" regardless of the query refresh rate. This setting helps avoid dashboards presenting stale live data. | -| preload | bool. When set to `true`, the dashboard loads all panels when the dashboard is loaded. | -| tags | Contains the list of tags associated with dashboard. | -| timeSettings | All time settings for the dashboard. | -| title | Title of the dashboard. | -| variables | Contains the list of configured template variables. | - - - -### `annotations` - -The configuration for the list of annotations that are associated with the dashboard. -For the JSON and field usage notes, refer to the [annotations schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/annotations-schema/). - -### `elements` - -Dashboards can contain the following elements: - -- [PanelKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/panel-schema/) -- [LibraryPanelKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/librarypanel-schema/) - -### `layout` - -Dashboards can have four layout options: - -- [GridLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#gridlayoutkind) -- [AutoGridLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#autogridlayoutkind) -- [RowsLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#rowslayoutkind) -- [TabsLayoutKind](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/#tabslayoutkind) - -For the JSON and field usage notes about each of these, refer to the [layout schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/layout-schema/). - -### `links` - -The configuration for links with references to other dashboards or external websites. - -For the JSON and field usage notes, refer to the [links schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/links-schema/). - -### `tags` - -Tags associated with the dashboard. Each tag can be up to 50 characters long. - -` [...string]` - -### `timesettings` - -The `TimeSettingsSpec` defines the default time configuration for the time picker and the refresh picker for the specific dashboard. -For the JSON and field usage notes about the `TimeSettingsSpec`, refer to the [timesettings schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/timesettings-schema/). - -### `variables` - -The `variables` schema defines which variables are used in the dashboard. - -There are eight variables types: - -- QueryVariableKind -- TextVariableKind -- ConstantVariableKind -- DatasourceVariableKind -- IntervalVariableKind -- CustomVariableKind -- GroupByVariableKind -- AdhocVariableKind - -For the JSON and field usage notes about the `variables` spec, refer to the [variables schema documentation](https://grafana.com/docs/grafana//observability-as-code/schema-v2/variables-schema/). - -## Notes and limitations - -### Existing dashboards - -With schema v2 enabled, you can still open and view your pre-existing dashboards. -Upon saving, they’ll be updated to the new schema where you can take advantage of the new features and functionalities. - -### Dashboard behavior with disabled feature flags - -If you disable the Dynamic dashboards or `kubernetesDashboards` feature flags, you should be aware of how dashboards will behave. - -#### Disable Dynamic dashboards - -If the Dynamic dashboards feature toggle is disabled, depending on how the dashboard was built, it will behave differently: - -- Dashboards built on the new schema through the UI - View only -- Dashboards built on Schema v1 - View and edit -- Dashboards built on the new schema by way of Terraform or the CLI - View and edit -- Provisioned dashboards built on the new schema - View and edit, but the edit experience will be the old experience - -#### Disable Dynamic dashboards and `kubernetesDashboards` - -You’ll be unable to view or edit dashboards created or updated in the new schema. - -### Import and export - -From the UI, dashboards created on schema v2 can be exported and imported like other dashboards. -When you export them to use in another instance, references of data sources are not persisted but data source types are. -You’ll have the option to select the data source of your choice in the import UI. diff --git a/docs/sources/as-code/observability-as-code/schema-v2/annotations-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/annotations-schema.md deleted file mode 100644 index e99e7c2cce6..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/annotations-schema.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -description: A reference for the JSON annotations schema used with Observability as Code. -keywords: - - configuration - - as code - - as-code - - dashboards - - git integration - - git sync - - github - - annotations -labels: - products: - - cloud - - enterprise - - oss -menuTitle: annotations schema -title: annotations -weight: 100 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/annotations-schema/ -aliases: - - ../../../observability-as-code/schema-v2/annotations-schema/ # /docs/grafana/next/observability-as-code/schema-v2/annotations-schema/ ---- - -# `annotations` - -The configuration for the list of annotations that are associated with the dashboard. - -```json - "annotations": [ - { - "kind": "AnnotationQuery", - "spec": { - "builtIn": false, - "datasource": { - "type": "", - "uid": "" - }, - "enable": false, - "hide": false, - "iconColor": "", - "name": "" - } - } - ], -``` - -`AnnotationsQueryKind` consists of: - -- kind: "AnnotationQuery" -- spec: [AnnotationQuerySpec](#annotationqueryspec) - -## `AnnotationQuerySpec` - -| Name | Type/Definition | -| ---------- | ----------------------------------------------------------------- | -| datasource | [`DataSourceRef`](#datasourceref) | -| query | [`DataQueryKind`](#dataquerykind) | -| enable | bool | -| hide | bool | -| iconColor | string | -| name | string | -| builtIn | bool. Default is `false`. | -| filter | [`AnnotationPanelFilter`](#annotationpanelfilter) | -| options | `[string]`: A catch-all field for datasource-specific properties. | - -### `DataSourceRef` - -| Name | Usage | -| ----- | ---------------------------------- | -| type? | string. The plugin type-id. | -| uid? | The specific data source instance. | - -### `DataQueryKind` - -| Name | Type | -| ---- | ------ | -| kind | string | -| spec | string | - -### `AnnotationPanelFilter` - -| Name | Type/Definition | -| -------- | ------------------------------------------------------------------------------ | -| exclude? | bool. Should the specified panels be included or excluded. Default is `false`. | -| ids | `[...uint8]`. Panel IDs that should be included or excluded. | diff --git a/docs/sources/as-code/observability-as-code/schema-v2/layout-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/layout-schema.md deleted file mode 100644 index ca31417bbcf..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/layout-schema.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -description: A reference for the JSON layout schema used with Observability as Code. -keywords: - - configuration - - as code - - as-code - - dashboards - - git integration - - git sync - - github - - layout -labels: - products: - - cloud - - enterprise - - oss -menuTitle: layout schema -title: layout -weight: 400 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/layout-schema/ -aliases: - - ../../../observability-as-code/schema-v2/layout-schema/ # /docs/grafana/next/observability-as-code/schema-v2/layout-schema/ ---- - -# `layout` - -There are four layout options offering two types of panel control: - -**Panel layout options** - -These options control the size and position of panels: - -- [GridLayoutKind](#gridlayoutkind) - Corresponds to the **Custom** option in the UI. You define panel size and panel positions using x- and y- settings. -- [AutoGridLayoutKind](#autogridlayoutkind) - Corresponds to the **Auto grid** option in the UI. Panel size and position are automatically set based on column and row parameters. - -**Panel grouping options** - -These options control the grouping of panels: - -- [RowsLayoutKind](#rowslayoutkind) - Groups panels into rows. -- [TabsLayoutKind](#tabslayoutkind) - Groups panels into tabs. - -## `GridLayoutKind` - -The grid layout allows you to manually size and position grid items by setting the height, width, x, and y of each item. -This layout corresponds to the **Custom** option in the UI. - -Following is the JSON for a default grid layout, a grid layout item, and a grid layout row: - -```json - "kind": "GridLayout", - "spec": { - "items": [ - { - "kind": "GridLayoutItem", - "spec": { - "element": {...}, - "height": 0, - "width": 0, - "x": 0, - "y": 0 - } - }, - { - "kind": "GridLayoutRow", - "spec": { - "collapsed": false, - "elements": [], - "title": "", - "y": 0 - } - }, - ] - } -``` - -`GridLayoutKind` consists of: - -- kind: "GridLayout" -- spec: GridLayoutSpec - - items: GridLayoutItemKind` or GridLayoutRowKind` - - GridLayoutItemKind - - kind: "GridLayoutItem" - - spec: [GridLayoutItemSpec](#gridlayoutitemspec) - - GridLayoutRowKind - - kind: "GridLayoutRow" - - spec: [GridLayoutRowSpec](#gridlayoutrowspec) - -### `GridLayoutItemSpec` - -The following table explains the usage of the grid layout item JSON fields: - -| Name | Usage | -| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| x | integer. Position of the item x-axis. | -| y | integer. Position of the item y-axis. | -| width | Width of the item in pixels. | -| height | Height of the item in pixels. | -| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana//observability-as-code/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. | -| repeat? | [RepeatOptions](#repeatoptions). Configured repeat options, if any | - -#### `RepeatOptions` - -The following table explains the usage of the repeat option JSON fields: - -| Name | Usage | -| ---------- | ---------------------------------------------------- | -| mode | `RepeatMode` - "variable" | -| value | string | -| direction? | Options are `h` for horizontal and `v` for vertical. | -| maxPerRow? | integer | - -### `GridLayoutRowSpec` - -The following table explains the usage of the grid layout row JSON fields: - - - -| Name | Usage | -| ---- | ----- | -| y | integer. Position of the row y-axis | -| collapsed | bool. Whether or not the row is collapsed | -| title | Row title | -| elements | [`[...GridLayoutItemKind]`](#gridlayoutitemspec). Grid items in the row will have their y value be relative to the row's y value. This means a panel positioned at `y: 0` in a row with `y: 10` will be positioned at `y: 11` (row header has a height of 1) in the dashboard. | -| repeat? | [RowRepeatOptions](#rowrepeatoptions) Configured row repeat options, if any

| - - - -#### `RowRepeatOptions` - -| Name | Usage | -| ----- | ------------------------- | -| mode | `RepeatMode` - "variable" | -| value | string | - -## `AutoGridLayoutKind` - -With an auto grid, Grafana sizes and positions your panels for the best fit based on the column and row constraints that you set. -This layout corresponds to the **Auto grid** option in the UI. - -Following is the JSON for a default auto grid layout and a grid layout item: - - - -```json - "kind": "AutoGridLayout", - "spec": { - "columnWidthMode": "standard", - "fillScreen": false, - "items": [ - { - "kind": "AutoGridLayoutItem", - "spec": { - "element": {...}, - } - } - ], - "maxColumnCount": 3, - "rowHeightMode": "standard" - } -``` - -`AutoGridLayoutKind` consists of: - -- kind: "AutoGridLayout" -- spec: [AutoGridLayoutSpec](#autogridlayoutspec) - -### `AutoGridLayoutSpec` - -The following table explains the usage of the auto grid layout JSON fields: - - - -| Name | Usage | -| ---- | ----- | -| maxColumnCount? | number. Default is `3`. | -| columnWidthMode | Options are: `narrow`, `standard`, `wide`, and `custom`. Default is `standard`. | -| columnWidth? | number | -| rowHeightMode | Options are: `short`, `standard`, `tall`, and `custom`. Default is `standard`. | -| rowHeight? | number | -| fillScreen? | bool. Default is `false`. | -| items | `AutoGridLayoutItemKind`. Consists of:
  • kind: "AutoGridLayoutItem"
  • spec: [AutoGridLayoutItemSpec](#autogridlayoutitemspec)
| - - - -#### `AutoGridLayoutItemSpec` - -The following table explains the usage of the auto grid layout item JSON fields: - - - -| Name | Usage | -| ---- | ----- | -| element | `ElementReference`. Reference to a [`PanelKind`](https://grafana.com/docs/grafana//observability-as-code/schema-v2/panel-schema/) from `dashboard.spec.elements` expressed as JSON Schema reference. | -| repeat? | [AutoGridRepeatOptions](#autogridrepeatoptions). Configured repeat options, if any. | -| conditionalRendering? | `ConditionalRenderingGroupKind`. Rules for hiding or showing panels, if any. Consists of:
  • kind: "ConditionalRenderingGroup"
  • spec: [ConditionalRenderingGroupSpec](#conditionalrenderinggroupspec)
| - - - -##### `AutoGridRepeatOptions` - -The following table explains the usage of the auto grid repeat option JSON fields: - -| Name | Usage | -| ----- | ------------------------- | -| mode | `RepeatMode` - "variable" | -| value | String | - -##### `ConditionalRenderingGroupSpec` - - - -| Name | Usage | -| ---- | ----- | -| visibility | Options are `show` and `hide` | -| condition | Options are `and` and `or` | -| items | Options are:
  • ConditionalRenderingVariableKind
    • kind: "ConditionalRenderingVariable"
    • spec: [ConditionalRenderingVariableSpec](#conditionalrenderingvariablespec)
  • ConditionalRenderingDataKind
    • kind: "ConditionalRenderingData"
    • spec: [ConditionalRenderingDataSpec](#conditionalrenderingdataspec)
  • ConditionalRenderingTimeRangeSizeKind
    • kind: "ConditionalRenderingTimeRangeSize"
    • spec: [ConditionalRenderingTimeRangeSizeSpec](#conditionalrenderingtimerangesizespec)
| - - - -###### `ConditionalRenderingVariableSpec` - -| Name | Usage | -| -------- | ------------------------------------ | -| variable | string | -| operator | Options are `equals` and `notEquals` | -| value | string | - -###### `ConditionalRenderingDataSpec` - -| Name | Type | -| ----- | ---- | -| value | bool | - -###### `ConditionalRenderingTimeRangeSizeSpec` - -| Name | Type | -| ----- | ------ | -| value | string | - -## `RowsLayoutKind` - -The `RowsLayoutKind` is one of two options that you can use to group panels. -You can nest any other kind of layout inside a layout row. -Rows can also be nested in auto grids or tabs. - -Following is the JSON for a default rows layout row: - -```json - "kind": "RowsLayout", - "spec": { - "rows": [ - { - "kind": "RowsLayoutRow", - "spec": { - "layout": { - "kind": "GridLayout", // Can also be AutoGridLayout or TabsLayout - "spec": {...} - }, - "title": "" - } - } - ] - } -``` - -`RowsLayoutKind` consists of: - -- kind: RowsLayout -- spec: RowsLayoutSpec - - rows: RowsLayoutRowKind - - kind: RowsLayoutRow - - spec: [RowsLayoutRowSpec](#rowslayoutrowspec) - -### `RowsLayoutRowSpec` - -The following table explains the usage of the rows layout row JSON fields: - - - -| Name | Usage | -| ---- | ----- | -| title? | Title of the row. | -| collapse | bool. Whether or not the row is collapsed. | -| hideHeader? | bool. Whether the row header is hidden or shown. | -| fullScreen? | bool. Whether or not the row takes up the full screen. | -| conditionalRendering? | `ConditionalRenderingGroupKind`. Rules for hiding or showing rows, if any. Consists of:
  • kind: "ConditionalRenderingGroup"
  • spec: [ConditionalRenderingGroupSpec](#conditionalrenderinggroupspec)
| -| repeat? | [RowRepeatOptions](#rowrepeatoptions). Configured repeat options, if any. | -| layout | Supported layouts are:
  • [GridLayoutKind](#gridlayoutkind)
  • [RowsLayoutKind](#rowslayoutkind)
  • [AutoGridLayoutKind](#autogridlayoutkind)
  • [TabsLayoutKind](#tabslayoutkind)
| - - - -## `TabsLayoutKind` - -The `TabsLayoutKind` is one of two options that you can use to group panels. -You can nest any other kind of layout inside a tab. -Tabs can also be nested in auto grids or rows. - -Following is the JSON for a default tabs layout tab and a tab: - -```json - "kind": "TabsLayout", - "spec": { - "tabs": [ - { - "kind": "TabsLayoutTab", - "spec": { - "layout": { - "kind": "GridLayout", // Can also be AutoGridLayout or RowsLayout - "spec": {...} - }, - "title": "New tab" - } - } - ] - } -``` - -`TabsLayoutKind` consists of: - -- kind: TabsLayout - - spec: TabsLayoutSpec - - tabs: TabsLayoutTabKind - - kind: TabsLayoutTab - - spec: [TabsLayoutTabSpec](#tabslayouttabspec) - -### `TabsLayoutTabSpec` - -The following table explains the usage of the tabs layout tab JSON fields: - - - -| Name | Usage | -| ---- | ----- | -| title? | The title of the tab. | -| layout | Supported layouts are:
  • [GridLayoutKind](#gridlayoutkind)
  • [RowsLayoutKind](#rowslayoutkind)
  • [AutoGridLayoutKind](#autogridlayoutkind)
  • [TabsLayoutKind](#tabslayoutkind)
| -| conditionalRendering? | `ConditionalRenderingGroupKind`. Rules for hiding or showing panels, if any. Consists of:
  • kind: "ConditionalRenderingGroup"
  • spec: [ConditionalRenderingGroupSpec](#conditionalrenderinggroupspec)
| - - diff --git a/docs/sources/as-code/observability-as-code/schema-v2/librarypanel-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/librarypanel-schema.md deleted file mode 100644 index 45715e15b15..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/librarypanel-schema.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -description: A reference for the JSON library panel schema used with Observability as Code. -keywords: - - configuration - - as code - - as-code - - dashboards - - git integration - - git sync - - github - - library panel -labels: - products: - - cloud - - enterprise - - oss -menuTitle: LibraryPanelKind schema -title: LibraryPanelKind -weight: 300 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/librarypanel-schema/ -aliases: - - ../../../observability-as-code/schema-v2/librarypanel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/librarypanel-schema/ ---- - -# `LibraryPanelKind` - -A library panel is a reusable panel that you can use in any dashboard. -When you make a change to a library panel, that change propagates to all instances of where the panel is used. -Library panels streamline reuse of panels across multiple dashboards. - -Following is the default library panel element JSON: - -```json - "kind": "LibraryPanel", - "spec": { - "id": 0, - "libraryPanel": { - name: "", - uid: "", - } - "title": "" - } -``` - -The `LibraryPanelKind` consists of: - -- kind: "LibraryPanel" -- spec: [LibraryPanelKindSpec](#librarypanelkindspec) - - libraryPanel: [LibraryPanelRef](#librarypanelref) - -## `LibraryPanelKindSpec` - -The following table explains the usage of the library panel element JSON fields: - -| Name | Usage | -| ------------ | ------------------------------------------------ | -| id | Panel ID for the library panel in the dashboard. | -| libraryPanel | [`LibraryPanelRef`](#librarypanelref) | -| title | Title for the library panel in the dashboard. | - -### `LibraryPanelRef` - -The following table explains the usage of the library panel reference JSON fields: - -| Name | Usage | -| ---- | ------------------ | -| name | Library panel name | -| uid | Library panel uid | diff --git a/docs/sources/as-code/observability-as-code/schema-v2/links-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/links-schema.md deleted file mode 100644 index 0ddc50376de..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/links-schema.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -description: A reference for the JSON links schema used with Observability as Code. -keywords: - - configuration - - as code - - as-code - - dashboards - - git integration - - git sync - - github - - links -labels: - products: - - cloud - - enterprise - - oss -menuTitle: links schema -title: links -weight: 500 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/links-schema/ -aliases: - - ../../../observability-as-code/schema-v2/links-schema/ # /docs/grafana/next/observability-as-code/schema-v2/links-schema/ ---- - -# `links` - -The `links` schema is the configuration for links with references to other dashboards or external websites. -Following are the default JSON fields: - -```json - "links": [ - { - "asDropdown": false, - "icon": "", - "includeVars": false, - "keepTime": false, - "tags": [], - "targetBlank": false, - "title": "", - "tooltip": "", - "type": "link", - }, - ], -``` - -## `DashboardLink` - -The following table explains the usage of the dashboard link JSON fields. -The table includes default and other fields: - - - -| Name | Usage | -| ----------- | --------------------------------------- | -| title | string. Title to display with the link. | -| type | `DashboardLinkType`. Link type. Accepted values are:
  • dashboards - To refer to another dashboard
  • link - To refer to an external resource
| -| icon | string. Icon name to be displayed with the link. | -| tooltip | string. Tooltip to display when the user hovers their mouse over it. | -| url? | string. Link URL. Only required/valid if the type is link. | -| tags | string. List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards. | -| asDropdown | bool. If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards. Default is `false`. | -| targetBlank | bool. If true, the link will be opened in a new tab. Default is `false`. | -| includeVars | bool. If true, includes current template variables values in the link as query params. Default is `false`. | -| keepTime | bool. If true, includes current time range in the link as query params. Default is `false`. | -| placement? | string. Use placement to display the link somewhere else on the dashboard other than above the visualizations. Use the `inControlsMenu` parameter to render the link in the dashboard controls dropdown menu. | - - diff --git a/docs/sources/as-code/observability-as-code/schema-v2/panel-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/panel-schema.md deleted file mode 100644 index 088ab8eebf4..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/panel-schema.md +++ /dev/null @@ -1,305 +0,0 @@ ---- -description: A reference for the JSON panel schema used with Observability as Code. -keywords: - - configuration - - as code - - as-code - - dashboards - - git integration - - git sync - - github - - panels -labels: - products: - - cloud - - enterprise - - oss -menuTitle: PanelKind schema -title: PanelKind -weight: 200 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/panel-schema/ -aliases: - - ../../../observability-as-code/schema-v2/panel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/panel-schema/ ---- - -# `PanelKind` - -The panel element contains all the information about the panel including the visualization type, panel and visualization configuration, queries, and transformations. -There's a panel element for each panel contained in the dashboard. - -Following is the default panel element JSON: - -```json - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": {...}, - "description": "", - "id": 0, - "links": [], - "title": "", - "vizConfig": { - "kind": "", - "spec": {...}, - } - } -``` - -The `PanelKind` consists of: - -- kind: "Panel" -- spec: [PanelSpec](#panelspec) - -## `PanelSpec` - -The following table explains the usage of the panel element JSON fields: - - - -| Name | Usage | -| ------------ | --------------------------------------------------------------------- | -| data | `QueryGroupKind`, which includes queries and transformations. Consists of:
  • kind: "QueryGroup"
  • spec: [QueryGroupSpec](#querygroupspec)
| -| description | The panel description. | -| id | The panel ID. | -| links | Links with references to other dashboards or external websites. | -| title | The panel title. | -| vizConfig | `VizConfigKind`. Includes visualization type, field configuration options, and all other visualization options. Consists of:
  • kind: string. Plugin ID.
  • spec: [VizConfigSpec](#vizconfigspec)
| -| transparent? | bool. Controls whether or not the panel background is transparent. | - - - -### `QueryGroupSpec` - - - -| Name | Usage | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| queries | `PanelQueryKind`. Consists of:
  • kind: PanelQuery
  • spec: [PanelQuerySpec](#panelqueryspec)
| -| transformations | `TransformationKind`. Consists of:
  • kind: string. The transformation ID.
  • spec: [DataTransformerConfig](#datatransformerconfig)
| -| queryOptions | [`QueryOptionsSpec`](#queryoptionsspec) | - - - -#### `PanelQuerySpec` - -| Name | Usage | -| ----------- | --------------------------------- | -| query | [`DataQueryKind`](#dataquerykind) | -| datasource? | [`DataSourceRef`](#datasourceref) | - -##### `DataQueryKind` - -| Name | Type | -| ---- | ------ | -| kind | string | -| spec | string | - -##### `DataSourceRef` - -| Name | Usage | -| ----- | ---------------------------------- | -| type? | string. The plugin type-id. | -| uid? | The specific data source instance. | - -#### `DataTransformerConfig` - -Transformations allow you to manipulate data returned by a query before the system applies a visualization. -Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, or use the output of one transformation as the input to another transformation. - - - -| Name | Usage | -| --------- | ------------------------------------------- | -| id | string. Unique identifier of transformer. | -| disabled? | bool. Disabled transformations are skipped. | -| filter? | [`MatcherConfig`](#matcherconfig). Optional frame matcher. When missing it will be applied to all results. | -| topic? | `DataTopic`. Where to pull `DataFrames` from as input to transformation. Options are: `series`, `annotations`, and `alertStates`. | -| options | Options to be passed to the transformer. Valid options depend on the transformer id. | - - - -##### `MatcherConfig` - -Matcher is a predicate configuration. -Based on the configuration a set of field or values, it's filtered to apply an override or transformation. -It comes with in id (to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. - -| Name | Usage | -| -------- | -------------------------------------------------------------------------------------- | -| id | string. The matcher id. This is used to find the matcher implementation from registry. | -| options? | The matcher options. This is specific to the matcher implementation. | - -#### `QueryOptionsSpec` - -| Name | Type | -| ----------------- | ------- | -| timeFrom? | string | -| maxDataPoints? | integer | -| timeShift? | string | -| queryCachingTTL? | integer | -| interval? | string | -| cacheTimeout? | string | -| hideTimeOverride? | bool | - -### `VizConfigSpec` - -| Name | Type/Definition | -| ------------- | --------------------------------------- | -| pluginVersion | string | -| options | string | -| fieldConfig | [FieldConfigSource](#fieldconfigsource) | - -#### `FieldConfigSource` - -The data model used in Grafana, namely the _data frame_, is a columnar-oriented table structure that unifies both time series and table query results. -Each column within this structure is called a field. -A field can represent a single time series or table column. -Field options allow you to change how the data is displayed in your visualizations. - - - -| Name | Type/Definition | -| ---------- | ------------------------------------- | -| defaults | [`FieldConfig`](#fieldconfig). Defaults are the options applied to all fields. | -| overrides | The options applied to specific fields overriding the defaults. | -| matcher | [`MatcherConfig`](#matcherconfig). Optional frame matcher. When missing it will be applied to all results. | -| properties | `DynamicConfigValue`. Consists of:
  • `id` - string
  • value?
| - - - -##### `FieldConfig` - - - -| Name | Type/Definition | -| ------------------ | --------------------------------------- | -| displayName? | string. The display value for this field. This supports template variables where empty is auto. | -| displayNameFromDS? | string. This can be used by data sources that return an explicit naming structure for values and labels. When this property is configured, this value is used rather than the default naming strategy. | -| description? | string. Human readable field metadata. | -| path? | string. An explicit path to the field in the data source. When the frame meta includes a path, this will default to `${frame.meta.path}/${field.name}`. When defined, this value can be used as an identifier within the data source scope, and may be used to update the results. | -| writeable? | bool. True if the data source can write a value to the path. Auth/authz are supported separately. | -| filterable? | bool. True if the data source field supports ad-hoc filters. | -| unit? | string. Unit a field should use. The unit you select is applied to all fields except time. You can use the unit's ID available in Grafana or a custom unit. [Available units in Grafana](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts). As custom units, you can use the following formats:
  • `suffix:` for custom unit that should go after value.
  • `prefix:` for custom unit that should go before value.
  • `time:` for custom date time formats type for example
  • `time:YYYY-MM-DD`
  • `si:` for custom SI units. For example: `si: mF`. You can specify both a unit and the source data scale, so if your source data is represented as milli (thousands of) something, prefix the unit with that SI scale character.
  • `count:` for a custom count unit.
  • `currency:` for custom a currency unit.
| -| decimals? | number. Specify the number of decimals Grafana includes in the rendered value. If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. For example 1.1234 will display as 1.12 and 100.456 will display as 100. To display all decimals, set the unit to `string`. | -| min? | number. The minimum value used in percentage threshold calculations. Leave empty for auto calculation based on all series and fields. | -| max? | number. The maximum value used in percentage threshold calculations. Leave empty for auto calculation based on all series and fields. | -| mappings? | `[...ValueMapping]`. Convert input values into a display string. Options are: [`ValueMap`](#valuemap), [`RangeMap`](#rangemap), [`RegexMap`](#rangemap), [`SpecialValueMap`](#specialvaluemap). | -| thresholds? | `ThresholdsConfig`. Map numeric values to states. Consists of:
  • `mode` - `ThresholdsMode`. Options are: `absolute` and `percentage`.
  • `steps` - `[...Threshold]`
| -| color? | [`FieldColor`](#fieldcolor). Panel color configuration. | -| links? | `[...]`. The behavior when clicking a result. | -| noValue? | string. Alternative to an empty string. | -| custom? | `{...}`. Specified by the `FieldConfig` field in panel plugin schemas. | - - - -###### `ValueMap` - -Maps text values to a color or different display text and color. -For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. - - - -| Name | Usage | -| ------- | -------- | -| type | `MappingType` & "value". `MappingType` options are: `value`, `range`, `regex`, and `special`. | -| options | string. [`ValueMappingResult`](#valuemappingresult). Map with ``: `ValueMappingResult`. For example: `{ "10": { text: "Perfection!", color: "green" } }`. | - - - -###### `RangeMap` - -Maps numerical ranges to a display text and color. -For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. - - - -| Name | Usage | -| ------- | ---------------------------------------------------------------------------------------------------- | -| type | `MappingType` & "range". `MappingType` options are: `value`, `range`, `regex`, and `special`. | -| options | Range to match against and the result to apply when the value is within the range. Spec:
  • `from` - `float64` or `null`. Min value of the range. It can be null which means `-Infinity`.
  • `to` - `float64` or `null`. Max value of the range. It can be null which means `+Infinity`.
  • `result` - [`ValueMappingResult`](#valuemappingresult) | - - - -###### `RegexMap` - -Maps regular expressions to replacement text and a color. -For example, if a value is `www.example.com`, you can configure a regex value mapping so that Grafana displays www and truncates the domain. - - - -| Name | Usage | -| ------- | --------------------------------------------------------------------------------------------- | -| type | `MappingType` & "regex". `MappingType` options are: `value`, `range`, `regex`, and `special`. | -| options | Regular expression to match against and the result to apply when the value matches the regex. Spec:
    • `pattern` - string. Regular expression to match against.
    • `result` - [`ValueMappingResult`](#valuemappingresult) | - - - -###### `SpecialValueMap` - -Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. -See `SpecialValueMatch` in the following table to see the list of special values. -For example, you can configure a special value mapping so that null values appear as N/A. - - - -| Name | Usage | -| ------- | ----------------------------------------------------------------------------------------------- | -| type | `MappingType` & "special". `MappingType` options are: `value`, `range`, `regex`, and `special`. | -| options | Spec:
      • `match` - `SpecialValueMatch`. Special value to match against. Types are:
        • true
        • false
        • null
        • nan
        • empty
      • `result` - [`ValueMappingResult`](#valuemappingresult) | - - - -###### `ValueMappingResult` - -Result used as replacement with text and color when the value matches. - -| Name | Usage | -| ----- | ----------------------------------------------------------------------------- | -| text | string. Text to display when the value matches. | -| color | string. Color to use when the value matches. | -| icon | string. Icon to display when the value matches. Only specific visualizations. | -| index | int32. Position in the mapping array. Only used internally. | - -###### `FieldColor` - -Map a field to a color. - - - -| Name | Usage | -| ----------- | -------------------------------------------------------------------- | -| mode | [`FieldColorModeId`](#fieldcolormodeid). The main color scheme mode. | -| FixedColor? | string. The fixed color value for fixed or shades color modes. | -| seriesBy? | `FieldColorSeriesByMode`. Some visualizations need to know how to assign a series color from by value color schemes. Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. Options are: `min`, `max`, and `last`. | - - - -###### `FieldColorModeId` - -Color mode for a field. -You can specify a single color, or select a continuous (gradient) color schemes, based on a value. -Continuous color interpolates a color using the percentage of a value relative to min and max. -Accepted values are: - - - -| Name | Description | -| --- | ---- | -| thresholds | From thresholds. Informs Grafana to take the color from the matching threshold. | -| palette-classic | Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for graphs and pie charts and other categorical data visualizations. | -| palette-classic-by-name | Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations | -| continuous-GrYlRd | Continuous Green-Yellow-Red palette mode | -| continuous-RdYlGr | Continuous Red-Yellow-Green palette mode | -| continuous-BlYlRd | Continuous Blue-Yellow-Red palette mode | -| continuous-YlRd | Continuous Yellow-Red palette mode | -| continuous-BlPu | Continuous Blue-Purple palette mode | -| continuous-YlBl | Continuous Yellow-Blue palette mode | -| continuous-blues | Continuous Blue palette mode | -| continuous-reds | Continuous Red palette mode | -| continuous-greens | Continuous Green palette mode | -| continuous-purples | Continuous Purple palette mode | -| shades | Shades of a single color. Specify a single color, useful in an override rule. | -| fixed | Fixed color mode. Specify a single color, useful in an override rule. | - - diff --git a/docs/sources/as-code/observability-as-code/schema-v2/timesettings-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/timesettings-schema.md deleted file mode 100644 index 8db14212740..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/timesettings-schema.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -description: A reference for the JSON timesettings schema used with Observability as Code. -keywords: - - configuration - - as code - - as-code - - dashboards - - git integration - - git sync - - github - - time settings -labels: - products: - - cloud - - enterprise - - oss -menuTitle: timesettings schema -title: timesettings -weight: 600 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/timesettings-schema/ -aliases: - - ../../../observability-as-code/schema-v2/timesettings-schema/ # /docs/grafana/next/observability-as-code/schema-v2/timesettings-schema/ ---- - -# `timeSettings` - -The `TimeSettingsSpec` defines the default time configuration for the time picker and the refresh picker for the specific dashboard. - -Following is the JSON for default time settings: - -```json - "timeSettings": { - "autoRefresh": "", - "autoRefreshIntervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "fiscalYearStartMonth": 0, - "from": "now-6h", - "hideTimepicker": false, - "timezone": "browser", - "to": "now" - }, -``` - -`timeSettings` consists of: - -- [TimeSettingsSpec](#timesettingsspec) - -## `TimeSettingsSpec` - -The following table explains the usage of the time settings JSON fields: - - - -| Name | Usage | -| ---- | ----- | -| timezone? | string. Timezone of dashboard. Accepted values are IANA TZDB zone ID, `browser`, or `utc`. Default is `browser`. | -| from | string. Start time range for dashboard. Accepted values are relative time strings like `now-6h` or absolute time strings like `2020-07-10T08:00:00.000Z`. Default is `now-6h`. | -| to | string. End time range for dashboard. Accepted values are relative time strings like `now-6h` or absolute time strings like `2020-07-10T08:00:00.000Z`. Default is `now`. | -| autoRefresh | string. Refresh rate of dashboard. Represented by interval string. For example: `5s`, `1m`, `1h`, `1d`. No default. In schema v1: `refresh`. | -| autoRefreshIntervals | string. Interval options available in the refresh picker drop-down menu. The default array is `["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]`. | -|quickRanges? | Selectable options available in the time picker drop-down menu. Has no effect on provisioned dashboard. Defined in the [`TimeRangeOption`](#timerangeoption) spec. In schema v1: `timepicker.quick_ranges`, not exposed in the UI. | -| hideTimepicker | bool. Whether or not the time picker is visible. Default is `false`. In schema v1: `timepicker.hidden`. | -| weekStart? | Day when the week starts. Expressed by the name of the day in lowercase. For example: `monday`. Options are `saturday`, `monday`, and `sunday`. | -| fiscalYearStartMonth | The month that the fiscal year starts on. `0` = January, `11` = December | -| nowDelay? | string. Override the "now" time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. In schema v1: `timepicker.nowDelay`. | - - - -### `TimeRangeOption` - -The following table explains the usage of the time range option JSON fields: - -| Name | Usage | -| ------- | ---------------------------------- | -| display | string. Default is `Last 6 hours`. | -| from | string. Default is `now-6h`. | -| to | string. Default is `now`. | diff --git a/docs/sources/as-code/observability-as-code/schema-v2/variables-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/variables-schema.md deleted file mode 100644 index 549478692f1..00000000000 --- a/docs/sources/as-code/observability-as-code/schema-v2/variables-schema.md +++ /dev/null @@ -1,501 +0,0 @@ ---- -description: A reference for the JSON variables schema used with Observability as Code. -keywords: - - configuration - - as code - - as-code - - dashboards - - git integration - - git sync - - github - - variables -labels: - products: - - cloud - - enterprise - - oss -menuTitle: variables schema -title: variables -weight: 700 -canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/variables-schema/ -aliases: - - ../../../observability-as-code/schema-v2/variables-schema/ # /docs/grafana/next/observability-as-code/schema-v2/variables-schema/ ---- - -# `variables` - -The available variable types described in the following sections: - -- [QueryVariableKind](#queryvariablekind) -- [TextVariableKind](#textvariablekind) -- [ConstantVariableKind](#constantvariablekind) -- [DatasourceVariableKind](#datasourcevariablekind) -- [IntervalVariableKind](#intervalvariablekind) -- [CustomVariableKind](#customvariablekind) -- [SwitchVariableKind](#switchvariablekind) -- [GroupByVariableKind](#groupbyvariablekind) -- [AdhocVariableKind](#adhocvariablekind) - -## `QueryVariableKind` - -Following is the JSON for a default query variable: - -```json - "variables": [ - { - "kind": "QueryVariable", - "spec": { - "current": { - "text": "", - "value": "" - }, - "hide": "dontHide", - "includeAll": false, - "multi": false, - "name": "", - "options": [], - "query": defaultDataQueryKind(), - "refresh": "never", - "regex": "", - "skipUrlSync": false, - "sort": "disabled" - } - } - ] -``` - -`QueryVariableKind` consists of: - -- kind: "QueryVariable" -- spec: [QueryVariableSpec](#queryvariablespec) - -### `QueryVariableSpec` - -The following table explains the usage of the query variable JSON fields: - - - -| Name | Usage | -| ------------ | ---------------------------------------------- | -| name | string. Name of the variable. | -| current | "Text" and a "value" or [`VariableOption`](#variableoption) | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| refresh | `VariableRefresh`. Options are `never`, `onDashboardLoad`, and `onTimeChanged`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string | -| datasource? | [`DataSourceRef`](#datasourceref) | -| query | `DataQueryKind`. Consists of:
        • kind: string
        • spec: string
        | -| regex | string | -| sort | `VariableSort`. Options are:
        • disabled
        • alphabeticalAsc
        • alphabeticalDesc
        • numericalAsc
        • numericalDesc
        • alphabeticalCaseInsensitiveAsc
        • alphabeticalCaseInsensitiveDesc
        • naturalAsc
        • naturalDesc
        | -| definition? | string | -| options | [`VariableOption`](#variableoption) | -| multi | bool. Default is `false`. | -| includeAll | bool. Default is `false`. | -| allValue? | string | -| placeholder? | string | - - - -#### `VariableOption` - -| Name | Usage | -| -------- | -------------------------------------------- | -| selected | bool. Whether or not the option is selected. | -| text | string. Text to be displayed for the option. | -| value | string. Value of the option. | - -#### `DataSourceRef` - -| Name | Usage | -| ----- | ---------------------------------- | -| type? | string. The plugin type-id. | -| uid? | The specific data source instance. | - -## `TextVariableKind` - -Following is the JSON for a default text variable: - -```json - "variables": [ - { - "kind": "TextVariable", - "spec": { - "current": { - "text": "", - "value": "" - }, - "hide": "dontHide", - "name": "", - "query": "", - "skipUrlSync": false - } - } - ] -``` - -`TextVariableKind` consists of: - -- kind: TextVariableKind -- spec: [TextVariableSpec](#textvariablespec) - -### `TextVariableSpec` - -The following table explains the usage of the query variable JSON fields: - -| Name | Usage | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable. | -| current | "Text" and a "value" or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| query | string | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string | - -## `ConstantVariableKind` - -Following is the JSON for a default constant variable: - -```json - "variables": [ - { - "kind": "ConstantVariable", - "spec": { - "current": { - "text": "", - "value": "" - }, - "hide": "hideVariable", - "name": "", - "query": "", - "skipUrlSync": true - } - } - ] -``` - -`ConstantVariableKind` consists of: - -- kind: "ConstantVariable" -- spec: [ConstantVariableSpec](#constantvariablespec) - -### `ConstantVariableSpec` - -The following table explains the usage of the constant variable JSON fields: - -| Name | Usage | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable. | -| query | string | -| current | "Text" and a "value" or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string | - -## `DatasourceVariableKind` - -Following is the JSON for a default data source variable: - -```json - "variables": [ - { - "kind": "DatasourceVariable", - "spec": { - "current": { - "text": "", - "value": "" - }, - "hide": "dontHide", - "includeAll": false, - "multi": false, - "name": "", - "options": [], - "pluginId": "", - "refresh": "never", - "regex": "", - "skipUrlSync": false - } - } - ] -``` - -`DatasourceVariableKind` consists of: - -- kind: "DatasourceVariable" -- spec: [DatasourceVariableSpec](#datasourcevariablespec) - -### `DatasourceVariableSpec` - -The following table explains the usage of the data source variable JSON fields: - -| Name | Usage | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable. | -| pluginId | string | -| refresh | `VariableRefresh`. Options are `never`, `onDashboardLoad`, and `onTimeChanged`. | -| regex | string | -| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| multi | bool. Default is `false`. | -| includeAll | bool. Default is `false`. | -| allValue? | string | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string | - -## `IntervalVariableKind` - -Following is the JSON for a default interval variable: - -```json - "variables": [ - { - "kind": "IntervalVariable", - "spec": { - "auto": false, - "auto_count": 0, - "auto_min": "", - "current": { - "text": "", - "value": "" - }, - "hide": "dontHide", - "name": "", - "options": [], - "query": "", - "refresh": "never", - "skipUrlSync": false - } - } - ] -``` - -`IntervalVariableKind` consists of: - -- kind: "IntervalVariable" -- spec: [IntervalVariableSpec](#intervalvariablespec) - -### `IntervalVariableSpec` - -The following table explains the usage of the interval variable JSON fields: - -| Name | Usage | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable. | -| query | string | -| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| auto | bool. Default is `false`. | -| auto_count | integer. Default is `0`. | -| refresh | `VariableRefresh`. Options are `never`, `onDashboardLoad`, and `onTimeChanged`. | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false` | -| description? | string | - -## `CustomVariableKind` - -Following is the JSON for a default custom variable: - -```json - "variables": [ - { - "kind": "CustomVariable", - "spec": { - "current": defaultVariableOption(), - "hide": "dontHide", - "includeAll": false, - "multi": false, - "name": "", - "options": [], - "query": "", - "skipUrlSync": false - } - } - ] -``` - -`CustomVariableKind` consists of: - -- kind: "CustomVariable" -- spec: [CustomVariableSpec](#customvariablespec) - -### `CustomVariableSpec` - -The following table explains the usage of the custom variable JSON fields: - -| Name | Usage | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable. | -| query | string | -| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| multi | bool. Default is `false`. | -| includeAll | bool. Default is `false`. | -| allValue? | string | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string | - -## `SwitchVariableKind` - -Following is the JSON for a default switch variable: - -```json - "variables": [ - { - "kind": "SwitchVariable", - "spec": { - "current": "false", - "enabledValue": "true", - "disabledValue": "false", - "hide": "dontHide", - "name": "", - "skipUrlSync": false - } - } - ] -``` - -`SwitchVariableKind` consists of: - -- kind: "SwitchVariable" -- spec: [SwitchVariableSpec](#switchvariablespec) - -### `SwitchVariableSpec` - -The following table explains the usage of the switch variable JSON fields: - - - -| Name | Usage | -| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable. | -| current | string. Current value of the switch variable (either `enabledValue` or `disabledValue`). | -| enabledValue | string. Value when the switch is in the enabled state. | -| disabledValue | string. Value when the switch is in the disabled state. | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string | - - - -## `GroupByVariableKind` - -Following is the JSON for a default group by variable: - -```json - "variables": [ - { - "kind": "GroupByVariable", - "spec": { - "current": { - "text": [ - "" - ], - "value": [ - "" - ] - }, - "datasource": {}, - "hide": "dontHide", - "multi": false, - "name": "", - "options": [], - "skipUrlSync": false - } - } - ] -``` - -`GroupByVariableKind` consists of: - -- kind: "GroupByVariable" -- spec: [GroupByVariableSpec](#groupbyvariablespec) - -### `GroupByVariableSpec` - -The following table explains the usage of the group by variable JSON fields: - -| Name | Usage | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable | -| datasource? | `DataSourceRef`. Refer to the [`DataSourceRef` definition](#datasourceref) under `QueryVariableKind`. | -| current | `Text` and a `value` or `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| options | `VariableOption`. Refer to the [`VariableOption` definition](#variableoption) under `QueryVariableKind`. | -| multi | bool. Default is `false`. | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string. | - -## `AdhocVariableKind` - -Following is the JSON for a default ad hoc variable: - -```json - "variables": [ - { - "kind": "AdhocVariable", - "spec": { - "baseFilters": [], - "defaultKeys": [], - "filters": [], - "hide": "dontHide", - "name": "", - "skipUrlSync": false - } - } - ] -``` - -`AdhocVariableKind` consists of: - -- kind: "AdhocVariable" -- spec: [AdhocVariableSpec](#adhocvariablespec) - -### `AdhocVariableSpec` - -The following table explains the usage of the ad hoc variable JSON fields: - -| Name | Usage | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string. Name of the variable. | -| datasource? | `DataSourceRef`. Consists of:
        • type? - string. The plugin type-id.
        • uid? - string. The specific data source instance.
        | -| baseFilters | [AdHocFilterWithLabels](#adhocfilterswithlabels) | -| filters | [AdHocFilterWithLabels](#adhocfilterswithlabels) | -| defaultKeys | [MetricFindValue](#metricfindvalue) | -| label? | string | -| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | -| skipUrlSync | bool. Default is `false`. | -| description? | string | - -#### `AdHocFiltersWithLabels` - -The following table explains the usage of the ad hoc variable with labels JSON fields: - -| Name | Type | -| ------------ | ------------- | -| key | string | -| operator | string | -| value | string | -| values? | `[...string]` | -| keyLabel | string | -| valueLabels? | `[...string]` | -| forceEdit? | bool | - -#### `MetricFindValue` - -The following table explains the usage of the metric find value JSON fields: - -| Name | Type | -| ----------- | ---------------- | -| text | string | -| value? | string or number | -| group? | string | -| expandable? | bool | diff --git a/docs/sources/datasources/google-cloud-monitoring/_index.md b/docs/sources/datasources/google-cloud-monitoring/_index.md index 27bc7b390cd..d5412c33ef7 100644 --- a/docs/sources/datasources/google-cloud-monitoring/_index.md +++ b/docs/sources/datasources/google-cloud-monitoring/_index.md @@ -103,10 +103,11 @@ To configure basic settings for the data source, complete the following steps: 1. Set the data source's basic configuration options: - | Name | Description | - | ----------- | ------------------------------------------------------------------------ | - | **Name** | Sets the name you use to refer to the data source in panels and queries. | - | **Default** | Sets whether the data source is pre-selected for new panels. | + | Name | Description | + | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | **Name** | Sets the name you use to refer to the data source in panels and queries. | + | **Default** | Sets whether the data source is pre-selected for new panels. | + | **Universe Domain** | The universe domain to connect to. For more information, refer to [Documentation on universe domains](https://docs.cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.service_monitoring_service.ServiceMonitoringServiceAsyncClient#google_cloud_monitoring_v3_services_service_monitoring_service_ServiceMonitoringServiceAsyncClient_universe_domain). Defaults to `googleapis.com`. | ### Provision the data source @@ -129,6 +130,7 @@ datasources: clientEmail: stackdriver@myproject.iam.gserviceaccount.com authenticationType: jwt defaultProject: my-project-name + universeDomain: googleapis.com secureJsonData: privateKey: | -----BEGIN PRIVATE KEY----- @@ -152,6 +154,7 @@ datasources: clientEmail: stackdriver@myproject.iam.gserviceaccount.com authenticationType: jwt defaultProject: my-project-name + universeDomain: googleapis.com privateKeyPath: /etc/secrets/gce.pem ``` @@ -166,6 +169,7 @@ datasources: access: proxy jsonData: authenticationType: gce + universeDomain: googleapis.com ``` ## Import pre-configured dashboards diff --git a/docs/sources/developer-resources/api-reference/http-api/dashboard_versions.md b/docs/sources/developer-resources/api-reference/http-api/dashboard_versions.md index f4b46adad3f..370b1fed7e4 100644 --- a/docs/sources/developer-resources/api-reference/http-api/dashboard_versions.md +++ b/docs/sources/developer-resources/api-reference/http-api/dashboard_versions.md @@ -171,146 +171,3 @@ Status Codes: - **200** - Ok - **401** - Unauthorized - **404** - Dashboard version not found - -## Restore dashboard by dashboard UID - -`POST /api/dashboards/uid/:uid/restore` - -Restores a dashboard to a given dashboard version using `uid`. - -**Example request for restoring a dashboard version**: - -```http -POST /api/dashboards/uid/QA7wKklGz/restore -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "version": 1 -} -``` - -JSON body schema: - -- **version** - The dashboard version to restore to - -**Example response**: - -```http -HTTP/1.1 200 OK -Content-Type: application/json; charset=UTF-8 -Content-Length: 67 - -{ - "id": 70, - "slug": "my-dashboard", - "status": "success", - "uid": "QA7wKklGz", - "url": "/d/QA7wKklGz/my-dashboard", - "version": 3 -} -``` - -JSON response body schema: - -- **slug** - the URL friendly slug of the dashboard's title -- **status** - whether the restoration was successful or not -- **version** - the new dashboard version, following the restoration - -Status codes: - -- **200** - OK -- **400** - Bad request (specified version has the same content as the current dashboard) -- **401** - Unauthorized -- **404** - Not found (dashboard not found or dashboard version not found) -- **500** - Internal server error (indicates issue retrieving dashboard tags from database) - -**Example error response** - -```http -HTTP/1.1 404 Not Found -Content-Type: application/json; charset=UTF-8 -Content-Length: 46 - -{ - "message": "Dashboard version not found" -} -``` - -JSON response body schema: - -- **message** - Message explaining the reason for the request failure. - -## Compare dashboard versions - -`POST /api/dashboards/calculate-diff` - -Compares two dashboard versions by calculating the JSON diff of them. - -**Example request**: - -```http -POST /api/dashboards/calculate-diff HTTP/1.1 -Accept: text/html -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "base": { - "dashboardId": 1, - "version": 1 - }, - "new": { - "dashboardId": 1, - "version": 2 - }, - "diffType": "json" -} -``` - -JSON body schema: - -- **base** - an object representing the base dashboard version -- **new** - an object representing the new dashboard version -- **diffType** - the type of diff to return. Can be "json" or "basic". - -**Example response (JSON diff)**: - -```http -HTTP/1.1 200 OK -Content-Type: text/html; charset=UTF-8 - -

        - -

        -``` - -The response is a textual representation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab. - -Status Codes: - -- **200** - Ok -- **400** - Bad request (invalid JSON sent) -- **401** - Unauthorized -- **404** - Not found - -**Example response (basic diff)**: - -```http -HTTP/1.1 200 OK -Content-Type: text/html; charset=UTF-8 - -
        - -
        -``` - -The response here is a summary of the changes, derived from the diff between the two JSON objects. - -Status Codes: - -- **200** - OK -- **400** - Bad request (invalid JSON sent) -- **401** - Unauthorized -- **404** - Not found diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md index 5500601829d..0fa0e059472 100644 --- a/docs/sources/introduction/grafana-enterprise.md +++ b/docs/sources/introduction/grafana-enterprise.md @@ -87,6 +87,7 @@ With a Grafana Enterprise license, you also get access to premium data sources, - [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource) - [Databricks](/grafana/plugins/grafana-databricks-datasource) - [DataDog](/grafana/plugins/grafana-datadog-datasource) +- [IBM Db2](/grafana/plugins/grafana-ibmdb2-datasource) - [Drone](/grafana/plugins/grafana-drone-datasource) - [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/) - [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource) diff --git a/docs/sources/visualizations/dashboards/build-dashboards/view-dashboard-json-model/index.md b/docs/sources/visualizations/dashboards/build-dashboards/view-dashboard-json-model/index.md index 7720a4c946a..ce6095d88e6 100644 --- a/docs/sources/visualizations/dashboards/build-dashboards/view-dashboard-json-model/index.md +++ b/docs/sources/visualizations/dashboards/build-dashboards/view-dashboard-json-model/index.md @@ -3,45 +3,75 @@ aliases: - ../../../reference/dashboard/ # /docs/grafana/next/reference/dashboard/ - ../../../dashboards/json-model/ # /docs/grafana/next/dashboards/json-model/ - ../../../dashboards/build-dashboards/view-dashboard-json-model/ # /docs/grafana/next/dashboards/build-dashboards/view-dashboard-json-model/ + - ../../../as-code/observability-as-code/schema-v2/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/ + - ../../../as-code/observability-as-code/schema-v2/annotations-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/annotations-schema/ + - ../../../as-code/observability-as-code/schema-v2/panel-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/panel-schema/ + - ../../../as-code/observability-as-code/schema-v2/librarypanel-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/librarypanel-schema/ + - ../../../as-code/observability-as-code/schema-v2/layout-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/layout-schema/ + - ../../../as-code/observability-as-code/schema-v2/links-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/links-schema/ + - ../../../as-code/observability-as-code/schema-v2/timesettings-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/timesettings-schema/ + - ../../../as-code/observability-as-code/schema-v2/variables-schema/ # /docs/grafana/latest/as-code/observability-as-code/schema-v2/variables-schema/ + - ../../../observability-as-code/schema-v2/ # /docs/grafana/latest/observability-as-code/schema-v2/ + - ../../../../next/observability-as-code/schema-v2/annotations-schema/ # /docs/grafana/next/observability-as-code/schema-v2/annotations-schema/ + - ../../../../next/observability-as-code/schema-v2/panel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/panel-schema/ + - ../../../../next/observability-as-code/schema-v2/librarypanel-schema/ # /docs/grafana/next/observability-as-code/schema-v2/librarypanel-schema/ + - ../../../../next/observability-as-code/schema-v2/layout-schema/ # /docs/grafana/next/observability-as-code/schema-v2/layout-schema/ + - ../../../../next/observability-as-code/schema-v2/links-schema/ # /docs/grafana/next/observability-as-code/schema-v2/links-schema/ + - ../../../../next/observability-as-code/schema-v2/timesettings-schema/ # /docs/grafana/next/observability-as-code/schema-v2/timesettings-schema/ + - ../../../../next/observability-as-code/schema-v2/variables-schema/ # /docs/grafana/next/observability-as-code/schema-v2/variables-schema/ keywords: - grafana - dashboard - documentation - json - model + - schema v2 + - v1 resource + - v2 resource + - classic labels: products: - cloud - enterprise - oss title: JSON model -description: View your Grafana dashboard JSON object +description: View and update your Grafana dashboard JSON object weight: 700 -refs: - annotations: - - pattern: /docs/grafana/ - destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/ --- # Dashboard JSON model -A dashboard in Grafana is represented by a JSON object, which stores metadata of its dashboard. Dashboard metadata includes dashboard properties, metadata from panels, template variables, panel queries, etc. +Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings. -To view the JSON of a dashboard: +## Different dashboard schema models -1. Click **Edit** in the top-right corner of the dashboard. -1. Click **Settings**. -1. Go to the **JSON Model** tab. -1. When you've finished viewing the JSON, click **Back to dashboard** and **Exit edit**. +There are currently three dashboard JSON schema models: -## JSON fields - -When a user creates a new dashboard, a new dashboard JSON object is initialized with the following fields: +- [Classic](#classic-model) - A non-Kubernetes resource used before the adoption of the Kubernetes API by Grafana in v12.2.0. It's been widely used for exporting, importing, and sharing dashboards in the Grafana dashboards collection at [grafana.com/dashboards](https://grafana.com/grafana/dashboards/). +- [V1 Resource](#v1-resource-model) - The Classic dashboard schema formatted as a Kubernetes-style resource. Its `spec` property contains the Classic model of the schema. This is the default format for API communication after Grafana v12.2.0, which enabled the Kubernetes Platform API as default backend for Grafana dashboards. Dashboards created using the Classic model can be exported using either the Classic or the V1 Resource format. +- [V2 Resource](#v2-resource-model) - The latest format, supporting new features such as advanced layouts and conditional rendering. It models all dashboard elements as Kubernetes kinds, following Kubernetes conventions for declaring dashboard components. This format is future-proof and represents the evolving standard for dashboards. {{< admonition type="note" >}} -In the following JSON, id is shown as null which is the default value assigned to it until a dashboard is saved. Once a dashboard is saved, an integer value is assigned to the `id` field. +[Observability as Code](https://grafana.com/docs/grafana/latest/as-code/observability-as-code/) works with all versions of the JSON model, and it's fully compatible with version 2. +{{< /admonition >}} + +## Access and update the JSON model (#view-json) + +To access the JSON representation of a dashboard: + +1. Click **Edit** in the top-right corner of the dashboard. +1. Click the gear icon in the right sidebar and click **Settings** in the secondary sidebar. +1. Select the **JSON Model** tab. +1. Update the JSON structure as needed. +1. Click **Save changes**. + +## Classic model + +When you create a new dashboard in self-managed Grafana, a new dashboard JSON object was initialized with the following fields: + +{{< admonition type="note" >}} +In the following JSON, id is shown as null which is the default value assigned to it until a dashboard is saved. +After a dashboard is saved, an integer value is assigned to the `id` field. {{< /admonition >}} ```json @@ -76,26 +106,30 @@ In the following JSON, id is shown as null which is the default value assigned t Each field in the dashboard JSON is explained below with its usage: -| Name | Usage | -| ----------------- | ----------------------------------------------------------------------------------------------------------------- | -| **id** | unique numeric identifier for the dashboard. (generated by the db) | -| **uid** | unique dashboard identifier that can be generated by anyone. string (8-40) | -| **title** | current title of dashboard | -| **tags** | tags associated with dashboard, an array of strings | -| **style** | theme of dashboard, i.e. `dark` or `light` | -| **timezone** | timezone of dashboard, i.e. `utc` or `browser` | -| **editable** | whether a dashboard is editable or not | -| **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip | -| **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc | -| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details | -| **templating** | templating metadata, see [templating section](#templating) for details | -| **annotations** | annotations metadata, see [annotations](ref:annotations) for how to add them | -| **refresh** | auto-refresh interval | -| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema | -| **version** | version of the dashboard (integer), incremented each time the dashboard is updated | -| **panels** | panels array, see below for detail. | + -## Panels +| Name | Usage | +| ----------------- | ------------------------------------------------------------------------------------------ | +| **id** | unique numeric identifier for the dashboard. (generated by the db) | +| **uid** | unique dashboard identifier that can be generated by anyone. string (8-40) | +| **title** | current title of dashboard | +| **tags** | tags associated with dashboard, an array of strings | +| **style** | theme of dashboard, i.e. `dark` or `light` | +| **timezone** | timezone of dashboard, i.e. `utc` or `browser` | +| **editable** | whether a dashboard is editable or not | +| **graphTooltip** | 0 for no shared crosshair or tooltip (default), 1 for shared crosshair, 2 for shared crosshair AND shared tooltip | +| **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc | +| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details | +| **templating** | templating metadata, see [templating section](#templating) for details | +| **annotations** | annotations metadata, see [annotations](https://grafana.com/docs/grafana//dashboards/build-dashboards/annotate-visualizations/) for how to add them | +| **refresh** | auto-refresh interval| +| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema | +| **version** | version of the dashboard (integer), incremented each time the dashboard is updated | +| **panels** | panels array, see below for detail. | + + + +### Panels Panels are the building blocks of a dashboard. It consists of data source queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depend on the panel type. Following is an example of panel JSON of a text panel. @@ -168,18 +202,22 @@ The grid has a negative gravity that moves panels up if there is empty space abo Usage of the fields is explained below: -| Name | Usage | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| **collapse** | whether timepicker is collapsed or not | -| **enable** | whether timepicker is enabled or not | -| **notice** | | -| **now** | | -| **hidden** | whether timepicker is hidden or not | -| **nowDelay** | override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. | -| **quick_ranges** | custom quick ranges | -| **refresh_intervals** | interval options available in the refresh picker dropdown | -| **status** | | -| **type** | | + + +| Name | Usage | +| --------------------- | --------------------------------------------------------- | +| **collapse** | whether timepicker is collapsed or not | +| **enable** | whether timepicker is enabled or not | +| **notice** | | +| **now** | | +| **hidden** | whether timepicker is hidden or not | +| **nowDelay** | override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. | +| **quick_ranges** | custom quick ranges | +| **refresh_intervals** | interval options available in the refresh picker dropdown | +| **status** | | +| **type** | | + + ### templating @@ -270,3 +308,82 @@ Usage of the above mentioned fields in the templating section is explained below | **refresh** | configures when to refresh a variable | | **regex** | extracts part of a series name or metric node segment | | **type** | type of variable, i.e. `custom`, `query` or `interval` | + +## V1 Resource model + +The V1 Resource schema model formats the [Classic JSON model](#classic-model) schema as a Kubernetes-style resource. +The `spec` property of the schema contains the Classic-style model of the schema. + +Dashboards created using the Classic model can be exported using either this model or the Classic one. + +The following code snippet shows the fields included in the V1 Resource model. + +```json +{ + "apiVersion": "dashboard.grafana.app/v1beta1", + "kind": "Dashboard", + "metadata": { + "name": "isnt5ss", + "namespace": "stacks-521104", + "uid": "92674c0e-0360-4bb4-99ab-fb150581376d", + "resourceVersion": "1764705030717045", + "generation": 1, + "creationTimestamp": "2025-12-02T19:50:30Z", + "labels": { + "grafana.app/deprecatedInternalID": "1329" + }, + "annotations": { + "grafana.app/createdBy": "user:u000000002", + "grafana.app/folder": "", + "grafana.app/saved-from-ui": "Grafana Cloud (instant)" + } + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1329, + "links": [], + "panels": [], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "Africa/Abidjan", + "title": "Graphite suggestions", + "uid": "isnt5ss", + "version": 1, + "weekStart": "" + }, + "status": {} +} +``` + +## V2 Resource model + +{{< docs/public-preview product="Dashboard JSON schema v2" >}} + +For the detailed V2 Resource model schema, refer to the [Swagger documentation](https://play.grafana.org/swagger?api=dashboard.grafana.app-v2beta1). diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f34e7c667e0..e3adb3c514e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3733,46 +3733,21 @@ "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 2 - } - }, "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/go.mod b/go.mod index fe3e62e3fde..ade26f2e7d1 100644 --- a/go.mod +++ b/go.mod @@ -44,8 +44,8 @@ require ( github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad - github.com/blevesearch/bleve/v2 v2.5.0 // @grafana/grafana-search-and-storage - github.com/blevesearch/bleve_index_api v1.2.7 // @grafana/grafana-search-and-storage + github.com/blevesearch/bleve/v2 v2.5.7 // @grafana/grafana-search-and-storage + github.com/blevesearch/bleve_index_api v1.3.0 // @grafana/grafana-search-and-storage github.com/blugelabs/bluge v0.2.2 // @grafana/grafana-backend-group github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // @grafana/grafana-backend-group @@ -365,22 +365,22 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect - github.com/blevesearch/geo v0.1.20 // indirect - github.com/blevesearch/go-faiss v1.0.25 // indirect + github.com/blevesearch/geo v0.2.4 // indirect + github.com/blevesearch/go-faiss v1.0.26 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.3.9 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect github.com/blevesearch/vellum v1.1.0 // indirect - github.com/blevesearch/zapx/v11 v11.4.1 // indirect - github.com/blevesearch/zapx/v12 v12.4.1 // indirect - github.com/blevesearch/zapx/v13 v13.4.1 // indirect - github.com/blevesearch/zapx/v14 v14.4.1 // indirect - github.com/blevesearch/zapx/v15 v15.4.1 // indirect - github.com/blevesearch/zapx/v16 v16.2.2 // indirect + github.com/blevesearch/zapx/v11 v11.4.2 // indirect + github.com/blevesearch/zapx/v12 v12.4.2 // indirect + github.com/blevesearch/zapx/v13 v13.4.2 // indirect + github.com/blevesearch/zapx/v14 v14.4.2 // indirect + github.com/blevesearch/zapx/v15 v15.4.2 // indirect + github.com/blevesearch/zapx/v16 v16.2.8 // indirect github.com/bluele/gcache v0.0.2 // indirect github.com/blugelabs/ice v1.0.0 // indirect github.com/blugelabs/ice/v2 v2.0.1 // indirect @@ -443,7 +443,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.1 // indirect diff --git a/go.sum b/go.sum index 67d95c625b6..f997af7c68e 100644 --- a/go.sum +++ b/go.sum @@ -931,14 +931,14 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4= -github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk= -github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY= -github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= -github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= -github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= -github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U= -github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= +github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= +github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E= +github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= +github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= +github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= +github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= @@ -947,8 +947,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+ github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o= -github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= @@ -960,18 +960,18 @@ github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+ github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= -github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA= -github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs= -github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ= -github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II= -github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE= -github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8= -github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE= -github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms= -github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g= -github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y= -github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk= -github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk= +github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= +github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= +github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= +github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= +github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= +github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI= +github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/blugelabs/bluge v0.2.2 h1:gat8CqE6P6tOgeX30XGLOVNTC26cpM2RWVcreXWtYcM= @@ -1442,8 +1442,6 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= -github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= diff --git a/go.work.sum b/go.work.sum index d064248a16a..6388285d2bf 100644 --- a/go.work.sum +++ b/go.work.sum @@ -520,14 +520,40 @@ github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= +github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= +github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= +github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s= +github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= +github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E= +github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= +github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= +github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI= github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= +github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc= github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A= github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc= github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc= +github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= +github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= +github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= +github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= +github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= +github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= +github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI= +github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= @@ -998,8 +1024,6 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ= github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= @@ -1092,6 +1116,7 @@ github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= diff --git a/jest.config.js b/jest.config.js index 17a2ce9ca32..f9d431cf5d3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -82,6 +82,7 @@ module.exports = { // Decoupled plugins run their own tests so ignoring them here. '/public/app/plugins/datasource/azuremonitor', '/public/app/plugins/datasource/cloud-monitoring', + '/public/app/plugins/datasource/elasticsearch', '/public/app/plugins/datasource/grafana-postgresql-datasource', '/public/app/plugins/datasource/grafana-pyroscope-datasource', '/public/app/plugins/datasource/grafana-testdata-datasource', diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index 4ceed793cad..0fbbb9cddc9 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -727,17 +727,6 @@ const injectedRtkApi = api }), invalidatesTags: ['dashboards', 'permissions'], }), - restoreDashboardVersionByUid: build.mutation< - RestoreDashboardVersionByUidApiResponse, - RestoreDashboardVersionByUidApiArg - >({ - query: (queryArg) => ({ - url: `/dashboards/uid/${queryArg.uid}/restore`, - method: 'POST', - body: queryArg.restoreDashboardVersionCommand, - }), - invalidatesTags: ['dashboards', 'versions'], - }), getDashboardVersionsByUid: build.query({ query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}/versions`, @@ -2628,26 +2617,6 @@ export type UpdateDashboardPermissionsByUidApiArg = { uid: string; updateDashboardAclCommand: UpdateDashboardAclCommand; }; -export type RestoreDashboardVersionByUidApiResponse = /** status 200 (empty) */ { - /** FolderUID The unique identifier (uid) of the folder the dashboard belongs to. */ - folderUid?: string; - /** ID The unique identifier (id) of the created/updated dashboard. */ - id: number; - /** Status status of the response. */ - status: string; - /** Slug The slug of the dashboard. */ - title: string; - /** UID The unique identifier (uid) of the created/updated dashboard. */ - uid: string; - /** URL The relative URL for accessing the created/updated dashboard. */ - url: string; - /** Version The version of the dashboard. */ - version: number; -}; -export type RestoreDashboardVersionByUidApiArg = { - uid: string; - restoreDashboardVersionCommand: RestoreDashboardVersionCommand; -}; export type GetDashboardVersionsByUidApiResponse = /** status 200 (empty) */ DashboardVersionResponseMeta; export type GetDashboardVersionsByUidApiArg = { uid: string; @@ -4568,9 +4537,6 @@ export type DashboardAclUpdateItem = { export type UpdateDashboardAclCommand = { items?: DashboardAclUpdateItem[]; }; -export type RestoreDashboardVersionCommand = { - version?: number; -}; export type DashboardVersionMeta = { created?: string; createdBy?: string; @@ -6633,7 +6599,6 @@ export const { useGetDashboardPermissionsListByUidQuery, useLazyGetDashboardPermissionsListByUidQuery, useUpdateDashboardPermissionsByUidMutation, - useRestoreDashboardVersionByUidMutation, useGetDashboardVersionsByUidQuery, useLazyGetDashboardVersionsByUidQuery, useGetDashboardVersionByUidQuery, diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 384666ea7f8..60db9295fb4 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -35,6 +35,14 @@ }, "./test": { "@grafana-app/source": "./test/index.ts" + }, + "./themes/schema.generated.json": { + "@grafana-app/source": "./src/themes/schema.generated.json", + "default": "./dist/esm/themes/schema.generated.json" + }, + "./themes/definitions/*.json": { + "@grafana-app/source": "./src/themes/themeDefinitions/*.json", + "default": "./dist/esm/themes/themeDefinitions/*.json" } }, "publishConfig": { @@ -52,7 +60,7 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json", - "themes-schema": "tsx ./src/themes/scripts/generateSchema.ts" + "themes-schema": "tsx ./scripts/generateSchema.ts" }, "dependencies": { "@braintree/sanitize-url": "7.0.1", @@ -102,6 +110,7 @@ "react-dom": "18.3.1", "rimraf": "6.0.1", "rollup": "^4.22.4", + "rollup-plugin-copy": "3.5.0", "rollup-plugin-esbuild": "6.2.1", "rollup-plugin-node-externals": "^8.0.0", "tsx": "^4.21.0", diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index 0c40d731724..50af331c37c 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -1,21 +1,40 @@ import json from '@rollup/plugin-json'; import { createRequire } from 'node:module'; +import copy from 'rollup-plugin-copy'; import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const grafanaDataPlugins = [ + ...plugins, + copy({ + targets: [ + { + src: 'src/themes/schema.generated.json', + dest: 'dist/esm/', + }, + { + src: 'src/themes/themeDefinitions/*.json', + dest: 'dist/esm/', + }, + ], + flatten: false, + }), + json(), +]; + export default [ { input: entryPoint, - plugins: [...plugins, json()], + plugins: grafanaDataPlugins, output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')], treeshake: false, }, { input: 'src/unstable.ts', - plugins: [...plugins, json()], + plugins: grafanaDataPlugins, output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')], treeshake: false, }, diff --git a/packages/grafana-data/scripts/generateSchema.ts b/packages/grafana-data/scripts/generateSchema.ts new file mode 100644 index 00000000000..f461999376e --- /dev/null +++ b/packages/grafana-data/scripts/generateSchema.ts @@ -0,0 +1,22 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { NewThemeOptionsSchema } from '../src/themes/createTheme'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const jsonOut = path.join(__dirname, '..', 'src', 'themes', 'schema.generated.json'); + +fs.writeFileSync( + jsonOut, + JSON.stringify( + NewThemeOptionsSchema.toJSONSchema({ + target: 'draft-07', + }), + undefined, + 2 + ) +); + +console.log('Successfully generated theme schema'); diff --git a/packages/grafana-data/src/internal/index.ts b/packages/grafana-data/src/internal/index.ts index 1b1e3c64a7d..230cdd2cbf9 100644 --- a/packages/grafana-data/src/internal/index.ts +++ b/packages/grafana-data/src/internal/index.ts @@ -93,7 +93,6 @@ export { DataTransformerID } from '../transformations/transformers/ids'; export { mergeTransformer } from '../transformations/transformers/merge'; export { getThemeById } from '../themes/registry'; -export * as experimentalThemeDefinitions from '../themes/themeDefinitions'; export { GrafanaEdition } from '../types/config'; export { SIPrefix } from '../valueFormats/symbolFormatters'; diff --git a/packages/grafana-data/src/themes/registry.ts b/packages/grafana-data/src/themes/registry.ts index 4fca3c5d7be..cfa4a10c6e4 100644 --- a/packages/grafana-data/src/themes/registry.ts +++ b/packages/grafana-data/src/themes/registry.ts @@ -1,7 +1,18 @@ import { Registry, RegistryItem } from '../utils/Registry'; import { createTheme, NewThemeOptionsSchema } from './createTheme'; -import * as extraThemes from './themeDefinitions'; +import aubergine from './themeDefinitions/aubergine.json'; +import debug from './themeDefinitions/debug.json'; +import desertbloom from './themeDefinitions/desertbloom.json'; +import gildedgrove from './themeDefinitions/gildedgrove.json'; +import gloom from './themeDefinitions/gloom.json'; +import mars from './themeDefinitions/mars.json'; +import matrix from './themeDefinitions/matrix.json'; +import sapphiredusk from './themeDefinitions/sapphiredusk.json'; +import synthwave from './themeDefinitions/synthwave.json'; +import tron from './themeDefinitions/tron.json'; +import victorian from './themeDefinitions/victorian.json'; +import zen from './themeDefinitions/zen.json'; import { GrafanaTheme2 } from './types'; export interface ThemeRegistryItem extends RegistryItem { @@ -9,6 +20,21 @@ export interface ThemeRegistryItem extends RegistryItem { build: () => GrafanaTheme2; } +const extraThemes: { [key: string]: unknown } = { + aubergine, + debug, + desertbloom, + gildedgrove, + gloom, + mars, + matrix, + sapphiredusk, + synthwave, + tron, + victorian, + zen, +}; + /** * @internal * Only for internal use, never use this from a plugin diff --git a/packages/grafana-data/src/themes/scripts/generateSchema.ts b/packages/grafana-data/src/themes/scripts/generateSchema.ts deleted file mode 100644 index 09369f5e67f..00000000000 --- a/packages/grafana-data/src/themes/scripts/generateSchema.ts +++ /dev/null @@ -1,19 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -import { NewThemeOptionsSchema } from '../createTheme'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -fs.writeFileSync( - path.join(__dirname, '../schema.generated.json'), - JSON.stringify( - NewThemeOptionsSchema.toJSONSchema({ - target: 'draft-07', - }), - undefined, - 2 - ) -); diff --git a/packages/grafana-data/src/themes/themeDefinitions/index.ts b/packages/grafana-data/src/themes/themeDefinitions/index.ts deleted file mode 100644 index b4270192032..00000000000 --- a/packages/grafana-data/src/themes/themeDefinitions/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { default as aubergine } from './aubergine.json'; -export { default as debug } from './debug.json'; -export { default as desertbloom } from './desertbloom.json'; -export { default as gildedgrove } from './gildedgrove.json'; -export { default as mars } from './mars.json'; -export { default as matrix } from './matrix.json'; -export { default as sapphiredusk } from './sapphiredusk.json'; -export { default as synthwave } from './synthwave.json'; -export { default as tron } from './tron.json'; -export { default as victorian } from './victorian.json'; -export { default as zen } from './zen.json'; -export { default as gloom } from './gloom.json'; diff --git a/packages/grafana-data/src/unstable.ts b/packages/grafana-data/src/unstable.ts index 3200085428a..43c2ff3071f 100644 --- a/packages/grafana-data/src/unstable.ts +++ b/packages/grafana-data/src/unstable.ts @@ -9,4 +9,4 @@ * and be subject to the standard policies */ -export { default as themeJsonSchema } from './themes/schema.generated.json'; +export {}; diff --git a/packages/grafana-data/tsconfig.json b/packages/grafana-data/tsconfig.json index 8e6013e32d9..3513caf9127 100644 --- a/packages/grafana-data/tsconfig.json +++ b/packages/grafana-data/tsconfig.json @@ -8,7 +8,8 @@ "emitDeclarationOnly": true, "isolatedModules": true, "rootDirs": ["."], - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "resolveJsonModule": true }, "exclude": ["dist/**/*"], "include": [ diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index 8d06591b46b..1627b2dc29b 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.4.0-pre"; +export const pluginVersion = "%VERSION%"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts index c0f8481a7f5..daead8f5295 100644 --- a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts @@ -29,11 +29,14 @@ export interface Options extends common.SingleStatBaseOptions { barWidthFactor: number; effects: GaugePanelEffects; endpointMarker?: ('point' | 'glow' | 'none'); + minVizHeight: number; + minVizWidth: number; segmentCount: number; segmentSpacing: number; shape: ('circle' | 'gauge'); showThresholdLabels: boolean; showThresholdMarkers: boolean; + sizing: common.BarGaugeSizing; sparkline?: boolean; textMode?: ('auto' | 'value_and_name' | 'value' | 'name' | 'none'); } @@ -43,11 +46,14 @@ export const defaultOptions: Partial = { barWidthFactor: 0.5, effects: {}, endpointMarker: 'point', + minVizHeight: 75, + minVizWidth: 75, segmentCount: 1, segmentSpacing: 0.3, shape: 'gauge', showThresholdLabels: false, showThresholdMarkers: true, + sizing: common.BarGaugeSizing.Auto, sparkline: true, textMode: 'auto', }; diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a560ff47c5d..2e3b955a247 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -795,6 +795,10 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R // swagger:route POST /dashboards/uid/{uid}/restore dashboards versions restoreDashboardVersionByUID // // Restore a dashboard to a given dashboard version using UID. +// This API will be removed when /apis/dashboards.grafana.app/v1 is released. +// You can restore a dashboard by reading it from history, then creating it again. +// +// Deprecated: true // // Responses: // 200: postDashboardResponse diff --git a/pkg/services/updatemanager/plugins.go b/pkg/services/updatemanager/plugins.go index 7ee11b261f9..815e9e67779 100644 --- a/pkg/services/updatemanager/plugins.go +++ b/pkg/services/updatemanager/plugins.go @@ -13,6 +13,8 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/open-feature/go-sdk/openfeature" "go.opentelemetry.io/otel/codes" @@ -47,6 +49,7 @@ type PluginsService struct { updateStrategy string features featuremgmt.FeatureToggles + cfg *setting.Cfg } func ProvidePluginsService(cfg *setting.Cfg, @@ -89,6 +92,7 @@ func ProvidePluginsService(cfg *setting.Cfg, features: features, updateChecker: updateChecker, updateStrategy: cfg.PluginUpdateStrategy, + cfg: cfg, }, nil } @@ -136,7 +140,7 @@ func (s *PluginsService) HasUpdate(ctx context.Context, pluginID string) (string // checkAndUpdate checks for updates and applies them if auto-update is enabled. func (s *PluginsService) checkAndUpdate(ctx context.Context) { s.instrumentedCheckForUpdates(ctx) - if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) { + if s.checkFlagPluginsAutoUpdate(ctx) { s.updateAll(ctx) } } @@ -218,6 +222,17 @@ func (s *PluginsService) checkForUpdates(ctx context.Context) error { return nil } +func (s *PluginsService) checkFlagPluginsAutoUpdate(ctx context.Context) bool { + ns := request.GetNamespaceMapper(s.cfg)(1) + ctx = identity.WithServiceIdentityForSingleNamespaceContext(ctx, ns) + flag, err := openfeature.NewDefaultClient().BooleanValueDetails(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) + if err != nil { + s.log.Error("flag evaluation error", "flag", featuremgmt.FlagPluginsAutoUpdate, "error", err) + } + + return flag.Value +} + func (s *PluginsService) canUpdate(ctx context.Context, plugin pluginstore.Plugin, gcomVersion string) bool { if !s.updateChecker.IsUpdatable(ctx, plugin) { return false @@ -227,7 +242,7 @@ func (s *PluginsService) canUpdate(ctx context.Context, plugin pluginstore.Plugi return false } - if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) { + if s.checkFlagPluginsAutoUpdate(ctx) { return s.updateChecker.CanUpdate(plugin.ID, plugin.Info.Version, gcomVersion, s.updateStrategy == setting.PluginUpdateStrategyMinor) } diff --git a/pkg/storage/unified/proto/search.proto b/pkg/storage/unified/proto/search.proto index 5018c97c9db..62c6afb323a 100644 --- a/pkg/storage/unified/proto/search.proto +++ b/pkg/storage/unified/proto/search.proto @@ -9,11 +9,13 @@ import "resource.proto"; // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics service ResourceIndex { + // Query for documents rpc Search(ResourceSearchRequest) returns (ResourceSearchResponse); // Get the resource stats rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse); + // Rebuild the search index rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse); } @@ -49,6 +51,20 @@ message ResourceStatsResponse { repeated Stats stats = 2; } +// This controls what query and analyzers are applied to the specified field +// See: https://blevesearch.com/docs/Analyzers/ +enum QueryFieldType { + // Picks a reasonable analyzer given the input. Currently this always uses TEXT + // In the future, it may change to depend on the indexed field type + DEFAULT = 0; + // Use free text analyzer. The query is broken into a normalized set of tokens + TEXT = 1; + // The query must exactly match the indexed token + KEYWORD = 2; + // Like a text query, but the position and offsets influence the score + PHRASE = 3; +} + // Search within a single resource message ResourceSearchRequest { message Sort { @@ -64,6 +80,18 @@ message ResourceSearchRequest { // date queries } + // Defines the field in the index to query + // Boost is optional, and allows weighting the field higher in the results + message QueryField { + // The field name in the index to query + string name = 1; + + QueryFieldType type = 2; + + // Boost value for this field + float boost = 3; + } + // The key must include namespace + group + resource ListOptions options = 1; @@ -99,6 +127,9 @@ message ResourceSearchRequest { int64 page = 11; int64 permission = 12; + + // Optionally specify which fields are included in the query + repeated QueryField query_fields = 13; } message ResourceSearchResponse { diff --git a/pkg/storage/unified/resource/document.go b/pkg/storage/unified/resource/document.go index 4e528b96df0..6a41689c0da 100644 --- a/pkg/storage/unified/resource/document.go +++ b/pkg/storage/unified/resource/document.go @@ -290,7 +290,6 @@ const SEARCH_FIELD_NAMESPACE = "namespace" const SEARCH_FIELD_NAME = "name" const SEARCH_FIELD_RV = "rv" const SEARCH_FIELD_TITLE = "title" -const SEARCH_FIELD_TITLE_NGRAM = "title_ngram" const SEARCH_FIELD_TITLE_PHRASE = "title_phrase" // filtering/sorting on title by full phrase const SEARCH_FIELD_DESCRIPTION = "description" const SEARCH_FIELD_TAGS = "tags" diff --git a/pkg/storage/unified/resourcepb/search.pb.go b/pkg/storage/unified/resourcepb/search.pb.go index 459e9aa3429..e523c112093 100644 --- a/pkg/storage/unified/resourcepb/search.pb.go +++ b/pkg/storage/unified/resourcepb/search.pb.go @@ -21,6 +21,65 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// This controls what query and analyzers are applied to the specified field +// See: https://blevesearch.com/docs/Analyzers/ +type QueryFieldType int32 + +const ( + // Picks a reasonable analyzer given the input. Currently this always uses TEXT + // In the future, it may change to depend on the indexed field type + QueryFieldType_DEFAULT QueryFieldType = 0 + // Use free text analyzer. The query is broken into a normalized set of tokens + QueryFieldType_TEXT QueryFieldType = 1 + // The query must exactly match the indexed token + QueryFieldType_KEYWORD QueryFieldType = 2 + // Like a text query, but the position and offsets influence the score + QueryFieldType_PHRASE QueryFieldType = 3 +) + +// Enum value maps for QueryFieldType. +var ( + QueryFieldType_name = map[int32]string{ + 0: "DEFAULT", + 1: "TEXT", + 2: "KEYWORD", + 3: "PHRASE", + } + QueryFieldType_value = map[string]int32{ + "DEFAULT": 0, + "TEXT": 1, + "KEYWORD": 2, + "PHRASE": 3, + } +) + +func (x QueryFieldType) Enum() *QueryFieldType { + p := new(QueryFieldType) + *p = x + return p +} + +func (x QueryFieldType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (QueryFieldType) Descriptor() protoreflect.EnumDescriptor { + return file_search_proto_enumTypes[0].Descriptor() +} + +func (QueryFieldType) Type() protoreflect.EnumType { + return &file_search_proto_enumTypes[0] +} + +func (x QueryFieldType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use QueryFieldType.Descriptor instead. +func (QueryFieldType) EnumDescriptor() ([]byte, []int) { + return file_search_proto_rawDescGZIP(), []int{0} +} + // Get statistics across multiple resources // For these queries, we do not need authorization to see the actual values type ResourceStatsRequest struct { @@ -165,10 +224,12 @@ type ResourceSearchRequest struct { // the return fields (empty will return everything) Fields []string `protobuf:"bytes,8,rep,name=fields,proto3" json:"fields,omitempty"` // explain each result (added to the each row) - Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` - IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` - Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"` - Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"` + Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` + IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` + Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"` + Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"` + // Optionally specify which fields are included in the query + QueryFields []*ResourceSearchRequest_QueryField `protobuf:"bytes,13,rep,name=query_fields,json=queryFields,proto3" json:"query_fields,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -287,6 +348,13 @@ func (x *ResourceSearchRequest) GetPermission() int64 { return 0 } +func (x *ResourceSearchRequest) GetQueryFields() []*ResourceSearchRequest_QueryField { + if x != nil { + return x.QueryFields + } + return nil +} + type ResourceSearchResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Error details @@ -670,6 +738,70 @@ func (x *ResourceSearchRequest_Facet) GetLimit() int64 { return 0 } +// Defines the field in the index to query +// Boost is optional, and allows weighting the field higher in the results +type ResourceSearchRequest_QueryField struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The field name in the index to query + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type QueryFieldType `protobuf:"varint,2,opt,name=type,proto3,enum=resource.QueryFieldType" json:"type,omitempty"` + // Boost value for this field + Boost float32 `protobuf:"fixed32,3,opt,name=boost,proto3" json:"boost,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceSearchRequest_QueryField) Reset() { + *x = ResourceSearchRequest_QueryField{} + mi := &file_search_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceSearchRequest_QueryField) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceSearchRequest_QueryField) ProtoMessage() {} + +func (x *ResourceSearchRequest_QueryField) ProtoReflect() protoreflect.Message { + mi := &file_search_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceSearchRequest_QueryField.ProtoReflect.Descriptor instead. +func (*ResourceSearchRequest_QueryField) Descriptor() ([]byte, []int) { + return file_search_proto_rawDescGZIP(), []int{2, 2} +} + +func (x *ResourceSearchRequest_QueryField) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ResourceSearchRequest_QueryField) GetType() QueryFieldType { + if x != nil { + return x.Type + } + return QueryFieldType_DEFAULT +} + +func (x *ResourceSearchRequest_QueryField) GetBoost() float32 { + if x != nil { + return x.Boost + } + return 0 +} + type ResourceSearchResponse_Facet struct { state protoimpl.MessageState `protogen:"open.v1"` Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"` @@ -685,7 +817,7 @@ type ResourceSearchResponse_Facet struct { func (x *ResourceSearchResponse_Facet) Reset() { *x = ResourceSearchResponse_Facet{} - mi := &file_search_proto_msgTypes[10] + mi := &file_search_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -697,7 +829,7 @@ func (x *ResourceSearchResponse_Facet) String() string { func (*ResourceSearchResponse_Facet) ProtoMessage() {} func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message { - mi := &file_search_proto_msgTypes[10] + mi := &file_search_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -751,7 +883,7 @@ type ResourceSearchResponse_TermFacet struct { func (x *ResourceSearchResponse_TermFacet) Reset() { *x = ResourceSearchResponse_TermFacet{} - mi := &file_search_proto_msgTypes[11] + mi := &file_search_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -763,7 +895,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string { func (*ResourceSearchResponse_TermFacet) ProtoMessage() {} func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message { - mi := &file_search_proto_msgTypes[11] + mi := &file_search_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -818,7 +950,7 @@ var file_search_proto_rawDesc = string([]byte{ 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x22, 0x8e, 0x05, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, + 0x74, 0x22, 0xc3, 0x06, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, @@ -846,93 +978,109 @@ var file_search_proto_rawDesc = string([]byte{ 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, - 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, - 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0c, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, - 0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, - 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, - 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, - 0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x74, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0b, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x1a, 0x64, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, + 0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, - 0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, - 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, - 0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, - 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, + 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, + 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, - 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, - 0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, - 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, - 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0xfe, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, - 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, - 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, - 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, + 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, + 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, + 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, + 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, + 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b, + 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, + 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, + 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2a, 0x40, 0x0a, 0x0e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, + 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x54, + 0x45, 0x58, 0x54, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x45, 0x59, 0x57, 0x4f, 0x52, 0x44, + 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x48, 0x52, 0x41, 0x53, 0x45, 0x10, 0x03, 0x32, 0xfe, + 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, + 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, + 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, + 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -947,53 +1095,58 @@ func file_search_proto_rawDescGZIP() []byte { return file_search_proto_rawDescData } -var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_search_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_search_proto_goTypes = []any{ - (*ResourceStatsRequest)(nil), // 0: resource.ResourceStatsRequest - (*ResourceStatsResponse)(nil), // 1: resource.ResourceStatsResponse - (*ResourceSearchRequest)(nil), // 2: resource.ResourceSearchRequest - (*ResourceSearchResponse)(nil), // 3: resource.ResourceSearchResponse - (*RebuildIndexesRequest)(nil), // 4: resource.RebuildIndexesRequest - (*RebuildIndexesResponse)(nil), // 5: resource.RebuildIndexesResponse - (*ResourceStatsResponse_Stats)(nil), // 6: resource.ResourceStatsResponse.Stats - (*ResourceSearchRequest_Sort)(nil), // 7: resource.ResourceSearchRequest.Sort - (*ResourceSearchRequest_Facet)(nil), // 8: resource.ResourceSearchRequest.Facet - nil, // 9: resource.ResourceSearchRequest.FacetEntry - (*ResourceSearchResponse_Facet)(nil), // 10: resource.ResourceSearchResponse.Facet - (*ResourceSearchResponse_TermFacet)(nil), // 11: resource.ResourceSearchResponse.TermFacet - nil, // 12: resource.ResourceSearchResponse.FacetEntry - (*ErrorResult)(nil), // 13: resource.ErrorResult - (*ListOptions)(nil), // 14: resource.ListOptions - (*ResourceKey)(nil), // 15: resource.ResourceKey - (*ResourceTable)(nil), // 16: resource.ResourceTable + (QueryFieldType)(0), // 0: resource.QueryFieldType + (*ResourceStatsRequest)(nil), // 1: resource.ResourceStatsRequest + (*ResourceStatsResponse)(nil), // 2: resource.ResourceStatsResponse + (*ResourceSearchRequest)(nil), // 3: resource.ResourceSearchRequest + (*ResourceSearchResponse)(nil), // 4: resource.ResourceSearchResponse + (*RebuildIndexesRequest)(nil), // 5: resource.RebuildIndexesRequest + (*RebuildIndexesResponse)(nil), // 6: resource.RebuildIndexesResponse + (*ResourceStatsResponse_Stats)(nil), // 7: resource.ResourceStatsResponse.Stats + (*ResourceSearchRequest_Sort)(nil), // 8: resource.ResourceSearchRequest.Sort + (*ResourceSearchRequest_Facet)(nil), // 9: resource.ResourceSearchRequest.Facet + (*ResourceSearchRequest_QueryField)(nil), // 10: resource.ResourceSearchRequest.QueryField + nil, // 11: resource.ResourceSearchRequest.FacetEntry + (*ResourceSearchResponse_Facet)(nil), // 12: resource.ResourceSearchResponse.Facet + (*ResourceSearchResponse_TermFacet)(nil), // 13: resource.ResourceSearchResponse.TermFacet + nil, // 14: resource.ResourceSearchResponse.FacetEntry + (*ErrorResult)(nil), // 15: resource.ErrorResult + (*ListOptions)(nil), // 16: resource.ListOptions + (*ResourceKey)(nil), // 17: resource.ResourceKey + (*ResourceTable)(nil), // 18: resource.ResourceTable } var file_search_proto_depIdxs = []int32{ - 13, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult - 6, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats - 14, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions - 15, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey - 7, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort - 9, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry - 13, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult - 15, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey - 16, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable - 12, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry - 15, // 10: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey - 13, // 11: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult - 8, // 12: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet - 11, // 13: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet - 10, // 14: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet - 2, // 15: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest - 0, // 16: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest - 4, // 17: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest - 3, // 18: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse - 1, // 19: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse - 5, // 20: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse - 18, // [18:21] is the sub-list for method output_type - 15, // [15:18] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 15, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult + 7, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats + 16, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions + 17, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey + 8, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort + 11, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry + 10, // 6: resource.ResourceSearchRequest.query_fields:type_name -> resource.ResourceSearchRequest.QueryField + 15, // 7: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult + 17, // 8: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey + 18, // 9: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable + 14, // 10: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry + 17, // 11: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey + 15, // 12: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult + 0, // 13: resource.ResourceSearchRequest.QueryField.type:type_name -> resource.QueryFieldType + 9, // 14: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet + 13, // 15: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet + 12, // 16: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet + 3, // 17: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest + 1, // 18: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest + 5, // 19: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest + 4, // 20: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse + 2, // 21: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse + 6, // 22: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse + 20, // [20:23] is the sub-list for method output_type + 17, // [17:20] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_search_proto_init() } @@ -1007,13 +1160,14 @@ func file_search_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)), - NumEnums: 0, - NumMessages: 13, + NumEnums: 1, + NumMessages: 14, NumExtensions: 0, NumServices: 1, }, GoTypes: file_search_proto_goTypes, DependencyIndexes: file_search_proto_depIdxs, + EnumInfos: file_search_proto_enumTypes, MessageInfos: file_search_proto_msgTypes, }.Build() File_search_proto = out.File diff --git a/pkg/storage/unified/resourcepb/search_grpc.pb.go b/pkg/storage/unified/resourcepb/search_grpc.pb.go index d69cbd14e38..d8db878ef55 100644 --- a/pkg/storage/unified/resourcepb/search_grpc.pb.go +++ b/pkg/storage/unified/resourcepb/search_grpc.pb.go @@ -31,9 +31,11 @@ const ( // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexClient interface { + // Query for documents Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error) // Get the resource stats GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error) + // Rebuild the search index RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error) } @@ -82,9 +84,11 @@ func (c *resourceIndexClient) RebuildIndexes(ctx context.Context, in *RebuildInd // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexServer interface { + // Query for documents Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error) // Get the resource stats GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) + // Rebuild the search index RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error) } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index a988b71aa38..09ef2dc9230 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -1182,6 +1182,7 @@ func (b *bleveIndex) getIndex( return b.index, nil } +// nolint:gocyclo func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.ResourceSearchRequest, access authlib.AccessClient) (*bleve.SearchRequest, *resourcepb.ErrorResult) { ctx, span := tracer.Start(ctx, "search.bleveIndex.toBleveSearchRequest") defer span.End() @@ -1240,42 +1241,62 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R } } - if len(req.Query) > 1 && strings.Contains(req.Query, "*") { - // wildcard query is expensive - should be used with caution - wildcard := bleve.NewWildcardQuery(req.Query) - queries = append(queries, wildcard) - } + if len(req.Query) > 1 { + if strings.Contains(req.Query, "*") { + // wildcard query is expensive - should be used with caution + wildcard := bleve.NewWildcardQuery(req.Query) + queries = append(queries, wildcard) + } else { + // When using a + searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE) + disjoin := bleve.NewDisjunctionQuery() + queries = append(queries, disjoin) - if req.Query != "" && !strings.Contains(req.Query, "*") { - // Add a text query - searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE) + queryFields := req.QueryFields + if len(queryFields) == 0 { + queryFields = []*resourcepb.ResourceSearchRequest_QueryField{ + { + Name: resource.SEARCH_FIELD_TITLE, + Type: resourcepb.QueryFieldType_KEYWORD, + Boost: 10, // exact match -- includes ngrams! If they lived on their own field, we could score them differently + }, { + Name: resource.SEARCH_FIELD_TITLE, + Type: resourcepb.QueryFieldType_TEXT, + Boost: 2, // standard analyzer (with ngrams!) + }, { + Name: resource.SEARCH_FIELD_TITLE_PHRASE, + Type: resourcepb.QueryFieldType_TEXT, + Boost: 5, // standard analyzer + }, + } + } - // There are multiple ways to match the query string to documents. The following queries are ordered by priority: + for _, field := range queryFields { + switch field.Type { + case resourcepb.QueryFieldType_TEXT, resourcepb.QueryFieldType_DEFAULT: + q := bleve.NewMatchQuery(removeSmallTerms(req.Query)) // removeSmallTerms should be part of the analyzer + q.SetBoost(float64(field.Boost)) + q.SetField(field.Name) + q.Analyzer = standard.Name // analyze the text + q.Operator = query.MatchQueryOperatorAnd // all terms must match + disjoin.AddQuery(q) - // Query 1: Match the exact query string - queryExact := bleve.NewMatchQuery(req.Query) - queryExact.SetBoost(10.0) - queryExact.SetField(resource.SEARCH_FIELD_TITLE) - queryExact.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token - queryExact.Operator = query.MatchQueryOperatorAnd // This doesn't make a difference for keyword analyzer, we add it just to be explicit. - searchQuery := bleve.NewDisjunctionQuery(queryExact) + case resourcepb.QueryFieldType_KEYWORD: + q := bleve.NewMatchQuery(req.Query) + q.SetBoost(float64(field.Boost)) + q.SetField(field.Name) + q.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token + disjoin.AddQuery(q) - // Query 2: Phrase query with standard analyzer - queryPhrase := bleve.NewMatchPhraseQuery(req.Query) - queryPhrase.SetBoost(5.0) - queryPhrase.SetField(resource.SEARCH_FIELD_TITLE) - queryPhrase.Analyzer = standard.Name - searchQuery.AddQuery(queryPhrase) - - // Query 3: Match query with standard analyzer - queryAnalyzed := bleve.NewMatchQuery(removeSmallTerms(req.Query)) - queryAnalyzed.SetField(resource.SEARCH_FIELD_TITLE) - queryAnalyzed.SetBoost(2.0) - queryAnalyzed.Analyzer = standard.Name - queryAnalyzed.Operator = query.MatchQueryOperatorAnd // Make sure all terms from the query are matched - searchQuery.AddQuery(queryAnalyzed) - - queries = append(queries, searchQuery) + case resourcepb.QueryFieldType_PHRASE: + q := bleve.NewMatchPhraseQuery(req.Query) + q.SetBoost(float64(field.Boost)) + q.SetField(field.Name) + q.Analyzer = standard.Name + disjoin.AddQuery(q) + } + } + } } switch len(queries) { @@ -1877,7 +1898,7 @@ func (q *permissionScopedQuery) Searcher(ctx context.Context, i index.IndexReade if err != nil { return nil, err } - filteringSearcher := bleveSearch.NewFilteringSearcher(ctx, searcher, func(d *search.DocumentMatch) bool { + filteringSearcher := bleveSearch.NewFilteringSearcher(ctx, searcher, func(_ *search.SearchContext, d *search.DocumentMatch) bool { // The doc ID has the format: /// // IndexInternalID will be the same as the doc ID when using an in-memory index, but when using a file-based // index it becomes a binary encoded number that has some other internal meaning. Using ExternalID() will get the diff --git a/pkg/tests/api/elasticsearch/elasticsearch_test.go b/pkg/tests/api/elasticsearch/elasticsearch_test.go index 09277c944f0..651dd74e9e2 100644 --- a/pkg/tests/api/elasticsearch/elasticsearch_test.go +++ b/pkg/tests/api/elasticsearch/elasticsearch_test.go @@ -24,6 +24,24 @@ func TestMain(m *testing.M) { testsuite.Run(m) } +// mockElasticsearchHandler returns a handler that mocks Elasticsearch endpoints. +// It responds to GET / with cluster info (required for datasource initialization) +// and returns 401 Unauthorized for all other requests. +func mockElasticsearchHandler(onRequest func(r *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":{"build_flavor":"default","number":"8.0.0"}}`)) + default: + if onRequest != nil { + onRequest(r) + } + w.WriteHeader(http.StatusUnauthorized) + } + } +} + func TestIntegrationElasticsearch(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -35,9 +53,8 @@ func TestIntegrationElasticsearch(t *testing.T) { ctx := context.Background() var outgoingRequest *http.Request - outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + outgoingServer := httptest.NewServer(mockElasticsearchHandler(func(r *http.Request) { outgoingRequest = r - w.WriteHeader(http.StatusUnauthorized) })) t.Cleanup(outgoingServer.Close) diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 3d1ccce6a59..83debc4c410 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -639,7 +639,7 @@ ] }, "dependencies": { - "grafanaDependency": "", + "grafanaDependency": "\u003e=11.6.0", "grafanaVersion": "*", "plugins": [], "extensions": { diff --git a/pkg/tests/apis/dashboard/search_test.go b/pkg/tests/apis/dashboard/search_test.go index 2227c67287e..df03e6a9670 100644 --- a/pkg/tests/apis/dashboard/search_test.go +++ b/pkg/tests/apis/dashboard/search_test.go @@ -97,7 +97,7 @@ func TestIntegrationSearchDevDashboards(t *testing.T) { require.Equal(t, 16, fileCount, "file count from %s", devenv) // Helper to call search - callSearch := func(user apis.User, params string) dashboardV0.SearchResults { + callSearch := func(user apis.User, params map[string]string) dashboardV0.SearchResults { require.NotNil(t, user) ns := user.Identity.GetNamespace() cfg := dynamic.ConfigFor(user.NewRestConfig()) @@ -107,17 +107,12 @@ func TestIntegrationSearchDevDashboards(t *testing.T) { var statusCode int req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search"). + //Param("explain", "true") // helpful to understand which field made things match Param("limit", "1000"). Param("type", "dashboard") // Only search dashboards - for kv := range strings.SplitSeq(params, "&") { - if kv == "" { - continue - } - parts := strings.SplitN(kv, "=", 2) - if len(parts) == 2 { - req = req.Param(parts[0], parts[1]) - } + for k, v := range params { + req = req.Param(k, v) } res := req.Do(ctx).StatusCode(&statusCode) require.NoError(t, res.Error()) @@ -140,22 +135,47 @@ func TestIntegrationSearchDevDashboards(t *testing.T) { testCases := []struct { name string user apis.User - params string + params map[string]string }{ { - name: "all", - user: helper.Org1.Admin, - params: "", // only dashboards + name: "all", + user: helper.Org1.Admin, }, { - name: "simple-query", - user: helper.Org1.Admin, - params: "query=stacking", + name: "query-single-word", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "stacking", + }, }, { - name: "with-text-panel", - user: helper.Org1.Admin, - params: "field=panel_types&panelType=text", + name: "query-multiple-words", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "graph softMin", // must match ALL terms + }, + }, + { + name: "with-text-panel", + user: helper.Org1.Admin, + params: map[string]string{ + "field": "panel_types", // return panel types + "panelType": "text", + }, + }, + { + name: "title-ngram-prefix", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "zer", // should match "Zero Decimals Y Ticks" + }, + }, + { + name: "title-ngram-middle-word", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "decim", // should match "Zero Decimals Y Ticks" + }, }, } for i, tc := range testCases { diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json b/pkg/tests/apis/dashboard/testdata/searchV0/t01-query-single-word.json similarity index 88% rename from pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json rename to pkg/tests/apis/dashboard/testdata/searchV0/t01-query-single-word.json index 6c9a935dfe8..02eed11383a 100644 --- a/pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t01-query-single-word.json @@ -10,7 +10,7 @@ "panel-tests", "graph-ng" ], - "score": 0.658 + "score": 0.284 }, { "resource": "dashboards", @@ -21,8 +21,8 @@ "panel-tests", "graph-ng" ], - "score": 0.625 + "score": 0.269 } ], - "maxScore": 0.658 + "maxScore": 0.284 } \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json b/pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json new file mode 100644 index 00000000000..270801994c0 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json @@ -0,0 +1,17 @@ +{ + "totalHits": 1, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-soft-limits", + "title": "Panel Tests - Graph NG - softMin/softMax", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.024 + } + ], + "maxScore": 0.024 +} \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t02-with-text-panel.json b/pkg/tests/apis/dashboard/testdata/searchV0/t03-with-text-panel.json similarity index 100% rename from pkg/tests/apis/dashboard/testdata/searchV0/t02-with-text-panel.json rename to pkg/tests/apis/dashboard/testdata/searchV0/t03-with-text-panel.json diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json b/pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json new file mode 100644 index 00000000000..8059db130a0 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json @@ -0,0 +1,17 @@ +{ + "totalHits": 1, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-y-ticks-zero-decimals", + "title": "Zero Decimals Y Ticks", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.35 + } + ], + "maxScore": 0.35 +} \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json b/pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json new file mode 100644 index 00000000000..8059db130a0 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json @@ -0,0 +1,17 @@ +{ + "totalHits": 1, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-y-ticks-zero-decimals", + "title": "Zero Decimals Y Ticks", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.35 + } + ], + "maxScore": 0.35 +} \ No newline at end of file diff --git a/pkg/tsdb/cloud-monitoring/cloudmonitoring.go b/pkg/tsdb/cloud-monitoring/cloudmonitoring.go index c88feb30a1a..37401de02c4 100644 --- a/pkg/tsdb/cloud-monitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloud-monitoring/cloudmonitoring.go @@ -92,7 +92,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque }, nil } - url := fmt.Sprintf("%v/v3/projects/%v/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject) + url := fmt.Sprintf("%s/v3/projects/%s/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject) request, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err @@ -139,6 +139,7 @@ type datasourceInfo struct { defaultProject string clientEmail string tokenUri string + universeDomain string services map[string]datasourceService privateKey string usingImpersonation bool @@ -150,6 +151,7 @@ type datasourceJSONData struct { DefaultProject string `json:"defaultProject"` ClientEmail string `json:"clientEmail"` TokenURI string `json:"tokenUri"` + UniverseDomain string `json:"universeDomain"` UsingImpersonation bool `json:"usingImpersonation"` ServiceAccountToImpersonate string `json:"serviceAccountToImpersonate"` } @@ -179,6 +181,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst defaultProject: jsonData.DefaultProject, clientEmail: jsonData.ClientEmail, tokenUri: jsonData.TokenURI, + universeDomain: jsonData.UniverseDomain, usingImpersonation: jsonData.UsingImpersonation, serviceAccountToImpersonate: jsonData.ServiceAccountToImpersonate, services: map[string]datasourceService{}, @@ -194,13 +197,13 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst return nil, err } - for name, info := range routes { + for name := range routes { client, err := newHTTPClient(dsInfo, opts, &httpClientProvider, name) if err != nil { return nil, err } dsInfo.services[name] = datasourceService{ - url: info.url, + url: buildURL(name, dsInfo.universeDomain), client: client, } } diff --git a/pkg/tsdb/cloud-monitoring/httpclient.go b/pkg/tsdb/cloud-monitoring/httpclient.go index 5a57e5f4ac8..aa0ed084194 100644 --- a/pkg/tsdb/cloud-monitoring/httpclient.go +++ b/pkg/tsdb/cloud-monitoring/httpclient.go @@ -23,12 +23,12 @@ type routeInfo struct { var routes = map[string]routeInfo{ cloudMonitor: { method: "GET", - url: "https://monitoring.googleapis.com", + url: "https://monitoring.", scopes: []string{cloudMonitorScope}, }, resourceManager: { method: "GET", - url: "https://cloudresourcemanager.googleapis.com", + url: "https://cloudresourcemanager.", scopes: []string{resourceManagerScope}, }, } @@ -68,6 +68,13 @@ func getMiddleware(model *datasourceInfo, routePath string) (httpclient.Middlewa return tokenprovider.AuthMiddleware(provider), nil } +func buildURL(route string, universeDomain string) string { + if universeDomain == "" { + universeDomain = "googleapis.com" + } + return routes[route].url + universeDomain +} + func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider *httpclient.Provider, route string) (*http.Client, error) { m, err := getMiddleware(model, route) if err != nil { diff --git a/pkg/tsdb/cloud-monitoring/resource_handler_test.go b/pkg/tsdb/cloud-monitoring/resource_handler_test.go index 5f315ef9a59..64742233453 100644 --- a/pkg/tsdb/cloud-monitoring/resource_handler_test.go +++ b/pkg/tsdb/cloud-monitoring/resource_handler_test.go @@ -111,7 +111,7 @@ func Test_setRequestVariables(t *testing.T) { im: &fakeInstance{ services: map[string]datasourceService{ cloudMonitor: { - url: routes[cloudMonitor].url, + url: buildURL(cloudMonitor, "googleapis.com"), client: &http.Client{}, }, }, diff --git a/pkg/tsdb/elasticsearch/aggregation_factory.go b/pkg/tsdb/elasticsearch/aggregation_factory.go index cc3e597e50b..3f702b745b4 100644 --- a/pkg/tsdb/elasticsearch/aggregation_factory.go +++ b/pkg/tsdb/elasticsearch/aggregation_factory.go @@ -3,8 +3,8 @@ package elasticsearch import ( "regexp" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // addDateHistogramAgg adds a date histogram aggregation to the aggregation builder diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index fbb3e09f092..12e1a8f5df4 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" - "github.com/grafana/grafana/pkg/services/featuremgmt" ) // Used in logging to mark a stage @@ -35,6 +34,7 @@ type DatasourceInfo struct { Interval string MaxConcurrentShardRequests int64 IncludeFrozen bool + ClusterInfo ClusterInfo } type ConfiguredFields struct { @@ -159,7 +159,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch resSpan.End() }() - improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing) + improvedParsingEnabled := isFeatureEnabled(c.ctx, "elasticsearchImprovedParsing") msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled) if err != nil { return nil, err @@ -197,7 +197,11 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque func (c *baseClientImpl) getMultiSearchQueryParameters() string { var qs []string - qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests)) + // if the build flavor is not serverless, we can use the max concurrent shard requests + // this is because serverless clusters do not support max concurrent shard requests + if !c.ds.ClusterInfo.IsServerless() && c.ds.MaxConcurrentShardRequests > 0 { + qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests)) + } if c.ds.IncludeFrozen { qs = append(qs, "ignore_throttled=false") diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go index 8f257873232..b8afb048c00 100644 --- a/pkg/tsdb/elasticsearch/client/client_test.go +++ b/pkg/tsdb/elasticsearch/client/client_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) func TestClient_ExecuteMultisearch(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/client/cluster_info.go b/pkg/tsdb/elasticsearch/client/cluster_info.go new file mode 100644 index 00000000000..eb89189804f --- /dev/null +++ b/pkg/tsdb/elasticsearch/client/cluster_info.go @@ -0,0 +1,51 @@ +package es + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type VersionInfo struct { + BuildFlavor string `json:"build_flavor"` +} + +// ClusterInfo represents Elasticsearch cluster information returned from the root endpoint. +// It is used to determine cluster capabilities and configuration like whether the cluster is serverless. +type ClusterInfo struct { + Version VersionInfo `json:"version"` +} + +const ( + BuildFlavorServerless = "serverless" +) + +// GetClusterInfo fetches cluster information from the Elasticsearch root endpoint. +// It returns the cluster build flavor which is used to determine if the cluster is serverless. +func GetClusterInfo(httpCli *http.Client, url string) (clusterInfo ClusterInfo, err error) { + resp, err := httpCli.Get(url) + if err != nil { + return ClusterInfo{}, fmt.Errorf("error getting ES cluster info: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return ClusterInfo{}, fmt.Errorf("unexpected status code %d getting ES cluster info", resp.StatusCode) + } + + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("error closing response body: %w", closeErr) + } + }() + + err = json.NewDecoder(resp.Body).Decode(&clusterInfo) + if err != nil { + return ClusterInfo{}, fmt.Errorf("error decoding ES cluster info: %w", err) + } + + return clusterInfo, nil +} + +func (ci ClusterInfo) IsServerless() bool { + return ci.Version.BuildFlavor == BuildFlavorServerless +} diff --git a/pkg/tsdb/elasticsearch/client/cluster_info_test.go b/pkg/tsdb/elasticsearch/client/cluster_info_test.go new file mode 100644 index 00000000000..0fdcc46e813 --- /dev/null +++ b/pkg/tsdb/elasticsearch/client/cluster_info_test.go @@ -0,0 +1,188 @@ +package es + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetClusterInfo(t *testing.T) { + t.Run("Should successfully get cluster info", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ + "name": "test-cluster", + "cluster_name": "elasticsearch", + "cluster_uuid": "abc123", + "version": { + "number": "8.0.0", + "build_flavor": "default", + "build_type": "tar", + "build_hash": "abc123", + "build_date": "2023-01-01T00:00:00.000Z", + "build_snapshot": false, + "lucene_version": "9.0.0" + } + }`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.NoError(t, err) + require.NotNil(t, clusterInfo) + assert.Equal(t, "default", clusterInfo.Version.BuildFlavor) + }) + + t.Run("Should successfully get serverless cluster info", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ + "name": "serverless-cluster", + "cluster_name": "elasticsearch", + "cluster_uuid": "def456", + "version": { + "number": "8.11.0", + "build_flavor": "serverless", + "build_type": "docker", + "build_hash": "def456", + "build_date": "2023-11-01T00:00:00.000Z", + "build_snapshot": false, + "lucene_version": "9.8.0" + } + }`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.NoError(t, err) + require.NotNil(t, clusterInfo) + assert.Equal(t, "serverless", clusterInfo.Version.BuildFlavor) + assert.True(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return error when HTTP request fails", func(t *testing.T) { + clusterInfo, err := GetClusterInfo(http.DefaultClient, "http://invalid-url-that-does-not-exist.local:9999") + + require.Error(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Contains(t, err.Error(), "error getting ES cluster info") + }) + + t.Run("Should return error when response body is invalid JSON", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{"invalid json`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.Error(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Contains(t, err.Error(), "error decoding ES cluster info") + }) + + t.Run("Should handle empty version object", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ + "name": "test-cluster", + "version": {} + }`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.NoError(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Equal(t, "", clusterInfo.Version.BuildFlavor) + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("Should handle HTTP error status codes", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusUnauthorized) + _, err := rw.Write([]byte(`{"error": "Unauthorized"}`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.Error(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Contains(t, err.Error(), "unexpected status code 401 getting ES cluster info") + }) +} + +func TestClusterInfo_IsServerless(t *testing.T) { + t.Run("Should return true when build_flavor is serverless", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: BuildFlavorServerless, + }, + } + + assert.True(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return false when build_flavor is default", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: "default", + }, + } + + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return false when build_flavor is empty", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: "", + }, + } + + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return false when build_flavor is unknown value", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: "unknown", + }, + } + + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("should return false when cluster info is empty", func(t *testing.T) { + clusterInfo := ClusterInfo{} + assert.False(t, clusterInfo.IsServerless()) + }) +} diff --git a/pkg/tsdb/elasticsearch/client/search_request_test.go b/pkg/tsdb/elasticsearch/client/search_request_test.go index 80113b4996e..7e2c592dddb 100644 --- a/pkg/tsdb/elasticsearch/client/search_request_test.go +++ b/pkg/tsdb/elasticsearch/client/search_request_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) func TestSearchRequest(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/data_query_processor.go b/pkg/tsdb/elasticsearch/data_query_processor.go index 288d6ce30de..4dc0afb109a 100644 --- a/pkg/tsdb/elasticsearch/data_query_processor.go +++ b/pkg/tsdb/elasticsearch/data_query_processor.go @@ -6,8 +6,8 @@ import ( "strconv" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // processQuery processes a single query and adds it to the multi-search request builder diff --git a/pkg/tsdb/elasticsearch/data_query_settings.go b/pkg/tsdb/elasticsearch/data_query_settings.go index fe286ccaeda..519eb6dc96d 100644 --- a/pkg/tsdb/elasticsearch/data_query_settings.go +++ b/pkg/tsdb/elasticsearch/data_query_settings.go @@ -3,7 +3,7 @@ package elasticsearch import ( "strconv" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // setFloatPath converts a string value at the specified path to float64 diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index 40073bf1740..0432bbcee20 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -88,6 +88,14 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins httpCliOpts.SigV4.Service = "es" } + apiKeyAuth, ok := jsonData["apiKeyAuth"].(bool) + if ok && apiKeyAuth { + apiKey := settings.DecryptedSecureJSONData["apiKey"] + if apiKey != "" { + httpCliOpts.Header.Add("Authorization", "ApiKey "+apiKey) + } + } + httpCli, err := httpClientProvider.New(httpCliOpts) if err != nil { return nil, err @@ -151,6 +159,11 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins includeFrozen = false } + clusterInfo, err := es.GetClusterInfo(httpCli, settings.URL) + if err != nil { + return nil, err + } + configuredFields := es.ConfiguredFields{ TimeField: timeField, LogLevelField: logLevelField, @@ -166,6 +179,7 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins ConfiguredFields: configuredFields, Interval: interval, IncludeFrozen: includeFrozen, + ClusterInfo: clusterInfo, } return model, nil } diff --git a/pkg/tsdb/elasticsearch/elasticsearch_test.go b/pkg/tsdb/elasticsearch/elasticsearch_test.go index 8ab3cabc7e5..35ec1f814ce 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch_test.go +++ b/pkg/tsdb/elasticsearch/elasticsearch_test.go @@ -3,6 +3,8 @@ package elasticsearch import ( "context" "encoding/json" + "net/http" + "net/http/httptest" "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -18,8 +20,26 @@ type datasourceInfo struct { Interval string `json:"interval"` } +// mockElasticsearchServer creates a test HTTP server that mocks Elasticsearch cluster info endpoint +func mockElasticsearchServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // Return a mock Elasticsearch cluster info response + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "version": map[string]interface{}{ + "build_flavor": "serverless", + "number": "8.0.0", + }, + }) + })) +} + func TestNewInstanceSettings(t *testing.T) { t.Run("fields exist", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 5, @@ -28,6 +48,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -37,6 +58,9 @@ func TestNewInstanceSettings(t *testing.T) { t.Run("timeField", func(t *testing.T) { t.Run("is nil", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ MaxConcurrentShardRequests: 5, Interval: "Daily", @@ -46,6 +70,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -54,6 +79,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("is empty", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ MaxConcurrentShardRequests: 5, Interval: "Daily", @@ -64,6 +92,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -74,6 +103,9 @@ func TestNewInstanceSettings(t *testing.T) { t.Run("maxConcurrentShardRequests", func(t *testing.T) { t.Run("no maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", } @@ -81,6 +113,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -90,6 +123,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("string maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: "10", @@ -98,6 +134,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -107,6 +144,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("number maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 10, @@ -115,6 +155,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -124,6 +165,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("zero maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 0, @@ -132,6 +176,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -141,6 +186,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("negative maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: -10, @@ -149,6 +197,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -158,6 +207,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("float maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 10.5, @@ -166,6 +218,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -175,6 +228,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: "invalid", @@ -183,6 +239,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } diff --git a/pkg/tsdb/elasticsearch/healthcheck.go b/pkg/tsdb/elasticsearch/healthcheck.go index 928945691de..cb5d0a866db 100644 --- a/pkg/tsdb/elasticsearch/healthcheck.go +++ b/pkg/tsdb/elasticsearch/healthcheck.go @@ -28,7 +28,6 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque Message: "Health check failed: Failed to get data source info", }, nil } - healthStatusUrl, err := url.Parse(ds.URL) if err != nil { logger.Error("Failed to parse data source URL", "error", err) @@ -38,6 +37,14 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque }, nil } + // If the cluster is serverless, return a healthy result + if ds.ClusterInfo.IsServerless() { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "Elasticsearch Serverless data source is healthy.", + }, nil + } + // check that ES is healthy healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health") healthStatusUrl.RawQuery = "wait_for_status=yellow" diff --git a/pkg/tsdb/elasticsearch/metrics_response_processor.go b/pkg/tsdb/elasticsearch/metrics_response_processor.go index 1e60a732d64..619180ccf90 100644 --- a/pkg/tsdb/elasticsearch/metrics_response_processor.go +++ b/pkg/tsdb/elasticsearch/metrics_response_processor.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // metricsResponseProcessor handles processing of metrics query responses diff --git a/pkg/tsdb/elasticsearch/models.go b/pkg/tsdb/elasticsearch/models.go index adb18554339..8df08182588 100644 --- a/pkg/tsdb/elasticsearch/models.go +++ b/pkg/tsdb/elasticsearch/models.go @@ -4,7 +4,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // Query represents the time series query model of the datasource diff --git a/pkg/tsdb/elasticsearch/parse_query.go b/pkg/tsdb/elasticsearch/parse_query.go index e1bfa189ab9..4d7b0cf7d5e 100644 --- a/pkg/tsdb/elasticsearch/parse_query.go +++ b/pkg/tsdb/elasticsearch/parse_query.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, error) { diff --git a/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go index b092763b57d..a569c92e7db 100644 --- a/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go +++ b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // AggregationParser parses raw Elasticsearch DSL aggregations diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index d05ca92e19b..2c0c5d33810 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -15,9 +15,9 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" "github.com/grafana/grafana/pkg/tsdb/elasticsearch/instrumentation" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) const ( diff --git a/pkg/tsdb/elasticsearch/response_utils.go b/pkg/tsdb/elasticsearch/response_utils.go index c101633d0c2..5dfd1f38360 100644 --- a/pkg/tsdb/elasticsearch/response_utils.go +++ b/pkg/tsdb/elasticsearch/response_utils.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // flatten flattens multi-level objects to single level objects. It uses dot notation to join keys. diff --git a/pkg/tsdb/elasticsearch/simplejson/simplejson.go b/pkg/tsdb/elasticsearch/simplejson/simplejson.go new file mode 100644 index 00000000000..d7759ac3c2b --- /dev/null +++ b/pkg/tsdb/elasticsearch/simplejson/simplejson.go @@ -0,0 +1,582 @@ +// Package simplejson provides a wrapper for arbitrary JSON objects that adds methods to access properties. +// Use of this package in place of types and the standard library's encoding/json package is strongly discouraged. +// +// Don't lint for stale code, since it's a copied library and we might as well keep the whole thing. +// nolint:unused +package simplejson + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "log" +) + +// returns the current implementation version +func Version() string { + return "0.5.0" +} + +type Json struct { + data any +} + +func (j *Json) FromDB(data []byte) error { + j.data = make(map[string]any) + + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +func (j *Json) ToDB() ([]byte, error) { + if j == nil || j.data == nil { + return nil, nil + } + + return j.Encode() +} + +func (j *Json) Scan(val any) error { + switch v := val.(type) { + case []byte: + if len(v) == 0 { + return nil + } + return json.Unmarshal(v, &j) + case string: + if len(v) == 0 { + return nil + } + return json.Unmarshal([]byte(v), &j) + default: + return fmt.Errorf("unsupported type: %T", v) + } +} + +func (j *Json) Value() (driver.Value, error) { + return j.ToDB() +} + +// DeepCopyInto creates a copy by serializing JSON +func (j *Json) DeepCopyInto(out *Json) { + b, err := j.Encode() + if err == nil { + _ = out.UnmarshalJSON(b) + } +} + +// DeepCopy will make a deep copy of the JSON object +func (j *Json) DeepCopy() *Json { + if j == nil { + return nil + } + out := new(Json) + j.DeepCopyInto(out) + return out +} + +// NewJson returns a pointer to a new `Json` object +// after unmarshaling `body` bytes +func NewJson(body []byte) (*Json, error) { + j := new(Json) + err := j.UnmarshalJSON(body) + if err != nil { + return nil, err + } + return j, nil +} + +// MustJson returns a pointer to a new `Json` object, panicking if `body` cannot be parsed. +func MustJson(body []byte) *Json { + j, err := NewJson(body) + + if err != nil { + panic(fmt.Sprintf("could not unmarshal JSON: %q", err)) + } + + return j +} + +// New returns a pointer to a new, empty `Json` object +func New() *Json { + return &Json{ + data: make(map[string]any), + } +} + +// NewFromAny returns a pointer to a new `Json` object with provided data. +func NewFromAny(data any) *Json { + return &Json{data: data} +} + +// Interface returns the underlying data +func (j *Json) Interface() any { + return j.data +} + +// Encode returns its marshaled data as `[]byte` +func (j *Json) Encode() ([]byte, error) { + return j.MarshalJSON() +} + +// EncodePretty returns its marshaled data as `[]byte` with indentation +func (j *Json) EncodePretty() ([]byte, error) { + return json.MarshalIndent(&j.data, "", " ") +} + +// Implements the json.Marshaler interface. +func (j *Json) MarshalJSON() ([]byte, error) { + return json.Marshal(&j.data) +} + +// Set modifies `Json` map by `key` and `value` +// Useful for changing single key/value in a `Json` object easily. +func (j *Json) Set(key string, val any) { + m, err := j.Map() + if err != nil { + return + } + m[key] = val +} + +// SetPath modifies `Json`, recursively checking/creating map keys for the supplied path, +// and then finally writing in the value +func (j *Json) SetPath(branch []string, val any) { + if len(branch) == 0 { + j.data = val + return + } + + // in order to insert our branch, we need map[string]any + if _, ok := (j.data).(map[string]any); !ok { + // have to replace with something suitable + j.data = make(map[string]any) + } + curr := j.data.(map[string]any) + + for i := 0; i < len(branch)-1; i++ { + b := branch[i] + // key exists? + if _, ok := curr[b]; !ok { + n := make(map[string]any) + curr[b] = n + curr = n + continue + } + + // make sure the value is the right sort of thing + if _, ok := curr[b].(map[string]any); !ok { + // have to replace with something suitable + n := make(map[string]any) + curr[b] = n + } + + curr = curr[b].(map[string]any) + } + + // add remaining k/v + curr[branch[len(branch)-1]] = val +} + +// Del modifies `Json` map by deleting `key` if it is present. +func (j *Json) Del(key string) { + m, err := j.Map() + if err != nil { + return + } + delete(m, key) +} + +// Get returns a pointer to a new `Json` object +// for `key` in its `map` representation +// +// useful for chaining operations (to traverse a nested JSON): +// +// js.Get("top_level").Get("dict").Get("value").Int() +func (j *Json) Get(key string) *Json { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val} + } + } + return &Json{nil} +} + +// GetPath searches for the item as specified by the branch +// without the need to deep dive using Get()'s. +// +// js.GetPath("top_level", "dict") +func (j *Json) GetPath(branch ...string) *Json { + jin := j + for _, p := range branch { + jin = jin.Get(p) + } + return jin +} + +// GetIndex returns a pointer to a new `Json` object +// for `index` in its `array` representation +// +// this is the analog to Get when accessing elements of +// a json array instead of a json object: +// +// js.Get("top_level").Get("array").GetIndex(1).Get("key").Int() +func (j *Json) GetIndex(index int) *Json { + a, err := j.Array() + if err == nil { + if len(a) > index { + return &Json{a[index]} + } + } + return &Json{nil} +} + +// CheckGetIndex returns a pointer to a new `Json` object +// for `index` in its `array` representation, and a `bool` +// indicating success or failure +// +// useful for chained operations when success is important: +// +// if data, ok := js.Get("top_level").CheckGetIndex(0); ok { +// log.Println(data) +// } +func (j *Json) CheckGetIndex(index int) (*Json, bool) { + a, err := j.Array() + if err == nil { + if len(a) > index { + return &Json{a[index]}, true + } + } + return nil, false +} + +// SetIndex modifies `Json` array by `index` and `value` +// for `index` in its `array` representation +func (j *Json) SetIndex(index int, val any) { + a, err := j.Array() + if err == nil { + if len(a) > index { + a[index] = val + } + } +} + +// CheckGet returns a pointer to a new `Json` object and +// a `bool` identifying success or failure +// +// useful for chained operations when success is important: +// +// if data, ok := js.Get("top_level").CheckGet("inner"); ok { +// log.Println(data) +// } +func (j *Json) CheckGet(key string) (*Json, bool) { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val}, true + } + } + return nil, false +} + +// Map type asserts to `map` +func (j *Json) Map() (map[string]any, error) { + if m, ok := (j.data).(map[string]any); ok { + return m, nil + } + return nil, errors.New("type assertion to map[string]any failed") +} + +// Array type asserts to an `array` +func (j *Json) Array() ([]any, error) { + if a, ok := (j.data).([]any); ok { + return a, nil + } + return nil, errors.New("type assertion to []any failed") +} + +// Bool type asserts to `bool` +func (j *Json) Bool() (bool, error) { + if s, ok := (j.data).(bool); ok { + return s, nil + } + return false, errors.New("type assertion to bool failed") +} + +// String type asserts to `string` +func (j *Json) String() (string, error) { + if s, ok := (j.data).(string); ok { + return s, nil + } + return "", errors.New("type assertion to string failed") +} + +// Bytes type asserts to `[]byte` +func (j *Json) Bytes() ([]byte, error) { + if s, ok := (j.data).(string); ok { + return []byte(s), nil + } + return nil, errors.New("type assertion to []byte failed") +} + +// StringArray type asserts to an `array` of `string` +func (j *Json) StringArray() ([]string, error) { + arr, err := j.Array() + if err != nil { + return nil, err + } + retArr := make([]string, 0, len(arr)) + for _, a := range arr { + if a == nil { + retArr = append(retArr, "") + continue + } + s, ok := a.(string) + if !ok { + return nil, err + } + retArr = append(retArr, s) + } + return retArr, nil +} + +// MustArray guarantees the return of a `[]any` (with optional default) +// +// useful when you want to iterate over array values in a succinct manner: +// +// for i, v := range js.Get("results").MustArray() { +// fmt.Println(i, v) +// } +func (j *Json) MustArray(args ...[]any) []any { + var def []any + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustArray() received too many arguments %d", len(args)) + } + + a, err := j.Array() + if err == nil { + return a + } + + return def +} + +// MustMap guarantees the return of a `map[string]any` (with optional default) +// +// useful when you want to iterate over map values in a succinct manner: +// +// for k, v := range js.Get("dictionary").MustMap() { +// fmt.Println(k, v) +// } +func (j *Json) MustMap(args ...map[string]any) map[string]any { + var def map[string]any + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustMap() received too many arguments %d", len(args)) + } + + a, err := j.Map() + if err == nil { + return a + } + + return def +} + +// MustString guarantees the return of a `string` (with optional default) +// +// useful when you explicitly want a `string` in a single value return context: +// +// myFunc(js.Get("param1").MustString(), js.Get("optional_param").MustString("my_default")) +func (j *Json) MustString(args ...string) string { + var def string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustString() received too many arguments %d", len(args)) + } + + s, err := j.String() + if err == nil { + return s + } + + return def +} + +// MustStringArray guarantees the return of a `[]string` (with optional default) +// +// useful when you want to iterate over array values in a succinct manner: +// +// for i, s := range js.Get("results").MustStringArray() { +// fmt.Println(i, s) +// } +func (j *Json) MustStringArray(args ...[]string) []string { + var def []string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustStringArray() received too many arguments %d", len(args)) + } + + a, err := j.StringArray() + if err == nil { + return a + } + + return def +} + +// MustInt guarantees the return of an `int` (with optional default) +// +// useful when you explicitly want an `int` in a single value return context: +// +// myFunc(js.Get("param1").MustInt(), js.Get("optional_param").MustInt(5150)) +func (j *Json) MustInt(args ...int) int { + var def int + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt() received too many arguments %d", len(args)) + } + + i, err := j.Int() + if err == nil { + return i + } + + return def +} + +// MustFloat64 guarantees the return of a `float64` (with optional default) +// +// useful when you explicitly want a `float64` in a single value return context: +// +// myFunc(js.Get("param1").MustFloat64(), js.Get("optional_param").MustFloat64(5.150)) +func (j *Json) MustFloat64(args ...float64) float64 { + var def float64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustFloat64() received too many arguments %d", len(args)) + } + + f, err := j.Float64() + if err == nil { + return f + } + + return def +} + +// MustBool guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// +// myFunc(js.Get("param1").MustBool(), js.Get("optional_param").MustBool(true)) +func (j *Json) MustBool(args ...bool) bool { + var def bool + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustBool() received too many arguments %d", len(args)) + } + + b, err := j.Bool() + if err == nil { + return b + } + + return def +} + +// MustInt64 guarantees the return of an `int64` (with optional default) +// +// useful when you explicitly want an `int64` in a single value return context: +// +// myFunc(js.Get("param1").MustInt64(), js.Get("optional_param").MustInt64(5150)) +func (j *Json) MustInt64(args ...int64) int64 { + var def int64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt64() received too many arguments %d", len(args)) + } + + i, err := j.Int64() + if err == nil { + return i + } + + return def +} + +// MustUInt64 guarantees the return of an `uint64` (with optional default) +// +// useful when you explicitly want an `uint64` in a single value return context: +// +// myFunc(js.Get("param1").MustUint64(), js.Get("optional_param").MustUint64(5150)) +func (j *Json) MustUint64(args ...uint64) uint64 { + var def uint64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustUint64() received too many arguments %d", len(args)) + } + + i, err := j.Uint64() + if err == nil { + return i + } + + return def +} + +// MarshalYAML implements yaml.Marshaller. +func (j *Json) MarshalYAML() (any, error) { + return j.data, nil +} + +// UnmarshalYAML implements yaml.Unmarshaller. +func (j *Json) UnmarshalYAML(unmarshal func(any) error) error { + var data any + if err := unmarshal(&data); err != nil { + return err + } + j.data = data + return nil +} diff --git a/pkg/tsdb/elasticsearch/simplejson/simplejson_go11.go b/pkg/tsdb/elasticsearch/simplejson/simplejson_go11.go new file mode 100644 index 00000000000..88748985576 --- /dev/null +++ b/pkg/tsdb/elasticsearch/simplejson/simplejson_go11.go @@ -0,0 +1,90 @@ +package simplejson + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "reflect" + "strconv" +) + +// Implements the json.Unmarshaler interface. +func (j *Json) UnmarshalJSON(p []byte) error { + dec := json.NewDecoder(bytes.NewBuffer(p)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +// NewFromReader returns a *Json by decoding from an io.Reader +func NewFromReader(r io.Reader) (*Json, error) { + j := new(Json) + dec := json.NewDecoder(r) + dec.UseNumber() + err := dec.Decode(&j.data) + return j, err +} + +// Float64 coerces into a float64 +func (j *Json) Float64() (float64, error) { + switch n := j.data.(type) { + case json.Number: + return n.Float64() + case float32, float64: + return reflect.ValueOf(j.data).Float(), nil + case int, int8, int16, int32, int64: + return float64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return float64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int coerces into an int +func (j *Json) Int() (int, error) { + switch n := j.data.(type) { + case json.Number: + i, err := n.Int64() + if err != nil { + return 0, err + } + return int(i), nil + case float32, float64: + return int(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return int(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return int(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int64 coerces into an int64 +func (j *Json) Int64() (int64, error) { + switch n := j.data.(type) { + case json.Number: + return n.Int64() + case float32, float64: + return int64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return reflect.ValueOf(j.data).Int(), nil + case uint, uint8, uint16, uint32, uint64: + return int64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Uint64 coerces into an uint64 +func (j *Json) Uint64() (uint64, error) { + switch n := j.data.(type) { + case json.Number: + return strconv.ParseUint(n.String(), 10, 64) + case float32, float64: + return uint64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return uint64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return reflect.ValueOf(j.data).Uint(), nil + } + return 0, errors.New("invalid value type") +} diff --git a/pkg/tsdb/elasticsearch/simplejson/simplejson_test.go b/pkg/tsdb/elasticsearch/simplejson/simplejson_test.go new file mode 100644 index 00000000000..efc786bc745 --- /dev/null +++ b/pkg/tsdb/elasticsearch/simplejson/simplejson_test.go @@ -0,0 +1,274 @@ +package simplejson + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSimplejson(t *testing.T) { + var ok bool + var err error + + js, err := NewJson([]byte(`{ + "test": { + "string_array": ["asdf", "ghjk", "zxcv"], + "string_array_null": ["abc", null, "efg"], + "array": [1, "2", 3], + "arraywithsubs": [{"subkeyone": 1}, + {"subkeytwo": 2, "subkeythree": 3}], + "int": 10, + "float": 5.150, + "string": "simplejson", + "bool": true, + "sub_obj": {"a": 1} + } + }`)) + + assert.NotEqual(t, nil, js) + assert.Equal(t, nil, err) + + _, ok = js.CheckGet("test") + assert.Equal(t, true, ok) + + _, ok = js.CheckGet("missing_key") + assert.Equal(t, false, ok) + + aws := js.Get("test").Get("arraywithsubs") + assert.NotEqual(t, nil, aws) + var awsval int + awsval, _ = aws.GetIndex(0).Get("subkeyone").Int() + assert.Equal(t, 1, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeytwo").Int() + assert.Equal(t, 2, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeythree").Int() + assert.Equal(t, 3, awsval) + + arr := js.Get("test").Get("array") + assert.NotEqual(t, nil, arr) + val, ok := arr.CheckGetIndex(0) + assert.Equal(t, ok, true) + valInt, _ := val.Int() + assert.Equal(t, valInt, 1) + val, ok = arr.CheckGetIndex(1) + assert.Equal(t, ok, true) + valStr, _ := val.String() + assert.Equal(t, valStr, "2") + val, ok = arr.CheckGetIndex(2) + assert.Equal(t, ok, true) + valInt, _ = val.Int() + assert.Equal(t, valInt, 3) + _, ok = arr.CheckGetIndex(3) + assert.Equal(t, ok, false) + + i, _ := js.Get("test").Get("int").Int() + assert.Equal(t, 10, i) + + f, _ := js.Get("test").Get("float").Float64() + assert.Equal(t, 5.150, f) + + s, _ := js.Get("test").Get("string").String() + assert.Equal(t, "simplejson", s) + + b, _ := js.Get("test").Get("bool").Bool() + assert.Equal(t, true, b) + + mi := js.Get("test").Get("int").MustInt() + assert.Equal(t, 10, mi) + + mi2 := js.Get("test").Get("missing_int").MustInt(5150) + assert.Equal(t, 5150, mi2) + + ms := js.Get("test").Get("string").MustString() + assert.Equal(t, "simplejson", ms) + + ms2 := js.Get("test").Get("missing_string").MustString("fyea") + assert.Equal(t, "fyea", ms2) + + ma2 := js.Get("test").Get("missing_array").MustArray([]any{"1", 2, "3"}) + assert.Equal(t, ma2, []any{"1", 2, "3"}) + + msa := js.Get("test").Get("string_array").MustStringArray() + assert.Equal(t, msa[0], "asdf") + assert.Equal(t, msa[1], "ghjk") + assert.Equal(t, msa[2], "zxcv") + + msa2 := js.Get("test").Get("string_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa2[0], "asdf") + assert.Equal(t, msa2[1], "ghjk") + assert.Equal(t, msa2[2], "zxcv") + + msa3 := js.Get("test").Get("missing_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa3, []string{"1", "2", "3"}) + + mm2 := js.Get("test").Get("missing_map").MustMap(map[string]any{"found": false}) + assert.Equal(t, mm2, map[string]any{"found": false}) + + strs, err := js.Get("test").Get("string_array").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs[0], "asdf") + assert.Equal(t, strs[1], "ghjk") + assert.Equal(t, strs[2], "zxcv") + + strs2, err := js.Get("test").Get("string_array_null").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs2[0], "abc") + assert.Equal(t, strs2[1], "") + assert.Equal(t, strs2[2], "efg") + + gp, _ := js.GetPath("test", "string").String() + assert.Equal(t, "simplejson", gp) + + gp2, _ := js.GetPath("test", "int").Int() + assert.Equal(t, 10, gp2) + + assert.Equal(t, js.Get("test").Get("bool").MustBool(), true) + + js.Set("float2", 300.0) + assert.Equal(t, js.Get("float2").MustFloat64(), 300.0) + + js.Set("test2", "setTest") + assert.Equal(t, "setTest", js.Get("test2").MustString()) + + js.Del("test2") + assert.NotEqual(t, "setTest", js.Get("test2").MustString()) + + js.Get("test").Get("sub_obj").Set("a", 2) + assert.Equal(t, 2, js.Get("test").Get("sub_obj").Get("a").MustInt()) + + js.GetPath("test", "sub_obj").Set("a", 3) + assert.Equal(t, 3, js.GetPath("test", "sub_obj", "a").MustInt()) +} + +func TestStdlibInterfaces(t *testing.T) { + val := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + val2 := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + + raw := `{"name":"myobject","params":{"string":"simplejson"}}` + + assert.Equal(t, nil, json.Unmarshal([]byte(raw), val)) + + assert.Equal(t, "myobject", val.Name) + assert.NotEqual(t, nil, val.Params.data) + s, _ := val.Params.Get("string").String() + assert.Equal(t, "simplejson", s) + + p, err := json.Marshal(val) + assert.Equal(t, nil, err) + assert.Equal(t, nil, json.Unmarshal(p, val2)) + assert.Equal(t, val, val2) // stable +} + +func TestSet(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.Set("baz", "bing") + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestReplace(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + err = js.UnmarshalJSON([]byte(`{"baz":"bing"}`)) + assert.Equal(t, nil, err) + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestSetPath(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"foo", "bar"}, "baz") + + s, err := js.GetPath("foo", "bar").String() + assert.Equal(t, nil, err) + assert.Equal(t, "baz", s) +} + +func TestSetPathNoPath(t *testing.T) { + js, err := NewJson([]byte(`{"some":"data","some_number":1.0,"some_bool":false}`)) + assert.Equal(t, nil, err) + + f := js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 1.0) + + js.SetPath([]string{}, map[string]any{"foo": "bar"}) + + s, err := js.GetPath("foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) + + f = js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 99.0) +} + +func TestPathWillAugmentExisting(t *testing.T) { + js, err := NewJson([]byte(`{"this":{"a":"aa","b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "d"}, "dd") + + cases := []struct { + path []string + outcome string + }{ + { + path: []string{"this", "a"}, + outcome: "aa", + }, + { + path: []string{"this", "b"}, + outcome: "bb", + }, + { + path: []string{"this", "c"}, + outcome: "cc", + }, + { + path: []string{"this", "d"}, + outcome: "dd", + }, + } + + for _, tc := range cases { + s, err := js.GetPath(tc.path...).String() + assert.Equal(t, nil, err) + assert.Equal(t, tc.outcome, s) + } +} + +func TestPathWillOverwriteExisting(t *testing.T) { + // notice how "a" is 0.1 - but then we'll try to set at path a, foo + js, err := NewJson([]byte(`{"this":{"a":0.1,"b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "a", "foo"}, "bar") + + s, err := js.GetPath("this", "a", "foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) +} + +func TestMustJson(t *testing.T) { + js := MustJson([]byte(`{"foo": "bar"}`)) + assert.Equal(t, js.Get("foo").MustString(), "bar") + + assert.PanicsWithValue(t, "could not unmarshal JSON: \"unexpected EOF\"", func() { + MustJson([]byte(`{`)) + }) +} diff --git a/pkg/tsdb/elasticsearch/standalone/datasource.go b/pkg/tsdb/elasticsearch/standalone/datasource.go new file mode 100644 index 00000000000..6b9b8ac3f82 --- /dev/null +++ b/pkg/tsdb/elasticsearch/standalone/datasource.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + elasticsearch "github.com/grafana/grafana/pkg/tsdb/elasticsearch" +) + +var ( + _ backend.QueryDataHandler = (*Datasource)(nil) + _ backend.CheckHealthHandler = (*Datasource)(nil) + _ backend.CallResourceHandler = (*Datasource)(nil) +) + +func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return &Datasource{ + Service: elasticsearch.ProvideService(httpclient.NewProvider()), + }, nil +} + +type Datasource struct { + Service *elasticsearch.Service +} + +func contextualMiddlewares(ctx context.Context) context.Context { + cfg := backend.GrafanaConfigFromContext(ctx) + responseLimitMiddleware := httpclient.ResponseLimitMiddleware(cfg.ResponseLimit()) + ctx = httpclient.WithContextualMiddleware(ctx, responseLimitMiddleware) + return ctx +} + +func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + ctx = contextualMiddlewares(ctx) + return d.Service.QueryData(ctx, req) +} + +func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + ctx = contextualMiddlewares(ctx) + return d.Service.CallResource(ctx, req, sender) +} + +func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + ctx = contextualMiddlewares(ctx) + return d.Service.CheckHealth(ctx, req) +} diff --git a/pkg/tsdb/elasticsearch/standalone/main.go b/pkg/tsdb/elasticsearch/standalone/main.go new file mode 100644 index 00000000000..22bd4169339 --- /dev/null +++ b/pkg/tsdb/elasticsearch/standalone/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func main() { + // Start listening to requests sent from Grafana. This call is blocking so + // it won't finish until Grafana shuts down the process or the plugin choose + // to exit by itself using os.Exit. Manage automatically manages life cycle + // of datasource instances. It accepts datasource instance factory as first + // argument. This factory will be automatically called on incoming request + // from Grafana to create different instances of SampleDatasource (per datasource + // ID). When datasource configuration changed Dispose method will be called and + // new datasource instance created using NewSampleDatasource factory. + if err := datasource.Manage("elasticsearch", NewDatasource, datasource.ManageOpts{}); err != nil { + log.DefaultLogger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/public/api-merged.json b/public/api-merged.json index d511139c7bd..8dc5868bb41 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -4024,12 +4024,14 @@ }, "/dashboards/uid/{uid}/restore": { "post": { + "description": "This API will be removed when /apis/dashboards.grafana.app/v1 is released.\nYou can restore a dashboard by reading it from history, then creating it again.", "tags": [ "dashboards", "versions" ], "summary": "Restore a dashboard to a given dashboard version using UID.", "operationId": "restoreDashboardVersionByUID", + "deprecated": true, "parameters": [ { "name": "Body", diff --git a/public/app/features/alerting/unified/components/Provisioning.test.tsx b/public/app/features/alerting/unified/components/Provisioning.test.tsx new file mode 100644 index 00000000000..4519a8db133 --- /dev/null +++ b/public/app/features/alerting/unified/components/Provisioning.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from 'test/test-utils'; + +import { KnownProvenance } from '../types/knownProvenance'; + +import { ProvisioningBadge } from './Provisioning'; + +describe('ProvisioningBadge', () => { + describe('when the provenance is file', () => { + it('should render the badge with the correct text', () => { + render(); + + expect(screen.getByText('Provisioned')).toBeInTheDocument(); + expect(screen.queryByText('Imported')).not.toBeInTheDocument(); + }); + + it('should render correct tooltip text', async () => { + const { user } = render(); + + const badge = screen.getByText('Provisioned'); + await user.hover(badge); + + expect( + screen.getByText('This resource has been provisioned via file and cannot be edited through the UI') + ).toBeInTheDocument(); + }); + }); + + describe('when the provenance is ConvertedPrometheus', () => { + it('should render the badge with the correct text', () => { + render(); + + expect(screen.getByText('Imported')).toBeInTheDocument(); + expect(screen.queryByText('Provisioned')).not.toBeInTheDocument(); + }); + + it('should render correct tooltip text', async () => { + const { user } = render(); + + const badge = screen.getByText('Imported'); + await user.hover(badge); + + expect( + screen.getByText('This resource has been provisioned via Prometheus/Mimir and cannot be edited through the UI') + ).toBeInTheDocument(); + }); + }); + + describe('when the provenance is API', () => { + it('should render the badge with the correct text', () => { + render(); + + expect(screen.getByText('Provisioned')).toBeInTheDocument(); + expect(screen.queryByText('Imported')).not.toBeInTheDocument(); + }); + + it('should render correct tooltip text', async () => { + const { user } = render(); + + const badge = screen.getByText('Provisioned'); + await user.hover(badge); + + expect( + screen.getByText('This resource has been provisioned via api and cannot be edited through the UI') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/Provisioning.tsx b/public/app/features/alerting/unified/components/Provisioning.tsx index 151b6800d24..5ec3d95687a 100644 --- a/public/app/features/alerting/unified/components/Provisioning.tsx +++ b/public/app/features/alerting/unified/components/Provisioning.tsx @@ -3,6 +3,8 @@ import { ComponentPropsWithoutRef } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Alert, Badge, Tooltip } from '@grafana/ui'; +import { KnownProvenance } from '../types/knownProvenance'; + export enum ProvisionedResource { ContactPoint = 'contact point', Template = 'template', @@ -82,11 +84,17 @@ export const ProvisioningBadge = ({ */ provenance?: string; }) => { - const badge = ; + const isConvertedPrometheus = provenance === KnownProvenance.ConvertedPrometheus; + const badgeText = isConvertedPrometheus + ? t('alerting.provisioning-badge.badge.text-converted-prometheus', 'Imported') + : t('alerting.provisioning-badge.badge.text-provisioned', 'Provisioned'); + const badgeColor = isConvertedPrometheus ? 'blue' : 'purple'; + const badge = ; if (tooltip) { + const provenanceText = isConvertedPrometheus ? 'Prometheus/Mimir' : provenance; const provenanceTooltip = ( - + This resource has been provisioned via {{ provenance }} and cannot be edited through the UI ); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.test.tsx new file mode 100644 index 00000000000..2879bbb57e1 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from 'test/test-utils'; + +import { AccessControlAction } from 'app/types/accessControl'; + +import { setupMswServer } from '../../mockApi'; +import { grantUserPermissions } from '../../mocks'; +import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; +import { KnownProvenance } from '../../types/knownProvenance'; + +import { ContactPointHeader } from './ContactPointHeader'; +import { ContactPointWithMetadata } from './utils'; + +setupMswServer(); + +const renderWithProvider = (component: React.ReactElement, alertmanagerSourceName?: string) => { + return render( + + {component} + + ); +}; + +describe('ContactPointHeader', () => { + beforeEach(() => { + grantUserPermissions([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsWrite, + ]); + }); + + const mockContactPoint: ContactPointWithMetadata = { + id: 'test-contact-point', + name: 'Test Contact Point', + provenance: KnownProvenance.API, + policies: [], + grafana_managed_receiver_configs: [], + }; + + it('shows Provisioned badge when contact point has file provenance via K8s annotations', () => { + const contactPointWithFile = { + ...mockContactPoint, + provenance: KnownProvenance.File, + }; + + renderWithProvider(); + + expect(screen.getByText('Provisioned')).toBeInTheDocument(); + }); + + it('shows correct badge when contact point has converted_prometheus provenance', () => { + const contactPointWithConvertedPrometheus = { + ...mockContactPoint, + provenance: KnownProvenance.ConvertedPrometheus, + }; + + renderWithProvider(); + + expect(screen.getByText('Imported')).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx index 0fb1403cb35..6e45c1b6512 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx @@ -13,6 +13,7 @@ import { canDeleteEntity, canEditEntity, getAnnotation, + isProvisionedResource, shouldUseK8sApi, } from 'app/features/alerting/unified/utils/k8s/utils'; @@ -31,13 +32,15 @@ interface ContactPointHeaderProps { } export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeaderProps) => { - const { name, id, provisioned, policies = [] } = contactPoint; + const { name, id, provenance, policies = [] } = contactPoint; const styles = useStyles2(getStyles); const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false); const { selectedAlertmanager } = useAlertmanager(); const usingK8sApi = shouldUseK8sApi(selectedAlertmanager!); + const isProvisioned = isProvisionedResource(provenance); + const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint); const [editSupported, editAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint); const [deleteSupported, deleteAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint); @@ -70,14 +73,14 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade /** Does the current user have permissions to edit the contact point? */ const hasAbilityToEdit = usingK8sApi ? canEditEntity(contactPoint) : editAllowed; /** Can the contact point actually be edited via the UI? */ - const contactPointIsEditable = !provisioned; + const contactPointIsEditable = !isProvisioned; /** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be edited? */ const canEdit = editSupported && hasAbilityToEdit && contactPointIsEditable; /** Does the current user have permissions to delete the contact point? */ const hasAbilityToDelete = usingK8sApi ? canDeleteEntity(contactPoint) : deleteAllowed; /** Can the contact point actually be deleted, regardless of permissions? i.e. ensuring it isn't provisioned and isn't referenced elsewhere */ - const contactPointIsDeleteable = !provisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules; + const contactPointIsDeleteable = !isProvisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules; /** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be deleted? */ const canBeDeleted = deleteSupported && hasAbilityToDelete && contactPointIsDeleteable; @@ -130,7 +133,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade const reasonsDeleteIsDisabled = [ !hasAbilityToDelete ? cannotDeleteNoPermissions : '', - provisioned ? cannotDeleteProvisioned : '', + isProvisioned ? cannotDeleteProvisioned : '', numberOfPoliciesPreventingDeletion > 0 ? cannotDeletePolicies : '', numberOfRules ? cannotDeleteRules : '', ].filter(Boolean); @@ -209,15 +212,13 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade {referencedByRulesText} )} - {provisioned && ( - - )} + {isProvisioned && } {!isReferencedByAnything && } { }); it('should disable buttons when provisioned', async () => { - const { user } = renderWithProvider(); + const { user } = renderWithProvider( + + ); expect(screen.getByText(/provisioned/i)).toBeInTheDocument(); diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap index 18a5bae9e28..7524d3ba37a 100644 --- a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap @@ -50,7 +50,7 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, }, ], - "provisioned": false, + "provenance": undefined, }, { "grafana_managed_receiver_configs": [ @@ -93,7 +93,7 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, "name": "lotsa-emails", "policies": [], - "provisioned": false, + "provenance": undefined, }, { "grafana_managed_receiver_configs": [ @@ -129,7 +129,7 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, "name": "OnCall Conctact point", "policies": [], - "provisioned": false, + "provenance": undefined, }, { "grafana_managed_receiver_configs": [ @@ -178,7 +178,7 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, }, ], - "provisioned": true, + "provenance": "api", }, { "grafana_managed_receiver_configs": [ @@ -243,7 +243,7 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, "name": "Slack with multiple channels", "policies": [], - "provisioned": false, + "provenance": undefined, }, ], "error": undefined, @@ -301,7 +301,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, }, ], - "provisioned": false, + "provenance": undefined, }, { "grafana_managed_receiver_configs": [ @@ -344,7 +344,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, "name": "lotsa-emails", "policies": [], - "provisioned": false, + "provenance": undefined, }, { "grafana_managed_receiver_configs": [ @@ -383,7 +383,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, "name": "OnCall Conctact point", "policies": [], - "provisioned": false, + "provenance": undefined, }, { "grafana_managed_receiver_configs": [ @@ -432,7 +432,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, }, ], - "provisioned": true, + "provenance": "api", }, { "grafana_managed_receiver_configs": [ @@ -497,7 +497,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, "name": "Slack with multiple channels", "policies": [], - "provisioned": false, + "provenance": undefined, }, ], "error": undefined, diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx index 2ac9c04f981..b539a239bd6 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx @@ -6,10 +6,13 @@ import { disablePlugin } from 'app/features/alerting/unified/mocks/server/config import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins'; import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; +import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; +import { setAlertmanagerConfig } from '../../mocks/server/entities/alertmanagers'; +import { KnownProvenance } from '../../types/knownProvenance'; import { useContactPointsWithStatus } from './useContactPoints'; @@ -69,4 +72,235 @@ describe('useContactPoints', () => { expect(snapshot).toMatchSnapshot(); }); }); + + describe('Provenance handling', () => { + it('should extract provenance when provenance is "api"', async () => { + // Set up alertmanager config with a receiver that has API provenance + const config: AlertManagerCortexConfig = { + template_files: {}, + alertmanager_config: { + receivers: [ + { + name: 'api-provenance-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid-1', + name: 'api-provenance-contact-point', + type: 'email', + disableResolveMessage: false, + settings: { + addresses: 'test@example.com', + }, + secureFields: {}, + provenance: 'api', // This will be used by the K8s mock handler + }, + ], + }, + ], + }, + }; + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config); + + const { result } = renderHook( + () => + useContactPointsWithStatus({ + alertmanager: GRAFANA_RULES_SOURCE_NAME, + fetchPolicies: false, + fetchStatuses: false, + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'api-provenance-contact-point'); + expect(contactPoint).toBeDefined(); + expect(contactPoint?.provenance).toBe(KnownProvenance.API); + }); + + it('should extract provenance when provenance is "file"', async () => { + const config: AlertManagerCortexConfig = { + template_files: {}, + alertmanager_config: { + receivers: [ + { + name: 'file-provenance-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid-2', + name: 'file-provenance-contact-point', + type: 'email', + disableResolveMessage: false, + settings: { + addresses: 'test@example.com', + }, + secureFields: {}, + provenance: 'file', + }, + ], + }, + ], + }, + }; + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config); + + const { result } = renderHook( + () => + useContactPointsWithStatus({ + alertmanager: GRAFANA_RULES_SOURCE_NAME, + fetchPolicies: false, + fetchStatuses: false, + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'file-provenance-contact-point'); + expect(contactPoint).toBeDefined(); + expect(contactPoint?.provenance).toBe(KnownProvenance.File); + }); + + it('should extract provenance when provenance is "converted_prometheus"', async () => { + const config: AlertManagerCortexConfig = { + template_files: {}, + alertmanager_config: { + receivers: [ + { + name: 'mimir-provenance-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid-3', + name: 'mimir-provenance-contact-point', + type: 'email', + disableResolveMessage: false, + settings: { + addresses: 'test@example.com', + }, + secureFields: {}, + provenance: 'converted_prometheus', + }, + ], + }, + ], + }, + }; + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config); + + const { result } = renderHook( + () => + useContactPointsWithStatus({ + alertmanager: GRAFANA_RULES_SOURCE_NAME, + fetchPolicies: false, + fetchStatuses: false, + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'mimir-provenance-contact-point'); + expect(contactPoint).toBeDefined(); + expect(contactPoint?.provenance).toBe(KnownProvenance.ConvertedPrometheus); + }); + + it('should map "none" provenance annotation to undefined', async () => { + const config: AlertManagerCortexConfig = { + template_files: {}, + alertmanager_config: { + receivers: [ + { + name: 'none-provenance-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid-4', + name: 'none-provenance-contact-point', + type: 'email', + disableResolveMessage: false, + settings: { + addresses: 'test@example.com', + }, + secureFields: {}, + // No provenance field - will default to PROVENANCE_NONE in mock handler + }, + ], + }, + ], + }, + }; + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config); + + const { result } = renderHook( + () => + useContactPointsWithStatus({ + alertmanager: GRAFANA_RULES_SOURCE_NAME, + fetchPolicies: false, + fetchStatuses: false, + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'none-provenance-contact-point'); + expect(contactPoint).toBeDefined(); + // The mock handler sets PROVENANCE_NONE ('none') when no provenance is found + // parseK8sReceiver converts 'none' to undefined + expect(contactPoint?.provenance).toBeUndefined(); + }); + + it('should handle missing annotations gracefully', async () => { + // This test verifies that when annotations are undefined, provenance is handled correctly + const config: AlertManagerCortexConfig = { + template_files: {}, + alertmanager_config: { + receivers: [ + { + name: 'no-annotations-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid-5', + name: 'no-annotations-contact-point', + type: 'email', + disableResolveMessage: false, + settings: { + addresses: 'test@example.com', + }, + secureFields: {}, + }, + ], + }, + ], + }, + }; + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config); + + const { result } = renderHook( + () => + useContactPointsWithStatus({ + alertmanager: GRAFANA_RULES_SOURCE_NAME, + fetchPolicies: false, + fetchStatuses: false, + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'no-annotations-contact-point'); + expect(contactPoint).toBeDefined(); + // When annotations are missing, the mock handler should set provenance to undefined + expect(contactPoint?.provenance).toBeUndefined(); + }); + }); }); diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts index 6627d5f69d2..bf5e8e5fcdf 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts @@ -11,7 +11,7 @@ import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } f import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; +import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { GrafanaManagedContactPoint, Receiver } from 'app/plugins/datasource/alertmanager/types'; import { getAPINamespace } from '../../../../../api/utils'; @@ -21,7 +21,9 @@ import { useAsync } from '../../hooks/useAsync'; import { usePluginBridge } from '../../hooks/usePluginBridge'; import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers'; +import { KnownProvenance } from '../../types/knownProvenance'; import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config'; +import { K8sAnnotations } from '../../utils/k8s/constants'; import { enhanceContactPointsWithMetadata } from './utils'; @@ -78,10 +80,13 @@ const useOnCallIntegrations = ({ skip }: Skippable = {}) => { type K8sReceiver = ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver; const parseK8sReceiver = (item: K8sReceiver): GrafanaManagedContactPoint => { + const metadataProvenance = item.metadata.annotations?.[K8sAnnotations.Provenance]; + const provenance = metadataProvenance === KnownProvenance.None ? undefined : metadataProvenance; + return { id: item.metadata.name || item.metadata.uid || item.spec.title, name: item.spec.title, - provisioned: isK8sEntityProvisioned(item), + provenance: provenance, grafana_managed_receiver_configs: item.spec.integrations, metadata: item.metadata, }; diff --git a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts index 91739aeca61..3083b66d300 100644 --- a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts +++ b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts @@ -16,7 +16,8 @@ import { deleteNotificationTemplateAction, updateNotificationTemplateAction, } from '../../reducers/alertmanager/notificationTemplates'; -import { K8sAnnotations, PROVENANCE_NONE } from '../../utils/k8s/constants'; +import { KnownProvenance } from '../../types/knownProvenance'; +import { K8sAnnotations } from '../../utils/k8s/constants'; import { getAnnotation, shouldUseK8sApi } from '../../utils/k8s/utils'; import { ensureDefine } from '../../utils/templates'; import { TemplateFormValues } from '../receivers/TemplateForm'; @@ -79,7 +80,7 @@ function templateGroupsToTemplates( function templateGroupToTemplate( templateGroup: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup ): NotificationTemplate { - const provenance = getAnnotation(templateGroup, K8sAnnotations.Provenance) ?? PROVENANCE_NONE; + const provenance = getAnnotation(templateGroup, K8sAnnotations.Provenance) ?? KnownProvenance.None; return { // K8s entities should always have a metadata.name property. The type is marked as optional because it's also used in other places uid: templateGroup.metadata.name ?? templateGroup.spec.title, @@ -96,8 +97,8 @@ function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemp uid: title, title, content, - // Undefined, null or empty string should be converted to PROVENANCE_NONE - provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE, + // Undefined, null or empty string should be converted to KnownProvenance.None + provenance: (config.template_file_provenances ?? {})[title] || KnownProvenance.None, missing: !templates.includes(title), })); } @@ -272,7 +273,7 @@ export function useValidateNotificationTemplate({ } interface NotificationTemplateMetadata { - isProvisioned: boolean; + provenance?: string; } export function useNotificationTemplateMetadata( @@ -280,11 +281,11 @@ export function useNotificationTemplateMetadata( ): NotificationTemplateMetadata { if (!template) { return { - isProvisioned: false, + provenance: KnownProvenance.None, }; } return { - isProvisioned: Boolean(template.provenance) && template.provenance !== PROVENANCE_NONE, + provenance: template.provenance, }; } diff --git a/public/app/features/alerting/unified/components/contact-points/utils.test.ts b/public/app/features/alerting/unified/components/contact-points/utils.test.ts index e8ded92bf74..ebf064ff6b0 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.test.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.test.ts @@ -1,8 +1,12 @@ +import { GrafanaManagedContactPoint } from 'app/plugins/datasource/alertmanager/types'; + +import { KnownProvenance } from '../../types/knownProvenance'; import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall'; import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from './constants'; import { ReceiverConfigWithMetadata, + enhanceContactPointsWithMetadata, getReceiverDescription, isAutoGeneratedPolicy, summarizeEmailAddresses, @@ -128,3 +132,110 @@ describe('summarizeEmailAddresses', () => { expect(summarizeEmailAddresses('foo@foo.com\n bar@bar.com ')).toBe(output); }); }); + +describe('enhanceContactPointsWithMetadata', () => { + it('should extract provenance from receiver configs when contact point has no provenance', () => { + const contactPoint: GrafanaManagedContactPoint = { + name: 'test-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid', + name: 'test-contact-point', + type: 'email', + settings: { addresses: 'test@example.com' }, + secureFields: {}, + provenance: KnownProvenance.API, + }, + ], + }; + + const enhanced = enhanceContactPointsWithMetadata({ + contactPoints: [contactPoint], + notifiers: [], + status: [], + }); + + expect(enhanced[0].provenance).toBe(KnownProvenance.API); + }); + + it('should prefer contact point provenance over receiver config provenance', () => { + const contactPoint: GrafanaManagedContactPoint = { + name: 'test-contact-point', + provenance: KnownProvenance.File, // Provenance on contact point (from K8s) + grafana_managed_receiver_configs: [ + { + uid: 'test-uid', + name: 'test-contact-point', + type: 'email', + settings: { addresses: 'test@example.com' }, + secureFields: {}, + provenance: KnownProvenance.API, // Different provenance on receiver config + }, + ], + }; + + const enhanced = enhanceContactPointsWithMetadata({ + contactPoints: [contactPoint], + notifiers: [], + status: [], + }); + + expect(enhanced[0].provenance).toBe(KnownProvenance.File); + }); + + it('should extract provenance from first receiver config that has it', () => { + const contactPoint: GrafanaManagedContactPoint = { + name: 'test-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid-1', + name: 'test-contact-point', + type: 'email', + settings: { addresses: 'test@example.com' }, + secureFields: {}, + // No provenance on first receiver + }, + { + uid: 'test-uid-2', + name: 'test-contact-point', + type: 'slack', + settings: { recipient: '#channel' }, + secureFields: {}, + provenance: KnownProvenance.ConvertedPrometheus, // Provenance on second receiver + }, + ], + }; + + const enhanced = enhanceContactPointsWithMetadata({ + contactPoints: [contactPoint], + notifiers: [], + status: [], + }); + + expect(enhanced[0].provenance).toBe(KnownProvenance.ConvertedPrometheus); + }); + + it('should have undefined provenance when neither contact point nor receiver configs have provenance', () => { + const contactPoint: GrafanaManagedContactPoint = { + name: 'test-contact-point', + grafana_managed_receiver_configs: [ + { + uid: 'test-uid', + name: 'test-contact-point', + type: 'email', + settings: { addresses: 'test@example.com' }, + secureFields: {}, + // No provenance + }, + ], + }; + + const enhanced = enhanceContactPointsWithMetadata({ + contactPoints: [contactPoint], + notifiers: [], + status: [], + }); + + expect(enhanced[0].provenance).toBeUndefined(); + }); +}); diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index d2cc43901c1..d24cfc2b0af 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -146,9 +146,16 @@ export function enhanceContactPointsWithMetadata({ const id = getContactPointIdentifier(contactPoint); + // Extract provenance from contactPoint first; else, search in its receivers + const contactPointProvenance = + 'provenance' in contactPoint && contactPoint.provenance !== undefined + ? contactPoint.provenance + : receivers.find((receiver) => Boolean(receiver.provenance))?.provenance; + return { ...contactPoint, id, + provenance: contactPointProvenance, policies: alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? []), grafana_managed_receiver_configs: receivers.map((receiver, index) => { diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx index 94e290a2087..ce0871ac2cd 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx @@ -9,7 +9,7 @@ import { IoK8SApimachineryPkgApisMetaV1ObjectMeta, } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; -import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance'; import { isK8sEntityProvisioned, shouldUseK8sApi, @@ -62,7 +62,7 @@ const parseAmTimeInterval: (interval: MuteTimeInterval, provenance: string) => M return { ...interval, id: interval.name, - provisioned: Boolean(provenance && provenance !== PROVENANCE_NONE), + provisioned: Boolean(provenance && provenance !== KnownProvenance.None), }; }; diff --git a/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx b/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx index 448c409b673..5e85c5f575c 100644 --- a/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx @@ -11,7 +11,7 @@ import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alertin import { FormAmRoute } from 'app/features/alerting/unified/types/amroutes'; import { addUniqueIdentifierToRoute } from 'app/features/alerting/unified/utils/amroutes'; import { getErrorCode, stringifyErrorLike } from 'app/features/alerting/unified/utils/misc'; -import { ObjectMatcher, ROUTES_META_SYMBOL, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; +import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { anyOfRequestState, isError } from '../../hooks/useAsync'; import { useAlertmanager } from '../../state/AlertmanagerContext'; @@ -27,6 +27,7 @@ import { useAddPolicyModal, useAlertGroupsModal, useDeletePolicyModal, useEditPo import { Policy } from './Policy'; import { TIMING_OPTIONS_DEFAULTS } from './timingOptions'; import { + isRouteProvisioned, useAddNotificationPolicy, useDeleteNotificationPolicy, useNotificationPolicyRoute, @@ -99,6 +100,8 @@ export const NotificationPoliciesList = () => { } return; }, [defaultPolicy]); + const routeProvenance = defaultPolicy?.provenance; + const isRootRouteProvisioned = rootRoute ? isRouteProvisioned(rootRoute) : false; // useAsync could also work but it's hard to wait until it's done in the tests // Combining with useEffect gives more predictable results because the condition is in useEffect @@ -244,7 +247,8 @@ export const NotificationPoliciesList = () => { currentRoute={defaults(rootRoute, TIMING_OPTIONS_DEFAULTS)} contactPointsState={contactPointsState.receivers} readOnly={!hasConfigurationAPI} - provisioned={rootRoute[ROUTES_META_SYMBOL]?.provisioned} + provisioned={isRootRouteProvisioned} + provenance={routeProvenance} alertManagerSourceName={selectedAlertmanager} onAddPolicy={openAddModal} onEditPolicy={openEditModal} diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx index a10bca42100..62f9a57ad73 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx @@ -17,6 +17,7 @@ import { import { useAlertmanagerAbilities } from '../../hooks/useAbilities'; import { mockReceiversState } from '../../mocks'; import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; +import { KnownProvenance } from '../../types/knownProvenance'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { @@ -331,6 +332,60 @@ describe('Policy', () => { const customPolicy = screen.getByTestId('am-route-container'); expect(within(customPolicy).getByTestId('matches-all')).toBeInTheDocument(); }); + + it('shows correct badge when policy has file provenance', () => { + const mockRoute: RouteWithID = { + id: 'test-route', + receiver: 'test-receiver', + routes: [], + }; + + renderPolicy( + + ); + + const badge = screen.getByText('Provisioned'); + expect(badge).toBeInTheDocument(); + }); + + it('shows correct badge when policy has converted_prometheus provenance', () => { + const mockRoute: RouteWithID = { + id: 'test-route', + receiver: 'test-receiver', + routes: [], + }; + + renderPolicy( + + ); + + const badge = screen.getByText('Imported'); + expect(badge).toBeInTheDocument(); + }); }); // Doesn't matter which path the routes use, it just needs to match the initialEntries history entry to render the element diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index c4b6a0c55b7..d638273e006 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -61,6 +61,7 @@ interface PolicyComponentProps { contactPointsState?: ReceiversState; readOnly?: boolean; provisioned?: boolean; + provenance?: string; inheritedProperties?: InheritableProperties; routesMatchingFilters?: RoutesMatchingFilters; @@ -89,6 +90,7 @@ const Policy = (props: PolicyComponentProps) => { contactPointsState, readOnly = false, provisioned = false, + provenance, alertManagerSourceName, currentRoute, inheritedProperties, @@ -255,7 +257,7 @@ const Policy = (props: PolicyComponentProps) => { {/* TODO maybe we should move errors to the gutter instead? */} {errors.length > 0 && } - {provisioned && } + {provisioned && } {!isAutoGenerated && !readOnly && ( diff --git a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.test.tsx b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.test.tsx index 512f8f2ac66..a57886534ff 100644 --- a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.test.tsx @@ -1,9 +1,15 @@ import { MatcherOperator, ROUTES_META_SYMBOL, Route } from 'app/plugins/datasource/alertmanager/types'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route } from '../../openapi/routesApi.gen'; +import { KnownProvenance } from '../../types/knownProvenance'; import { ROOT_ROUTE_NAME } from '../../utils/k8s/constants'; -import { createKubernetesRoutingTreeSpec, k8sSubRouteToRoute, routeToK8sSubRoute } from './useNotificationPolicyRoute'; +import { + createKubernetesRoutingTreeSpec, + isRouteProvisioned, + k8sSubRouteToRoute, + routeToK8sSubRoute, +} from './useNotificationPolicyRoute'; test('k8sSubRouteToRoute', () => { const input: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route = { @@ -115,3 +121,86 @@ test('createKubernetesRoutingTreeSpec', () => { expect(tree.metadata.name).toBe(ROOT_ROUTE_NAME); expect(tree).toMatchSnapshot(); }); + +describe('isRouteProvisioned', () => { + it('returns false when route has no provenance', () => { + const route: Route = { + receiver: 'test-receiver', + }; + + expect(isRouteProvisioned(route)).toBeFalsy(); + }); + + it('returns false when route has KnownProvenance.None in metadata', () => { + const route: Route = { + receiver: 'test-receiver', + [ROUTES_META_SYMBOL]: { + provenance: KnownProvenance.None, + }, + }; + + expect(isRouteProvisioned(route)).toBeFalsy(); + }); + + it('returns false when route has KnownProvenance.None at top level', () => { + const route: Route = { + receiver: 'test-receiver', + provenance: KnownProvenance.None, + }; + expect(isRouteProvisioned(route)).toBeFalsy(); + }); + + it('returns true when route has file provenance in metadata', () => { + const route: Route = { + receiver: 'test-receiver', + [ROUTES_META_SYMBOL]: { + provenance: KnownProvenance.File, + }, + }; + + expect(isRouteProvisioned(route)).toBeTruthy(); + }); + + it('returns true when route has api provenance in metadata', () => { + const route: Route = { + receiver: 'test-receiver', + [ROUTES_META_SYMBOL]: { + provenance: KnownProvenance.API, + }, + }; + + expect(isRouteProvisioned(route)).toBeTruthy(); + }); + + it('returns true when route has converted_prometheus provenance in metadata', () => { + const route: Route = { + receiver: 'test-receiver', + [ROUTES_META_SYMBOL]: { + provenance: KnownProvenance.ConvertedPrometheus, + }, + }; + + expect(isRouteProvisioned(route)).toBeTruthy(); + }); + + it('returns true when route has file provenance at top level', () => { + const route: Route = { + receiver: 'test-receiver', + provenance: KnownProvenance.File, + }; + + expect(isRouteProvisioned(route)).toBeTruthy(); + }); + + it('falls back to top-level provenance when metadata provenance is missing', () => { + const route: Route = { + receiver: 'test-receiver', + provenance: KnownProvenance.File, + [ROUTES_META_SYMBOL]: { + provenance: undefined, + }, + }; + + expect(isRouteProvisioned(route)).toBeTruthy(); + }); +}); diff --git a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts index e6a0b7a0cc5..ca9be820463 100644 --- a/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts +++ b/public/app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute.ts @@ -22,8 +22,8 @@ import { } from '../../reducers/alertmanager/notificationPolicyRoutes'; import { FormAmRoute } from '../../types/amroutes'; import { addUniqueIdentifierToRoute } from '../../utils/amroutes'; -import { PROVENANCE_NONE, ROOT_ROUTE_NAME } from '../../utils/k8s/constants'; -import { isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils'; +import { K8sAnnotations, ROOT_ROUTE_NAME } from '../../utils/k8s/constants'; +import { getAnnotation, isProvisionedResource, shouldUseK8sApi } from '../../utils/k8s/utils'; import { routeAdapter } from '../../utils/routeAdapter'; import { InsertPosition, @@ -33,6 +33,11 @@ import { omitRouteFromRouteTree, } from '../../utils/routeTree'; +export function isRouteProvisioned(route: Route): boolean { + const provenance = route[ROUTES_META_SYMBOL]?.provenance ?? route.provenance; + return isProvisionedResource(provenance); +} + const k8sRoutesToRoutesMemoized = memoize(k8sRoutesToRoutes, { maxSize: 1 }); const { @@ -82,7 +87,7 @@ const parseAmConfigRoute = memoize((route: Route): Route => { return { ...route, [ROUTES_META_SYMBOL]: { - provisioned: Boolean(route.provenance && route.provenance !== PROVENANCE_NONE), + provenance: route.provenance, }, }; }); @@ -232,10 +237,11 @@ function k8sRoutesToRoutes(routes: ComGithubGrafanaGrafanaPkgApisAlertingNotific ...route.spec.defaults, routes: route.spec.routes?.map(k8sSubRouteToRoute), [ROUTES_META_SYMBOL]: { - provisioned: isK8sEntityProvisioned(route), + provenance: getAnnotation(route, K8sAnnotations.Provenance), resourceVersion: route.metadata.resourceVersion, name: route.metadata.name, }, + provenance: getAnnotation(route, K8sAnnotations.Provenance), }; }); } diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 5a50ab55cd4..92872e56bef 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -33,6 +33,7 @@ import { AccessControlAction } from 'app/types/accessControl'; import { AITemplateButtonComponent } from '../../enterprise-components/AI/AIGenTemplateButton/addAITemplateButton'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { isProvisionedResource } from '../../utils/k8s/utils'; import { makeAMLink, stringifyErrorLike } from '../../utils/misc'; import { EditorColumnHeader } from '../EditorColumnHeader'; import { ProvisionedResource, ProvisioningAlert } from '../Provisioning'; @@ -122,7 +123,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) // AI feedback state const [aiGeneratedTemplate, setAiGeneratedTemplate] = useState(false); - const { isProvisioned } = useNotificationTemplateMetadata(originalTemplate); + const { provenance } = useNotificationTemplateMetadata(originalTemplate); + const isProvisioned = isProvisionedResource(provenance); const originalTemplatePrefill: TemplateFormValues | undefined = originalTemplate ? { title: originalTemplate.title, content: originalTemplate.content } : undefined; diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx new file mode 100644 index 00000000000..f707d1d6b79 --- /dev/null +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx @@ -0,0 +1,98 @@ +import { render, screen, within } from 'test/test-utils'; + +import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; +import { AccessControlAction } from 'app/types/accessControl'; + +import { setupMswServer } from '../../mockApi'; +import { grantUserPermissions } from '../../mocks'; +import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; +import { KnownProvenance } from '../../types/knownProvenance'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; +import { NotificationTemplate } from '../contact-points/useNotificationTemplates'; + +import { TemplatesTable } from './TemplatesTable'; + +const mockTemplates: Array> = [ + { + uid: 'mimir-template', + title: 'mimir-template', + content: '{{ define "mimir-template" }}Template from Mimir{{ end }}', + provenance: KnownProvenance.ConvertedPrometheus, + }, + { + uid: 'file-template', + title: 'file-template', + content: '{{ define "file-template" }}File provisioned template{{ end }}', + provenance: KnownProvenance.File, + }, + { + uid: 'api-template', + title: 'api-template', + content: '{{ define "api-template" }}API provisioned template{{ end }}', + provenance: KnownProvenance.API, + }, + { + uid: 'no-provenance-template', + title: 'no-provenance-template', + content: '{{ define "no-provenance-template" }}No provenance template{{ end }}', + provenance: KnownProvenance.None, + }, + { + uid: 'undefined-provenance-template', + title: 'undefined-provenance-template', + content: '{{ define "undefined-provenance-template" }}Undefined provenance template{{ end }}', + provenance: undefined, + }, +]; + +const renderWithProvider = (templates: Array>) => { + return render( + + + + + ); +}; + +setupMswServer(); + +describe('TemplatesTable', () => { + beforeEach(() => { + grantUserPermissions([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalRead, + AccessControlAction.AlertingNotificationsExternalWrite, + ]); + }); + + it('shows "Imported" badge for templates with converted_prometheus provenance', () => { + const templates = [mockTemplates[0]]; // mimir-template + renderWithProvider(templates); + + const templateRow = screen.getByRole('row', { name: /mimir-template/i }); + const badge = within(templateRow).getByText('Imported'); + expect(badge).toBeInTheDocument(); + }); + + it('shows "Provisioned" badge for templates with other provenance', () => { + // api and file templates + [mockTemplates[1], mockTemplates[2]].forEach((template) => { + renderWithProvider([template]); + + const templateRow = screen.getByRole('row', { name: new RegExp(template.title ?? '', 'i') }); + const badge = within(templateRow).getByText('Provisioned'); + expect(badge).toBeInTheDocument(); + }); + }); + + it('does not show badge for templates with KnownProvenance.None or empty string provenance', () => { + // no-provenance-template and undefined-provenance-template + [mockTemplates[3], mockTemplates[4]].forEach((template) => { + renderWithProvider([template]); + + const templateRow = screen.getByRole('row', { name: new RegExp(template.title ?? '', 'i') }); + expect(within(templateRow).queryByText('Provisioned')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index ea00e22b280..4f71904dd73 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -10,6 +10,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d import { Authorize } from '../../components/Authorize'; import { AlertmanagerAction } from '../../hooks/useAbilities'; import { getAlertTableStyles } from '../../styles/table'; +import { isProvisionedResource } from '../../utils/k8s/utils'; import { makeAMLink, stringifyErrorLike } from '../../utils/misc'; import { CollapseToggle } from '../CollapseToggle'; import { DetailsField } from '../DetailsField'; @@ -128,7 +129,8 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic const isGrafanaAlertmanager = alertManagerName === GRAFANA_RULES_SOURCE_NAME; const [isExpanded, setIsExpanded] = useState(false); - const { isProvisioned } = useNotificationTemplateMetadata(notificationTemplate); + const { provenance } = useNotificationTemplateMetadata(notificationTemplate); + const isProvisioned = isProvisionedResource(provenance); const { uid, title: name, content: template, missing } = notificationTemplate; const misconfiguredBadgeText = t('alerting.templates.misconfigured-badge-text', 'Misconfigured'); @@ -139,7 +141,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic setIsExpanded(!isExpanded)} /> - {name} {isProvisioned && }{' '} + {name} {isProvisioned && }{' '} {missing && !isGrafanaAlertmanager && ( )} - {contactPoint?.provisioned && hasLegacyIntegrations(contactPoint, grafanaNotifiers) && ( - - )} - {contactPoint?.provisioned && !hasLegacyIntegrations(contactPoint, grafanaNotifiers) && ( + {isProvisioned && hasLegacyIntegrations(contactPoint, grafanaNotifiers) && } + {isProvisioned && !hasLegacyIntegrations(contactPoint, grafanaNotifiers) && ( )} diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx index 6134ace8fbb..866f489e6fa 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx @@ -7,8 +7,8 @@ import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { NotificationChannelOption } from 'app/features/alerting/unified/types/alerting'; +import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { DEFAULT_TEMPLATES } from 'app/features/alerting/unified/utils/template-constants'; import { AccessControlAction } from 'app/types/accessControl'; @@ -68,7 +68,7 @@ describe('getTemplateOptions function', () => { uid: title, title, content, - provenance: PROVENANCE_NONE, + provenance: KnownProvenance.None, }; }); const defaultTemplates = parseTemplates(DEFAULT_TEMPLATES); diff --git a/public/app/features/alerting/unified/mocks/server/entities/k8s/routingtrees.ts b/public/app/features/alerting/unified/mocks/server/entities/k8s/routingtrees.ts index 9babb1332bf..1005267badf 100644 --- a/public/app/features/alerting/unified/mocks/server/entities/k8s/routingtrees.ts +++ b/public/app/features/alerting/unified/mocks/server/entities/k8s/routingtrees.ts @@ -4,7 +4,8 @@ import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route, ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree, } from 'app/features/alerting/unified/openapi/routesApi.gen'; -import { K8sAnnotations, PROVENANCE_NONE, ROOT_ROUTE_NAME } from 'app/features/alerting/unified/utils/k8s/constants'; +import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance'; +import { K8sAnnotations, ROOT_ROUTE_NAME } from 'app/features/alerting/unified/utils/k8s/constants'; import { AlertManagerCortexConfig, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types'; /** @@ -66,7 +67,7 @@ export const getUserDefinedRoutingTree: ( name: ROOT_ROUTE_NAME, namespace: 'default', annotations: { - [K8sAnnotations.Provenance]: PROVENANCE_NONE, + [K8sAnnotations.Provenance]: KnownProvenance.None, }, // Resource versions are much shorter than this in reality, but this is an easy way // for us to mock the concurrency logic and check if the policies have updated since the last fetch diff --git a/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts b/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts index a620fe528c2..177b5c23499 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts @@ -6,8 +6,9 @@ import { } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen'; +import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; const usedByPolicies = ['grafana-default-email']; const usedByRules = ['grafana-default-email']; @@ -23,7 +24,7 @@ const getReceiversList = () => { const provenance = contactPoint.grafana_managed_receiver_configs?.find((integration) => { return integration.provenance; - })?.provenance || PROVENANCE_NONE; + })?.provenance || KnownProvenance.None; return { metadata: { // This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses diff --git a/public/app/features/alerting/unified/mocks/server/handlers/k8s/templates.k8s.ts b/public/app/features/alerting/unified/mocks/server/handlers/k8s/templates.k8s.ts index f3baa318342..fff0d120629 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/k8s/templates.k8s.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/k8s/templates.k8s.ts @@ -3,8 +3,9 @@ import { HttpResponse, http } from 'msw'; import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup } from 'app/features/alerting/unified/openapi/templatesApi.gen'; +import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +import { PROVENANCE_ANNOTATION } from 'app/features/alerting/unified/utils/k8s/constants'; const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); @@ -14,7 +15,7 @@ const mappedTemplates = Object.entries( ).map(([title, template]) => ({ metadata: { name: titleToK8sResourceName(title), // K8s uses unique identifiers for resources - annotations: { [PROVENANCE_ANNOTATION]: config.template_file_provenances?.[title] || PROVENANCE_NONE }, + annotations: { [PROVENANCE_ANNOTATION]: config.template_file_provenances?.[title] || KnownProvenance.None }, }, spec: { title: title, diff --git a/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts b/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts index 84503c2ce13..77f6eef9f50 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts @@ -4,7 +4,8 @@ import { base64UrlEncode } from '@grafana/alerting'; import { filterBySelector } from 'app/features/alerting/unified/mocks/server/handlers/k8s/utils'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; -import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance'; +import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; /** UID of a time interval that we expect to follow all happy paths within tests/mocks */ export const TIME_INTERVAL_UID_HAPPY_PATH = 'f4eae7a4895fa786'; @@ -21,7 +22,7 @@ const allTimeIntervals = getK8sResponse { it('should escape backslashes', () => { @@ -25,3 +27,29 @@ describe('encodeFieldSelector', () => { expect(encodeFieldSelector('foo=bar,bar=baz,qux\\foo')).toBe('foo\\=bar\\,bar\\=baz\\,qux\\\\foo'); }); }); + +describe('isProvisionedResource', () => { + it('should return true when provenance is API', () => { + expect(isProvisionedResource(KnownProvenance.API)).toBe(true); + }); + + it('should return true when provenance is File', () => { + expect(isProvisionedResource(KnownProvenance.File)).toBe(true); + }); + + it('should return true when provenance is ConvertedPrometheus', () => { + expect(isProvisionedResource(KnownProvenance.ConvertedPrometheus)).toBe(true); + }); + + it('should return false when provenance is none', () => { + expect(isProvisionedResource(KnownProvenance.None)).toBe(false); + }); + + it('should return false when provenance is undefined', () => { + expect(isProvisionedResource(undefined)).toBe(false); + }); + + it('should return true for any other non-empty string', () => { + expect(isProvisionedResource('custom-provenance')).toBe(true); + }); +}); diff --git a/public/app/features/alerting/unified/utils/k8s/utils.ts b/public/app/features/alerting/unified/utils/k8s/utils.ts index 48ec5685a69..015ba8f17a2 100644 --- a/public/app/features/alerting/unified/utils/k8s/utils.ts +++ b/public/app/features/alerting/unified/utils/k8s/utils.ts @@ -1,6 +1,8 @@ import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/unified/openapi/receiversApi.gen'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; + +import { KnownProvenance } from '../../types/knownProvenance'; /** * Should we call the kubernetes-style API for managing alertmanager entities? @@ -22,7 +24,7 @@ type EntityToCheck = { */ export const isK8sEntityProvisioned = (k8sEntity: EntityToCheck) => { const provenance = getAnnotation(k8sEntity, K8sAnnotations.Provenance); - return Boolean(provenance && provenance !== PROVENANCE_NONE); + return isProvisionedResource(provenance); }; export const ANNOTATION_PREFIX_ACCESS = 'grafana.com/access/'; @@ -59,3 +61,7 @@ export const stringifyFieldSelector = (fieldSelectors: FieldSelector[]): string .map(([key, value, operator = '=']) => `${key}${operator}${encodeFieldSelector(value)}`) .join(','); }; + +export function isProvisionedResource(provenance?: string): boolean { + return Boolean(provenance && provenance !== KnownProvenance.None); +} diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index c5d01b433e3..4b0e109e864 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -781,6 +781,10 @@ export function tabItemToSaveModel( panels: [], }; + if (tab.state.repeatByVariable) { + rowPanel.repeat = tab.state.repeatByVariable; + } + panelsArray.push(rowPanel); // The base Y position for panels in this tab (after the row panel) @@ -912,6 +916,15 @@ function autoGridLayoutToPanels(layout: AutoGridLayoutManager, isSnapshot = fals }, isSnapshot ); + + // Handle repeat properties for AutoGridItem + // AutoGrid always uses horizontal direction, and maxPerRow is derived from maxColumnCount + if (item.state.variableName) { + panel.repeat = item.state.variableName; + panel.repeatDirection = 'h'; + panel.maxPerRow = maxColumnCount; + } + panels.push(panel); // Move to next position diff --git a/public/app/features/datasources/state/buildCategories.test.ts b/public/app/features/datasources/state/buildCategories.test.ts index 3231a029ff7..da64a6b412d 100644 --- a/public/app/features/datasources/state/buildCategories.test.ts +++ b/public/app/features/datasources/state/buildCategories.test.ts @@ -53,7 +53,7 @@ describe('buildCategories', () => { it('should add enterprise phantom plugins', () => { const enterprisePluginsCategory = categories[3]; expect(enterprisePluginsCategory.title).toBe('Enterprise plugins'); - expect(enterprisePluginsCategory.plugins.length).toBe(31); + expect(enterprisePluginsCategory.plugins.length).toBe(32); expect(enterprisePluginsCategory.plugins[0].name).toBe('Adobe Analytics'); expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Zendesk'); }); diff --git a/public/app/features/datasources/state/buildCategories.ts b/public/app/features/datasources/state/buildCategories.ts index 7aeeed6abb4..1be01fae638 100644 --- a/public/app/features/datasources/state/buildCategories.ts +++ b/public/app/features/datasources/state/buildCategories.ts @@ -13,6 +13,7 @@ import catchpointSvg from 'img/plugins/catchpoint.svg'; import cloudflareJpg from 'img/plugins/cloudflare.jpg'; import cockroachdbJpg from 'img/plugins/cockroachdb.jpg'; import datadogPng from 'img/plugins/datadog.png'; +import db2Svg from 'img/plugins/db2.svg'; import droneSvg from 'img/plugins/drone.svg'; import dynatracePng from 'img/plugins/dynatrace.png'; import gitlabSvg from 'img/plugins/gitlab.svg'; @@ -418,6 +419,12 @@ function getEnterprisePhantomPlugins(): DataSourcePluginMeta[] { name: 'SolarWinds', imgUrl: solarWindsSvg, }), + getPhantomPlugin({ + id: 'grafana-ibmdb2-datasource', + description: t('datasources.get-enterprise-phantom-plugins.description.ibmdb2-datasource', 'IBM Db2 data source'), + name: 'IBM Db2', + imgUrl: db2Svg, + }), ]; } diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index a529952eff3..03025e484cb 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -4,8 +4,6 @@ const cloudwatchPlugin = async () => await import(/* webpackChunkName: "cloudwatchPlugin" */ 'app/plugins/datasource/cloudwatch/module'); const dashboardDSPlugin = async () => await import(/* webpackChunkName "dashboardDSPlugin" */ 'app/plugins/datasource/dashboard/module'); -const elasticsearchPlugin = async () => - await import(/* webpackChunkName: "elasticsearchPlugin" */ 'app/plugins/datasource/elasticsearch/module'); const grafanaPlugin = async () => await import(/* webpackChunkName: "grafanaPlugin" */ 'app/plugins/datasource/grafana/module'); const influxdbPlugin = async () => @@ -75,7 +73,6 @@ const builtInPlugins: Record Promise info.group === 'dashboard.grafana.app' && info.kind === 'Dashboard' - )?.names || []; + const items = result.data.items; + starsIds = items?.length + ? items[0].spec.resource.find(({ group, kind }) => group === DASHBOARD_API_GROUP && kind === 'Dashboard') + ?.names || [] + : []; } else { starsIds = await dispatch(legacyUserAPI.endpoints.getStars.initiate()).unwrap(); } @@ -331,7 +335,7 @@ export class UnifiedSearcher implements GrafanaSearcher { } if (query.deleted) { - uri = `${getAPIBaseURL('dashboard.grafana.app', 'v1beta1')}/dashboards/?labelSelector=grafana.app/get-trash=true`; + uri = `${getAPIBaseURL(DASHBOARD_API_GROUP, 'v1beta1')}/dashboards/?labelSelector=grafana.app/get-trash=true`; } return uri; } diff --git a/public/app/features/theme-playground/ThemePlayground.tsx b/public/app/features/theme-playground/ThemePlayground.tsx index 85dee240c7f..d331f3932bc 100644 --- a/public/app/features/theme-playground/ThemePlayground.tsx +++ b/public/app/features/theme-playground/ThemePlayground.tsx @@ -2,8 +2,20 @@ import { css } from '@emotion/css'; import { useId, useState } from 'react'; import { createTheme, GrafanaTheme2, NewThemeOptions } from '@grafana/data'; -import { experimentalThemeDefinitions, NewThemeOptionsSchema } from '@grafana/data/internal'; -import { themeJsonSchema } from '@grafana/data/unstable'; +import { NewThemeOptionsSchema } from '@grafana/data/internal'; +import aubergine from '@grafana/data/themes/definitions/aubergine.json'; +import debug from '@grafana/data/themes/definitions/debug.json'; +import desertbloom from '@grafana/data/themes/definitions/desertbloom.json'; +import gildedgrove from '@grafana/data/themes/definitions/gildedgrove.json'; +import gloom from '@grafana/data/themes/definitions/gloom.json'; +import mars from '@grafana/data/themes/definitions/mars.json'; +import matrix from '@grafana/data/themes/definitions/matrix.json'; +import sapphiredusk from '@grafana/data/themes/definitions/sapphiredusk.json'; +import synthwave from '@grafana/data/themes/definitions/synthwave.json'; +import tron from '@grafana/data/themes/definitions/tron.json'; +import victorian from '@grafana/data/themes/definitions/victorian.json'; +import zen from '@grafana/data/themes/definitions/zen.json'; +import themeJsonSchema from '@grafana/data/themes/schema.generated.json'; import { t } from '@grafana/i18n'; import { useChromeHeaderHeight } from '@grafana/runtime'; import { CodeEditor, Combobox, Field, Stack, useStyles2 } from '@grafana/ui'; @@ -34,8 +46,23 @@ const themeMap: Record = { }, }; +const experimentalDefinitions: Record = { + aubergine, + debug, + desertbloom, + gildedgrove, + gloom, + mars, + matrix, + sapphiredusk, + synthwave, + tron, + victorian, + zen, +}; + // Add additional themes -for (const [name, json] of Object.entries(experimentalThemeDefinitions)) { +for (const [name, json] of Object.entries(experimentalDefinitions)) { const result = NewThemeOptionsSchema.safeParse(json); if (!result.success) { console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`); diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index 3fb512c88c6..1f964e6b664 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -108,7 +108,7 @@ export interface GrafanaManagedContactPoint { /** If parsed from k8s API, we'll have an ID property */ id?: string; metadata?: IoK8SApimachineryPkgApisMetaV1ObjectMeta; - provisioned?: boolean; + provenance?: string; grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[]; } @@ -148,7 +148,7 @@ export type Route = { provenance?: string; /** this is used to add additional metadata to the routes without interfering with original route definition (symbols aren't iterable) */ [ROUTES_META_SYMBOL]?: { - provisioned?: boolean; + provenance?: string; resourceVersion?: string; name?: string; }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx index d866ad49185..e3ddd378078 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx @@ -1,10 +1,10 @@ import { memo } from 'react'; -import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; import { ConnectionConfig } from '@grafana/google-sdk'; import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui'; -import { reportInteraction, config } from '@grafana/runtime'; -import { Divider, SecureSocksProxySettings } from '@grafana/ui'; +import { config, reportInteraction } from '@grafana/runtime'; +import { Divider, Field, Input, SecureSocksProxySettings, Stack } from '@grafana/ui'; import { CloudMonitoringOptions, CloudMonitoringSecureJsonData } from '../../types/types'; @@ -36,14 +36,33 @@ export const ConfigEditor = memo(({ options, onOptionsChange }: Props) => { - + + + + updateDatasourcePluginJsonDataOption( + { options, onOptionsChange }, + 'universeDomain', + event.currentTarget.value + ) + } + placeholder="googleapis.com" + > + + + )} + ); }); diff --git a/public/app/plugins/datasource/cloud-monitoring/types/types.ts b/public/app/plugins/datasource/cloud-monitoring/types/types.ts index 849f72ea69b..f2e17444fb0 100644 --- a/public/app/plugins/datasource/cloud-monitoring/types/types.ts +++ b/public/app/plugins/datasource/cloud-monitoring/types/types.ts @@ -38,6 +38,7 @@ export interface Aggregation { export interface CloudMonitoringOptions extends DataSourceOptions { gceDefaultProject?: string; enableSecureSocksProxy?: boolean; + universeDomain?: string; } export interface CloudMonitoringSecureJsonData extends DataSourceSecureJsonData {} diff --git a/public/app/plugins/datasource/elasticsearch/CHANGELOG.md b/public/app/plugins/datasource/elasticsearch/CHANGELOG.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx index a9d0ad0710c..110e57e33f2 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx @@ -1,8 +1,7 @@ import { render, screen } from '@testing-library/react'; import { select } from 'react-select-event'; -import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { DateHistogram } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx index 0dda5b13703..3ca8cb46631 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx @@ -4,9 +4,9 @@ import { GroupBase, OptionsOrGroups } from 'react-select'; import { InternalTimeZones, SelectableValue } from '@grafana/data'; import { InlineField, Input, Select, TimeZonePicker } from '@grafana/ui'; -import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { calendarIntervals } from '../../../../QueryBuilder'; +import { DateHistogram } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour'; import { changeBucketAggregationSetting } from '../state/actions'; @@ -37,11 +37,11 @@ const hasValue = const isValidNewOption = ( inputValue: string, _: SelectableValue | null, - options: OptionsOrGroups> + options: OptionsOrGroups, GroupBase>> ) => { // TODO: would be extremely nice here to allow only template variables and values that are // valid date histogram's Interval options - const valueExists = (options as Array>).some(hasValue(inputValue)); + const valueExists = options.some(hasValue(inputValue)); // we also don't want users to create "empty" values return !valueExists && inputValue.trim().length > 0; }; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx index 6fded5be996..ddfd02b0cc4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx @@ -3,8 +3,8 @@ import { uniqueId } from 'lodash'; import { useEffect, useRef } from 'react'; import { InlineField, Input, QueryField } from '@grafana/ui'; -import { Filters } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filters } from '../../../../../dataquery.gen'; import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer'; import { AddRemove } from '../../../../AddRemove'; import { changeBucketAggregationSetting } from '../../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts index e60a664f066..a5e3cb3d172 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts @@ -1,6 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filter } from '../../../../../../dataquery.gen'; export const addFilter = createAction('@bucketAggregations/filter/add'); export const removeFilter = createAction('@bucketAggregations/filter/remove'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts index 56b5ac9555c..eb03fcc96a3 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts @@ -1,6 +1,5 @@ -import { reducerTester } from 'test/core/redux/reducerTester'; - -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filter } from '../../../../../../dataquery.gen'; +import { reducerTester } from '../../../../../reducerTester'; import { addFilter, changeFilter, removeFilter } from './actions'; import { reducer } from './reducer'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts index 022de8233b4..b99818d1850 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from 'redux'; -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { Filter } from '../../../../../../dataquery.gen'; import { defaultFilter } from '../utils'; import { addFilter, changeFilter, removeFilter } from './actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts index adf5646381d..3538a497bf4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts @@ -1,3 +1,3 @@ -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filter } from '../../../../../dataquery.gen'; export const defaultFilter = (): Filter => ({ label: '', query: '*' }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx index 14862e8f664..9012730c8e2 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx @@ -1,14 +1,7 @@ import { fireEvent, screen } from '@testing-library/react'; import selectEvent from 'react-select-event'; -import { - Average, - Derivative, - ElasticsearchDataQuery, - Terms, - TopMetrics, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { Average, Derivative, ElasticsearchDataQuery, Terms, TopMetrics } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { renderWithESProvider } from '../../../../test-helpers/render'; import { describeMetric } from '../../../../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx index e1bc64980ab..852b7d8bb84 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx @@ -2,15 +2,9 @@ import { uniqueId } from 'lodash'; import { useRef } from 'react'; import { SelectableValue } from '@grafana/data'; -import { InlineField, Select, Input } from '@grafana/ui'; -import { - Terms, - ExtendedStats, - ExtendedStatMetaType, - Percentiles, - MetricAggregation, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { InlineField, Input, Select } from '@grafana/ui'; +import { ExtendedStats, MetricAggregation, Percentiles, Terms } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { describeMetric } from '../../../../utils'; import { useQuery } from '../../ElasticsearchQueryContext'; @@ -105,7 +99,7 @@ function createOrderByOptionsForExtendedStats(metric: ExtendedStats): Selectable if (!metric.meta) { return []; } - const metaKeys = Object.keys(metric.meta) as ExtendedStatMetaType[]; + const metaKeys = Object.keys(metric.meta); return metaKeys .filter((key) => metric.meta?.[key]) .map((key) => { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx index 9191ca25d1c..63be91fad00 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx @@ -2,8 +2,8 @@ import { uniqueId } from 'lodash'; import { ComponentProps, useRef } from 'react'; import { InlineField, Input } from '@grafana/ui'; -import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { BucketAggregation } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { SettingsEditorContainer } from '../../SettingsEditorContainer'; import { changeBucketAggregationSetting } from '../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts index 3e4e5cfea7c..a0f60b799a4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts @@ -1,7 +1,6 @@ -import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { BucketAggregation } from '../../../../dataquery.gen'; import { defaultGeoHashPrecisionString } from '../../../../queryDef'; -import { describeMetric, convertOrderByToMetricId } from '../../../../utils'; +import { convertOrderByToMetricId, describeMetric } from '../../../../utils'; import { useQuery } from '../../ElasticsearchQueryContext'; import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts index dfab9ac0279..e3dff091246 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts @@ -1,10 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { - BucketAggregation, - BucketAggregationType, - BucketAggregationWithField, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { BucketAggregation, BucketAggregationType, BucketAggregationWithField } from '../../../../dataquery.gen'; export const addBucketAggregation = createAction('@bucketAggs/add'); export const removeBucketAggregation = createAction('@bucketAggs/remove'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts index f4a5cc02dde..462f5938b81 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts @@ -1,9 +1,4 @@ -import { - BucketAggregation, - DateHistogram, - ElasticsearchDataQuery, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { BucketAggregation, DateHistogram, ElasticsearchDataQuery } from '../../../../dataquery.gen'; import { defaultBucketAgg } from '../../../../queryDef'; import { reducerTester } from '../../../reducerTester'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts index 5ba29e656d8..789405c97be 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from '@reduxjs/toolkit'; -import { BucketAggregation, ElasticsearchDataQuery, Terms } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { BucketAggregation, ElasticsearchDataQuery, Terms } from '../../../../dataquery.gen'; import { defaultBucketAgg } from '../../../../queryDef'; import { removeEmpty } from '../../../../utils'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; @@ -47,11 +46,12 @@ export const createReducer = } /* - TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations - in the new selected one (such as field or some settings). - It the future would be nice to have the same behavior but it's hard without a proper definition, - as Elasticsearch will error sometimes if some settings are not compatible. - */ + TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations + in the new selected one (such as field or some settings). + It the future would be nice to have the same behavior but it's hard without a proper definition, + as Elasticsearch will error sometimes if some settings are not compatible. + */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { id: bucketAgg.id, type: action.payload.newType, diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx index 70d53eddbc4..348a8a630ce 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx @@ -2,10 +2,10 @@ import { css } from '@emotion/css'; import { uniqueId } from 'lodash'; import { Fragment, useEffect } from 'react'; -import { Input, InlineLabel } from '@grafana/ui'; -import { BucketScript, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { InlineLabel, Input } from '@grafana/ui'; -import { useStatelessReducer, useDispatch } from '../../../../../hooks/useStatelessReducer'; +import { BucketScript, MetricAggregation } from '../../../../../dataquery.gen'; +import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer'; import { AddRemove } from '../../../../AddRemove'; import { MetricPicker } from '../../../../MetricPicker'; import { changeMetricAttribute } from '../../state/actions'; @@ -13,9 +13,9 @@ import { SettingField } from '../SettingField'; import { addPipelineVariable, + changePipelineVariableMetric, removePipelineVariable, renamePipelineVariable, - changePipelineVariableMetric, } from './state/actions'; import { reducer } from './state/reducer'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts index 02fc628ecc3..276ca285c64 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts @@ -1,5 +1,4 @@ -import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { PipelineVariable } from '../../../../../../dataquery.gen'; import { reducerTester } from '../../../../../reducerTester'; import { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts index 406b1f5b590..8d798a5717b 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from '@reduxjs/toolkit'; -import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { PipelineVariable } from '../../../../../../dataquery.gen'; import { defaultPipelineVariable, generatePipelineVariableName } from '../utils'; import { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts index e2da781d190..4c3991c69a9 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts @@ -1,4 +1,4 @@ -import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { PipelineVariable } from '../../../../../dataquery.gen'; export const defaultPipelineVariable = (name: string): PipelineVariable => ({ name, pipelineAgg: '' }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx index e4a3ad907a9..588b4692f5e 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx @@ -2,11 +2,8 @@ import { uniqueId } from 'lodash'; import { ComponentProps, useState } from 'react'; import { InlineField, Input, TextArea } from '@grafana/ui'; -import { - MetricAggregationWithSettings, - MetricAggregationWithInlineScript, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { MetricAggregationWithInlineScript, MetricAggregationWithSettings } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { getScriptValue } from '../../../../utils'; import { SettingKeyOf } from '../../../types'; @@ -33,9 +30,11 @@ export function SettingField (object: { value: string }) => object.value === value; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts index 9adff8781b1..b0b52dd39e7 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts @@ -1,7 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { MetricAggregation, MetricAggregationWithSettings } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { MetricAggregation, MetricAggregationWithSettings } from '../../../../dataquery.gen'; import { MetricAggregationWithMeta } from '../../../../types'; export const addMetric = createAction('@metrics/add'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts index 9dcbaa9f974..38a4e0f05d1 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts @@ -1,10 +1,4 @@ -import { - MetricAggregation, - ElasticsearchDataQuery, - Derivative, - ExtendedStats, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { Derivative, ElasticsearchDataQuery, ExtendedStats, MetricAggregation } from '../../../../dataquery.gen'; import { defaultMetricAgg } from '../../../../queryDef'; import { reducerTester } from '../../../reducerTester'; import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts index c0dab7bd4b1..57acf1e87e5 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from '@reduxjs/toolkit'; -import { ElasticsearchDataQuery, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { ElasticsearchDataQuery, MetricAggregation } from '../../../../dataquery.gen'; import { defaultMetricAgg, queryTypeToMetricType } from '../../../../queryDef'; import { removeEmpty } from '../../../../utils'; import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; @@ -57,6 +56,7 @@ export const reducer = ( It the future would be nice to have the same behavior but it's hard without a proper definition, as Elasticsearch will error sometimes if some settings are not compatible. */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { id: metric.id, type: action.payload.type, diff --git a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts index 79e2fddbc2d..b6a53173cdd 100644 --- a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts +++ b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts @@ -2,7 +2,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import { Action } from 'redux'; -import { StoreState } from 'app/types/store'; +import { StoreState } from '../types/store'; type GrafanaReducer = (state: S, action: A) => S; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ApiKeyConfig.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ApiKeyConfig.tsx new file mode 100644 index 00000000000..8433160d4f2 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/ApiKeyConfig.tsx @@ -0,0 +1,22 @@ +import { onUpdateDatasourceSecureJsonDataOption, updateDatasourcePluginResetOption } from '@grafana/data'; +import { InlineField, SecretInput } from '@grafana/ui'; + +import { Props } from './ConfigEditor'; + +export const ApiKeyConfig = (props: Props) => { + const { options } = props; + + return ( + + updateDatasourcePluginResetOption(props, 'apiKey')} + onChange={onUpdateDatasourceSecureJsonDataOption(props, 'apiKey')} + /> + + ); +}; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx index 5961ebef510..57e3f8dc92a 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx @@ -14,14 +14,15 @@ import { import { config } from '@grafana/runtime'; import { Alert, SecureSocksProxySettings, Divider, Stack } from '@grafana/ui'; -import { ElasticsearchOptions } from '../types'; +import { ElasticsearchOptions, ElasticsearchSecureJsonData } from '../types'; +import { ApiKeyConfig } from './ApiKeyConfig'; import { DataLinks } from './DataLinks'; import { ElasticDetails } from './ElasticDetails'; import { LogsConfig } from './LogsConfig'; import { coerceOptions, isValidOptions } from './utils'; -export type Props = DataSourcePluginOptionsEditorProps; +export type Props = DataSourcePluginOptionsEditorProps; export const ConfigEditor = (props: Props) => { const { options, onOptionsChange } = props; @@ -48,6 +49,16 @@ export const ConfigEditor = (props: Props) => { authProps.selectedMethod = options.jsonData.sigV4Auth ? 'custom-sigv4' : authProps.selectedMethod; } + authProps.customMethods = [ + { + id: 'custom-api-key', + label: 'API Key', + description: 'API Key authentication', + component: , + }, + ]; + authProps.selectedMethod = options.jsonData.apiKeyAuth ? 'custom-api-key' : authProps.selectedMethod; + return ( <> {options.access === 'direct' && ( @@ -73,6 +84,7 @@ export const ConfigEditor = (props: Props) => { jsonData: { ...options.jsonData, sigV4Auth: method === 'custom-sigv4', + apiKeyAuth: method === 'custom-api-key', oauthPassThru: method === AuthMethod.OAuthForward, }, }); diff --git a/public/app/plugins/datasource/elasticsearch/jest-setup.js b/public/app/plugins/datasource/elasticsearch/jest-setup.js new file mode 100644 index 00000000000..c85bf9d3a57 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/jest-setup.js @@ -0,0 +1 @@ +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/elasticsearch/jest.config.js b/public/app/plugins/datasource/elasticsearch/jest.config.js new file mode 100644 index 00000000000..fabef448081 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/jest.config.js @@ -0,0 +1,3 @@ +import defaultConfig from '@grafana/plugin-configs/jest/jest.config.js'; + +export default defaultConfig; diff --git a/public/app/plugins/datasource/elasticsearch/package.json b/public/app/plugins/datasource/elasticsearch/package.json new file mode 100644 index 00000000000..bd1470fa2c6 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/package.json @@ -0,0 +1,62 @@ +{ + "name": "@grafana-plugins/elasticsearch", + "description": "Grafana data source for Elasticsearch", + "private": true, + "version": "12.4.0-pre", + "dependencies": { + "@emotion/css": "11.13.5", + "@grafana/aws-sdk": "0.8.3", + "@grafana/data": "12.4.0-pre", + "@grafana/plugin-ui": "^0.11.1", + "@grafana/runtime": "12.4.0-pre", + "@grafana/schema": "12.4.0-pre", + "@grafana/ui": "12.4.0-pre", + "@reduxjs/toolkit": "2.10.1", + "lodash": "4.17.21", + "lucene": "^2.1.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-redux": "9.2.0", + "react-select": "5.10.2", + "react-use": "17.6.0", + "redux": "5.0.1", + "redux-thunk": "3.1.0", + "rxjs": "7.8.2", + "semver": "7.7.3", + "tslib": "2.8.1" + }, + "devDependencies": { + "@grafana/e2e-selectors": "12.4.0-pre", + "@grafana/plugin-configs": "12.4.0-pre", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.6.4", + "@testing-library/react": "16.3.0", + "@testing-library/user-event": "14.6.1", + "@types/jest": "29.5.14", + "@types/lodash": "4.17.20", + "@types/lucene": "^2", + "@types/node": "24.10.1", + "@types/react": "18.3.18", + "@types/react-dom": "18.3.5", + "@types/semver": "7.7.1", + "jest": "29.7.0", + "react-select-event": "5.5.1", + "ts-node": "10.9.2", + "typescript": "5.9.2", + "webpack": "5.101.0" + }, + "peerDependencies": { + "@grafana/runtime": "*" + }, + "resolutions": { + "redux": "^5.0.0" + }, + "scripts": { + "build": "webpack -c ./webpack.config.ts --env production", + "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "webpack -w -c ./webpack.config.ts --env development", + "test": "jest --watch --onlyChanged", + "test:ci": "jest --maxWorkers 4" + }, + "packageManager": "yarn@4.11.0" +} diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index 0e056ffa447..9440fcfed54 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -2,6 +2,7 @@ "type": "datasource", "name": "Elasticsearch", "id": "elasticsearch", + "executable": "gpx_elasticsearch", "category": "logging", "info": { "description": "Open source logging & analytics database", @@ -27,7 +28,8 @@ "name": "Documentation", "url": "https://grafana.com/docs/grafana/latest/datasources/elasticsearch/" } - ] + ], + "version": "%VERSION%" }, "alerting": true, "annotations": true, @@ -36,5 +38,9 @@ "backend": true, "queryOptions": { "minInterval": true + }, + "dependencies": { + "grafanaDependency": ">=11.6.0", + "plugins": [] } } diff --git a/public/app/plugins/datasource/elasticsearch/project.json b/public/app/plugins/datasource/elasticsearch/project.json new file mode 100644 index 00000000000..4247352791d --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/project.json @@ -0,0 +1,9 @@ +{ + "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "tags": ["scope:plugin", "type:datasource"], + "targets": { + "build": {}, + "dev": {} + } +} diff --git a/public/app/plugins/datasource/elasticsearch/reducers/actions/cleanUp.ts b/public/app/plugins/datasource/elasticsearch/reducers/actions/cleanUp.ts new file mode 100644 index 00000000000..3aa81581e06 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/reducers/actions/cleanUp.ts @@ -0,0 +1,11 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { StoreState } from '../../types/store'; + +export type CleanUpAction = (state: StoreState) => void; + +export interface CleanUpPayload { + cleanupAction: CleanUpAction; +} + +export const cleanUpAction = createAction('core/cleanUpState'); diff --git a/public/app/plugins/datasource/elasticsearch/reducers/root.ts b/public/app/plugins/datasource/elasticsearch/reducers/root.ts new file mode 100644 index 00000000000..5e13826691a --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/reducers/root.ts @@ -0,0 +1,21 @@ +import { ReducersMapObject } from '@reduxjs/toolkit'; +import { Action as AnyAction, combineReducers } from 'redux'; + +const addedReducers = { + defaultReducer: (state = {}) => state, + templating: (state = { lastKey: 'key' }) => state, +}; + +export const addReducer = (newReducers: ReducersMapObject) => { + Object.assign(addedReducers, newReducers); +}; + +export const createRootReducer = () => { + const appReducer = combineReducers({ + ...addedReducers, + }); + + return (state: Parameters[0], action: AnyAction) => { + return appReducer(state, action); + }; +}; diff --git a/public/app/plugins/datasource/elasticsearch/store/configureStore.ts b/public/app/plugins/datasource/elasticsearch/store/configureStore.ts new file mode 100644 index 00000000000..319cccd193d --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/store/configureStore.ts @@ -0,0 +1,47 @@ +import { createListenerMiddleware, configureStore as reduxConfigureStore } from '@reduxjs/toolkit'; +import { setupListeners } from '@reduxjs/toolkit/query'; +import { Middleware } from 'redux'; + +import { addReducer, createRootReducer } from '../reducers/root'; +import { StoreState } from '../types/store'; + +import { setStore } from './store'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function addRootReducer(reducers: any) { + // this is ok now because we add reducers before configureStore is called + // in the future if we want to add reducers during runtime + // we'll have to solve this in a more dynamic way + addReducer(reducers); +} + +const listenerMiddleware = createListenerMiddleware(); +const extraMiddleware: Middleware[] = []; + +export function addExtraMiddleware(middleware: Middleware) { + extraMiddleware.push(middleware); +} + +export function configureStore(initialState?: Partial) { + const store = reduxConfigureStore({ + reducer: createRootReducer(), + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat( + listenerMiddleware.middleware, + ...extraMiddleware + ), + devTools: process.env.NODE_ENV !== 'production', + preloadedState: { + ...initialState, + }, + }); + + // this enables "refetchOnFocus" and "refetchOnReconnect" for RTK Query + setupListeners(store.dispatch); + + setStore(store); + return store; +} + +export type RootState = ReturnType['getState']>; +export type AppDispatch = ReturnType['dispatch']; diff --git a/public/app/plugins/datasource/elasticsearch/store/store.ts b/public/app/plugins/datasource/elasticsearch/store/store.ts new file mode 100644 index 00000000000..aaccaca84f5 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/store/store.ts @@ -0,0 +1,26 @@ +import { Store } from 'redux'; + +import { StoreState } from '../types/store'; + +export let store: Store; + +export function setStore(newStore: Store) { + store = newStore; +} + +export function getState(): StoreState { + if (!store || !store.getState) { + return { defaultReducer: () => ({}), templating: { lastKey: 'key' } }; // used by tests + } + + return store.getState(); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function dispatch(action: any) { + if (!store || !store.getState) { + return; + } + + return store.dispatch(action); +} diff --git a/public/app/plugins/datasource/elasticsearch/tsconfig.json b/public/app/plugins/datasource/elasticsearch/tsconfig.json new file mode 100644 index 00000000000..40352099203 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "types": ["node", "jest", "@testing-library/jest-dom"] + }, + "extends": "@grafana/plugin-configs/tsconfig.json", + "include": ["."] +} diff --git a/public/app/plugins/datasource/elasticsearch/types.ts b/public/app/plugins/datasource/elasticsearch/types.ts index 4645a2a824f..d435f1b4594 100644 --- a/public/app/plugins/datasource/elasticsearch/types.ts +++ b/public/app/plugins/datasource/elasticsearch/types.ts @@ -64,6 +64,11 @@ export interface ElasticsearchOptions extends DataSourceJsonData { sigV4Auth?: boolean; oauthPassThru?: boolean; defaultQueryMode?: QueryType; + apiKeyAuth?: boolean; +} + +export interface ElasticsearchSecureJsonData { + apiKey?: string; } export type QueryType = 'metrics' | 'logs' | 'raw_data' | 'raw_document'; diff --git a/public/app/plugins/datasource/elasticsearch/types/store.ts b/public/app/plugins/datasource/elasticsearch/types/store.ts new file mode 100644 index 00000000000..1ff65f1a7af --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/types/store.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-restricted-imports */ +import { + Action, + addListener as addListenerUntyped, + AsyncThunk, + AsyncThunkOptions, + AsyncThunkPayloadCreator, + createAsyncThunk as createAsyncThunkUntyped, + PayloadAction, + TypedAddListener, +} from '@reduxjs/toolkit'; +import { + TypedUseSelectorHook, + useDispatch as useDispatchUntyped, + useSelector as useSelectorUntyped, +} from 'react-redux'; +import { ThunkDispatch as GenericThunkDispatch, ThunkAction } from 'redux-thunk'; + +import type { createRootReducer } from '../reducers/root'; +import { AppDispatch, RootState } from '../store/configureStore'; +import { dispatch as storeDispatch } from '../store/store'; + +export type StoreState = ReturnType>; + +/* + * Utility type to get strongly types thunks + */ +export type ThunkResult = ThunkAction>; + +export type ThunkDispatch = GenericThunkDispatch; + +// Typed useDispatch & useSelector hooks +export const useDispatch: () => AppDispatch = useDispatchUntyped; +export const useSelector: TypedUseSelectorHook = useSelectorUntyped; + +type DefaultThunkApiConfig = { dispatch: AppDispatch; state: StoreState }; +export const createAsyncThunk = ( + typePrefix: string, + payloadCreator: AsyncThunkPayloadCreator, + options?: AsyncThunkOptions +): AsyncThunk => + createAsyncThunkUntyped(typePrefix, payloadCreator, options); + +// eslint-disable-next-line @typescript-eslint/consistent-type-assertions +export const addListener = addListenerUntyped as TypedAddListener; +export const dispatch: AppDispatch = storeDispatch; diff --git a/public/app/plugins/datasource/elasticsearch/webpack.config.ts b/public/app/plugins/datasource/elasticsearch/webpack.config.ts new file mode 100644 index 00000000000..f64bb95e3c0 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/webpack.config.ts @@ -0,0 +1,9 @@ +import type { Configuration } from 'webpack'; + +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; + +const config = async (env: Env): Promise => { + return await grafanaConfig(env); +}; + +export default config; diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx index 8b32e651f84..3b952381554 100644 --- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx +++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx @@ -85,9 +85,6 @@ export function RadialBarPanel({ }); } - const minVizHeight = 60; - const minVizWidth = 60; - if (getValues()[0]?.display?.text === 'No data') { return ; } @@ -104,8 +101,8 @@ export function RadialBarPanel({ itemSpacing={16} renderCounter={renderCounter} orientation={options.orientation} - minVizHeight={minVizHeight} - minVizWidth={minVizWidth} + minVizHeight={options.sizing === 'auto' ? 0 : options.minVizHeight} + minVizWidth={options.sizing === 'auto' ? 0 : options.minVizWidth} getAlignmentFactors={getDisplayValueAlignmentFactors} /> diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index f7afa0ef2c9..b40eb2c594b 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -1,5 +1,6 @@ import { PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { BarGaugeSizing, VizOrientation } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { addOrientationOption, addStandardDataReduceOptions } from '../stat/common'; @@ -16,7 +17,7 @@ export const plugin = new PanelPlugin(RadialBarPanel) const category = [t('gauge.category-radial-bar', 'Gauge')]; addStandardDataReduceOptions(builder); - addOrientationOption(builder, category); + commonOptionsBuilder.addTextSizeOptions(builder, { withTitle: true, withValue: true }); builder.addRadio({ @@ -32,6 +33,51 @@ export const plugin = new PanelPlugin(RadialBarPanel) }, }); + addOrientationOption(builder, category); + + builder + .addRadio({ + path: 'sizing', + name: t('gauge.name-gauge-size', 'Gauge size'), + settings: { + options: [ + { value: BarGaugeSizing.Auto, label: t('gauge.gauge-size-options.label-auto', 'Auto') }, + { value: BarGaugeSizing.Manual, label: t('gauge.gauge-size-options.label-manual', 'Manual') }, + ], + }, + category, + defaultValue: defaultOptions.sizing, + showIf: (options: Options) => options.orientation !== VizOrientation.Auto, + }) + .addSliderInput({ + path: 'minVizWidth', + name: t('gauge.name-min-width', 'Min width'), + description: t('gauge.description-min-width', 'Minimum column width (vertical orientation)'), + defaultValue: defaultOptions.minVizWidth, + settings: { + min: 0, + max: 600, + step: 1, + }, + category, + showIf: (options: Options) => + options.sizing === BarGaugeSizing.Manual && options.orientation === VizOrientation.Vertical, + }) + .addSliderInput({ + path: 'minVizHeight', + name: t('gauge.name-min-height', 'Min height'), + description: t('gauge.description-min-height', 'Minimum row height (horizontal orientation)'), + defaultValue: defaultOptions.minVizHeight, + category, + settings: { + min: 0, + max: 600, + step: 1, + }, + showIf: (options: Options) => + options.sizing === BarGaugeSizing.Manual && options.orientation === VizOrientation.Horizontal, + }); + builder.addSliderInput({ path: 'barWidthFactor', name: t('radialbar.config.bar-width', 'Bar width'), diff --git a/public/app/plugins/panel/radialbar/panelcfg.cue b/public/app/plugins/panel/radialbar/panelcfg.cue index 6e5fd7eec21..01e2b5e3b98 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.cue +++ b/public/app/plugins/panel/radialbar/panelcfg.cue @@ -44,6 +44,9 @@ composableKinds: PanelCfg: { endpointMarker?: "point" | "glow" | "none" | *"point" textMode?: "auto" | "value_and_name" | "value" | "name" | "none" | *"auto" effects: GaugePanelEffects | *{} + sizing: common.BarGaugeSizing & (*"auto" | _) + minVizWidth: uint32 | *75 + minVizHeight: uint32 | *75 } @cuetsy(kind="interface") } }] diff --git a/public/app/plugins/panel/radialbar/panelcfg.gen.ts b/public/app/plugins/panel/radialbar/panelcfg.gen.ts index 594747136ef..58b17cbd293 100644 --- a/public/app/plugins/panel/radialbar/panelcfg.gen.ts +++ b/public/app/plugins/panel/radialbar/panelcfg.gen.ts @@ -27,11 +27,14 @@ export interface Options extends common.SingleStatBaseOptions { barWidthFactor: number; effects: GaugePanelEffects; endpointMarker?: ('point' | 'glow' | 'none'); + minVizHeight: number; + minVizWidth: number; segmentCount: number; segmentSpacing: number; shape: ('circle' | 'gauge'); showThresholdLabels: boolean; showThresholdMarkers: boolean; + sizing: common.BarGaugeSizing; sparkline?: boolean; textMode?: ('auto' | 'value_and_name' | 'value' | 'name' | 'none'); } @@ -41,11 +44,14 @@ export const defaultOptions: Partial = { barWidthFactor: 0.5, effects: {}, endpointMarker: 'point', + minVizHeight: 75, + minVizWidth: 75, segmentCount: 1, segmentSpacing: 0.3, shape: 'gauge', showThresholdLabels: false, showThresholdMarkers: true, + sizing: common.BarGaugeSizing.Auto, sparkline: true, textMode: 'auto', }; diff --git a/public/img/plugins/db2.svg b/public/img/plugins/db2.svg new file mode 100644 index 00000000000..950f0a73e32 --- /dev/null +++ b/public/img/plugins/db2.svg @@ -0,0 +1,937 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 186a3786bea..c233eadf763 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2186,6 +2186,7 @@ }, "provisioning-badge": { "badge": { + "text-converted-prometheus": "Imported", "text-provisioned": "Provisioned" } }, @@ -6985,6 +6986,7 @@ "drone-datasource": "Drone datasource", "git-lab-integration-and-datasource": "GitLab integration and datasource", "honeycomb-integration-and-datasource": "Honeycomb integration and datasource", + "ibmdb2-datasource": "IBM Db2 data source", "jira-integration-and-datasource": "Jira integration and datasource", "logic-monitor-devices-datasource": "LogicMonitor Devices datasource", "mongo-db-integration-and-data-source": "MongoDB integration and data source", diff --git a/public/openapi3.json b/public/openapi3.json index 4a7c5f6be08..8dac2bbc044 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -18377,6 +18377,8 @@ }, "/dashboards/uid/{uid}/restore": { "post": { + "deprecated": true, + "description": "This API will be removed when /apis/dashboards.grafana.app/v1 is released.\nYou can restore a dashboard by reading it from history, then creating it again.", "operationId": "restoreDashboardVersionByUID", "parameters": [ { diff --git a/scripts/validate-npm-packages.sh b/scripts/validate-npm-packages.sh index a3e07c4d7be..80e36b968d1 100755 --- a/scripts/validate-npm-packages.sh +++ b/scripts/validate-npm-packages.sh @@ -11,6 +11,7 @@ failed_checks=() for file in "$ARTIFACTS_DIR"/*.tgz; do echo "🔍 Checking NPM package: $file" + # If you need to debug ATTW issues, pass "--format json" to get verbose output. if ! NODE_OPTIONS="-C @grafana-app/source" yarn attw "$file" --ignore-rules "false-cjs" --profile "node16"; then echo "attw check failed for $file" echo "" diff --git a/yarn.lock b/yarn.lock index 1069acd8544..5559a4bed77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2610,6 +2610,53 @@ __metadata: languageName: node linkType: hard +"@grafana-plugins/elasticsearch@workspace:public/app/plugins/datasource/elasticsearch": + version: 0.0.0-use.local + resolution: "@grafana-plugins/elasticsearch@workspace:public/app/plugins/datasource/elasticsearch" + dependencies: + "@emotion/css": "npm:11.13.5" + "@grafana/aws-sdk": "npm:0.8.3" + "@grafana/data": "npm:12.4.0-pre" + "@grafana/e2e-selectors": "npm:12.4.0-pre" + "@grafana/plugin-configs": "npm:12.4.0-pre" + "@grafana/plugin-ui": "npm:^0.11.1" + "@grafana/runtime": "npm:12.4.0-pre" + "@grafana/schema": "npm:12.4.0-pre" + "@grafana/ui": "npm:12.4.0-pre" + "@reduxjs/toolkit": "npm:2.10.1" + "@testing-library/dom": "npm:10.4.1" + "@testing-library/jest-dom": "npm:6.6.4" + "@testing-library/react": "npm:16.3.0" + "@testing-library/user-event": "npm:14.6.1" + "@types/jest": "npm:29.5.14" + "@types/lodash": "npm:4.17.20" + "@types/lucene": "npm:^2" + "@types/node": "npm:24.10.1" + "@types/react": "npm:18.3.18" + "@types/react-dom": "npm:18.3.5" + "@types/semver": "npm:7.7.1" + jest: "npm:29.7.0" + lodash: "npm:4.17.21" + lucene: "npm:^2.1.1" + react: "npm:18.3.1" + react-dom: "npm:18.3.1" + react-redux: "npm:9.2.0" + react-select: "npm:5.10.2" + react-select-event: "npm:5.5.1" + react-use: "npm:17.6.0" + redux: "npm:5.0.1" + redux-thunk: "npm:3.1.0" + rxjs: "npm:7.8.2" + semver: "npm:7.7.3" + ts-node: "npm:10.9.2" + tslib: "npm:2.8.1" + typescript: "npm:5.9.2" + webpack: "npm:5.101.0" + peerDependencies: + "@grafana/runtime": "*" + languageName: unknown + linkType: soft + "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor": version: 0.0.0-use.local resolution: "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor" @@ -3324,6 +3371,7 @@ __metadata: react-use: "npm:17.6.0" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" + rollup-plugin-copy: "npm:3.5.0" rollup-plugin-esbuild: "npm:6.2.1" rollup-plugin-node-externals: "npm:^8.0.0" rxjs: "npm:7.8.2"