Merge branch 'main' into drclau/unistor/replace-authenticators-3

This commit is contained in:
Claudiu Dragalina-Paraipan 2024-08-02 14:44:06 +03:00
commit c46b42a595
107 changed files with 1519 additions and 936 deletions

View File

@ -1969,4 +1969,6 @@ domain = grafana.net
snapshot_folder = ""
# Link to form to give feedback on the feature
feedback_url = https://docs.google.com/forms/d/e/1FAIpQLSeEE33vhbSpR8A8S1A1ocZ1ByVRRwiRl1GZr2FSrEer_tSa8w/viewform?usp=sf_link
# How frequently should the frontend UI poll for changes while resources are migrating
frontend_poll_interval = 2s

View File

@ -1900,3 +1900,5 @@ timeout = 30s
;snapshot_folder = ""
# Link to form to give feedback on the feature
;feedback_url = ""
# How frequently should the frontend UI poll for changes while resources are migrating
;frontend_poll_interval = 2s

View File

@ -76,7 +76,7 @@ Once you've added the Loki data source, you can [configure it](#configure-the-da
To troubleshoot configuration and other issues, check the log file located at `/var/log/grafana/grafana.log` on Unix systems, or in `<grafana_install_dir>/data/log` on other platforms and manual installations.
{{% /admonition %}}
## Provision the Loki data source
## Provision the data source
You can define and configure the data source in YAML files as part of Grafana's provisioning system.
For more information about provisioning, and for available configuration options, refer to [Provisioning Grafana](ref:provisioning-data-sources).

View File

@ -1,5 +1,4 @@
---
description: Tracing in Explore
keywords:
- explore
- trace
@ -8,167 +7,161 @@ labels:
- cloud
- enterprise
- oss
title: Tracing in Explore
title: Traces in Explore
weight: 20
---
# Tracing in Explore
# Traces in Explore
You can use Explore to query and visualize traces from tracing data sources.
You can use Explore to query and visualize traces from tracing data sources. Supported data sources include:
Supported data sources are:
- [Tempo]({{< relref "../datasources/tempo/" >}}) (supported ingestion formats: OpenTelemetry, Jaeger, and Zipkin)
- [Jaeger]({{< relref "../datasources/jaeger/" >}})
- [Zipkin]({{< relref "../datasources/zipkin/" >}})
- [Tempo](/docs/grafana/<GRAFANA_VERSION>/datasources/tempo/)
- [Jaeger](/docs/grafana/<GRAFANA_VERSION>/datasources/jaeger/)
- [Zipkin](/docs/grafana/<GRAFANA_VERSION>/datasources/zipkin/)
- [X-Ray](https://grafana.com/grafana/plugins/grafana-x-ray-datasource)
- [Azure Monitor Application Insights]({{< relref "../datasources/azure-monitor/" >}})
- [Azure Monitor](/docs/grafana/latest/datasources/azure-monitor/)
- [ClickHouse](https://github.com/grafana/clickhouse-datasource)
- [New Relic](https://grafana.com/grafana/plugins/grafana-newrelic-datasource)
- [Infinity](https://grafana.com/grafana/plugins/yesoreyeram-infinity-datasource)
- [New Relic](/docs/plugins/grafana-newrelic-datasource/latest/)
- [Infinity](/docs/plugins/yesoreyeram-infinity-datasource/latest/)
For information on how to configure queries for the data sources listed above, refer to the documentation for specific data source.
Here are some references to learn more about traces and how you can use them:
## Query editor
- [Introduction to tracing](https://grafana.com/docs/tempo/<TEMPO_VERSION>/introduction/)
- [Trace structure](https://grafana.com/docs/tempo/<TEMPO_VERSION>/traceql/trace-structure/#trace-structure)
- [Traces and telemetry](https://grafana.com/docs/tempo/<TEMPO_VERSION>/introduction/telemetry/)
- [Using traces to find solutions to problems](https://grafana.com/docs/tempo/<TEMPO_VERSION>/introduction/solutions-with-traces/)
- [Best practices for tracing](/docs/grafana/<GRAFANA_VERSION>/datasources/tempo/tracing-best-practices/)
You can query and search tracing data using a data source's query editor.
## Query editors
Each data source can have it's own query editor. The query editor for the Tempo data source is slightly different than the query editor for the Jaeger data source.
You can query and search tracing data using a data source's query editor. Note that data sources in Grafana have unique query editors.
For information on querying each data source, refer to their documentation:
- [Tempo query editor]({{< relref "../datasources/tempo/query-editor" >}})
- [Jaeger query editor]({{< relref "../datasources/jaeger/#query-the-data-source" >}})
- [Zipkin query editor]({{< relref "../datasources/zipkin/#query-the-data-source" >}})
- [Azure Monitor Application Insights query editor]({{< relref "../datasources/azure-monitor/query-editor/#query-application-insights-traces" >}})
- [ClickHouse query editor](https://clickhouse.com/docs/en/integrations/grafana/query-builder#traces)
For information on how to use the query editor to create queries for tracing data sources, refer to the documentation for each individual data source.
## Trace view
This section explains the elements of the Trace View.
Grafana's trace view provides an overview of a request as it travels through your system. The following sections provide detail on various elements of the trace view.
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view" >}}
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view" >}}
### Header
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-header.png" class="docs-image--no-shadow" max-width= "750px" caption="Screenshot of the trace view header" >}}
The trace view header includes the following:
- Header title: Shows the name of the root span and trace ID.
- Search: Highlights spans containing the searched text.
- Metadata: Various metadata about the trace.
- **Header title** - Shows the name of the root span and trace ID.
- **Search** - Highlights spans containing the searched text.
- **Metadata** - Various metadata about the trace.
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-header.png" class="docs-image--no-shadow" max-width= "750px" caption="Trace view header" >}}
### Minimap
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-minimap.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view minimap" >}}
**Minimap** displays a condensed view of the trace timeline. Drag your mouse over the minimap to zoom into a smaller time range. This also updates the main timeline, making it easier to view shorter spans
When zoomed in, hovering over the minimap displays **Reset selection**, which resets the zoom.
Shows condensed view or the trace timeline. Drag your mouse over the minimap to zoom into smaller time range. Zooming will also update the main timeline, so it is easy to see shorter spans. Hovering over the minimap, when zoomed, will show Reset Selection button which resets the zoom.
### Span filters
![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters-v10-1.png)
Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans.
You can add one or more of the following filters:
- Resource service name
- Span name
- Duration
- Tags (which include tags, process tags, and log fields)
To only show the spans you have matched, you can press the `Show matches only` toggle.
{{< youtube id="VP2XV3IIc80" >}}
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-minimap.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view minimap example" >}}
### Timeline
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-timeline.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view timeline" >}}
Timeline shows list of spans within the trace. Each span row consists of the following components:
Shows list of spans within the trace. Each span row consists of these components:
- **Expand children** - Expands or collapses all the children spans of the selected span.
- **Service name** - Name of the service logged the span.
- **Operation name** - Name of the operation that this span represents.
- **Span duration bar** - Visual representation of the operation duration within the trace.
- Expand children button: Expands or collapses all the children spans of selected span.
- Service name: Name of the service logged the span.
- Operation name: Name of the operation that this span represents.
- Span duration bar: Visual representation of the operation duration within the trace.
Click anywhere on the span row to reveal span details.
Clicking anywhere on the span row shows span details.
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-timeline.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view timeline" >}}
### Span details
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-span-details.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view span details" >}}
Traces are composed of one or more spans.
A span is a unit of work within a trace that has a start time relative to the beginning of the trace, a duration and an operation name for the unit of work.
It usually has a reference to a parent span, unless its the first span, the root span, in a trace.
It frequently includes key/value attributes that are relevant to the span itself, for example the HTTP method used in the request, as well as other metadata such as the service name, sub-span events, or links to other spans.
- Operation name.
- Span metadata.
- Tags: Any tags associated with this span.
- Process metadata: Metadata about the process that logged this span.
- Logs: List of logs logged by this span and associated key values. In case of Zipkin logs section shows Zipkin annotations.
You can expand any span in a trace and view the details, including the span and resource attributes.
For more information about spans and traces, refer to [Introduction to tracing](https://grafana.com/docs/tempo/latest/introduction/) in the Tempo documentation.
Span details include:
- **Span attributes** - Key/value pairs that provides context for spans. For example, if the span deals with calling another service via HTTP, an attribute could include the HTTP URL (maybe as the span attribute key `http.url`) and the HTTP status code returned (as the span attribute `http.status_code`).
- **Resource attributes** - Key/value pairs that describe the context of how the span was collected.
Refer to [Span and resource attributes](/docs/tempo/<TEMPO_VERSION>/operations/best-practices/#span-and-resource-attributes) for more detail.
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-span-details.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view span details" >}}
### Span filters
Span filters allow you to refine the spans displayed in the trace timeline viewer.
The more filters you add, the more specific the filtered spans become.
Click on a trace to access Span filters.
![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters-v10-1.png)
You can add one or more of the following filters:
- **Service name** - Filter by selecting a service name from the dropdown.
- **Span name** - Filter by selecting a span name from the dropdown.
- **Duration** - Filter by duration. Accepted units include ns, us, ms, s, m, h.
- **Tags** - Filter by tags, process tags, or log fields in your span.
To only show the spans you have matched, toggle **Show matches only**.
Refer to [Span filters](/docs/grafana/<GRAFANA_VERSION>/datasources/tempo/span-filters/) for more in depth information.
Watch the following video to learn more about filtering trace spans in Grafana:
{{< youtube id="VP2XV3IIc80" >}}
### Trace to logs
You can navigate from a span in a trace view directly to logs relevant for that span. This feature is available for Tempo, Jaeger, and Zipkin data sources. Refer to their [relevant documentation](/docs/grafana/latest/datasources/tempo/#trace-to-logs) for configuration instructions.
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-trace-to-logs.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view in Explore with icon next to the spans" >}}
You can navigate from a span in a trace view directly to logs relevant for that span.
This feature is available for the Tempo, Jaeger, and Zipkin data sources.
Refer to each individual data source's documentation for configuration instructions.
Click the document icon to open a split view in Explore with the configured data source and query relevant logs for the span.
{{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-trace-to-logs.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace to logs" >}}
### Trace to metrics
{{% admonition type="note" %}}
This feature is currently in beta and behind the `traceToMetrics` feature toggle.
{{% /admonition %}}
You can navigate from a span in a trace view directly to metrics relevant for that span.
This feature is available for the Tempo, Jaeger, and Zipkin data sources.
You can navigate from a span in a trace view directly to metrics relevant for that span. This feature is available for Tempo, Jaeger, and Zipkin data sources. Refer to their [relevant documentation](/docs/grafana/latest/datasources/tempo/configure-tempo-data-source/#trace-to-metrics) for configuration instructions.
Refer to each individual data source's documentation for configuration instructions.
For Tempo, refer to [Trace to metrics configuration](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/datasources/tempo/configure-tempo-data-source/#trace-to-metrics).
### Trace to profiles
Using Trace to profiles, you can use Grafanas ability to correlate different signals by adding the functionality to link between traces and profiles.
Refer to the [relevant documentation](/docs/grafana/latest/datasources/tempo/configure-tempo-data-source#trace-to-profiles) for configuration instructions.
For Tempo refer to [Trace to profiles](/docs/grafana/<GRAFANA_VERSION>/datasources/tempo/configure-tempo-data-source#trace-to-profiles) for configuration instructions.
{{< figure src="/static/img/docs/tempo/profiles/tempo-trace-to-profile.png" max-width="900px" class="docs-image--no-shadow" alt="Selecting a link in the span queries the profile data source" >}}
## Node graph
You can optionally expand the node graph for the displayed trace. Depending on the data source, this can show spans of the trace as nodes in the graph, or as some additional context like service graph based on the current trace.
You can also expand the node graph for a displayed trace. If the data source supports it, this displays spans of the trace as nodes in the graph, or provides additional context, such as a service graph based on the current trace.
{{< figure src="/media/docs/tempo/screenshot-grafana-node-graph.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the node graph" >}}
Refer to [Node graph](/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/node-graph/) for additional information.
## Service Graph
{{< admonition type="note" >}}
The node graph requires data to be returned from the data source in a specific format to display correctly. Refer to [Data API](/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/node-graph/#data-api), [Nodes data frame structure](/docs/grafana/latest/panels-visualizations/visualizations/node-graph/#nodes-data-frame-structure) and [Node graph data requirements](/docs/grafana/latest/panels-visualizations/visualizations/node-graph/#data-requirements) for additional information and configuration instructions.
{{< /admonition >}}
The Service Graph visualizes the span metrics (traces data for rates, error rates, and durations (RED)) and service graphs.
Once the requirements are set up, this pre-configured view is immediately available.
{{< figure src="/media/docs/tempo/screenshot-grafana-node-graph.png" class="docs-image--no-shadow" max-width= "900px" caption="Node graph" >}}
For more information, refer to the [Service Graph view section]({{< relref "../datasources/tempo/#open-the-service-graph-view" >}}) of the Tempo data source page and the [service graph view page](/docs/tempo/latest/metrics-generator/service-graph-view/) in the Tempo documentation.
## Service graph
{{< figure src="/static/img/docs/grafana-cloud/apm-overview.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the Service Graph view" >}}
A service graph visualizes span metrics, including rates, error rates, and durations (RED), along with service relationships. Once the requirements are configured, this pre-configured view is immediately available.
## Data API
For additional information refer to the following documentation:
This visualization needs a specific shape of the data to be returned from the data source in order to correctly display it.
- [Service Graph and Service Graph view](/docs/grafana/<GRAFANA_VERSION>/datasources/tempo/service-graph/)
- [Service graph view](/docs/tempo/<TEMPO_VERSION>/metrics-generator/service-graph-view/) in Tempo documentation
Data source needs to return data frame and set `frame.meta.preferredVisualisationType = 'trace'`.
### Data frame structure
Required fields:
| Field name | Type | Description |
| ------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| traceID | string | Identifier for the entire trace. There should be only one trace in the data frame. |
| spanID | string | Identifier for the current span. SpanIDs should be unique per trace. |
| parentSpanID | string | SpanID of the parent span to create child parent relationship in the trace view. Can be `undefined` for root span without a parent. |
| serviceName | string | Name of the service this span is part of. |
| serviceTags | TraceKeyValuePair[] | List of tags relevant for the service. |
| startTime | number | Start time of the span in millisecond epoch time. |
| duration | number | Duration of the span in milliseconds. |
Optional fields:
| Field name | Type | Description |
| -------------- | ------------------- | ------------------------------------------------------------------ |
| logs | TraceLog[] | List of logs associated with the current span. |
| tags | TraceKeyValuePair[] | List of tags associated with the current span. |
| warnings | string[] | List of warnings associated with the current span. |
| stackTraces | string[] | List of stack traces associated with the current span. |
| errorIconColor | string | Color of the error icon in case span is tagged with `error: true`. |
For details about the types see [TraceSpanRow](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/trace.ts#L28), [TraceKeyValuePair](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/trace.ts#L4) and [TraceLog](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/trace.ts#L12).
{{< figure src="/static/img/docs/grafana-cloud/apm-overview.png" class="docs-image--no-shadow" max-width= "900px" caption="Service graph view" >}}

View File

@ -80,6 +80,7 @@ With a Grafana Enterprise license, you also get access to premium data sources,
- [Azure Devops](/grafana/plugins/grafana-azuredevops-datasource)
- [Catchpoint](/grafana/plugins/grafana-catchpoint-datasource)
- [Cloudflare](/grafana/plugins/grafana-cloudflare-datasource)
- [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource)
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)

View File

@ -59,21 +59,26 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-grafana-managed-rule/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule/
panel-editor-alerts:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/panel-editor-overview/#data-section
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-editor-overview/#data-section
---
# Time series
Time series visualizations are the default and primary way to visualize data points over intervals of time as a graph. They can render series as lines, points, or bars. They're versatile enough to display almost any time-series data.
Time series visualizations are the default way to visualize data points over intervals of time, as a graph. They can render series as lines, points, or bars and are versatile enough to display almost any time-series data.
{{< figure src="/static/img/docs/time-series-panel/time_series_small_example.png" max-width="1200px" alt="Time series" >}}
{{< admonition type="note" >}}
You can migrate from the old Graph visualization to the new time series visualization. To migrate, open the panel and click the **Migrate** button in the side pane.
You can migrate from the legacy Graph visualization to the time series visualization. To migrate, open the panel and click the **Migrate** button in the side pane.
{{< /admonition >}}
## Configure a time series visualization
The following video guides you through the creation steps and common customizations of time series visualizations and is great for beginners:
The following video guides you through the creation steps and common customizations of time series visualizations, and is great for beginners:
{{< youtube id="RKtW87cPxsw" >}}
@ -81,40 +86,37 @@ The following video guides you through the creation steps and common customizati
## Supported data formats
Time series visualizations require time series data; that is a sequence of measurements, ordered in time, where every row in the table represents one individual measurement at a specific time. Learn more about [time series data](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/fundamentals/timeseries/).
Time series visualizations require time-series data&mdash;a sequence of measurements, ordered in time, and formatted as a table&mdash;where every row in the table represents one individual measurement at a specific time. Learn more about [time-series data](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/fundamentals/timeseries/).
## Alert rules
You can [link alert rules](ref:link-alert) to time series visualizations to observe when alerts fire and are resolved in the form of annotations. In addition, you can create alert rules from the **Alert** tab within the panel editor.
You can [link alert rules](ref:link-alert) to time series visualizations in the form of annotations to observe when alerts fire and are resolved. In addition, you can create alert rules from the **Alert** tab within the [panel editor](ref:panel-editor-alerts).
## Transform override property
## Special overrides
Use the **Transform** override property to transform series values without affecting the values shown in the tooltip, context menu, or legend.
The following overrides help you further refine a time series visualization.
<!-- add more information about how to access this property -->
### Transform override property
- **Negative Y transform:** Flip the results to negative values on the Y axis.
- **Constant:** Show the first value as a constant line.
Use the **Graph styles > Transform** [override property](#field-overrides) to transform series values without affecting the values shown in the tooltip, context menu, or legend. Choose from the following transform options:
{{< docs/shared lookup="visualizations/multiple-y-axes.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}}
- **Constant** - Show the first value as a constant line.
- **Negative Y transform** - Flip the results to negative values on the y-axis.
<!-- update shared filed above to add actual steps for adding this override -->
### Fill below to override property
## Add the Fill below to override
The **Fill below to** option fills the area between two series. This option is only available as a series/field override.
1. Edit the panel and click **Overrides**.
1. Select the fields to fill below.
1. In **Add override property**, select **Fill below to**.
1. Select the series for which you want the fill to stop.
The **Graph styles > Fill below to** [override property](#field-overrides) fills the area between two series. When you configure the property, select the series for which you want the fill to stop.
The following example shows three series: Min, Max, and Value. The Min and Max series have **Line width** set to 0. Max has a **Fill below to** override set to Min, which fills the area between Max and Min with the Max line color.
{{< figure src="/static/img/docs/time-series-panel/fill-below-to-7-4.png" max-width="600px" alt="Fill below to example" >}}
{{< docs/shared lookup="visualizations/multiple-y-axes.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+2" >}}
## Configuration options
{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="<GRAFANA_VERSION>" >}}
### Panel options
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
@ -129,30 +131,30 @@ The following example shows three series: Min, Max, and Value. The Min and Max s
### Axis options
Options under the axis category change how the x- and y-axes are rendered. Some options do not take effect until you click outside of the field option box you are editing. You can also or press `Enter`.
Options under the **Axis** section control how the x- and y-axes are rendered. Some options don't take effect until you click outside of the field option box you're editing. You can also press `Enter`.
| Option | Description |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Time zone | Set the desired time zone(s) to display along the x-axis. |
| [Placement](#placement) | Select the placement of the y-axis. |
| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. |
| Width | Set a fixed width of the axis. By default, Grafana dynamically calculates the width of an axis. By setting the width of the axis, data with different axes types can share the same display proportions. This setting makes it easier for you to compare more than one graphs worth of data because the axes are not shifted or stretched within visual proximity to each other. |
| Show grid lines | Set the axis grid line visibility.<br> |
| Color | Set the color of the axis. |
| Show border | Set the axis border visibility. |
| Scale | Set the y-axis values scale.<br> |
| Centered zero | Set the y-axis to be centered on zero. |
| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. |
| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. |
| Option | Description |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Time zone | Set the desired time zones to display along the x-axis. |
| [Placement](#placement) | Select the placement of the y-axis. |
| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. |
| Width | Set a fixed width of the axis. By default, Grafana dynamically calculates the width of an axis. By setting the width of the axis, data with different axes types can share the same display proportions. This setting makes it easier for you to compare more than one graphs worth of data because the axes aren't shifted or stretched within visual proximity to each other. |
| Show grid lines | Set the axis grid line visibility.<br> |
| Color | Set the color of the axis. |
| Show border | Set the axis border visibility. |
| Scale | Set the y-axis values scale.<br> |
| Centered zero | Set the y-axis so it's centered on zero. |
| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. |
| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. |
#### Placement
Select the placement of the y-axis.
Select the placement of the y-axis. Choose from the following:
- **Auto:** Automatically assigns the y-axis to the series. When there are two or more series with different units, Grafana assigns the left axis to the first unit and the right axis to the units that follow.
- **Left:** Display all y-axes on the left side.
- **Right:** Display all y-axes on the right side.
- **Hidden:** Hide all axes. To selectively hide axes, [Add a field override](ref:add-a-field-override) that targets specific fields.
- **Auto** - Automatically assigns the y-axis to the series. When there are two or more series with different units, Grafana assigns the left axis to the first unit and the right axis to the units that follow.
- **Left** - Display all y-axes on the left side.
- **Right** - Display all y-axes on the right side.
- **Hidden** - Hide all axes. To selectively hide axes, [Add a field override](ref:add-a-field-override) that targets specific fields.
#### Soft min and soft max
@ -166,71 +168,69 @@ To define hard limits of the y-axis, set standard min/max options. For more info
### Graph styles options
The options under the **Graph styles** section let you control the general appearance of the graph, excluding [color](#standard-options).
| Option | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Style](#style) | Use this option to define how to display your time series data. |
| [Line interpolation](#line-interpolation) | This option controls how the graph interpolates the series line. |
| [Line width](#line-width) | Line width is a slider that controls the thickness for series lines or the outline for bars. |
| [Fill opacity](#fill-opacity) | Use opacity to specify the series area fill color. |
| [Gradient mode](#gradient-mode) | Gradient mode specifies the gradient fill, which is based on the series color. |
| [Line style](#line-style) | Set the style of the line. |
| [Style](#style) | Choose whether to display your time-series data as lines, bars, or points. |
| [Line interpolation](#line-interpolation) | Choose how the graph interpolates the series line. |
| Line width | Set the thickness of the series lines or the outline for bars using the **Line width** slider. |
| [Fill opacity](#fill-opacity) | Set the series area fill color using the **Fill opacity** slider. |
| [Gradient mode](#gradient-mode) | Choose a gradient mode to control the gradient fill, which is based on the series color. |
| [Line style](#line-style) | Choose a solid, dashed, or dotted line style. |
| [Connect null values](#connect-null-values) | Choose how null values, which are gaps in the data, appear on the graph. |
| [Disconnect values](#disconnect-values) | Choose whether to set a threshold above which values in the data should be disconnected. |
| [Show points](#show-points) | You can configure your visualization to add points to lines or bars. |
| [Show points](#show-points) | Set whether to show data points to lines or bars. |
| Point size | Set the size of the points, from 1 to 40 pixels in diameter. |
| [Stack series](#stack-series) | Stacking allows Grafana to display series on top of each other. |
| [Stack series](#stack-series) | Set whether Grafana displays series on top of each other. |
| [Bar alignment](#bar-alignment) | Set the position of the bar relative to a data point. |
| Bar width factor | Set the width of the bar relative to minimum space between data points. A factor of 0.5 means that the bars take up half of the available space between data points. A factor of 1.0 means that the bars take up all available space. |
#### Style
Use this option to define how to display your time series data. You can use overrides to combine multiple styles in the same graph.
- Lines
- Bars
- Points
Choose whether to display your time-series data as lines, bars, or points. You can use overrides to combine multiple styles in the same graph. Choose from the following:
![Style modes](/static/img/docs/time-series-panel/style-modes-v9.png)
#### Line interpolation
This option controls how the graph interpolates the series line.
Choose how the graph interpolates the series line:
- **Linear:** Points are joined by straight lines.
- **Smooth:** Points are joined by curved lines that smooths transitions between points.
- **Step before:** The line is displayed as steps between points. Points are rendered at the end of the step.
- **Step after:** The line is displayed as steps between points. Points are rendered at the beginning of the step.
- **Linear** - Points are joined by straight lines.
- **Smooth** - Points are joined by curved lines that smooths transitions between points.
- **Step before** - The line is displayed as steps between points. Points are rendered at the end of the step.
- **Step after** - The line is displayed as steps between points. Points are rendered at the beginning of the step.
#### Line width
Line width is a slider that controls the thickness for series lines or the outline for bars.
Set the thickness of the series lines or the outline for bars using the **Line width** slider.
#### Fill opacity
Use opacity to specify the series area fill color.
Set the series area fill color using the **Fill opacity** slider.
![Fill opacity examples](/static/img/docs/time-series-panel/fill-opacity.png)
#### Gradient mode
Gradient mode specifies the gradient fill, which is based on the series color. To change the color, use the standard color scheme field option. For more information, refer to [Color scheme](ref:color-scheme).
Choose a gradient mode to control the gradient fill, which is based on the series color. To change the color, use the standard color scheme field option. For more information, refer to [Color scheme](ref:color-scheme).
- **None:** No gradient fill. This is the default setting.
- **Opacity:** An opacity gradient where the opacity of the fill increases as y-axis values increase.
- **Hue:** A subtle gradient that is based on the hue of the series color.
- **Scheme:** A color gradient defined by your [Color scheme](ref:color-scheme). This setting is used for the fill area and line. For more information about scheme, refer to [Scheme gradient mode](#scheme-gradient-mode).
- **None** - No gradient fill. This is the default setting.
- **Opacity** - An opacity gradient where the opacity of the fill increases as y-axis values increase.
- **Hue** - A subtle gradient that's based on the hue of the series color.
- **Scheme** - A color gradient defined by your [Color scheme](ref:color-scheme). This setting is used for the fill area and line. For more information about scheme, refer to [Scheme gradient mode](#scheme-gradient-mode).
Gradient appearance is influenced by the **Fill opacity** setting. The following image show, the **Fill opacity** is set to 50.
Gradient appearance is influenced by the **Fill opacity** setting. The following image shows the **Fill opacity** set to 50.
![Gradient mode examples](/static/img/docs/time-series-panel/gradient-modes-v9.png)
##### Scheme gradient mode
The **Gradient mode** option located under the **Graph styles** has a mode named **Scheme**. When you enable **Scheme**, the line or bar receives a gradient color defined from the selected **Color scheme**.
The **Gradient mode** option located under the **Graph styles** section has a mode called **Scheme**. When you enable **Scheme**, the line or bar receives a gradient color defined from the selected **Color scheme**.
###### From thresholds
If the **Color scheme** is set to **From thresholds (by value)** and **Gradient mode** is set to **Scheme**, then the line or bar color changes as they cross the defined thresholds.
If the **Color scheme** is set to **From thresholds (by value)** and **Gradient mode** is set to **Scheme**, then the line or bar color changes as it crosses the defined thresholds.
{{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_thresholds_line.png" max-width="1200px" alt="Colors scheme: From thresholds" >}}
@ -242,11 +242,11 @@ The following image shows a line chart with the **Green-Yellow-Red (by value)**
#### Line style
Set the style of the line. To change the color, use the standard [color scheme](ref:color-scheme) field option.
Choose a solid, dashed, or dotted line style:
- **Solid:** Display a solid line. This is the default setting.
- **Dash:** Display a dashed line. When you choose this option, a list appears for you to select the length and gap (length, gap) for the line dashes. Dash spacing set to 10, 10 (default).
- **Dots:** Display dotted lines. When you choose this option, a list appears for you to select the gap (length = 0, gap) for the dot spacing. Dot spacing set to 0, 10 (default)
- **Solid** - Display a solid line. This is the default setting.
- **Dash** - Display a dashed line. When you choose this option, a list appears for you to select the length and gap (length, gap) for the line dashes. Dash spacing is 10, 10 by default.
- **Dots** - Display dotted lines. When you choose this option, a list appears for you to select the gap (length = 0, gap) for the dot spacing. Dot spacing is 0, 10 by default.
![Line styles examples](/static/img/docs/time-series-panel/line-styles-examples-v9.png)
@ -254,21 +254,23 @@ Set the style of the line. To change the color, use the standard [color scheme](
{{< docs/shared lookup="visualizations/disconnect-values.md" source="grafana" version="<GRAFANA_VERSION>" leveloffset="+1" >}}
To change the color, use the standard [color scheme](ref:color-scheme) field option.
#### Show points
You can configure your visualization to add points to lines or bars.
Set whether to show data points as lines or bars. Choose from the following:
- **Auto:** Grafana determines to show or not to show points based on the density of the data. If the density is low, then points appear.
- **Always:** Show the points regardless of how dense the data set is.
- **Never:** Do not show points.
- **Auto** - Grafana determines a point's visibility based on the density of the data. If the density is low, then points appear.
- **Always** - Show the points regardless of how dense the data set is.
- **Never** - Don't show points.
#### Stack series
_Stacking_ allows Grafana to display series on top of each other. Be cautious when using stacking in the visualization as it can easily create misleading graphs. To read more about why stacking might not be the best approach, refer to [The issue with stacking](https://www.data-to-viz.com/caveat/stacking.html).
Set whether Grafana stacks or displays series on top of each other. Be cautious when using stacking because it can create misleading graphs. To read more about why stacking might not be the best approach, refer to [The issue with stacking](https://www.data-to-viz.com/caveat/stacking.html). Choose from the following:
- **Off:** Turns off series stacking. When **Off**, all series share the same space in the visualization.
- **Normal:** Stacks series on top of each other.
- **100%:** Stack by percentage where all series add up to 100%.
- **Off** - Turns off series stacking. When **Off**, all series share the same space in the visualization.
- **Normal** - Stacks series on top of each other.
- **100%** - Stack by percentage where all series add up to 100%.
##### Stack series in groups
@ -283,7 +285,7 @@ The stacking group option is only available as an override. For more information
#### Bar alignment
Set the position of the bar relative to a data point. In the examples below, **Show points** is set to **Always** which makes it easier to see the difference this setting makes. The points do not change; the bars change in relationship to the points.
Set the position of the bar relative to a data point. In the examples below, **Show points** is set to **Always** which makes it easier to see the difference this setting makes. The points don't change, but the bars change in relationship to the points. Choose from the following:
- **Before** ![Bar alignment before icon](/static/img/docs/time-series-panel/bar-alignment-before.png)
The bar is drawn before the point. The point is placed on the trailing corner of the bar.

View File

@ -59,10 +59,6 @@ refs:
# XY chart
{{< admonition type="note">}}
To use xy charts, enable the `autoMigrateXYChartPanel` [feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/).
{{< /admonition >}}
XY charts provide a way to visualize arbitrary x and y values in a graph so that you can easily show the relationship between two variables. XY charts are typically used to create scatter plots. You can also use them to create bubble charts where field values determine the size of each bubble:
![An xy chart showing height weight distribution](/media/docs/grafana/panels-visualizations/screenshot-xy-charts-v11.0.png)

View File

@ -27,6 +27,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `publicDashboards` | [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. | Yes |
| `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes |
| `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `nestedFolders` | Enable folder nesting | Yes |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
@ -80,7 +81,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `autoMigratePiechartPanel` | Migrate old piechart panel to supported piechart panel - broken out from autoMigrateOldPanels to enable granular tracking |
| `autoMigrateWorldmapPanel` | Migrate old worldmap panel to supported geomap panel - broken out from autoMigrateOldPanels to enable granular tracking |
| `autoMigrateStatPanel` | Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking |
| `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model |
| `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. |
| `grpcServer` | Run the GRPC server |
| `accessControlOnCall` | Access control primitives for OnCall |

View File

@ -0,0 +1,7 @@
---
title: Configuration options intro text
comments: |
This file is used in the following in all visualizations except: alert list, annotiations list, logs, news, text
---
The following section describes the configuration options available in the panel editor pane for this visualization. These options are, as much as possible, ordered as they appear in Grafana.

View File

@ -6,6 +6,6 @@ title: Connect null values
Choose how null values, which are gaps in the data, appear on the graph. Null values can be connected to form a continuous line or set to a threshold above which gaps in the data are no longer connected.
- **Never:** Time series data points with gaps in the data are never connected.
- **Always:** Time series data points with gaps in the data are always connected.
- **Threshold:** Specify a threshold above which gaps in the data are no longer connected. This can be useful when the connected gaps in the data are of a known size and/or within a known range, and gaps outside this range should no longer be connected.
- **Never** - Time series data points with gaps in the data are never connected.
- **Always** - Time series data points with gaps in the data are always connected.
- **Threshold** - Specify a threshold above which gaps in the data are no longer connected. This can be useful when the connected gaps in the data are of a known size and/or within a known range, and gaps outside this range should no longer be connected.

View File

@ -6,5 +6,5 @@ title: Disconnect values
Choose whether to set a threshold above which values in the data should be disconnected.
- **Never:** Time series data points in the data are never disconnected.
- **Threshold:** Specify a threshold above which values in the data are disconnected. This can be useful when desired values in the data are of a known size and/or within a known range, and values outside this range should no longer be connected.
- **Never** - Time series data points in the data are never disconnected.
- **Threshold** - Specify a threshold above which values in the data are disconnected. This can be useful when desired values in the data are of a known size and/or within a known range, and values outside this range should no longer be connected.

View File

@ -12,6 +12,6 @@ Legend options control the series names and statistics that appear under or to t
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Visibility | Toggle the switch to turn the legend on or off. |
| Mode | Use these settings to define how the legend appears in your visualization. **List** displays the legend as a list. This is a default display mode of the legend. **Table** displays the legend as a table. |
| Placement | Choose where to display the legend. **Bottom -** Below the graph. **Right -** To the right of the graph. |
| Placement | Choose where to display the legend. **Bottom** places the legend below the graph. **Right** places the legend to the right of the graph. |
| Width | Control how wide the legend is when placed on the right side of the visualization. This option is only displayed if you set the legend placement to **Right**. |
| Values | Choose which of the [standard calculations](../../query-transform-data/calculation-types/) to show in the legend. You can have more than one. |

View File

@ -1,9 +1,9 @@
---
title: Display multiple y-axes
title: Multiple y-axes
---
# Display multiple y-axes
# Multiple y-axes
In some cases, you may want to display multiple y-axes. For example, if you have a dataset showing both temperature and humidity over time, you may want to show two y-axes with different units for these two series.
In some cases, you might want to display multiple y-axes. For example, if you have a dataset showing both temperature and humidity over time, you might want to show two y-axes with different units for the two series.
You can do this by [adding field overrides]({{< relref "../../panels-visualizations/configure-overrides#add-a-field-override" >}}). Follow the steps as many times as required to add as many y-axes as you need.
You can configure multiple y-axes and control where they're displayed in the visualization by adding field overrides. [This example of a dataset that includes temperature and humidity](../../configure-overrides/#example-2-format-temperature-and-humidity) describes how you can configure that. Repeat the steps for every y-axis you wish to display.

View File

@ -6,7 +6,7 @@ comments: |
Overrides allow you to customize visualization settings for specific fields or series. When you add an override rule, it targets a particular set of fields and lets you define multiple options for how that field is displayed.
Choose from one the following override options:
Choose from the following override options:
| Option | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------- |

View File

@ -4,4 +4,4 @@ comments: |
This file is used in all visualizations pages
---
In the **Panel options** section of the panel editor pane, you set basic options like the panel title and description. You can also configure repeating panels in this section. To learn more, refer to [Configure panel options](../../configure-panel-options/).
In the **Panel options** section of the panel editor pane, set basic options like panel title and description, as well as panel links. To learn more, refer to [Configure panel options](../../configure-panel-options/).

View File

@ -6,8 +6,6 @@ comments: |
**Standard options** in the panel editor pane let you change how field data is displayed in your visualizations. When you set a standard option, the change is applied to all fields or series. For more granular control over the display of fields, refer to [Configure overrides](../../configure-overrides/).
You can customize the following standard options:
| Option | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unit | Choose which unit a field should use. |

View File

@ -8,7 +8,7 @@ comments: |
A threshold is a value or limit you set for a metric thats reflected visually when its met or exceeded. Thresholds are one way you can conditionally style and color your visualizations based on query results.
Set the following options:
For each threshold, set the following options:
| Option | Description |
| --------------- | ------------------------------------------------------------------------------------ |

View File

@ -119,7 +119,7 @@
"@types/lodash": "4.17.7",
"@types/logfmt": "^1.2.3",
"@types/lucene": "^2",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/node-forge": "^1",
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4",
"@types/pluralize": "^0.0.33",
@ -172,7 +172,7 @@
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "28.6.0",
"eslint-plugin-jsdoc": "48.10.2",
"eslint-plugin-jsdoc": "48.11.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-lodash": "7.4.0",
"eslint-plugin-no-barrel-files": "^1.1.0",
@ -187,7 +187,7 @@
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.0",
"http-server": "14.1.1",
"i18next-parser": "8.13.0",
"i18next-parser": "9.0.1",
"jest": "29.7.0",
"jest-canvas-mock": "2.5.2",
"jest-date-mock": "1.0.10",
@ -199,7 +199,7 @@
"knip": "^5.10.0",
"lerna": "8.1.7",
"mini-css-extract-plugin": "2.9.0",
"msw": "2.3.4",
"msw": "2.3.5",
"mutationobserver-shim": "0.3.7",
"ngtemplate-loader": "2.1.0",
"node-notifier": "10.0.1",

View File

@ -66,7 +66,7 @@
"@types/dompurify": "^3.0.0",
"@types/history": "4.7.11",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/papaparse": "5.3.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",

View File

@ -40,7 +40,7 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "15.2.3",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"esbuild": "0.20.2",
"rimraf": "5.0.7",
"rollup": "2.79.1",

View File

@ -68,7 +68,7 @@
"@types/d3": "^7",
"@types/jest": "^29.5.4",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/react-virtualized-auto-sizer": "1.0.4",
"@types/tinycolor2": "1.4.6",

View File

@ -45,7 +45,7 @@
"@svgr/plugin-prettier": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"@types/babel__core": "^7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"esbuild": "0.20.2",

View File

@ -36,7 +36,7 @@
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/jest": "^29.5.4",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/systemjs": "6.13.5",
"@types/testing-library__jest-dom": "5.14.9",

View File

@ -92,7 +92,7 @@
"@types/jest": "29.5.12",
"@types/jquery": "3.5.30",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/pluralize": "^0.0.33",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
@ -111,7 +111,7 @@
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "28.6.0",
"eslint-plugin-jsdoc": "48.10.2",
"eslint-plugin-jsdoc": "48.11.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-lodash": "7.4.0",
"eslint-plugin-react": "7.35.0",

View File

@ -182,6 +182,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
localFileSystemAvailable: boolean | undefined;
cloudMigrationIsTarget: boolean | undefined;
cloudMigrationFeedbackURL = '';
cloudMigrationPollIntervalMs = 2000;
reportingStaticContext?: Record<string, string>;
exploreDefaultTimeOffset = '1h';

View File

@ -42,7 +42,7 @@
"@testing-library/user-event": "14.5.2",
"@types/jest": "^29.5.4",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"@types/react-virtualized-auto-sizer": "1.0.4",

View File

@ -145,7 +145,7 @@
"@types/is-hotkey": "0.1.10",
"@types/jest": "29.5.12",
"@types/mock-raf": "1.0.6",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
"@types/react-color": "3.0.12",

View File

@ -1,8 +1,11 @@
import { css, cx } from '@emotion/css';
import { PropsWithChildren, useLayoutEffect, useRef } from 'react';
import * as React from 'react';
import ReactDOM from 'react-dom';
import { useTheme2 } from '../../themes';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, useTheme2 } from '../../themes';
interface Props {
className?: string;
@ -47,9 +50,27 @@ export function getPortalContainer() {
/** @internal */
export function PortalContainer() {
return <div id="grafana-portal-container" />;
const styles = useStyles2(getStyles);
const isBodyScrolling = window.grafanaBootData?.settings.featureToggles.bodyScrolling;
return (
<div
id="grafana-portal-container"
className={cx({
[styles.grafanaPortalContainer]: isBodyScrolling,
})}
/>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
grafanaPortalContainer: css({
position: 'fixed',
top: 0,
width: '100%',
zIndex: theme.zIndex.portal,
}),
});
export const RefForwardingPortal = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
return <Portal {...props} forwardedRef={ref} />;
});

View File

@ -257,8 +257,9 @@ type FrontendSettingsDTO struct {
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
PublicDashboardsEnabled bool `json:"publicDashboardsEnabled"`
CloudMigrationIsTarget bool `json:"cloudMigrationIsTarget"`
CloudMigrationFeedbackURL string `json:"cloudMigrationFeedbackURL"`
CloudMigrationIsTarget bool `json:"cloudMigrationIsTarget"`
CloudMigrationFeedbackURL string `json:"cloudMigrationFeedbackURL"`
CloudMigrationPollIntervalMs int `json:"cloudMigrationPollIntervalMs"`
DateFormats setting.DateFormats `json:"dateFormats,omitempty"`

View File

@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
@ -463,7 +464,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
actionSets := resourcepermissions.NewActionSetService(features)
acSvc := acimpl.ProvideOSSService(
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db.DB(),
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db.DB(), permreg.ProvidePermissionRegistry(),
)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(

View File

@ -224,6 +224,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled,
CloudMigrationIsTarget: isCloudMigrationTarget,
CloudMigrationFeedbackURL: hs.Cfg.CloudMigration.FeedbackURL,
CloudMigrationPollIntervalMs: int(hs.Cfg.CloudMigration.FrontendPollInterval.Milliseconds()),
SharedWithMeFolderUID: folder.SharedWithMeFolderUID,
RootFolderUID: accesscontrol.GeneralFolderUID,
LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable,

View File

@ -3,6 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery
go 1.21.10
require (
github.com/grafana/authlib v0.0.0-20240730122259-a0d13672efb1
github.com/stretchr/testify v1.9.0
k8s.io/apimachinery v0.29.3
k8s.io/apiserver v0.29.2
@ -12,6 +13,7 @@ require (
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
@ -25,10 +27,15 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

View File

@ -1,5 +1,6 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
@ -9,6 +10,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/grafana/authlib v0.0.0-20240730122259-a0d13672efb1 h1:EiaupmOnt6XF/LPxvagjTofWmByzYaf5VyMIF+w/71M=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -16,12 +18,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
authnlib "github.com/grafana/authlib/authn"
"k8s.io/apiserver/pkg/authentication/user"
)
@ -77,6 +78,8 @@ type Requester interface {
// GetIDToken returns a signed token representing the identity that can be forwarded to plugins and external services.
// Will only be set when featuremgmt.FlagIdForwarding is enabled.
GetIDToken() string
// GetIDClaims returns the claims of the ID token.
GetIDClaims() *authnlib.Claims[authnlib.IDTokenClaims]
}
// IntIdentifier converts a string identifier to an int64.

View File

@ -1,6 +1,10 @@
package identity
import "fmt"
import (
"fmt"
authnlib "github.com/grafana/authlib/authn"
)
var _ Requester = &StaticRequester{}
@ -25,9 +29,10 @@ type StaticRequester struct {
AllowedKubernetesNamespace string
IsGrafanaAdmin bool
// Permissions grouped by orgID and actions
Permissions map[int64]map[string][]string
IDToken string
CacheKey string
Permissions map[int64]map[string][]string
IDToken string
IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims]
CacheKey string
}
// GetRawIdentifier implements Requester.
@ -208,3 +213,7 @@ func (u *StaticRequester) GetDisplayName() string {
func (u *StaticRequester) GetIDToken() string {
return u.IDToken
}
func (u *StaticRequester) GetIDClaims() *authnlib.Claims[authnlib.IDTokenClaims] {
return u.IDTokenClaims
}

View File

@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
@ -90,7 +91,7 @@ func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx
if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to initialize tracer service", err)
}
acService, err := acimpl.ProvideService(cfg, replstore, routing, nil, nil, nil, features, tracer, zanzana.NewNoopClient())
acService, err := acimpl.ProvideService(cfg, replstore, routing, nil, nil, nil, features, tracer, zanzana.NewNoopClient(), permreg.ProvidePermissionRegistry())
if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to get access control", err)
}

View File

@ -39,6 +39,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
@ -345,6 +346,7 @@ var wireBasicSet = wire.NewSet(
resourcepermissions.NewActionSetService,
wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)),
wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)),
permreg.ProvidePermissionRegistry,
acimpl.ProvideAccessControl,
navtreeimpl.ProvideService,
wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)),

View File

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -50,9 +51,9 @@ var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.E
func ProvideService(
cfg *setting.Cfg, db db.ReplDB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
accessControl accesscontrol.AccessControl, actionResolver accesscontrol.ActionResolver,
features featuremgmt.FeatureToggles, tracer tracing.Tracer, zclient zanzana.Client,
features featuremgmt.FeatureToggles, tracer tracing.Tracer, zclient zanzana.Client, permRegistry permreg.PermissionRegistry,
) (*Service, error) {
service := ProvideOSSService(cfg, database.ProvideService(db), actionResolver, cache, features, tracer, zclient, db.DB())
service := ProvideOSSService(cfg, database.ProvideService(db), actionResolver, cache, features, tracer, zclient, db.DB(), permRegistry)
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
@ -73,7 +74,7 @@ func ProvideService(
func ProvideOSSService(
cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver,
cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer,
zclient zanzana.Client, db db.DB,
zclient zanzana.Client, db db.DB, permRegistry permreg.PermissionRegistry,
) *Service {
s := &Service{
actionResolver: actionResolver,
@ -85,6 +86,7 @@ func ProvideOSSService(
store: store,
tracer: tracer,
sync: migrator.NewZanzanaSynchroniser(zclient, db),
permRegistry: permRegistry,
}
return s
@ -102,6 +104,7 @@ type Service struct {
store accesscontrol.Store
tracer tracing.Tracer
sync *migrator.ZanzanaSynchroniser
permRegistry permreg.PermissionRegistry
}
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
@ -406,6 +409,10 @@ func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistrat
return err
}
for i := range r.Role.Permissions {
s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope)
}
s.registrations.Append(r)
}
@ -458,6 +465,12 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
return err
}
for i := range r.Role.Permissions {
// Register plugin actions and their possible scopes for permission validation
s.permRegistry.RegisterPluginScope(r.Role.Permissions[i].Scope)
s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope)
}
s.log.Debug("Registering plugin role", "role", r.Role.Name)
s.registrations.Append(r)
}

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
@ -42,6 +43,7 @@ func setupTestEnv(t testing.TB) *Service {
roles: accesscontrol.BuildBasicRoleDefinitions(),
tracer: tracing.InitializeTracerForTest(),
store: database.ProvideService(db.InitTestReplDB(t)),
permRegistry: permreg.ProvidePermissionRegistry(),
}
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
return ac
@ -71,6 +73,7 @@ func TestUsageMetrics(t *testing.T) {
tracing.InitializeTracerForTest(),
nil,
nil,
permreg.ProvidePermissionRegistry(),
)
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
})

View File

@ -0,0 +1,183 @@
package permreg
import (
"strings"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/infra/log"
)
var (
// ErrInvalidScope is returned when the scope is not valid for the action
ErrInvalidScopeTplt = "invalid scope: {{.Public.Scope}}, for action: {{.Public.Action}}, expected prefixes are {{.Public.ValidScopesFormat}}"
ErrBaseInvalidScope = errutil.BadRequest("permreg.invalid-scope").MustTemplate(ErrInvalidScopeTplt, errutil.WithPublic(ErrInvalidScopeTplt))
ErrUnknownActionTplt = "unknown action: {{.Public.Action}}, was not found in the list of valid actions"
ErrBaseUnknownAction = errutil.BadRequest("permreg.unknown-action").MustTemplate(ErrUnknownActionTplt, errutil.WithPublic(ErrUnknownActionTplt))
)
func ErrInvalidScope(scope string, action string, validScopePrefixes PrefixSet) error {
if len(validScopePrefixes) == 0 {
return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": "[none]"}})
}
formats := generateValidScopeFormats(validScopePrefixes)
return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": formats}})
}
func ErrUnknownAction(action string) error {
return ErrBaseUnknownAction.Build(errutil.TemplateData{Public: map[string]any{"Action": action}})
}
func generateValidScopeFormats(acceptedScopePrefixes PrefixSet) []string {
if len(acceptedScopePrefixes) == 0 {
return []string{}
}
acceptedPrefixesList := make([]string, 0, 10)
acceptedPrefixesList = append(acceptedPrefixesList, "*")
for prefix := range acceptedScopePrefixes {
parts := strings.Split(prefix, ":")
// If the prefix has an attribute part add the intermediate format kind:*
if len(parts) > 2 {
acceptedPrefixesList = append(acceptedPrefixesList, parts[0]+":*")
}
// Add the most specific format kind:attribute:*
acceptedPrefixesList = append(acceptedPrefixesList, prefix+"*")
}
return acceptedPrefixesList
}
type PermissionRegistry interface {
RegisterPluginScope(scope string)
RegisterPermission(action, scope string)
IsPermissionValid(action, scope string) error
GetScopePrefixes(action string) (PrefixSet, bool)
}
type PrefixSet map[string]bool
var _ PermissionRegistry = &permissionRegistry{}
type permissionRegistry struct {
actionScopePrefixes map[string]PrefixSet // TODO use thread safe map
kindScopePrefix map[string]string
logger log.Logger
}
func ProvidePermissionRegistry() PermissionRegistry {
return newPermissionRegistry()
}
func newPermissionRegistry() *permissionRegistry {
// defaultKindScopes maps the most specific accepted scope prefix for a given kind (folders, dashboards, etc)
defaultKindScopes := map[string]string{
"teams": "teams:id:",
"users": "users:id:",
"datasources": "datasources:uid:",
"dashboards": "dashboards:uid:",
"folders": "folders:uid:",
"annotations": "annotations:type:",
"apikeys": "apikeys:id:",
"orgs": "orgs:id:",
"plugins": "plugins:id:",
"provisioners": "provisioners:",
"reports": "reports:id:",
"permissions": "permissions:type:",
"serviceaccounts": "serviceaccounts:id:",
"settings": "settings:",
"global.users": "global.users:id:",
"roles": "roles:uid:",
"services": "services:",
}
return &permissionRegistry{
actionScopePrefixes: make(map[string]PrefixSet, 200),
kindScopePrefix: defaultKindScopes,
logger: log.New("accesscontrol.permreg"),
}
}
func (pr *permissionRegistry) RegisterPluginScope(scope string) {
if scope == "" {
return
}
scopeParts := strings.Split(scope, ":")
// If the scope is already registered, return
if _, found := pr.kindScopePrefix[scopeParts[0]]; found {
return
}
// If the scope contains an attribute part, register the kind and attribute
if len(scopeParts) > 2 {
kind, attr := scopeParts[0], scopeParts[1]
pr.kindScopePrefix[kind] = kind + ":" + attr + ":"
pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":"+attr+":")
return
}
pr.logger.Debug("registered scope prefix", "kind", scopeParts[0], "scope_prefix", scopeParts[0]+":")
pr.kindScopePrefix[scopeParts[0]] = scopeParts[0] + ":"
}
func (pr *permissionRegistry) RegisterPermission(action, scope string) {
if _, ok := pr.actionScopePrefixes[action]; !ok {
pr.actionScopePrefixes[action] = PrefixSet{}
}
if scope == "" {
// scopeless action
return
}
kind := strings.Split(scope, ":")[0]
scopePrefix, ok := pr.kindScopePrefix[kind]
if !ok {
pr.logger.Warn("unknown scope prefix", "scope", scope)
return
}
// Add a new entry in case the scope is not empty
pr.actionScopePrefixes[action][scopePrefix] = true
}
func (pr *permissionRegistry) IsPermissionValid(action, scope string) error {
validScopePrefixes, ok := pr.actionScopePrefixes[action]
if !ok {
return ErrUnknownAction(action)
}
if ok && len(validScopePrefixes) == 0 {
// Expecting action without any scope
if scope != "" {
return ErrInvalidScope(scope, action, nil)
}
return nil
}
if !isScopeValid(scope, validScopePrefixes) {
return ErrInvalidScope(scope, action, validScopePrefixes)
}
return nil
}
func isScopeValid(scope string, validScopePrefixes PrefixSet) bool {
// Super wildcard scope
if scope == "*" {
return true
}
for scopePrefix := range validScopePrefixes {
// Correct scope prefix
if strings.HasPrefix(scope, scopePrefix) {
return true
}
// Scope is wildcard of the correct prefix
if strings.HasSuffix(scope, ":*") && strings.HasPrefix(scopePrefix, scope[:len(scope)-2]) {
return true
}
}
return false
}
func (pr *permissionRegistry) GetScopePrefixes(action string) (PrefixSet, bool) {
set, ok := pr.actionScopePrefixes[action]
return set, ok
}

View File

@ -0,0 +1,246 @@
package permreg
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_permissionRegistry_RegisterPluginScope(t *testing.T) {
tests := []struct {
scope string
wantKind string
wantScope string
}{
{
scope: "folders:uid:AABBCC",
wantKind: "folders",
wantScope: "folders:uid:",
},
{
scope: "plugins:id:test-app",
wantKind: "plugins",
wantScope: "plugins:id:",
},
{
scope: "resource:uid:res",
wantKind: "resource",
wantScope: "resource:uid:",
},
{
scope: "resource:*",
wantKind: "resource",
wantScope: "resource:",
},
}
for _, tt := range tests {
t.Run(tt.scope, func(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPluginScope(tt.scope)
got, ok := pr.kindScopePrefix[tt.wantKind]
require.True(t, ok)
require.Equal(t, tt.wantScope, got)
})
}
}
func Test_permissionRegistry_RegisterPermission(t *testing.T) {
tests := []struct {
name string
action string
scope string
wantKind string
wantPrefixSet PrefixSet
wantSkip bool
}{
{
name: "register folders read",
action: "folders:read",
scope: "folders:*",
wantKind: "folders",
wantPrefixSet: PrefixSet{"folders:uid:": true},
},
{
name: "register app plugin settings read",
action: "test-app.settings:read",
wantKind: "settings",
wantPrefixSet: PrefixSet{},
},
{
name: "register an action on an unknown kind",
action: "unknown:action",
scope: "unknown:uid:*",
wantPrefixSet: PrefixSet{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPermission(tt.action, tt.scope)
got, ok := pr.actionScopePrefixes[tt.action]
require.True(t, ok)
for k, v := range got {
require.Equal(t, v, tt.wantPrefixSet[k])
}
})
}
}
func Test_permissionRegistry_IsPermissionValid(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPermission("folders:read", "folders:uid:")
pr.RegisterPermission("test-app.settings:read", "")
tests := []struct {
name string
action string
scope string
wantErr bool
}{
{
name: "valid folders read",
action: "folders:read",
scope: "folders:uid:AABBCC",
wantErr: false,
},
{
name: "valid folders read with wildcard",
action: "folders:read",
scope: "folders:uid:*",
wantErr: false,
},
{
name: "valid folders read with kind level wildcard",
action: "folders:read",
scope: "folders:*",
wantErr: false,
},
{
name: "valid folders read with super wildcard",
action: "folders:read",
scope: "*",
wantErr: false,
},
{
name: "invalid folders read with wrong kind",
action: "folders:read",
scope: "unknown:uid:AABBCC",
wantErr: true,
},
{
name: "invalid folders read with wrong attribute",
action: "folders:read",
scope: "folders:id:3",
wantErr: true,
},
{
name: "valid app plugin settings read",
action: "test-app.settings:read",
scope: "",
wantErr: false,
},
{
name: "app plugin settings read with a scope",
action: "test-app.settings:read",
scope: "folders:uid:*",
wantErr: true,
},
{
name: "unknown action",
action: "unknown:write",
scope: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := pr.IsPermissionValid(tt.action, tt.scope)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func Test_permissionRegistry_GetScopePrefixes(t *testing.T) {
pr := newPermissionRegistry()
pr.RegisterPermission("folders:read", "folders:uid:")
pr.RegisterPermission("test-app.settings:read", "")
tests := []struct {
name string
action string
want PrefixSet
shouldExist bool
}{
{
name: "get folders read scope prefixes",
action: "folders:read",
want: PrefixSet{"folders:uid:": true},
shouldExist: true,
},
{
name: "get app plugin settings read scope prefixes",
action: "test-app.settings:read",
want: PrefixSet{},
shouldExist: true,
},
{
name: "get unknown action scope prefixes",
action: "unknown:write",
want: PrefixSet{},
shouldExist: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := pr.GetScopePrefixes(tt.action)
if !tt.shouldExist {
require.False(t, got1)
return
}
require.True(t, got1)
require.Len(t, tt.want, len(got))
for k, v := range got {
require.Equal(t, v, tt.want[k])
}
})
}
}
func Test_generateValidScopeFormats(t *testing.T) {
tests := []struct {
name string
prefixSet PrefixSet
want []string
}{
{
name: "empty prefix set",
prefixSet: PrefixSet{},
want: []string{},
},
{
name: "short prefix",
prefixSet: PrefixSet{"folders:": true},
want: []string{"*", "folders:*"},
},
{
name: "single prefix",
prefixSet: PrefixSet{"folders:uid:": true},
want: []string{"*", "folders:*", "folders:uid:*"},
},
{
name: "multiple prefixes",
prefixSet: PrefixSet{"folders:uid:": true, "dashboards:uid:": true},
want: []string{"*", "folders:*", "folders:uid:*", "dashboards:*", "dashboards:uid:*"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateValidScopeFormats(tt.prefixSet)
require.ElementsMatch(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,22 @@
package test
import "github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
func ProvidePermissionRegistry() permreg.PermissionRegistry {
permReg := permreg.ProvidePermissionRegistry()
// Test core permissions
permReg.RegisterPermission("datasources:read", "datasources:uid:")
permReg.RegisterPermission("dashboards:read", "dashboards:uid:")
permReg.RegisterPermission("dashboards:read", "folders:uid:")
permReg.RegisterPermission("folders:read", "folders:uid:")
// Test plugins permissions
permReg.RegisterPermission("plugins.app:access", "plugins:id:")
// App
permReg.RegisterPermission("test-app:read", "")
permReg.RegisterPermission("test-app.settings:read", "")
permReg.RegisterPermission("test-app.projects:read", "")
// App 1
permReg.RegisterPermission("test-app1.catalog:read", "")
permReg.RegisterPermission("test-app1.announcements:read", "")
return permReg
}

View File

@ -10,7 +10,7 @@ import (
type IDService interface {
// SignIdentity signs a id token for provided identity that can be forwarded to plugins and external services
SignIdentity(ctx context.Context, identity identity.Requester) (string, error)
SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error)
// RemoveIDToken removes any locally stored id tokens for key
RemoveIDToken(ctx context.Context, identity identity.Requester) error

View File

@ -58,21 +58,30 @@ type Service struct {
nsMapper request.NamespaceMapper
}
func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, error) {
func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
defer func(t time.Time) {
s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds())
}(time.Now())
cacheKey := prefixCacheKey(id.GetCacheKey())
result, err, _ := s.si.Do(cacheKey, func() (interface{}, error) {
type resultType struct {
token string
idClaims *auth.IDClaims
}
result, err, _ := s.si.Do(cacheKey, func() (any, error) {
namespace, identifier := id.GetTypedID()
cachedToken, err := s.cache.Get(ctx, cacheKey)
if err == nil {
s.metrics.tokenSigningFromCacheCounter.Inc()
s.logger.FromContext(ctx).Debug("Cached token found", "namespace", namespace, "id", identifier)
return string(cachedToken), nil
tokenClaims, err := s.extractTokenClaims(string(cachedToken))
if err != nil {
return resultType{}, err
}
return resultType{token: string(cachedToken), idClaims: tokenClaims}, nil
}
s.metrics.tokenSigningCounter.Inc()
@ -104,21 +113,12 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
token, err := s.signer.SignIDToken(ctx, claims)
if err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return "", err
return resultType{}, nil
}
parsed, err := jwt.ParseSigned(token)
extracted, err := s.extractTokenClaims(token)
if err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return "", err
}
extracted := auth.IDClaims{}
// We don't need to verify the signature here, we are only interested in checking
// when the token expires.
if err := parsed.UnsafeClaimsWithoutVerification(&extracted); err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return "", err
return resultType{}, err
}
expires := time.Until(extracted.Expiry.Time())
@ -126,14 +126,14 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
s.logger.FromContext(ctx).Error("Failed to add id token to cache", "error", err)
}
return token, nil
return resultType{token: token, idClaims: claims}, nil
})
if err != nil {
return "", err
return "", nil, err
}
return result.(string), nil
return result.(resultType).token, result.(resultType).idClaims, nil
}
func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error {
@ -142,7 +142,7 @@ func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) erro
func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
// FIXME(kalleep): we should probably lazy load this
token, err := s.SignIdentity(ctx, identity)
token, claims, err := s.SignIdentity(ctx, identity)
if err != nil {
if shouldLogErr(err) {
namespace, id := identity.GetTypedID()
@ -153,9 +153,28 @@ func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.R
}
identity.IDToken = token
identity.IDTokenClaims = claims
return nil
}
func (s *Service) extractTokenClaims(token string) (*authnlib.Claims[authnlib.IDTokenClaims], error) {
parsed, err := jwt.ParseSigned(token)
if err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return nil, err
}
extracted := authnlib.Claims[authnlib.IDTokenClaims]{}
// We don't need to verify the signature here, we are only interested in checking
// when the token expires.
if err := parsed.UnsafeClaimsWithoutVerification(&extracted); err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return nil, err
}
return &extracted, nil
}
func getAudience(orgID int64) jwt.Audience {
return jwt.Audience{fmt.Sprintf("org:%d", orgID)}
}

View File

@ -70,7 +70,7 @@ func TestService_SignIdentity(t *testing.T) {
featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding),
&authntest.FakeService{}, nil,
)
token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: identity.MustParseTypedID("user:1")})
token, _, err := s.SignIdentity(context.Background(), &authn.Identity{ID: identity.MustParseTypedID("user:1")})
require.NoError(t, err)
require.NotEmpty(t, token)
})
@ -81,7 +81,7 @@ func TestService_SignIdentity(t *testing.T) {
featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding),
&authntest.FakeService{}, nil,
)
token, err := s.SignIdentity(context.Background(), &authn.Identity{
token, _, err := s.SignIdentity(context.Background(), &authn.Identity{
ID: identity.MustParseTypedID("user:1"),
AuthenticatedBy: login.AzureADAuthModule,
Login: "U1",
@ -97,4 +97,22 @@ func TestService_SignIdentity(t *testing.T) {
assert.Equal(t, "U1", claims.Rest.Username)
assert.Equal(t, "user:edpu3nnt61se8e", claims.Rest.UID)
})
t.Run("should sign identity with authenticated by if user is externally authenticated", func(t *testing.T) {
s := ProvideService(
setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(),
featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding),
&authntest.FakeService{}, nil,
)
_, gotClaims, err := s.SignIdentity(context.Background(), &authn.Identity{
ID: identity.MustParseTypedID("user:1"),
AuthenticatedBy: login.AzureADAuthModule,
Login: "U1",
UID: identity.NewTypedIDString(identity.TypeUser, "edpu3nnt61se8e")})
require.NoError(t, err)
assert.Equal(t, login.AzureADAuthModule, gotClaims.Rest.AuthenticatedBy)
assert.Equal(t, "U1", gotClaims.Rest.Username)
assert.Equal(t, "user:edpu3nnt61se8e", gotClaims.Rest.UID)
})
}

View File

@ -3,6 +3,8 @@ package idtest
import (
"context"
authnlib "github.com/grafana/authlib/authn"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/auth"
)
@ -10,15 +12,15 @@ import (
var _ auth.IDService = (*MockService)(nil)
type MockService struct {
SignIdentityFn func(ctx context.Context, identity identity.Requester) (string, error)
SignIdentityFn func(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error)
RemoveIDTokenFn func(ctx context.Context, identity identity.Requester) error
}
func (m *MockService) SignIdentity(ctx context.Context, identity identity.Requester) (string, error) {
func (m *MockService) SignIdentity(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
if m.SignIdentityFn != nil {
return m.SignIdentityFn(ctx, identity)
}
return "", nil
return "", nil, nil
}
func (m *MockService) RemoveIDToken(ctx context.Context, identity identity.Requester) error {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/grafana/authlib/authn"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@ -69,7 +70,8 @@ type Identity struct {
Permissions map[int64]map[string][]string
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
// Will only be set when featuremgmt.FlagIdForwarding is enabled.
IDToken string
IDToken string
IDTokenClaims *authn.Claims[authn.IDTokenClaims]
}
// GetRawIdentifier implements Requester.
@ -156,6 +158,10 @@ func (i *Identity) GetIDToken() string {
return i.IDToken
}
func (i *Identity) GetIDClaims() *authn.Claims[authn.IDTokenClaims] {
return i.IDTokenClaims
}
func (i *Identity) GetIsGrafanaAdmin() bool {
return i.IsGrafanaAdmin != nil && *i.IsGrafanaAdmin
}

View File

@ -144,8 +144,9 @@ var (
{
Name: "autoMigrateXYChartPanel",
Description: "Migrate old XYChart panel to new XYChart2 model",
Stage: FeatureStagePublicPreview,
Stage: FeatureStageGeneralAvailability,
FrontendOnly: true,
Expression: "true", // enabled by default
Owner: grafanaDatavizSquad,
},
{

View File

@ -16,7 +16,7 @@ autoMigrateTablePanel,preview,@grafana/dataviz-squad,false,false,true
autoMigratePiechartPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateWorldmapPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateStatPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateXYChartPanel,preview,@grafana/dataviz-squad,false,false,true
autoMigrateXYChartPanel,GA,@grafana/dataviz-squad,false,false,true
disableAngular,preview,@grafana/dataviz-squad,false,false,true
canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true
disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,true,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
16 autoMigratePiechartPanel preview @grafana/dataviz-squad false false true
17 autoMigrateWorldmapPanel preview @grafana/dataviz-squad false false true
18 autoMigrateStatPanel preview @grafana/dataviz-squad false false true
19 autoMigrateXYChartPanel preview GA @grafana/dataviz-squad false false true
20 disableAngular preview @grafana/dataviz-squad false false true
21 canvasPanelNesting experimental @grafana/dataviz-squad false false true
22 disableSecretsCompatibility experimental @grafana/hosted-grafana-team false true false

View File

@ -429,14 +429,18 @@
{
"metadata": {
"name": "autoMigrateXYChartPanel",
"resourceVersion": "1718727528075",
"creationTimestamp": "2024-03-22T15:44:37Z"
"resourceVersion": "1722537244598",
"creationTimestamp": "2024-03-22T15:44:37Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-08-01 18:34:04.598082 +0000 UTC"
}
},
"spec": {
"description": "Migrate old XYChart panel to new XYChart2 model",
"stage": "preview",
"stage": "GA",
"codeowner": "@grafana/dataviz-squad",
"frontend": true
"frontend": true,
"expression": "true"
}
},
{

View File

@ -346,7 +346,7 @@ func (ng *AlertNG) init() error {
evalFactory := eval.NewEvaluatorFactory(ng.Cfg.UnifiedAlerting, ng.DataSourceCache, ng.ExpressionService)
conditionValidator := eval.NewConditionValidator(ng.DataSourceCache, ng.ExpressionService, ng.pluginsStore)
recordingWriter, err := createRecordingWriter(ng.FeatureToggles, ng.Cfg.UnifiedAlerting.RecordingRules, ng.httpClientProvider, clk, ng.tracer, ng.Metrics.GetRemoteWriterMetrics())
recordingWriter, err := createRecordingWriter(ng.FeatureToggles, ng.Cfg.UnifiedAlerting.RecordingRules, ng.httpClientProvider, clk, ng.Metrics.GetRemoteWriterMetrics())
if err != nil {
return fmt.Errorf("failed to initialize recording writer: %w", err)
}
@ -663,11 +663,11 @@ func createRemoteAlertmanager(cfg remote.AlertmanagerConfig, kvstore kvstore.KVS
return remote.NewAlertmanager(cfg, notifier.NewFileStore(cfg.OrgID, kvstore), decryptFn, autogenFn, m, tracer)
}
func createRecordingWriter(featureToggles featuremgmt.FeatureToggles, settings setting.RecordingRuleSettings, httpClientProvider httpclient.Provider, clock clock.Clock, tracer tracing.Tracer, m *metrics.RemoteWriter) (schedule.RecordingWriter, error) {
func createRecordingWriter(featureToggles featuremgmt.FeatureToggles, settings setting.RecordingRuleSettings, httpClientProvider httpclient.Provider, clock clock.Clock, m *metrics.RemoteWriter) (schedule.RecordingWriter, error) {
logger := log.New("ngalert.writer")
if featureToggles.IsEnabledGlobally(featuremgmt.FlagGrafanaManagedRecordingRules) {
return writer.NewPrometheusWriter(settings, httpClientProvider, clock, tracer, logger, m)
return writer.NewPrometheusWriter(settings, httpClientProvider, clock, logger, m)
}
return writer.NoopWriter{}, nil

View File

@ -12,6 +12,7 @@ import (
"github.com/prometheus/alertmanager/config"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@ -516,13 +517,39 @@ func RemoveSecretsForContactPoint(e *apimodels.EmbeddedContactPoint) (map[string
return nil, err
}
for _, secretKey := range secretKeys {
secretValue := e.Settings.Get(secretKey).MustString()
e.Settings.Del(secretKey)
foundSecretKey, secretValue, err := getCaseInsensitive(e.Settings, secretKey)
if err != nil {
return nil, err
}
e.Settings.Del(foundSecretKey)
s[secretKey] = secretValue
}
return s, nil
}
// getCaseInsensitive returns the value of the specified key, preferring an exact match but accepting a case-insensitive match.
// If no key matches, the second return value is an empty string.
func getCaseInsensitive(jsonObj *simplejson.Json, key string) (string, string, error) {
// Check for an exact key match first.
if value, ok := jsonObj.CheckGet(key); ok {
return key, value.MustString(), nil
}
// If no exact match is found, look for a case-insensitive match.
settingsMap, err := jsonObj.Map()
if err != nil {
return "", "", err
}
for k, v := range settingsMap {
if strings.EqualFold(k, key) {
return k, v.(string), nil
}
}
return key, "", nil
}
// convertRecSvcErr converts errors from notifier.ReceiverService to errors expected from ContactPointService.
func convertRecSvcErr(err error) error {
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {

View File

@ -40,6 +40,11 @@ func TestContactPointService(t *testing.T) {
accesscontrol.ActionAlertingProvisioningRead: nil,
},
}}
decryptedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingProvisioningReadSecrets: nil,
},
}}
t.Run("service gets contact points from AM config", func(t *testing.T) {
sut := createContactPointServiceSut(t, secretsService)
@ -265,6 +270,52 @@ func TestContactPointService(t *testing.T) {
intercepted := fakeConfigStore.LastSaveCommand
require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash)
})
t.Run("secrets are parsed in a case-insensitive way", func(t *testing.T) {
// JSON unmarshalling is case-insensitive. This means we can have
// a setting named "TOKEN" instead of "token". This test ensures that
// we handle such cases correctly and the token value is properly parsed,
// even if the setting key does not match the JSON key exactly.
tests := []struct {
settingsJSON string
expectedValue string
name string
}{
{
settingsJSON: `{"recipient":"value_recipient","TOKEN":"some-other-token"}`,
expectedValue: "some-other-token",
name: "token key is uppercased",
},
// This test checks that if multiple token keys are present in the settings,
// the key with the exact matching name is used.
{
settingsJSON: `{"recipient":"value_recipient","TOKEN":"some-other-token", "token": "second-token"}`,
expectedValue: "second-token",
name: "multiple token keys",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
sut := createContactPointServiceSut(t, secretsService)
newCp := createTestContactPoint()
settings, _ := simplejson.NewJson([]byte(tc.settingsJSON))
newCp.Settings = settings
_, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
require.NoError(t, err)
q := cpsQueryWithName(1, newCp.Name)
q.Decrypt = true
cps, err := sut.GetContactPoints(context.Background(), q, decryptedUser)
require.NoError(t, err)
require.Len(t, cps, 1)
require.Equal(t, tc.expectedValue, cps[0].Settings.Get("token").MustString())
})
}
})
}
func TestContactPointServiceDecryptRedact(t *testing.T) {

View File

@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
models "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -532,7 +531,7 @@ func withQueryForHealth(health string) models.AlertRuleMutator {
func setupWriter(t *testing.T, target *writer.TestRemoteWriteTarget, reg prometheus.Registerer) *writer.PrometheusWriter {
provider := testClientProvider{}
m := metrics.NewNGAlert(reg)
wr, err := writer.NewPrometheusWriter(target.ClientSettings(), provider, clock.NewMock(), tracing.InitializeTracerForTest(), log.NewNopLogger(), m.GetRemoteWriterMetrics())
wr, err := writer.NewPrometheusWriter(target.ClientSettings(), provider, clock.NewMock(), log.NewNopLogger(), m.GetRemoteWriterMetrics())
require.NoError(t, err)
return wr
}

View File

@ -11,7 +11,6 @@ import (
"github.com/benbjohnson/clock"
"github.com/grafana/dataplane/sdata/numeric"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/setting"
@ -106,7 +105,6 @@ func NewPrometheusWriter(
settings setting.RecordingRuleSettings,
httpClientProvider HttpClientProvider,
clock clock.Clock,
tracer tracing.Tracer,
l log.Logger,
metrics *metrics.RemoteWriter,
) (*PrometheusWriter, error) {
@ -119,14 +117,9 @@ func NewPrometheusWriter(
headers.Add(k, v)
}
middlewares := []httpclient.Middleware{
httpclient.TracingMiddleware(tracer),
}
cl, err := httpClientProvider.New(httpclient.Options{
Middlewares: middlewares,
BasicAuth: createAuthOpts(settings.BasicAuthUsername, settings.BasicAuthPassword),
Header: headers,
BasicAuth: createAuthOpts(settings.BasicAuthUsername, settings.BasicAuthPassword),
Header: headers,
})
if err != nil {
return nil, err

View File

@ -15,6 +15,7 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/extsvcauth"
@ -48,6 +49,7 @@ func setupTestEnv(t *testing.T) *TestEnv {
acSvc: acimpl.ProvideOSSService(
cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{},
localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil,
permreg.ProvidePermissionRegistry(),
),
features: fmgt,
logger: logger,

View File

@ -5,6 +5,8 @@ import (
"strconv"
"time"
authnlib "github.com/grafana/authlib/authn"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
@ -40,9 +42,11 @@ type SignedInUser struct {
Teams []int64
// Permissions grouped by orgID and actions
Permissions map[int64]map[string][]string `json:"-"`
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
// Will only be set when featuremgmt.FlagIdForwarding is enabled.
IDToken string `json:"-" xorm:"-"`
IDToken string `json:"-" xorm:"-"`
IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] `json:"-" xorm:"-"`
// When other settings are not deterministic, this value is used
FallbackType identity.IdentityType
@ -309,3 +313,7 @@ func (u *SignedInUser) GetDisplayName() string {
func (u *SignedInUser) GetIDToken() string {
return u.IDToken
}
func (u *SignedInUser) GetIDClaims() *authnlib.Claims[authnlib.IDTokenClaims] {
return u.IDTokenClaims
}

View File

@ -24,6 +24,7 @@ type CloudMigrationSettings struct {
DeleteTokenTimeout time.Duration
TokenExpiresAfter time.Duration
FeedbackURL string
FrontendPollInterval time.Duration
IsDeveloperMode bool
}
@ -49,6 +50,7 @@ func (cfg *Cfg) readCloudMigrationSettings() {
cfg.CloudMigration.TokenExpiresAfter = cloudMigration.Key("token_expires_after").MustDuration(7 * 24 * time.Hour)
cfg.CloudMigration.IsDeveloperMode = cloudMigration.Key("developer_mode").MustBool(false)
cfg.CloudMigration.FeedbackURL = cloudMigration.Key("feedback_url").MustString("")
cfg.CloudMigration.FrontendPollInterval = cloudMigration.Key("frontend_poll_interval").MustDuration(2 * time.Second)
if cfg.CloudMigration.SnapshotFolder == "" {
cfg.CloudMigration.SnapshotFolder = filepath.Join(cfg.DataPath, "cloud_migration")

View File

@ -12,7 +12,6 @@ import (
// Package-level errors.
var (
ErrOptimisticLockingFailed = errors.New("optimistic locking failed")
ErrUserNotFoundInContext = errors.New("user not found in context")
ErrNotImplementedYet = errors.New("not implemented yet")
)

View File

@ -211,19 +211,15 @@ func (s *server) Stop(ctx context.Context) error {
}
// Old value indicates an update -- otherwise a create
func (s *server) newEvent(ctx context.Context, key *ResourceKey, value, oldValue []byte) (*WriteEvent, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, ErrUserNotFoundInContext
}
func (s *server) newEvent(ctx context.Context, user identity.Requester, key *ResourceKey, value, oldValue []byte) (*WriteEvent, *ErrorResult) {
tmp := &unstructured.Unstructured{}
err = tmp.UnmarshalJSON(value)
err := tmp.UnmarshalJSON(value)
if err != nil {
return nil, err
return nil, AsErrorResult(err)
}
obj, err := utils.MetaAccessor(tmp)
if err != nil {
return nil, err
return nil, AsErrorResult(err)
}
event := &WriteEvent{
@ -239,27 +235,27 @@ func (s *server) newEvent(ctx context.Context, key *ResourceKey, value, oldValue
temp := &unstructured.Unstructured{}
err = temp.UnmarshalJSON(oldValue)
if err != nil {
return nil, err
return nil, AsErrorResult(err)
}
event.ObjectOld, err = utils.MetaAccessor(temp)
if err != nil {
return nil, err
return nil, AsErrorResult(err)
}
}
if key.Namespace != obj.GetNamespace() {
return nil, apierrors.NewBadRequest("key/namespace do not match")
return nil, NewBadRequestError("key/namespace do not match")
}
gvk := obj.GetGroupVersionKind()
if gvk.Kind == "" {
return nil, apierrors.NewBadRequest("expecting resources with a kind in the body")
return nil, NewBadRequestError("expecting resources with a kind in the body")
}
if gvk.Version == "" {
return nil, apierrors.NewBadRequest("expecting resources with an apiVersion")
return nil, NewBadRequestError("expecting resources with an apiVersion")
}
if gvk.Group != "" && gvk.Group != key.Group {
return nil, apierrors.NewBadRequest(
return nil, NewBadRequestError(
fmt.Sprintf("group in key does not match group in the body (%s != %s)", key.Group, gvk.Group),
)
}
@ -267,15 +263,14 @@ func (s *server) newEvent(ctx context.Context, key *ResourceKey, value, oldValue
// This needs to be a create function
if key.Name == "" {
if obj.GetName() == "" {
return nil, apierrors.NewBadRequest("missing name")
return nil, NewBadRequestError("missing name")
}
key.Name = obj.GetName()
} else if key.Name != obj.GetName() {
return nil, apierrors.NewBadRequest(
return nil, NewBadRequestError(
fmt.Sprintf("key/name do not match (key: %s, name: %s)", key.Name, obj.GetName()))
}
err = validateName(obj.GetName())
if err != nil {
if err := validateName(obj.GetName()); err != nil {
return nil, err
}
@ -283,17 +278,17 @@ func (s *server) newEvent(ctx context.Context, key *ResourceKey, value, oldValue
if folder != "" {
err = s.access.CanWriteFolder(ctx, user, folder)
if err != nil {
return nil, err
return nil, AsErrorResult(err)
}
}
origin, err := obj.GetOriginInfo()
if err != nil {
return nil, apierrors.NewBadRequest("invalid origin info")
return nil, NewBadRequestError("invalid origin info")
}
if origin != nil {
err = s.access.CanWriteOrigin(ctx, user, origin.Name)
if err != nil {
return nil, err
return nil, AsErrorResult(err)
}
}
return event, nil
@ -308,6 +303,15 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons
}
rsp := &CreateResponse{}
user, err := identity.GetRequester(ctx)
if err != nil || user == nil {
rsp.Error = &ErrorResult{
Message: "no user found in context",
Code: http.StatusUnauthorized,
}
return rsp, nil
}
found := s.backend.ReadResource(ctx, &ReadRequest{Key: req.Key})
if found != nil && len(found.Value) > 0 {
rsp.Error = &ErrorResult{
@ -317,9 +321,9 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons
return rsp, nil
}
event, err := s.newEvent(ctx, req.Key, req.Value, nil)
if err != nil {
rsp.Error = AsErrorResult(err)
event, e := s.newEvent(ctx, user, req.Key, req.Value, nil)
if e != nil {
rsp.Error = e
return rsp, nil
}
@ -339,6 +343,14 @@ func (s *server) Update(ctx context.Context, req *UpdateRequest) (*UpdateRespons
}
rsp := &UpdateResponse{}
user, err := identity.GetRequester(ctx)
if err != nil || user == nil {
rsp.Error = &ErrorResult{
Message: "no user found in context",
Code: http.StatusUnauthorized,
}
return rsp, nil
}
if req.ResourceVersion < 0 {
rsp.Error = AsErrorResult(apierrors.NewBadRequest("update must include the previous version"))
return rsp, nil
@ -359,9 +371,9 @@ func (s *server) Update(ctx context.Context, req *UpdateRequest) (*UpdateRespons
return nil, ErrOptimisticLockingFailed
}
event, err := s.newEvent(ctx, req.Key, req.Value, latest.Value)
if err != nil {
rsp.Error = AsErrorResult(err)
event, e := s.newEvent(ctx, user, req.Key, req.Value, latest.Value)
if e != nil {
rsp.Error = e
return rsp, err
}

View File

@ -1,22 +1,21 @@
package resource
import (
"fmt"
"regexp"
)
var validNameCharPattern = `a-zA-Z0-9\-\_\.`
var validNamePattern = regexp.MustCompile(`^[` + validNameCharPattern + `]*$`).MatchString
func validateName(name string) error {
func validateName(name string) *ErrorResult {
if len(name) == 0 {
return fmt.Errorf("name is too short")
return NewBadRequestError("name is too short")
}
if len(name) > 64 {
return fmt.Errorf("name is too long")
return NewBadRequestError("name is too long")
}
if !validNamePattern(name) {
return fmt.Errorf("name includes invalid characters")
return NewBadRequestError("name includes invalid characters")
}
// In standard k8s, it must not start with a number
// however that would force us to update many many many existing resources

View File

@ -7,24 +7,24 @@ import (
)
func TestNameValidation(t *testing.T) {
require.Error(t, validateName("")) // too short
require.Error(t, validateName( // too long (max 64)
require.NotNil(t, validateName("")) // too short
require.NotNil(t, validateName( // too long (max 64)
"0123456789012345678901234567890123456789012345678901234567890123456789",
))
// OK
require.NoError(t, validateName("a"))
require.NoError(t, validateName("hello-world"))
require.NoError(t, validateName("hello.world"))
require.NoError(t, validateName("hello_world"))
require.Nil(t, validateName("a"))
require.Nil(t, validateName("hello-world"))
require.Nil(t, validateName("hello.world"))
require.Nil(t, validateName("hello_world"))
// Bad characters
require.Error(t, validateName("hello world"))
require.Error(t, validateName("hello!"))
require.Error(t, validateName("hello~"))
require.Error(t, validateName("hello "))
require.Error(t, validateName("hello*"))
require.Error(t, validateName("hello+"))
require.Error(t, validateName("hello="))
require.Error(t, validateName("hello%"))
require.NotNil(t, validateName("hello world"))
require.NotNil(t, validateName("hello!"))
require.NotNil(t, validateName("hello~"))
require.NotNil(t, validateName("hello "))
require.NotNil(t, validateName("hello*"))
require.NotNil(t, validateName("hello+"))
require.NotNil(t, validateName("hello="))
require.NotNil(t, validateName("hello%"))
}

View File

@ -25,6 +25,7 @@
"unicons/bell-slash",
"unicons/bolt",
"unicons/book",
"unicons/bookmark",
"unicons/book-open",
"unicons/brackets-curly",
"unicons/bug",
@ -170,5 +171,6 @@
"mono/heart-break",
"mono/panel-add",
"mono/library-panel",
"unicons/record-audio"
"unicons/record-audio",
"solid/bookmark"
]

View File

@ -33,152 +33,154 @@ import u1022 from '../../../img/icons/unicons/bell.svg';
import u1023 from '../../../img/icons/unicons/bell-slash.svg';
import u1024 from '../../../img/icons/unicons/bolt.svg';
import u1025 from '../../../img/icons/unicons/book.svg';
import u1026 from '../../../img/icons/unicons/book-open.svg';
import u1027 from '../../../img/icons/unicons/brackets-curly.svg';
import u1028 from '../../../img/icons/unicons/bug.svg';
import u1029 from '../../../img/icons/unicons/building.svg';
import u1030 from '../../../img/icons/unicons/calculator-alt.svg';
import u1031 from '../../../img/icons/unicons/calendar-alt.svg';
import u1032 from '../../../img/icons/unicons/calendar-slash.svg';
import u1033 from '../../../img/icons/unicons/camera.svg';
import u1034 from '../../../img/icons/unicons/channel-add.svg';
import u1035 from '../../../img/icons/unicons/chart-line.svg';
import u1036 from '../../../img/icons/unicons/check.svg';
import u1037 from '../../../img/icons/unicons/check-circle.svg';
import u1038 from '../../../img/icons/unicons/circle.svg';
import u1039 from '../../../img/icons/unicons/clipboard-alt.svg';
import u1040 from '../../../img/icons/unicons/clock-nine.svg';
import u1041 from '../../../img/icons/unicons/cloud.svg';
import u1042 from '../../../img/icons/unicons/cloud-download.svg';
import u1043 from '../../../img/icons/unicons/code-branch.svg';
import u1044 from '../../../img/icons/unicons/cog.svg';
import u1045 from '../../../img/icons/unicons/columns.svg';
import u1046 from '../../../img/icons/unicons/comment-alt.svg';
import u1047 from '../../../img/icons/unicons/comment-alt-share.svg';
import u1048 from '../../../img/icons/unicons/comments-alt.svg';
import u1049 from '../../../img/icons/unicons/compass.svg';
import u1050 from '../../../img/icons/unicons/copy.svg';
import u1051 from '../../../img/icons/unicons/corner-down-right-alt.svg';
import u1052 from '../../../img/icons/unicons/cube.svg';
import u1053 from '../../../img/icons/unicons/dashboard.svg';
import u1054 from '../../../img/icons/unicons/database.svg';
import u1055 from '../../../img/icons/unicons/document-info.svg';
import u1056 from '../../../img/icons/unicons/download-alt.svg';
import u1057 from '../../../img/icons/unicons/draggabledots.svg';
import u1058 from '../../../img/icons/unicons/edit.svg';
import u1059 from '../../../img/icons/unicons/ellipsis-v.svg';
import u1060 from '../../../img/icons/unicons/ellipsis-h.svg';
import u1061 from '../../../img/icons/unicons/envelope.svg';
import u1062 from '../../../img/icons/unicons/exchange-alt.svg';
import u1063 from '../../../img/icons/unicons/exclamation-circle.svg';
import u1064 from '../../../img/icons/unicons/exclamation-triangle.svg';
import u1065 from '../../../img/icons/unicons/external-link-alt.svg';
import u1066 from '../../../img/icons/unicons/eye.svg';
import u1067 from '../../../img/icons/unicons/eye-slash.svg';
import u1068 from '../../../img/icons/unicons/file-alt.svg';
import u1069 from '../../../img/icons/unicons/file-blank.svg';
import u1070 from '../../../img/icons/unicons/filter.svg';
import u1071 from '../../../img/icons/unicons/folder.svg';
import u1072 from '../../../img/icons/unicons/folder-open.svg';
import u1073 from '../../../img/icons/unicons/folder-plus.svg';
import u1074 from '../../../img/icons/unicons/folder-upload.svg';
import u1075 from '../../../img/icons/unicons/forward.svg';
import u1076 from '../../../img/icons/unicons/graph-bar.svg';
import u1077 from '../../../img/icons/unicons/history.svg';
import u1078 from '../../../img/icons/unicons/history-alt.svg';
import u1079 from '../../../img/icons/unicons/home-alt.svg';
import u1080 from '../../../img/icons/unicons/import.svg';
import u1081 from '../../../img/icons/unicons/info.svg';
import u1082 from '../../../img/icons/unicons/info-circle.svg';
import u1083 from '../../../img/icons/unicons/k6.svg';
import u1084 from '../../../img/icons/unicons/key-skeleton-alt.svg';
import u1085 from '../../../img/icons/unicons/keyboard.svg';
import u1086 from '../../../img/icons/unicons/link.svg';
import u1087 from '../../../img/icons/unicons/list-ul.svg';
import u1088 from '../../../img/icons/unicons/lock.svg';
import u1089 from '../../../img/icons/unicons/minus.svg';
import u1090 from '../../../img/icons/unicons/minus-circle.svg';
import u1091 from '../../../img/icons/unicons/mobile-android.svg';
import u1092 from '../../../img/icons/unicons/monitor.svg';
import u1093 from '../../../img/icons/unicons/pause.svg';
import u1094 from '../../../img/icons/unicons/pen.svg';
import u1095 from '../../../img/icons/unicons/play.svg';
import u1096 from '../../../img/icons/unicons/plug.svg';
import u1097 from '../../../img/icons/unicons/plus.svg';
import u1098 from '../../../img/icons/unicons/plus-circle.svg';
import u1099 from '../../../img/icons/unicons/power.svg';
import u1100 from '../../../img/icons/unicons/presentation-play.svg';
import u1101 from '../../../img/icons/unicons/process.svg';
import u1102 from '../../../img/icons/unicons/question-circle.svg';
import u1103 from '../../../img/icons/unicons/repeat.svg';
import u1104 from '../../../img/icons/unicons/rocket.svg';
import u1105 from '../../../img/icons/unicons/rss.svg';
import u1106 from '../../../img/icons/unicons/save.svg';
import u1107 from '../../../img/icons/unicons/search.svg';
import u1108 from '../../../img/icons/unicons/search-minus.svg';
import u1109 from '../../../img/icons/unicons/search-plus.svg';
import u1110 from '../../../img/icons/unicons/share-alt.svg';
import u1111 from '../../../img/icons/unicons/shield.svg';
import u1112 from '../../../img/icons/unicons/signal.svg';
import u1113 from '../../../img/icons/unicons/signin.svg';
import u1114 from '../../../img/icons/unicons/signout.svg';
import u1115 from '../../../img/icons/unicons/sitemap.svg';
import u1116 from '../../../img/icons/unicons/slack.svg';
import u1117 from '../../../img/icons/unicons/sliders-v-alt.svg';
import u1118 from '../../../img/icons/unicons/sort-amount-down.svg';
import u1119 from '../../../img/icons/unicons/sort-amount-up.svg';
import u1120 from '../../../img/icons/unicons/square-shape.svg';
import u1121 from '../../../img/icons/unicons/star.svg';
import u1122 from '../../../img/icons/unicons/step-backward.svg';
import u1123 from '../../../img/icons/unicons/sync.svg';
import u1124 from '../../../img/icons/unicons/stopwatch.svg';
import u1125 from '../../../img/icons/unicons/table.svg';
import u1126 from '../../../img/icons/unicons/tag-alt.svg';
import u1127 from '../../../img/icons/unicons/times.svg';
import u1128 from '../../../img/icons/unicons/trash-alt.svg';
import u1129 from '../../../img/icons/unicons/unlock.svg';
import u1130 from '../../../img/icons/unicons/upload.svg';
import u1131 from '../../../img/icons/unicons/user.svg';
import u1132 from '../../../img/icons/unicons/users-alt.svg';
import u1133 from '../../../img/icons/unicons/wrap-text.svg';
import u1134 from '../../../img/icons/unicons/cloud-upload.svg';
import u1135 from '../../../img/icons/unicons/credit-card.svg';
import u1136 from '../../../img/icons/unicons/file-copy-alt.svg';
import u1137 from '../../../img/icons/unicons/fire.svg';
import u1138 from '../../../img/icons/unicons/hourglass.svg';
import u1139 from '../../../img/icons/unicons/layer-group.svg';
import u1140 from '../../../img/icons/unicons/layers-alt.svg';
import u1141 from '../../../img/icons/unicons/line-alt.svg';
import u1142 from '../../../img/icons/unicons/list-ui-alt.svg';
import u1143 from '../../../img/icons/unicons/message.svg';
import u1144 from '../../../img/icons/unicons/palette.svg';
import u1145 from '../../../img/icons/unicons/percentage.svg';
import u1146 from '../../../img/icons/unicons/shield-exclamation.svg';
import u1147 from '../../../img/icons/unicons/plus-square.svg';
import u1148 from '../../../img/icons/unicons/x.svg';
import u1149 from '../../../img/icons/unicons/capture.svg';
import u1150 from '../../../img/icons/custom/gf-grid.svg';
import u1151 from '../../../img/icons/custom/gf-landscape.svg';
import u1152 from '../../../img/icons/custom/gf-layout-simple.svg';
import u1153 from '../../../img/icons/custom/gf-portrait.svg';
import u1154 from '../../../img/icons/custom/gf-show-context.svg';
import u1155 from '../../../img/icons/custom/gf-bar-alignment-after.svg';
import u1156 from '../../../img/icons/custom/gf-bar-alignment-before.svg';
import u1157 from '../../../img/icons/custom/gf-bar-alignment-center.svg';
import u1158 from '../../../img/icons/custom/gf-interpolation-linear.svg';
import u1159 from '../../../img/icons/custom/gf-interpolation-smooth.svg';
import u1160 from '../../../img/icons/custom/gf-interpolation-step-after.svg';
import u1161 from '../../../img/icons/custom/gf-interpolation-step-before.svg';
import u1162 from '../../../img/icons/custom/gf-logs.svg';
import u1163 from '../../../img/icons/custom/gf-movepane-left.svg';
import u1164 from '../../../img/icons/custom/gf-movepane-right.svg';
import u1165 from '../../../img/icons/mono/favorite.svg';
import u1166 from '../../../img/icons/mono/grafana.svg';
import u1167 from '../../../img/icons/mono/heart.svg';
import u1168 from '../../../img/icons/mono/heart-break.svg';
import u1169 from '../../../img/icons/mono/panel-add.svg';
import u1170 from '../../../img/icons/mono/library-panel.svg';
import u1171 from '../../../img/icons/unicons/record-audio.svg';
import u1026 from '../../../img/icons/unicons/bookmark.svg';
import u1027 from '../../../img/icons/unicons/book-open.svg';
import u1028 from '../../../img/icons/unicons/brackets-curly.svg';
import u1029 from '../../../img/icons/unicons/bug.svg';
import u1030 from '../../../img/icons/unicons/building.svg';
import u1031 from '../../../img/icons/unicons/calculator-alt.svg';
import u1032 from '../../../img/icons/unicons/calendar-alt.svg';
import u1033 from '../../../img/icons/unicons/calendar-slash.svg';
import u1034 from '../../../img/icons/unicons/camera.svg';
import u1035 from '../../../img/icons/unicons/channel-add.svg';
import u1036 from '../../../img/icons/unicons/chart-line.svg';
import u1037 from '../../../img/icons/unicons/check.svg';
import u1038 from '../../../img/icons/unicons/check-circle.svg';
import u1039 from '../../../img/icons/unicons/circle.svg';
import u1040 from '../../../img/icons/unicons/clipboard-alt.svg';
import u1041 from '../../../img/icons/unicons/clock-nine.svg';
import u1042 from '../../../img/icons/unicons/cloud.svg';
import u1043 from '../../../img/icons/unicons/cloud-download.svg';
import u1044 from '../../../img/icons/unicons/code-branch.svg';
import u1045 from '../../../img/icons/unicons/cog.svg';
import u1046 from '../../../img/icons/unicons/columns.svg';
import u1047 from '../../../img/icons/unicons/comment-alt.svg';
import u1048 from '../../../img/icons/unicons/comment-alt-share.svg';
import u1049 from '../../../img/icons/unicons/comments-alt.svg';
import u1050 from '../../../img/icons/unicons/compass.svg';
import u1051 from '../../../img/icons/unicons/copy.svg';
import u1052 from '../../../img/icons/unicons/corner-down-right-alt.svg';
import u1053 from '../../../img/icons/unicons/cube.svg';
import u1054 from '../../../img/icons/unicons/dashboard.svg';
import u1055 from '../../../img/icons/unicons/database.svg';
import u1056 from '../../../img/icons/unicons/document-info.svg';
import u1057 from '../../../img/icons/unicons/download-alt.svg';
import u1058 from '../../../img/icons/unicons/draggabledots.svg';
import u1059 from '../../../img/icons/unicons/edit.svg';
import u1060 from '../../../img/icons/unicons/ellipsis-v.svg';
import u1061 from '../../../img/icons/unicons/ellipsis-h.svg';
import u1062 from '../../../img/icons/unicons/envelope.svg';
import u1063 from '../../../img/icons/unicons/exchange-alt.svg';
import u1064 from '../../../img/icons/unicons/exclamation-circle.svg';
import u1065 from '../../../img/icons/unicons/exclamation-triangle.svg';
import u1066 from '../../../img/icons/unicons/external-link-alt.svg';
import u1067 from '../../../img/icons/unicons/eye.svg';
import u1068 from '../../../img/icons/unicons/eye-slash.svg';
import u1069 from '../../../img/icons/unicons/file-alt.svg';
import u1070 from '../../../img/icons/unicons/file-blank.svg';
import u1071 from '../../../img/icons/unicons/filter.svg';
import u1072 from '../../../img/icons/unicons/folder.svg';
import u1073 from '../../../img/icons/unicons/folder-open.svg';
import u1074 from '../../../img/icons/unicons/folder-plus.svg';
import u1075 from '../../../img/icons/unicons/folder-upload.svg';
import u1076 from '../../../img/icons/unicons/forward.svg';
import u1077 from '../../../img/icons/unicons/graph-bar.svg';
import u1078 from '../../../img/icons/unicons/history.svg';
import u1079 from '../../../img/icons/unicons/history-alt.svg';
import u1080 from '../../../img/icons/unicons/home-alt.svg';
import u1081 from '../../../img/icons/unicons/import.svg';
import u1082 from '../../../img/icons/unicons/info.svg';
import u1083 from '../../../img/icons/unicons/info-circle.svg';
import u1084 from '../../../img/icons/unicons/k6.svg';
import u1085 from '../../../img/icons/unicons/key-skeleton-alt.svg';
import u1086 from '../../../img/icons/unicons/keyboard.svg';
import u1087 from '../../../img/icons/unicons/link.svg';
import u1088 from '../../../img/icons/unicons/list-ul.svg';
import u1089 from '../../../img/icons/unicons/lock.svg';
import u1090 from '../../../img/icons/unicons/minus.svg';
import u1091 from '../../../img/icons/unicons/minus-circle.svg';
import u1092 from '../../../img/icons/unicons/mobile-android.svg';
import u1093 from '../../../img/icons/unicons/monitor.svg';
import u1094 from '../../../img/icons/unicons/pause.svg';
import u1095 from '../../../img/icons/unicons/pen.svg';
import u1096 from '../../../img/icons/unicons/play.svg';
import u1097 from '../../../img/icons/unicons/plug.svg';
import u1098 from '../../../img/icons/unicons/plus.svg';
import u1099 from '../../../img/icons/unicons/plus-circle.svg';
import u1100 from '../../../img/icons/unicons/power.svg';
import u1101 from '../../../img/icons/unicons/presentation-play.svg';
import u1102 from '../../../img/icons/unicons/process.svg';
import u1103 from '../../../img/icons/unicons/question-circle.svg';
import u1104 from '../../../img/icons/unicons/repeat.svg';
import u1105 from '../../../img/icons/unicons/rocket.svg';
import u1106 from '../../../img/icons/unicons/rss.svg';
import u1107 from '../../../img/icons/unicons/save.svg';
import u1108 from '../../../img/icons/unicons/search.svg';
import u1109 from '../../../img/icons/unicons/search-minus.svg';
import u1110 from '../../../img/icons/unicons/search-plus.svg';
import u1111 from '../../../img/icons/unicons/share-alt.svg';
import u1112 from '../../../img/icons/unicons/shield.svg';
import u1113 from '../../../img/icons/unicons/signal.svg';
import u1114 from '../../../img/icons/unicons/signin.svg';
import u1115 from '../../../img/icons/unicons/signout.svg';
import u1116 from '../../../img/icons/unicons/sitemap.svg';
import u1117 from '../../../img/icons/unicons/slack.svg';
import u1118 from '../../../img/icons/unicons/sliders-v-alt.svg';
import u1119 from '../../../img/icons/unicons/sort-amount-down.svg';
import u1120 from '../../../img/icons/unicons/sort-amount-up.svg';
import u1121 from '../../../img/icons/unicons/square-shape.svg';
import u1122 from '../../../img/icons/unicons/star.svg';
import u1123 from '../../../img/icons/unicons/step-backward.svg';
import u1124 from '../../../img/icons/unicons/sync.svg';
import u1125 from '../../../img/icons/unicons/stopwatch.svg';
import u1126 from '../../../img/icons/unicons/table.svg';
import u1127 from '../../../img/icons/unicons/tag-alt.svg';
import u1128 from '../../../img/icons/unicons/times.svg';
import u1129 from '../../../img/icons/unicons/trash-alt.svg';
import u1130 from '../../../img/icons/unicons/unlock.svg';
import u1131 from '../../../img/icons/unicons/upload.svg';
import u1132 from '../../../img/icons/unicons/user.svg';
import u1133 from '../../../img/icons/unicons/users-alt.svg';
import u1134 from '../../../img/icons/unicons/wrap-text.svg';
import u1135 from '../../../img/icons/unicons/cloud-upload.svg';
import u1136 from '../../../img/icons/unicons/credit-card.svg';
import u1137 from '../../../img/icons/unicons/file-copy-alt.svg';
import u1138 from '../../../img/icons/unicons/fire.svg';
import u1139 from '../../../img/icons/unicons/hourglass.svg';
import u1140 from '../../../img/icons/unicons/layer-group.svg';
import u1141 from '../../../img/icons/unicons/layers-alt.svg';
import u1142 from '../../../img/icons/unicons/line-alt.svg';
import u1143 from '../../../img/icons/unicons/list-ui-alt.svg';
import u1144 from '../../../img/icons/unicons/message.svg';
import u1145 from '../../../img/icons/unicons/palette.svg';
import u1146 from '../../../img/icons/unicons/percentage.svg';
import u1147 from '../../../img/icons/unicons/shield-exclamation.svg';
import u1148 from '../../../img/icons/unicons/plus-square.svg';
import u1149 from '../../../img/icons/unicons/x.svg';
import u1150 from '../../../img/icons/unicons/capture.svg';
import u1151 from '../../../img/icons/custom/gf-grid.svg';
import u1152 from '../../../img/icons/custom/gf-landscape.svg';
import u1153 from '../../../img/icons/custom/gf-layout-simple.svg';
import u1154 from '../../../img/icons/custom/gf-portrait.svg';
import u1155 from '../../../img/icons/custom/gf-show-context.svg';
import u1156 from '../../../img/icons/custom/gf-bar-alignment-after.svg';
import u1157 from '../../../img/icons/custom/gf-bar-alignment-before.svg';
import u1158 from '../../../img/icons/custom/gf-bar-alignment-center.svg';
import u1159 from '../../../img/icons/custom/gf-interpolation-linear.svg';
import u1160 from '../../../img/icons/custom/gf-interpolation-smooth.svg';
import u1161 from '../../../img/icons/custom/gf-interpolation-step-after.svg';
import u1162 from '../../../img/icons/custom/gf-interpolation-step-before.svg';
import u1163 from '../../../img/icons/custom/gf-logs.svg';
import u1164 from '../../../img/icons/custom/gf-movepane-left.svg';
import u1165 from '../../../img/icons/custom/gf-movepane-right.svg';
import u1166 from '../../../img/icons/mono/favorite.svg';
import u1167 from '../../../img/icons/mono/grafana.svg';
import u1168 from '../../../img/icons/mono/heart.svg';
import u1169 from '../../../img/icons/mono/heart-break.svg';
import u1170 from '../../../img/icons/mono/panel-add.svg';
import u1171 from '../../../img/icons/mono/library-panel.svg';
import u1172 from '../../../img/icons/unicons/record-audio.svg';
import u1173 from '../../../img/icons/solid/bookmark.svg';
// do not edit this list directly
// the list of icons live here: @grafana/ui/components/Icon/cached.json
@ -238,152 +240,154 @@ export function initIconCache() {
cacheItem(u1023, resolvePath('unicons/bell-slash.svg'));
cacheItem(u1024, resolvePath('unicons/bolt.svg'));
cacheItem(u1025, resolvePath('unicons/book.svg'));
cacheItem(u1026, resolvePath('unicons/book-open.svg'));
cacheItem(u1027, resolvePath('unicons/brackets-curly.svg'));
cacheItem(u1028, resolvePath('unicons/bug.svg'));
cacheItem(u1029, resolvePath('unicons/building.svg'));
cacheItem(u1030, resolvePath('unicons/calculator-alt.svg'));
cacheItem(u1031, resolvePath('unicons/calendar-alt.svg'));
cacheItem(u1032, resolvePath('unicons/calendar-slash.svg'));
cacheItem(u1033, resolvePath('unicons/camera.svg'));
cacheItem(u1034, resolvePath('unicons/channel-add.svg'));
cacheItem(u1035, resolvePath('unicons/chart-line.svg'));
cacheItem(u1036, resolvePath('unicons/check.svg'));
cacheItem(u1037, resolvePath('unicons/check-circle.svg'));
cacheItem(u1038, resolvePath('unicons/circle.svg'));
cacheItem(u1039, resolvePath('unicons/clipboard-alt.svg'));
cacheItem(u1040, resolvePath('unicons/clock-nine.svg'));
cacheItem(u1041, resolvePath('unicons/cloud.svg'));
cacheItem(u1042, resolvePath('unicons/cloud-download.svg'));
cacheItem(u1043, resolvePath('unicons/code-branch.svg'));
cacheItem(u1044, resolvePath('unicons/cog.svg'));
cacheItem(u1045, resolvePath('unicons/columns.svg'));
cacheItem(u1046, resolvePath('unicons/comment-alt.svg'));
cacheItem(u1047, resolvePath('unicons/comment-alt-share.svg'));
cacheItem(u1048, resolvePath('unicons/comments-alt.svg'));
cacheItem(u1049, resolvePath('unicons/compass.svg'));
cacheItem(u1050, resolvePath('unicons/copy.svg'));
cacheItem(u1051, resolvePath('unicons/corner-down-right-alt.svg'));
cacheItem(u1052, resolvePath('unicons/cube.svg'));
cacheItem(u1053, resolvePath('unicons/dashboard.svg'));
cacheItem(u1054, resolvePath('unicons/database.svg'));
cacheItem(u1055, resolvePath('unicons/document-info.svg'));
cacheItem(u1056, resolvePath('unicons/download-alt.svg'));
cacheItem(u1057, resolvePath('unicons/draggabledots.svg'));
cacheItem(u1058, resolvePath('unicons/edit.svg'));
cacheItem(u1059, resolvePath('unicons/ellipsis-v.svg'));
cacheItem(u1060, resolvePath('unicons/ellipsis-h.svg'));
cacheItem(u1061, resolvePath('unicons/envelope.svg'));
cacheItem(u1062, resolvePath('unicons/exchange-alt.svg'));
cacheItem(u1063, resolvePath('unicons/exclamation-circle.svg'));
cacheItem(u1064, resolvePath('unicons/exclamation-triangle.svg'));
cacheItem(u1065, resolvePath('unicons/external-link-alt.svg'));
cacheItem(u1066, resolvePath('unicons/eye.svg'));
cacheItem(u1067, resolvePath('unicons/eye-slash.svg'));
cacheItem(u1068, resolvePath('unicons/file-alt.svg'));
cacheItem(u1069, resolvePath('unicons/file-blank.svg'));
cacheItem(u1070, resolvePath('unicons/filter.svg'));
cacheItem(u1071, resolvePath('unicons/folder.svg'));
cacheItem(u1072, resolvePath('unicons/folder-open.svg'));
cacheItem(u1073, resolvePath('unicons/folder-plus.svg'));
cacheItem(u1074, resolvePath('unicons/folder-upload.svg'));
cacheItem(u1075, resolvePath('unicons/forward.svg'));
cacheItem(u1076, resolvePath('unicons/graph-bar.svg'));
cacheItem(u1077, resolvePath('unicons/history.svg'));
cacheItem(u1078, resolvePath('unicons/history-alt.svg'));
cacheItem(u1079, resolvePath('unicons/home-alt.svg'));
cacheItem(u1080, resolvePath('unicons/import.svg'));
cacheItem(u1081, resolvePath('unicons/info.svg'));
cacheItem(u1082, resolvePath('unicons/info-circle.svg'));
cacheItem(u1083, resolvePath('unicons/k6.svg'));
cacheItem(u1084, resolvePath('unicons/key-skeleton-alt.svg'));
cacheItem(u1085, resolvePath('unicons/keyboard.svg'));
cacheItem(u1086, resolvePath('unicons/link.svg'));
cacheItem(u1087, resolvePath('unicons/list-ul.svg'));
cacheItem(u1088, resolvePath('unicons/lock.svg'));
cacheItem(u1089, resolvePath('unicons/minus.svg'));
cacheItem(u1090, resolvePath('unicons/minus-circle.svg'));
cacheItem(u1091, resolvePath('unicons/mobile-android.svg'));
cacheItem(u1092, resolvePath('unicons/monitor.svg'));
cacheItem(u1093, resolvePath('unicons/pause.svg'));
cacheItem(u1094, resolvePath('unicons/pen.svg'));
cacheItem(u1095, resolvePath('unicons/play.svg'));
cacheItem(u1096, resolvePath('unicons/plug.svg'));
cacheItem(u1097, resolvePath('unicons/plus.svg'));
cacheItem(u1098, resolvePath('unicons/plus-circle.svg'));
cacheItem(u1099, resolvePath('unicons/power.svg'));
cacheItem(u1100, resolvePath('unicons/presentation-play.svg'));
cacheItem(u1101, resolvePath('unicons/process.svg'));
cacheItem(u1102, resolvePath('unicons/question-circle.svg'));
cacheItem(u1103, resolvePath('unicons/repeat.svg'));
cacheItem(u1104, resolvePath('unicons/rocket.svg'));
cacheItem(u1105, resolvePath('unicons/rss.svg'));
cacheItem(u1106, resolvePath('unicons/save.svg'));
cacheItem(u1107, resolvePath('unicons/search.svg'));
cacheItem(u1108, resolvePath('unicons/search-minus.svg'));
cacheItem(u1109, resolvePath('unicons/search-plus.svg'));
cacheItem(u1110, resolvePath('unicons/share-alt.svg'));
cacheItem(u1111, resolvePath('unicons/shield.svg'));
cacheItem(u1112, resolvePath('unicons/signal.svg'));
cacheItem(u1113, resolvePath('unicons/signin.svg'));
cacheItem(u1114, resolvePath('unicons/signout.svg'));
cacheItem(u1115, resolvePath('unicons/sitemap.svg'));
cacheItem(u1116, resolvePath('unicons/slack.svg'));
cacheItem(u1117, resolvePath('unicons/sliders-v-alt.svg'));
cacheItem(u1118, resolvePath('unicons/sort-amount-down.svg'));
cacheItem(u1119, resolvePath('unicons/sort-amount-up.svg'));
cacheItem(u1120, resolvePath('unicons/square-shape.svg'));
cacheItem(u1121, resolvePath('unicons/star.svg'));
cacheItem(u1122, resolvePath('unicons/step-backward.svg'));
cacheItem(u1123, resolvePath('unicons/sync.svg'));
cacheItem(u1124, resolvePath('unicons/stopwatch.svg'));
cacheItem(u1125, resolvePath('unicons/table.svg'));
cacheItem(u1126, resolvePath('unicons/tag-alt.svg'));
cacheItem(u1127, resolvePath('unicons/times.svg'));
cacheItem(u1128, resolvePath('unicons/trash-alt.svg'));
cacheItem(u1129, resolvePath('unicons/unlock.svg'));
cacheItem(u1130, resolvePath('unicons/upload.svg'));
cacheItem(u1131, resolvePath('unicons/user.svg'));
cacheItem(u1132, resolvePath('unicons/users-alt.svg'));
cacheItem(u1133, resolvePath('unicons/wrap-text.svg'));
cacheItem(u1134, resolvePath('unicons/cloud-upload.svg'));
cacheItem(u1135, resolvePath('unicons/credit-card.svg'));
cacheItem(u1136, resolvePath('unicons/file-copy-alt.svg'));
cacheItem(u1137, resolvePath('unicons/fire.svg'));
cacheItem(u1138, resolvePath('unicons/hourglass.svg'));
cacheItem(u1139, resolvePath('unicons/layer-group.svg'));
cacheItem(u1140, resolvePath('unicons/layers-alt.svg'));
cacheItem(u1141, resolvePath('unicons/line-alt.svg'));
cacheItem(u1142, resolvePath('unicons/list-ui-alt.svg'));
cacheItem(u1143, resolvePath('unicons/message.svg'));
cacheItem(u1144, resolvePath('unicons/palette.svg'));
cacheItem(u1145, resolvePath('unicons/percentage.svg'));
cacheItem(u1146, resolvePath('unicons/shield-exclamation.svg'));
cacheItem(u1147, resolvePath('unicons/plus-square.svg'));
cacheItem(u1148, resolvePath('unicons/x.svg'));
cacheItem(u1149, resolvePath('unicons/capture.svg'));
cacheItem(u1150, resolvePath('custom/gf-grid.svg'));
cacheItem(u1151, resolvePath('custom/gf-landscape.svg'));
cacheItem(u1152, resolvePath('custom/gf-layout-simple.svg'));
cacheItem(u1153, resolvePath('custom/gf-portrait.svg'));
cacheItem(u1154, resolvePath('custom/gf-show-context.svg'));
cacheItem(u1155, resolvePath('custom/gf-bar-alignment-after.svg'));
cacheItem(u1156, resolvePath('custom/gf-bar-alignment-before.svg'));
cacheItem(u1157, resolvePath('custom/gf-bar-alignment-center.svg'));
cacheItem(u1158, resolvePath('custom/gf-interpolation-linear.svg'));
cacheItem(u1159, resolvePath('custom/gf-interpolation-smooth.svg'));
cacheItem(u1160, resolvePath('custom/gf-interpolation-step-after.svg'));
cacheItem(u1161, resolvePath('custom/gf-interpolation-step-before.svg'));
cacheItem(u1162, resolvePath('custom/gf-logs.svg'));
cacheItem(u1163, resolvePath('custom/gf-movepane-left.svg'));
cacheItem(u1164, resolvePath('custom/gf-movepane-right.svg'));
cacheItem(u1165, resolvePath('mono/favorite.svg'));
cacheItem(u1166, resolvePath('mono/grafana.svg'));
cacheItem(u1167, resolvePath('mono/heart.svg'));
cacheItem(u1168, resolvePath('mono/heart-break.svg'));
cacheItem(u1169, resolvePath('mono/panel-add.svg'));
cacheItem(u1170, resolvePath('mono/library-panel.svg'));
cacheItem(u1171, resolvePath('unicons/record-audio.svg'));
cacheItem(u1026, resolvePath('unicons/bookmark.svg'));
cacheItem(u1027, resolvePath('unicons/book-open.svg'));
cacheItem(u1028, resolvePath('unicons/brackets-curly.svg'));
cacheItem(u1029, resolvePath('unicons/bug.svg'));
cacheItem(u1030, resolvePath('unicons/building.svg'));
cacheItem(u1031, resolvePath('unicons/calculator-alt.svg'));
cacheItem(u1032, resolvePath('unicons/calendar-alt.svg'));
cacheItem(u1033, resolvePath('unicons/calendar-slash.svg'));
cacheItem(u1034, resolvePath('unicons/camera.svg'));
cacheItem(u1035, resolvePath('unicons/channel-add.svg'));
cacheItem(u1036, resolvePath('unicons/chart-line.svg'));
cacheItem(u1037, resolvePath('unicons/check.svg'));
cacheItem(u1038, resolvePath('unicons/check-circle.svg'));
cacheItem(u1039, resolvePath('unicons/circle.svg'));
cacheItem(u1040, resolvePath('unicons/clipboard-alt.svg'));
cacheItem(u1041, resolvePath('unicons/clock-nine.svg'));
cacheItem(u1042, resolvePath('unicons/cloud.svg'));
cacheItem(u1043, resolvePath('unicons/cloud-download.svg'));
cacheItem(u1044, resolvePath('unicons/code-branch.svg'));
cacheItem(u1045, resolvePath('unicons/cog.svg'));
cacheItem(u1046, resolvePath('unicons/columns.svg'));
cacheItem(u1047, resolvePath('unicons/comment-alt.svg'));
cacheItem(u1048, resolvePath('unicons/comment-alt-share.svg'));
cacheItem(u1049, resolvePath('unicons/comments-alt.svg'));
cacheItem(u1050, resolvePath('unicons/compass.svg'));
cacheItem(u1051, resolvePath('unicons/copy.svg'));
cacheItem(u1052, resolvePath('unicons/corner-down-right-alt.svg'));
cacheItem(u1053, resolvePath('unicons/cube.svg'));
cacheItem(u1054, resolvePath('unicons/dashboard.svg'));
cacheItem(u1055, resolvePath('unicons/database.svg'));
cacheItem(u1056, resolvePath('unicons/document-info.svg'));
cacheItem(u1057, resolvePath('unicons/download-alt.svg'));
cacheItem(u1058, resolvePath('unicons/draggabledots.svg'));
cacheItem(u1059, resolvePath('unicons/edit.svg'));
cacheItem(u1060, resolvePath('unicons/ellipsis-v.svg'));
cacheItem(u1061, resolvePath('unicons/ellipsis-h.svg'));
cacheItem(u1062, resolvePath('unicons/envelope.svg'));
cacheItem(u1063, resolvePath('unicons/exchange-alt.svg'));
cacheItem(u1064, resolvePath('unicons/exclamation-circle.svg'));
cacheItem(u1065, resolvePath('unicons/exclamation-triangle.svg'));
cacheItem(u1066, resolvePath('unicons/external-link-alt.svg'));
cacheItem(u1067, resolvePath('unicons/eye.svg'));
cacheItem(u1068, resolvePath('unicons/eye-slash.svg'));
cacheItem(u1069, resolvePath('unicons/file-alt.svg'));
cacheItem(u1070, resolvePath('unicons/file-blank.svg'));
cacheItem(u1071, resolvePath('unicons/filter.svg'));
cacheItem(u1072, resolvePath('unicons/folder.svg'));
cacheItem(u1073, resolvePath('unicons/folder-open.svg'));
cacheItem(u1074, resolvePath('unicons/folder-plus.svg'));
cacheItem(u1075, resolvePath('unicons/folder-upload.svg'));
cacheItem(u1076, resolvePath('unicons/forward.svg'));
cacheItem(u1077, resolvePath('unicons/graph-bar.svg'));
cacheItem(u1078, resolvePath('unicons/history.svg'));
cacheItem(u1079, resolvePath('unicons/history-alt.svg'));
cacheItem(u1080, resolvePath('unicons/home-alt.svg'));
cacheItem(u1081, resolvePath('unicons/import.svg'));
cacheItem(u1082, resolvePath('unicons/info.svg'));
cacheItem(u1083, resolvePath('unicons/info-circle.svg'));
cacheItem(u1084, resolvePath('unicons/k6.svg'));
cacheItem(u1085, resolvePath('unicons/key-skeleton-alt.svg'));
cacheItem(u1086, resolvePath('unicons/keyboard.svg'));
cacheItem(u1087, resolvePath('unicons/link.svg'));
cacheItem(u1088, resolvePath('unicons/list-ul.svg'));
cacheItem(u1089, resolvePath('unicons/lock.svg'));
cacheItem(u1090, resolvePath('unicons/minus.svg'));
cacheItem(u1091, resolvePath('unicons/minus-circle.svg'));
cacheItem(u1092, resolvePath('unicons/mobile-android.svg'));
cacheItem(u1093, resolvePath('unicons/monitor.svg'));
cacheItem(u1094, resolvePath('unicons/pause.svg'));
cacheItem(u1095, resolvePath('unicons/pen.svg'));
cacheItem(u1096, resolvePath('unicons/play.svg'));
cacheItem(u1097, resolvePath('unicons/plug.svg'));
cacheItem(u1098, resolvePath('unicons/plus.svg'));
cacheItem(u1099, resolvePath('unicons/plus-circle.svg'));
cacheItem(u1100, resolvePath('unicons/power.svg'));
cacheItem(u1101, resolvePath('unicons/presentation-play.svg'));
cacheItem(u1102, resolvePath('unicons/process.svg'));
cacheItem(u1103, resolvePath('unicons/question-circle.svg'));
cacheItem(u1104, resolvePath('unicons/repeat.svg'));
cacheItem(u1105, resolvePath('unicons/rocket.svg'));
cacheItem(u1106, resolvePath('unicons/rss.svg'));
cacheItem(u1107, resolvePath('unicons/save.svg'));
cacheItem(u1108, resolvePath('unicons/search.svg'));
cacheItem(u1109, resolvePath('unicons/search-minus.svg'));
cacheItem(u1110, resolvePath('unicons/search-plus.svg'));
cacheItem(u1111, resolvePath('unicons/share-alt.svg'));
cacheItem(u1112, resolvePath('unicons/shield.svg'));
cacheItem(u1113, resolvePath('unicons/signal.svg'));
cacheItem(u1114, resolvePath('unicons/signin.svg'));
cacheItem(u1115, resolvePath('unicons/signout.svg'));
cacheItem(u1116, resolvePath('unicons/sitemap.svg'));
cacheItem(u1117, resolvePath('unicons/slack.svg'));
cacheItem(u1118, resolvePath('unicons/sliders-v-alt.svg'));
cacheItem(u1119, resolvePath('unicons/sort-amount-down.svg'));
cacheItem(u1120, resolvePath('unicons/sort-amount-up.svg'));
cacheItem(u1121, resolvePath('unicons/square-shape.svg'));
cacheItem(u1122, resolvePath('unicons/star.svg'));
cacheItem(u1123, resolvePath('unicons/step-backward.svg'));
cacheItem(u1124, resolvePath('unicons/sync.svg'));
cacheItem(u1125, resolvePath('unicons/stopwatch.svg'));
cacheItem(u1126, resolvePath('unicons/table.svg'));
cacheItem(u1127, resolvePath('unicons/tag-alt.svg'));
cacheItem(u1128, resolvePath('unicons/times.svg'));
cacheItem(u1129, resolvePath('unicons/trash-alt.svg'));
cacheItem(u1130, resolvePath('unicons/unlock.svg'));
cacheItem(u1131, resolvePath('unicons/upload.svg'));
cacheItem(u1132, resolvePath('unicons/user.svg'));
cacheItem(u1133, resolvePath('unicons/users-alt.svg'));
cacheItem(u1134, resolvePath('unicons/wrap-text.svg'));
cacheItem(u1135, resolvePath('unicons/cloud-upload.svg'));
cacheItem(u1136, resolvePath('unicons/credit-card.svg'));
cacheItem(u1137, resolvePath('unicons/file-copy-alt.svg'));
cacheItem(u1138, resolvePath('unicons/fire.svg'));
cacheItem(u1139, resolvePath('unicons/hourglass.svg'));
cacheItem(u1140, resolvePath('unicons/layer-group.svg'));
cacheItem(u1141, resolvePath('unicons/layers-alt.svg'));
cacheItem(u1142, resolvePath('unicons/line-alt.svg'));
cacheItem(u1143, resolvePath('unicons/list-ui-alt.svg'));
cacheItem(u1144, resolvePath('unicons/message.svg'));
cacheItem(u1145, resolvePath('unicons/palette.svg'));
cacheItem(u1146, resolvePath('unicons/percentage.svg'));
cacheItem(u1147, resolvePath('unicons/shield-exclamation.svg'));
cacheItem(u1148, resolvePath('unicons/plus-square.svg'));
cacheItem(u1149, resolvePath('unicons/x.svg'));
cacheItem(u1150, resolvePath('unicons/capture.svg'));
cacheItem(u1151, resolvePath('custom/gf-grid.svg'));
cacheItem(u1152, resolvePath('custom/gf-landscape.svg'));
cacheItem(u1153, resolvePath('custom/gf-layout-simple.svg'));
cacheItem(u1154, resolvePath('custom/gf-portrait.svg'));
cacheItem(u1155, resolvePath('custom/gf-show-context.svg'));
cacheItem(u1156, resolvePath('custom/gf-bar-alignment-after.svg'));
cacheItem(u1157, resolvePath('custom/gf-bar-alignment-before.svg'));
cacheItem(u1158, resolvePath('custom/gf-bar-alignment-center.svg'));
cacheItem(u1159, resolvePath('custom/gf-interpolation-linear.svg'));
cacheItem(u1160, resolvePath('custom/gf-interpolation-smooth.svg'));
cacheItem(u1161, resolvePath('custom/gf-interpolation-step-after.svg'));
cacheItem(u1162, resolvePath('custom/gf-interpolation-step-before.svg'));
cacheItem(u1163, resolvePath('custom/gf-logs.svg'));
cacheItem(u1164, resolvePath('custom/gf-movepane-left.svg'));
cacheItem(u1165, resolvePath('custom/gf-movepane-right.svg'));
cacheItem(u1166, resolvePath('mono/favorite.svg'));
cacheItem(u1167, resolvePath('mono/grafana.svg'));
cacheItem(u1168, resolvePath('mono/heart.svg'));
cacheItem(u1169, resolvePath('mono/heart-break.svg'));
cacheItem(u1170, resolvePath('mono/panel-add.svg'));
cacheItem(u1171, resolvePath('mono/library-panel.svg'));
cacheItem(u1172, resolvePath('unicons/record-audio.svg'));
cacheItem(u1173, resolvePath('solid/bookmark.svg'));
// do not edit this list directly
// the list of icons live here: @grafana/ui/components/Icon/cached.json
}

View File

@ -1,5 +1,6 @@
import i18n, { InitOptions, TFunction } from 'i18next';
import LanguageDetector, { DetectorOptions } from 'i18next-browser-languagedetector';
import { ReactElement } from 'react';
import { Trans as I18NextTrans, initReactI18next } from 'react-i18next'; // eslint-disable-line no-restricted-imports
import { DEFAULT_LANGUAGE, NAMESPACES, VALID_LANGUAGES } from './constants';
@ -56,7 +57,14 @@ export function changeLanguage(locale: string) {
return i18n.changeLanguage(validLocale);
}
export const Trans: typeof I18NextTrans = (props) => {
type I18NextTransType = typeof I18NextTrans;
type I18NextTransProps = Parameters<I18NextTransType>[0];
interface TransProps extends I18NextTransProps {
i18nKey: string;
}
export const Trans = (props: TransProps): ReactElement => {
return <I18NextTrans shouldUnescape ns={NAMESPACES} {...props} />;
};

View File

@ -41,7 +41,7 @@ export const ProvisioningBadge = ({
if (tooltip) {
const provenanceTooltip = (
<Trans i18nKey="alerting.provisioning.badge-tooltip-provenance" provenance={provenance}>
<Trans i18nKey="alerting.provisioning.badge-tooltip-provenance" values={{ provenance }}>
This resource has been provisioned via {{ provenance }} and cannot be edited through the UI
</Trans>
);

View File

@ -39,7 +39,7 @@ export const ContactPoint = ({
const styles = useStyles2(getStyles);
// TODO probably not the best way to figure out if we want to show either only the summary or full metadata for the receivers?
const showFullMetadata = receivers.some((receiver) => Boolean(receiver[RECEIVER_STATUS_KEY]));
const showFullMetadata = receivers.some((receiver) => Boolean(receiver[RECEIVER_META_KEY]));
return (
<div className={styles.contactPointWrapper} data-testid="contact-point">

View File

@ -11,6 +11,7 @@ import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver,
generatedReceiversApi,
} from 'app/features/alerting/unified/openapi/receiversApi.gen';
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 { getNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
@ -175,7 +176,7 @@ export function useContactPointsWithStatus() {
contactPoints: result.data
? enhanceContactPointsWithMetadata(
fetchContactPointsStatus.data,
fetchReceiverMetadata.data,
isGrafanaAlertmanager ? fetchReceiverMetadata.data : cloudNotifierTypes,
onCallMetadata,
result.data.alertmanager_config.receivers ?? [],
result.data

View File

@ -34,13 +34,14 @@ export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): Re
if (!receiver.settings) {
return undefined;
}
const { settings } = receiver;
switch (receiver.type) {
case 'email': {
const hasEmailAddresses = 'addresses' in receiver.settings; // when dealing with alertmanager email_configs we don't normalize the settings
return hasEmailAddresses ? summarizeEmailAddresses(receiver.settings['addresses']) : undefined;
const addresses = settings.addresses || settings.to; // when dealing with alertmanager email_configs we don't normalize the settings
return addresses ? summarizeEmailAddresses(addresses) : undefined;
}
case 'slack': {
const recipient: string | undefined = receiver.settings['recipient'];
const recipient = settings.recipient || settings.channel;
if (!recipient) {
return;
}
@ -50,12 +51,10 @@ export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): Re
return `#${channelName}`;
}
case 'kafka': {
const topicName: string | undefined = receiver.settings['kafkaTopic'];
return topicName;
return settings.kafkaTopic;
}
case 'webhook': {
const url: string | undefined = receiver.settings['url'];
return url;
return settings.url;
}
case ReceiverTypes.OnCall: {
return receiver[RECEIVER_PLUGIN_META_KEY]?.description;
@ -123,11 +122,7 @@ export function enhanceContactPointsWithMetadata(
const usedContactPoints = getUsedContactPoints(fullyInheritedTree);
const usedContactPointsByName = groupBy(usedContactPoints, 'receiver');
const contactPointsList = alertmanagerConfiguration
? (alertmanagerConfiguration?.alertmanager_config.receivers ?? [])
: (contactPoints ?? []);
const enhanced = contactPointsList.map((contactPoint) => {
const enhanced = contactPoints.map((contactPoint) => {
const receivers = extractReceivers(contactPoint);
const statusForReceiver = status.find((status) => status.name === contactPoint.name);

View File

@ -146,7 +146,7 @@ function FolderGroupAndEvaluationInterval({
<Stack direction="column" gap={1}>
{getValues('group') && getValues('evaluateEvery') && (
<span>
<Trans i18nKey="alert-rule-form.evaluation-behaviour-group.text" evaluateEvery={evaluateEvery}>
<Trans i18nKey="alert-rule-form.evaluation-behaviour-group.text" values={{ evaluateEvery }}>
All rules in the selected group are evaluated every {{ evaluateEvery }}.
</Trans>
{!isNewGroup && (

View File

@ -1,4 +1,4 @@
import { isEmpty, times } from 'lodash';
import { isEmpty } from 'lodash';
import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/alertmanager/types';
@ -12,10 +12,14 @@ import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/a
* We don't normalize the configuration settings and those are blank for vanilla Alertmanager receivers.
*
* Example input:
* { name: 'my receiver', email_configs: [{ from: "foo@bar.com" }] }
* ```
* { name: 'my receiver', email_configs: [{ from: "foo@bar.com" }] }
* ```
*
* Example output:
* { name: 'my receiver', grafana_managed_receiver_configs: [{ type: 'email', settings: {} }] }
* ```
* { name: 'my receiver', grafana_managed_receiver_configs: [{ type: 'email', settings: {} }] }
* ```
*/
export function extractReceivers(receiver: Receiver): GrafanaManagedReceiverConfig[] {
if ('grafana_managed_receiver_configs' in receiver) {
@ -27,13 +31,14 @@ export function extractReceivers(receiver: Receiver): GrafanaManagedReceiverConf
.filter(([_, value]) => Array.isArray(value) && !isEmpty(value))
.reduce((acc: GrafanaManagedReceiverConfig[], [key, value]) => {
const type = key.replace('_configs', '');
const configs = times(value.length, () => ({
name: receiver.name,
type: type,
settings: [], // we don't normalize the configuration values
disableResolveMessage: false,
}));
const configs = value.map((settings: unknown) => {
return {
name: receiver.name,
type,
settings,
disableResolveMessage: false,
};
});
return acc.concat(configs);
}, []);

View File

@ -10,10 +10,10 @@ export function EmptyTransformationsMessage(props: EmptyTransformationsProps) {
<Box alignItems="center" padding={4}>
<Stack direction="column" alignItems="center" gap={2}>
<Text element="h3" textAlignment="center">
<Trans key="transformations.empty.add-transformation-header">Start transforming data</Trans>
<Trans i18nKey="transformations.empty.add-transformation-header">Start transforming data</Trans>
</Text>
<Text element="p" textAlignment="center" data-testid={selectors.components.Transforms.noTransformationsMessage}>
<Trans key="transformations.empty.add-transformation-body">
<Trans i18nKey="transformations.empty.add-transformation-body">
Transformations allow data to be changed in various ways before your visualization is shown.
<br />
This includes joining data together, renaming fields, making calculations, formatting data for display, and

View File

@ -496,7 +496,6 @@ export function ToolbarActions({ dashboard }: Props) {
return (
<Button
onClick={() => {
DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({});
}}
className={styles.buttonWithExtraMargin}
@ -516,7 +515,6 @@ export function ToolbarActions({ dashboard }: Props) {
return (
<Button
onClick={() => {
DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({ saveAsCopy: true });
}}
className={styles.buttonWithExtraMargin}
@ -537,7 +535,6 @@ export function ToolbarActions({ dashboard }: Props) {
label="Save"
icon="save"
onClick={() => {
DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({});
}}
/>
@ -545,7 +542,6 @@ export function ToolbarActions({ dashboard }: Props) {
label="Save as copy"
icon="copy"
onClick={() => {
DashboardInteractions.toolbarSaveAsClick();
dashboard.openSaveDrawer({ saveAsCopy: true });
}}
/>
@ -556,7 +552,6 @@ export function ToolbarActions({ dashboard }: Props) {
<ButtonGroup className={styles.buttonWithExtraMargin} key="save">
<Button
onClick={() => {
DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({});
}}
tooltip="Save changes"

View File

@ -64,7 +64,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
text: t('panel.header-menu.view', `View`),
iconClassName: 'eye',
shortcut: 'v',
onClick: () => DashboardInteractions.panelMenuItemClicked('view'),
href: getViewPanelUrl(panel),
});
}
@ -76,7 +75,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
text: t('panel.header-menu.edit', `Edit`),
iconClassName: 'edit',
shortcut: 'e',
onClick: () => DashboardInteractions.panelMenuItemClicked('edit'),
href: getEditPanelUrl(getPanelIdForVizPanel(panel)),
});
}
@ -111,7 +109,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
text: t('panel.header-menu.share', 'Share'),
iconClassName: 'share-alt',
onClick: () => {
DashboardInteractions.panelMenuItemClicked('share');
dashboard.showModal(new ShareModal({ panelRef: panel.getRef() }));
},
shortcut: 'p s',
@ -122,7 +119,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
moreSubMenu.push({
text: t('panel.header-menu.duplicate', `Duplicate`),
onClick: () => {
DashboardInteractions.panelMenuItemClicked('duplicate');
dashboard.duplicatePanel(panel);
},
shortcut: 'p d',
@ -133,7 +129,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
moreSubMenu.push({
text: t('panel.header-menu.copy', `Copy`),
onClick: () => {
DashboardInteractions.panelMenuItemClicked('copy');
dashboard.copyPanel(panel);
},
});
@ -144,7 +139,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
moreSubMenu.push({
text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`),
onClick: () => {
DashboardInteractions.panelMenuItemClicked('unlinkLibraryPanel');
dashboard.showModal(
new UnlinkLibraryPanelModal({
panelRef: parent.getRef(),
@ -156,7 +150,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
moreSubMenu.push({
text: t('panel.header-menu.replace-library-panel', `Replace library panel`),
onClick: () => {
DashboardInteractions.panelMenuItemClicked('replaceLibraryPanel');
dashboard.onShowAddLibraryPanelDrawer(parent.getRef());
},
});
@ -164,7 +157,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
moreSubMenu.push({
text: t('panel.header-menu.create-library-panel', `Create library panel`),
onClick: () => {
DashboardInteractions.panelMenuItemClicked('createLibraryPanel');
dashboard.showModal(
new ShareModal({
panelRef: panel.getRef(),
@ -253,7 +245,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
text: t('panel.header-menu.remove', `Remove`),
iconClassName: 'trash-alt',
onClick: () => {
DashboardInteractions.panelMenuItemClicked('remove');
onRemovePanel(dashboard, panel);
},
shortcut: 'p r',
@ -278,7 +269,6 @@ async function getExploreMenuItem(panel: VizPanel): Promise<PanelMenuItem | unde
text: t('panel.header-menu.explore', `Explore`),
iconClassName: 'compass',
shortcut: 'p x',
onClick: () => DashboardInteractions.panelMenuItemClicked('explore'),
href: exploreUrl,
};
}
@ -297,7 +287,6 @@ function getInspectMenuItem(
onClick: (e) => {
e.preventDefault();
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Data });
DashboardInteractions.panelMenuInspectClicked(InspectTab.Data);
},
});
@ -308,7 +297,6 @@ function getInspectMenuItem(
onClick: (e) => {
e.preventDefault();
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Query });
DashboardInteractions.panelMenuInspectClicked(InspectTab.Query);
},
});
}
@ -320,7 +308,6 @@ function getInspectMenuItem(
onClick: (e) => {
e.preventDefault();
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.JSON });
DashboardInteractions.panelMenuInspectClicked(InspectTab.JSON);
},
});
@ -332,7 +319,6 @@ function getInspectMenuItem(
onClick: (e) => {
if (!e.isDefaultPrevented()) {
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Data });
DashboardInteractions.panelMenuInspectClicked(InspectTab.Data);
}
},
subMenu: inspectSubMenu.length > 0 ? inspectSubMenu : undefined,
@ -447,17 +433,12 @@ export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) {
}
const onCreateAlert = async (panel: VizPanel) => {
DashboardInteractions.panelMenuItemClicked('create-alert');
const formValues = await scenesPanelToRuleFormValues(panel);
const ruleFormUrl = urlUtil.renderUrl('/alerting/new', {
defaults: JSON.stringify(formValues),
returnTo: location.pathname + location.search,
});
locationService.push(ruleFormUrl);
DashboardInteractions.panelMenuItemClicked('create-alert');
};
export function toggleVizPanelLegend(vizPanel: VizPanel): void {
@ -469,8 +450,6 @@ export function toggleVizPanelLegend(vizPanel: VizPanel): void {
},
});
}
DashboardInteractions.panelMenuItemClicked('toggleLegend');
}
function hasLegendOptions(optionsWithLegend: unknown): optionsWithLegend is OptionsWithLegend {
@ -482,5 +461,4 @@ const onInspectPanel = (vizPanel: VizPanel, tab?: InspectTab) => {
inspect: vizPanel.state.key,
inspectTab: tab,
});
DashboardInteractions.panelMenuInspectClicked(tab ?? InspectTab.Data);
};

View File

@ -521,9 +521,6 @@ function registerPanelInteractionsReporter(scene: DashboardScene) {
case 'panel-cancel-query-clicked':
DashboardInteractions.panelCancelQueryClicked();
break;
case 'panel-menu-shown':
DashboardInteractions.panelMenuShown();
break;
}
});
}

View File

@ -1,5 +1,4 @@
import { reportInteraction } from '@grafana/runtime';
import { InspectTab } from 'app/features/inspector/types';
let isScenesContextSet = false;
@ -9,30 +8,6 @@ export const DashboardInteractions = {
reportDashboardInteraction('init_dashboard_completed', { ...properties });
},
// Panel interactions:
panelMenuShown: (properties?: Record<string, unknown>) => {
reportDashboardInteraction('panelheader_menu', { ...properties, item: 'menu' });
},
panelMenuItemClicked: (
item:
| 'view'
| 'edit'
| 'share'
| 'createLibraryPanel'
| 'unlinkLibraryPanel'
| 'replaceLibraryPanel'
| 'duplicate'
| 'copy'
| 'remove'
| 'explore'
| 'toggleLegend'
| 'create-alert'
) => {
reportDashboardInteraction('panelheader_menu', { item });
},
panelMenuInspectClicked(tab: InspectTab) {
reportDashboardInteraction('panelheader_menu', { item: 'inspect', tab });
},
panelLinkClicked: (properties?: Record<string, unknown>) => {
reportDashboardInteraction('panelheader_datalink_clicked', properties);
},
@ -43,6 +18,20 @@ export const DashboardInteractions = {
reportDashboardInteraction('panelheader_cancelquery_clicked', properties);
},
// Dashboard interactions from toolbar
toolbarFavoritesClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'favorites' });
},
toolbarSettingsClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'settings' });
},
toolbarShareClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'share' });
},
toolbarAddClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'add' });
},
// Sharing interactions:
sharingCategoryClicked: (properties?: Record<string, unknown>) => {
reportDashboardInteraction('sharing_category_clicked', properties);
@ -111,35 +100,6 @@ export const DashboardInteractions = {
toolbarAddButtonClicked: (properties?: Record<string, unknown>) => {
reportDashboardInteraction('toolbar_add_clicked', properties);
},
toolbarFavoritesClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'favorites' });
},
toolbarSettingsClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'settings' });
},
toolbarRefreshClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'refresh' });
},
toolbarTimePickerClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'time_picker' });
},
toolbarZoomClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'zoom_out_time_range' });
},
toolbarShareClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'share' });
},
toolbarSaveClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'save' });
},
toolbarSaveAsClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'save_as' });
},
toolbarAddClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'add' });
},
setScenesContext: () => {
isScenesContextSet = true;

View File

@ -270,14 +270,7 @@ export const DashNav = memo<Props>((props) => {
return null;
}
return (
<DashNavTimeControls
dashboard={dashboard}
onChangeTimeZone={updateTimeZoneForSession}
onToolbarRefreshClick={DashboardInteractions.toolbarRefreshClick}
onToolbarZoomClick={DashboardInteractions.toolbarZoomClick}
onToolbarTimePickerClick={DashboardInteractions.toolbarTimePickerClick}
key="time-controls"
/>
<DashNavTimeControls dashboard={dashboard} onChangeTimeZone={updateTimeZoneForSession} key="time-controls" />
);
};
@ -315,7 +308,6 @@ export const DashNav = memo<Props>((props) => {
tooltip={t('dashboard.toolbar.save', 'Save dashboard')}
icon="save"
onClick={() => {
DashboardInteractions.toolbarSaveClick();
showModal(SaveDashboardDrawer, {
dashboard,
onDismiss: hideModal,
@ -343,8 +335,8 @@ export const DashNav = memo<Props>((props) => {
if (canEdit && !isFullscreen) {
buttons.push(
<AddPanelButton
dashboard={dashboard}
onToolbarAddMenuOpen={DashboardInteractions.toolbarAddClick}
dashboard={dashboard}
key="panel-add-dropdown"
/>
);

View File

@ -209,7 +209,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
hoverHeader={panelChromeProps.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={panelChromeProps.onCancelQuery}
onOpenMenu={panelChromeProps.onOpenMenu}
>
{() => <div ref={(element) => (this.element = element)} className="panel-height-helper" />}
</PanelChrome>

View File

@ -587,7 +587,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
hoverHeader={panelChromeProps.hasOverlayHeader()}
displayMode={transparent ? 'transparent' : 'default'}
onCancelQuery={panelChromeProps.onCancelQuery}
onOpenMenu={panelChromeProps.onOpenMenu}
onFocus={() => this.setPanelAttention()}
onMouseEnter={() => this.setPanelAttention()}
onMouseMove={() => this.debouncedSetPanelAttention()}

View File

@ -113,10 +113,6 @@ export function getPanelChromeProps(props: CommonProps) {
const title = props.panel.getDisplayTitle();
const onOpenMenu = () => {
DashboardInteractions.panelMenuShown();
};
return {
hasOverlayHeader,
onShowPanelDescription,
@ -129,6 +125,5 @@ export function getPanelChromeProps(props: CommonProps) {
dragClass,
title,
titleItems,
onOpenMenu,
};
}

View File

@ -21,7 +21,6 @@ import {
toggleLegend,
unlinkLibraryPanel,
} from 'app/features/dashboard/utils/panel';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
import { InspectTab } from 'app/features/inspector/types';
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
@ -43,7 +42,6 @@ export function getPanelMenu(
locationService.partial({
viewPanel: panel.id,
});
DashboardInteractions.panelMenuItemClicked('view');
};
const onEditPanel = (event: React.MouseEvent) => {
@ -51,26 +49,21 @@ export function getPanelMenu(
locationService.partial({
editPanel: panel.id,
});
DashboardInteractions.panelMenuItemClicked('edit');
};
const onSharePanel = (event: React.MouseEvent) => {
event.preventDefault();
sharePanel(dashboard, panel);
DashboardInteractions.panelMenuItemClicked('share');
};
const onAddLibraryPanel = (event: React.MouseEvent) => {
event.preventDefault();
addLibraryPanel(dashboard, panel);
DashboardInteractions.panelMenuItemClicked('createLibraryPanel');
};
const onUnlinkLibraryPanel = (event: React.MouseEvent) => {
event.preventDefault();
unlinkLibraryPanel(panel);
DashboardInteractions.panelMenuItemClicked('unlinkLibraryPanel');
};
const onInspectPanel = (tab?: InspectTab) => {
@ -78,7 +71,6 @@ export function getPanelMenu(
inspect: panel.id,
inspectTab: tab,
});
DashboardInteractions.panelMenuInspectClicked(tab ?? InspectTab.Data);
};
const onMore = (event: React.MouseEvent) => {
@ -88,19 +80,16 @@ export function getPanelMenu(
const onDuplicatePanel = (event: React.MouseEvent) => {
event.preventDefault();
duplicatePanel(dashboard, panel);
DashboardInteractions.panelMenuItemClicked('duplicate');
};
const onCopyPanel = (event: React.MouseEvent) => {
event.preventDefault();
copyPanel(panel);
DashboardInteractions.panelMenuItemClicked('copy');
};
const onRemovePanel = (event: React.MouseEvent) => {
event.preventDefault();
removePanel(dashboard, panel, true);
DashboardInteractions.panelMenuItemClicked('remove');
};
const onNavigateToExplore = (event: React.MouseEvent) => {
@ -114,13 +103,11 @@ export function getPanelMenu(
openInNewWindow,
}) as any
);
DashboardInteractions.panelMenuItemClicked('explore');
};
const onToggleLegend = (event: React.MouseEvent) => {
event.preventDefault();
toggleLegend(panel);
DashboardInteractions.panelMenuItemClicked('toggleLegend');
};
const menu: PanelMenuItem[] = [];
@ -224,7 +211,6 @@ export function getPanelMenu(
const onCreateAlert = (event: React.MouseEvent) => {
event.preventDefault();
createAlert();
DashboardInteractions.panelMenuItemClicked('create-alert');
};
const subMenu: PanelMenuItem[] = [];

View File

@ -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(23);
expect(enterprisePluginsCategory.plugins.length).toBe(24);
expect(enterprisePluginsCategory.plugins[0].name).toBe('Adobe Analytics');
expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Wavefront');
});

View File

@ -233,6 +233,12 @@ function getEnterprisePhantomPlugins(): DataSourcePluginMeta[] {
name: 'Cloudflare',
imgUrl: 'public/img/plugins/cloudflare.jpg',
}),
getPhantomPlugin({
id: 'grafana-cockroachdb-datasource',
description: 'CockroachDB datasource',
name: 'CockroachDB',
imgUrl: 'public/img/plugins/cockroachdb.jpg',
}),
];
}

View File

@ -1,6 +1,7 @@
import { skipToken } from '@reduxjs/toolkit/query/react';
import { useCallback, useEffect, useState } from 'react';
import { config } from '@grafana/runtime';
import { AlertVariant, Box, Stack, Text } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
@ -62,8 +63,6 @@ const SNAPSHOT_REBUILD_STATUSES: Array<SnapshotDto['status']> = ['PENDING_UPLOAD
const SNAPSHOT_BUILDING_STATUSES: Array<SnapshotDto['status']> = ['INITIALIZING', 'CREATING'];
const SNAPSHOT_UPLOADING_STATUSES: Array<SnapshotDto['status']> = ['UPLOADING', 'PENDING_PROCESSING', 'PROCESSING'];
const STATUS_POLL_INTERVAL = 5 * 1000;
const PAGE_SIZE = 50;
function useGetLatestSnapshot(sessionUid?: string, page = 1) {
@ -78,7 +77,7 @@ function useGetLatestSnapshot(sessionUid?: string, page = 1) {
: skipToken;
const snapshotResult = useGetSnapshotQuery(getSnapshotQueryArgs, {
pollingInterval: shouldPoll ? STATUS_POLL_INTERVAL : 0,
pollingInterval: shouldPoll ? config.cloudMigrationPollIntervalMs : 0,
skipPollingIfUnfocused: true,
});

View File

@ -78,45 +78,39 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
<>
<Stack direction="column" gap={0.5}>
<Text weight={'medium'}>
<Trans>Description</Trans>
<Trans i18nKey="trails.metric-overview.description-label">Description</Trans>
</Text>
<div style={{ maxWidth: 360 }}>
{metadata?.help ? (
<div>{metadata?.help}</div>
) : (
<i>
<Trans>No description available</Trans>
<Trans i18nKey="trails.metric-overview.no-description">No description available</Trans>
</i>
)}
</div>
</Stack>
<Stack direction="column" gap={0.5}>
<Text weight={'medium'}>
<Trans>Type</Trans>
<Trans i18nKey="trails.metric-overview.type-label">Type</Trans>
</Text>
{metadata?.type ? (
<div>{metadata?.type}</div>
) : (
<i>
<Trans>Unknown</Trans>
<Trans i18nKey="trails.metric-overview.unknown-type">Unknown</Trans>
</i>
)}
</Stack>
<Stack direction="column" gap={0.5}>
<Text weight={'medium'}>
<Trans>Unit</Trans>
<Trans i18nKey="trails.metric-overview.unit-label">Unit</Trans>
</Text>
{metadata?.unit ? (
<div>{metadata?.unit}</div>
) : (
<i>
<Trans>{unit}</Trans>
</i>
)}
{metadata?.unit ? <div>{metadata?.unit}</div> : <i>{unit}</i>}
</Stack>
<Stack direction="column" gap={0.5}>
<Text weight={'medium'}>
<Trans>Labels</Trans>
<Trans i18nKey="trails.metric-overview.labels-label">Labels</Trans>
</Text>
{labelOptions.length === 0 && 'Unable to fetch labels.'}
{labelOptions.map((l) => (

View File

@ -32,7 +32,7 @@
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",

View File

@ -34,7 +34,7 @@
"@types/debounce-promise": "3.1.9",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",

View File

@ -22,7 +22,7 @@
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/testing-library__jest-dom": "5.14.9",
"ts-node": "10.9.2",

View File

@ -27,7 +27,7 @@
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",

View File

@ -30,7 +30,7 @@
"@types/d3-random": "^3.0.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"@types/testing-library__jest-dom": "5.14.9",

View File

@ -31,7 +31,7 @@
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/logfmt": "^1.2.3",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"@types/react-window": "1.8.8",

View File

@ -22,7 +22,7 @@
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/testing-library__jest-dom": "5.14.9",
"ts-node": "10.9.2",

View File

@ -23,7 +23,7 @@
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"ts-node": "10.9.2",

View File

@ -38,31 +38,31 @@
"method": "GET",
"path": "/rules",
"reqRole": "Viewer",
"reqAction": "datasources:query"
"reqAction": "alert.rules.external:read"
},
{
"method": "POST",
"path": "/rules",
"reqRole": "Editor",
"reqAction": "datasources:write"
"reqAction": "alert.rules.external:write"
},
{
"method": "DELETE",
"path": "/rules",
"reqRole": "Editor",
"reqAction": "datasources:write"
"reqAction": "alert.rules.external:write"
},
{
"method": "DELETE",
"path": "/config/v1/rules",
"reqRole": "Editor",
"reqAction": "datasources:write"
"reqAction": "alert.rules.external:write"
},
{
"method": "POST",
"path": "/config/v1/rules",
"reqRole": "Editor",
"reqAction": "datasources:write"
"reqAction": "alert.rules.external:write"
}
],
"includes": [

View File

@ -46,7 +46,7 @@
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",

View File

@ -25,7 +25,7 @@
"@testing-library/react": "15.0.2",
"@types/jest": "29.5.12",
"@types/lodash": "4.17.7",
"@types/node": "20.14.13",
"@types/node": "20.14.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"ts-node": "10.9.2",

View File

@ -597,7 +597,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) {
);
for (let i = 0; i < dlen; i++) {
if (counts[i] > hideLE && counts[i] < hideGE) {
if (counts[i] != null && counts[i] > hideLE && counts[i] < hideGE) {
let cx = cxs[~~(i / yBinQty)];
let cy = cys[i % yBinQty];
@ -826,7 +826,7 @@ export const boundedMinMax = (
minValue = Infinity;
for (let i = 0; i < values.length; i++) {
if (values[i] > hideLE && values[i] < hideGE) {
if (values[i] != null && values[i] > hideLE && values[i] < hideGE) {
minValue = Math.min(minValue, values[i]);
}
}
@ -836,7 +836,7 @@ export const boundedMinMax = (
maxValue = -Infinity;
for (let i = 0; i < values.length; i++) {
if (values[i] > hideLE && values[i] < hideGE) {
if (values[i] != null && values[i] > hideLE && values[i] < hideGE) {
maxValue = Math.max(maxValue, values[i]);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,6 +1,5 @@
{
"_comment": "",
"{unit}": "",
"access-control": {
"add-permission": {
"role-label": "Rolle",
@ -669,7 +668,6 @@
"message": "Keine Datenquellen gefunden"
}
},
"Description": "",
"explore": {
"add-to-dashboard": "Zum Dashboard hinzufügen",
"add-to-library-modal": {
@ -940,7 +938,6 @@
"refresh": "Aktualisieren"
}
},
"Labels": "",
"library-panel": {
"add-modal": {
"cancel": "Abbrechen",
@ -1541,7 +1538,6 @@
},
"title": "Das Neueste aus dem Blog"
},
"No description available": "",
"notifications": {
"empty-state": {
"description": "",
@ -2233,15 +2229,22 @@
"select-search-input": "Suchbegriff eingeben (Land, Stadt, Abkürzung)"
}
},
"trails": {
"metric-overview": {
"description-label": "",
"labels-label": "",
"no-description": "",
"type-label": "",
"unit-label": "",
"unknown-type": ""
}
},
"transformations": {
"empty": {
"add-transformation-body": "Mithilfe von Transformationen können Daten auf verschiedene Arten geändert werden, bevor Ihre Visualisierung angezeigt wird.<1></1>Dies beinhaltet die Verknüpfung von Daten, das Umbenennen von Feldern, die Erstellung von Berechnungen, das Formatieren von Daten für die Anzeige und mehr.",
"add-transformation-header": "Daten transformieren beginnen"
}
},
"Type": "",
"Unit": "",
"Unknown": "",
"user-orgs": {
"current-org-button": "Aktuell",
"name-column": "Name",

View File

@ -1,6 +1,5 @@
{
"_comment": "The code is the source of truth for English phrases. They should be updated in the components directly, and additional plurals specified in this file.",
"{unit}": "{unit}",
"access-control": {
"add-permission": {
"role-label": "Role",
@ -669,7 +668,6 @@
"message": "No data sources found"
}
},
"Description": "Description",
"explore": {
"add-to-dashboard": "Add to dashboard",
"add-to-library-modal": {
@ -940,7 +938,6 @@
"refresh": "Refresh"
}
},
"Labels": "Labels",
"library-panel": {
"add-modal": {
"cancel": "Cancel",
@ -1541,7 +1538,6 @@
},
"title": "Latest from the blog"
},
"No description available": "No description available",
"notifications": {
"empty-state": {
"description": "Notifications you have received will appear here",
@ -2233,15 +2229,22 @@
"select-search-input": "Type to search (country, city, abbreviation)"
}
},
"trails": {
"metric-overview": {
"description-label": "Description",
"labels-label": "Labels",
"no-description": "No description available",
"type-label": "Type",
"unit-label": "Unit",
"unknown-type": "Unknown"
}
},
"transformations": {
"empty": {
"add-transformation-body": "Transformations allow data to be changed in various ways before your visualization is shown.<1></1>This includes joining data together, renaming fields, making calculations, formatting data for display, and more.",
"add-transformation-header": "Start transforming data"
}
},
"Type": "Type",
"Unit": "Unit",
"Unknown": "Unknown",
"user-orgs": {
"current-org-button": "Current",
"name-column": "Name",

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