Merge branch 'main' into grafana-git-ui-sync
This commit is contained in:
commit
4672e27663
65 files changed
+589
-282
No files matched your search
@@ -55,7 +55,7 @@ To add the Prometheus data source, complete the following steps:
|
||||
1. Click **Connections** in the left-side menu.
|
||||
1. Under **Connections**, click **Add new connection**.
|
||||
1. Enter `Prometheus` in the search bar.
|
||||
1. Select **Prometheus**.
|
||||
1. Click **Prometheus data source**.
|
||||
1. Click **Add new data source** in the upper right.
|
||||
1. Enter a name for the data source.
|
||||
|
||||
|
||||
@@ -18,11 +18,16 @@ labels:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Configure data links
|
||||
title: Configure data links
|
||||
menuTitle: Configure data links and actions
|
||||
title: Configure data links and actions
|
||||
description: Configure data links to create links between dashboards and link to external resources
|
||||
weight: 80
|
||||
refs:
|
||||
api-settings:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/canvas/#button-api-options
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/latest/panels-visualizations/visualizations/canvas/#button-api-options
|
||||
global-variables:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/#__from-and-__to
|
||||
@@ -125,9 +130,14 @@ refs:
|
||||
destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-chart/
|
||||
---
|
||||
|
||||
# Configure data links
|
||||
# Configure data links and actions
|
||||
|
||||
Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. For example, if your visualization shows four servers, you can add a data link to one or two of them.
|
||||
_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls.
|
||||
In both cases, you can carry out these tasks while maintaining the context of the source panel.
|
||||
|
||||
## Data links
|
||||
|
||||
With data links, you can create links that include the series name or even the value under the cursor. For example, if your visualization shows four servers, you can add a data link to one or two of them.
|
||||
|
||||
The link itself is accessible in different ways depending on the visualization. For the time series visualization you need to click a data point or line:
|
||||
|
||||
@@ -139,7 +149,7 @@ For visualizations like stat, gauge, or bar gauge you can click anywhere on the
|
||||
|
||||
If there's only one data link in the visualization, clicking anywhere on the visualization opens the link rather than the context menu.
|
||||
|
||||
## Supported visualizations
|
||||
### Supported visualizations
|
||||
|
||||
You can configure data links for the following visualizations:
|
||||
|
||||
@@ -164,11 +174,32 @@ You can configure data links for the following visualizations:
|
||||
|
||||
{{< /column-list >}}
|
||||
|
||||
## Data link variables
|
||||
## Actions
|
||||
|
||||
Variables in data links let you send people to a detailed dashboard with preserved data filters. For example, you could use variables to specify a label, time range, series, or variable selection.
|
||||
Using actions, you can trigger processes like starting or shutting down a server, directly from a dashboard panel. [API settings](ref:api-settings) are configured in the **Add action** dialog box. You can also pass variables in the API editor.
|
||||
|
||||
To see a list of available variables, enter `$` in the data link **URL** field.
|
||||
### Supported visualizations
|
||||
|
||||
You can configure actions for the following visualizations:
|
||||
|
||||
{{< column-list >}}
|
||||
|
||||
- [Bar chart](ref:bar-chart)
|
||||
- [Candlestick](ref:candlestick)
|
||||
- [State timeline](ref:state-timeline)
|
||||
- [Status history](ref:status-history)
|
||||
- [Table](ref:table)
|
||||
- [Time series](ref:time-series)
|
||||
- [Trend](ref:trend)
|
||||
- [XY chart](ref:xy-chart)
|
||||
|
||||
{{< /column-list >}}
|
||||
|
||||
## Data link and action variables {#data-link-variables}
|
||||
|
||||
Variables in data links and actions let you send people to a detailed dashboard or trigger an API call with preserved data filters. For example, you could use variables to specify a label, time range, series, or variable selection.
|
||||
|
||||
To see a list of available variables, enter `$` in the data link or action **URL** field.
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
These variables changed in 6.4 so if you have an older version of Grafana, then use the version picker to select docs for an older version of Grafana.
|
||||
@@ -176,11 +207,11 @@ These variables changed in 6.4 so if you have an older version of Grafana, then
|
||||
|
||||
Azure Monitor, [CloudWatch](ref:cloudwatch), and [Google Cloud Monitoring](ref:google-cloud-monitoring) have pre-configured data links called _deep links_.
|
||||
|
||||
You can also use template variables in your data links URLs. For more information, refer to [Templates and variables](ref:templates-and-variables).
|
||||
You can also use template variables in your data links or actions URLs. For more information, refer to [Templates and variables](ref:templates-and-variables).
|
||||
|
||||
### Time range panel variables
|
||||
|
||||
These variables allow you to include the current time range in the data link URL:
|
||||
These variables allow you to include the current time range in the data link or action URL:
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------ |
|
||||
@@ -188,7 +219,7 @@ These variables allow you to include the current time range in the data link URL
|
||||
| `__from` | For more information, refer to [Global variables](ref:global-variables). |
|
||||
| `__to` | For more information, refer to [Global variables](ref:global-variables). |
|
||||
|
||||
When you create data links using time range variables like `__url_time_range` in the URL, you have to form the query parameter syntax yourself; that is, you must format the URL by appending query parameters using the question mark (`?`) and ampersand (`&`) syntax. These characters aren't automatically generated.
|
||||
When you create data links and actions using time range variables like `__url_time_range` in the URL, you have to form the query parameter syntax yourself; that is, you must format the URL by appending query parameters using the question mark (`?`) and ampersand (`&`) syntax. These characters aren't automatically generated.
|
||||
|
||||
### Series variables
|
||||
|
||||
@@ -219,9 +250,9 @@ Value-specific variables are available under `__value` namespace:
|
||||
| `__value.text` | Text representation of a value |
|
||||
| `__value.calc` | Calculation name if the value is result of calculation |
|
||||
|
||||
Using value-specific variables in data links can show different results depending on the set option of Tooltip mode.
|
||||
Using value-specific variables in data links and actions can show different results depending on the set option of Tooltip mode.
|
||||
|
||||
When you create data links using time range variables like `__value.time` in the URL, you have to form the query parameter syntax yourself; that is, you must add the question mark (`?`) and ampersand (`&`). These characters aren't automatically generated.
|
||||
When you create data links and actions using time range variables like `__value.time` in the URL, you have to form the query parameter syntax yourself; that is, you must add the question mark (`?`) and ampersand (`&`). These characters aren't automatically generated.
|
||||
|
||||
### Data variables
|
||||
|
||||
@@ -247,20 +278,29 @@ When linking to another dashboard that uses template variables, select variable
|
||||
|
||||
If you want to add all of the current dashboard's variables to the URL, then use `${__all_variables}`.
|
||||
|
||||
## Add a data link
|
||||
## Add data links or actions {#add-a-data-link}
|
||||
|
||||
The following tasks describe how to configure data links and actions.
|
||||
|
||||
{{< tabs >}}
|
||||
{{< tab-content name="Add data links" >}}
|
||||
To add a data link, follow these steps:
|
||||
|
||||
1. Navigate to the panel to which you want to add the data link.
|
||||
1. Hover over any part of the panel to display the menu icon in the upper-right corner.
|
||||
1. Click the menu icon and select **Edit** to open the panel editor.
|
||||
1. In the panel edit pane, scroll down to the **Data links** section and expand it.
|
||||
1. Scroll down to the **Data links and actions** section and expand it.
|
||||
1. Click **+ Add link**.
|
||||
1. In the dialog box that opens, enter a **Title**. This is a human-readable label for the link displayed in the UI. This is a required field.
|
||||
1. Enter the **URL** or variable to which you want to link. This is a required field.
|
||||
1. In the dialog box that opens, enter a **Title**.
|
||||
|
||||
To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables.
|
||||
This is a human-readable label for the link displayed in the UI. This is a required field.
|
||||
|
||||
1. Enter the **URL** to which you want to link.
|
||||
|
||||
To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. This is a required field.
|
||||
|
||||
1. If you want the link to open in a new tab, toggle the **Open in a new tab** switch.
|
||||
1. If you want the data link to open with a single click, toggle the **One click** switch.
|
||||
1. If you want the data link to open with a single click on the visualization, toggle the **One click** switch.
|
||||
|
||||
Only one data link can have **One click** enabled at a time. **One click** is only supported for some visualizations.
|
||||
|
||||
@@ -268,4 +308,37 @@ If you want to add all of the current dashboard's variables to the URL, then use
|
||||
1. Click **Save dashboard**.
|
||||
1. Click **Back to dashboard** and then **Exit edit**.
|
||||
|
||||
If you add multiple data links, you can control the order in which they appear in the visualization. To do this, click and drag the data link to the desired position.
|
||||
{{< /tab-content >}}
|
||||
{{< tab-content name="Add actions" >}}
|
||||
|
||||
{{< admonition type="note">}}
|
||||
Actions are not supported for all visualizations. For the list of supported visualizations, refer to [Supported visualizations](#supported-visualizations-1).
|
||||
{{< /admonition >}}
|
||||
|
||||
To add an action, by follow these steps:
|
||||
|
||||
1. Navigate to the panel to which you want to add the action.
|
||||
1. Hover over any part of the panel to display the menu icon in the upper-right corner.
|
||||
1. Click the menu icon and select **Edit** to open the panel editor.
|
||||
1. Scroll down to the **Data links and actions** section and expand it.
|
||||
1. Click **+ Add action**.
|
||||
1. In the dialog box that opens, define the API call settings:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Title | A human-readable label for the action that's displayed in the UI. |
|
||||
| Confirmation message | A descriptive prompt to confirm or cancel the action. |
|
||||
| Method | Select from **POST**, **PUT**, or **GET**. |
|
||||
| URL | The request URL.</p><p>To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. |
|
||||
| Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. |
|
||||
| Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.</p><p>Click the **+** icon to add as many key/value pairs as you need. |
|
||||
| Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. |
|
||||
| Body | The body of the request. |
|
||||
|
||||
1. Click **Save** to save changes and close the dialog box.
|
||||
1. Click **Save dashboard**.
|
||||
1. Click **Back to dashboard** and then **Exit edit**.
|
||||
{{< /tab-content >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
If you add multiple data links or actions, you can control the order in which they appear in the visualization. To do this, click and drag the data link or action to the desired position.
|
||||
@@ -199,7 +199,7 @@ You can set standard min/max options to define hard limits of the Y-axis. For mo
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ This option only applies when bar size is set to manual.
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Data links
|
||||
## Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ The candlestick visualization is based on the time series visualization. It can
|
||||
|
||||
{{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Data links
|
||||
## Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ Adjust the sizes of the gauge text.
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -711,7 +711,7 @@ Displays debug information in the upper right corner. This can be useful for deb
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ Choose whether you want to display the heatmap legend on the visualization by to
|
||||
|
||||
Set the color used to show exemplar data.
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ Choose from the following:
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Data links
|
||||
## Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ Use these settings to define how the legend appears in your visualization. For m
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ Adjust the sizes of the gauge text.
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ The **Page size** option lets you paginate the state timeline visualization to l
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ Controls the opacity of state regions.
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Data links
|
||||
## Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -246,6 +246,7 @@ If you want to apply a cell type to only some fields instead of all fields, you
|
||||
| Data links | If you've configured data links, when the cell type is **Auto**, the cell text becomes clickable. If you change the cell type to **Data links**, the cell text reflects the titles of the configured data links. To control the application of data link text more granularly, use a **Cell option > Cell type > Data links** field override. |
|
||||
| [JSON View](#json-view) | Shows values formatted as code. |
|
||||
| [Image](#image) | If the field value is an image URL or a base64 encoded image, the table displays the image. |
|
||||
| [Actions](#actions) | The cell displays a button that triggers a basic, unauthenticated API call when clicked. |
|
||||
|
||||
##### Sparkline
|
||||
|
||||
@@ -333,6 +334,23 @@ Set the following options:
|
||||
- **Alt text** - Set the alternative text of an image. The text will be available for screen readers and in cases when images can't be loaded.
|
||||
- **Title text** - Set the text that's displayed when the image is hovered over with a cursor.
|
||||
|
||||
##### Actions
|
||||
|
||||
The cell displays a button that triggers a basic, unauthenticated API call when clicked.
|
||||
Configure the API call with the following options:
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
| Option | Description |
|
||||
| ------- | ------------ |
|
||||
| Endpoint | Enter the endpoint URL. |
|
||||
| Method | Choose from **GET**, **POST**, and **PUT**. |
|
||||
| Content-Type | Select an option in the drop-down list. Choose from: JSON, Text, JavaScript, HTML, XML, and x-www-form-urlencoded. |
|
||||
| Query parameters | Enter as many **Key**, **Value** pairs as you need. |
|
||||
| Header parameters | Enter as many **Key**, **Value** pairs as you need. |
|
||||
| Payload | Enter the body of the API call. |
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
#### Wrap text
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
@@ -362,9 +380,9 @@ If you want to apply this setting to only some fields instead of all fields, you
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
{{< docs/shared lookup="visualizations/datalink-options-3.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Value mappings
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ Set the position of the bar relative to a data point. In the examples below, **S
|
||||
|
||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Data links
|
||||
### Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ For example, you could represent engine power and torque versus speed where spee
|
||||
|
||||
{{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Data links
|
||||
## Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ You can customize the following standard options:
|
||||
|
||||
To learn more, refer to [Configure standard options](ref:configure-standard-options).
|
||||
|
||||
## Data links
|
||||
## Data links and actions
|
||||
|
||||
{{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) |
|
||||
| `storage` | Configurable storage for dashboards, datasources, and resources |
|
||||
| `canvasPanelNesting` | Allow elements nesting |
|
||||
| `vizActions` | Allow actions in visualizations |
|
||||
| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables |
|
||||
| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown |
|
||||
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
---
|
||||
title: Data link options
|
||||
title: Data links and actions options
|
||||
comments: |
|
||||
This file is used in the following visualizations: bar gauge, gauge, pie chart, stat, table
|
||||
This file is used in the following visualizations: bar gauge, gauge, pie chart, stat
|
||||
---
|
||||
|
||||
Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel.
|
||||
You can create links that include the series name or even the value under the cursor.
|
||||
To learn more, refer to [Configure data links](../../configure-data-links/).
|
||||
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Actions are not supported for this visualization.
|
||||
{{< /admonition >}}
|
||||
|
||||
For each data link, set the following options:
|
||||
|
||||
@@ -14,4 +18,5 @@ For each data link, set the following options:
|
||||
- **URL**
|
||||
- **Open in new tab**
|
||||
|
||||
Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality. If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links.
|
||||
Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality.
|
||||
If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links.
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
title: Data link options
|
||||
title: Data links and actions options
|
||||
comments: |
|
||||
This file is used in the following visualizations: bar chart, candlestick, state timeline, status history, time series, trend, xy chart
|
||||
---
|
||||
|
||||
Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor.
|
||||
_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls.
|
||||
In both cases, you can carry out these tasks while maintaining the context of the source panel.
|
||||
|
||||
For each data link, set the following options:
|
||||
|
||||
@@ -13,4 +14,21 @@ For each data link, set the following options:
|
||||
- **Open in new tab**
|
||||
- **One click** - Opens the data link with a single click. Only one data link can have **One click** enabled at a time.
|
||||
|
||||
To learn more, refer to [Configure data links](../../configure-data-links/).
|
||||
For each action, define the following API call settings:
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ----------------------------------------------------------------- |
|
||||
| Title | A human-readable label for the action that's displayed in the UI. |
|
||||
| Confirmation message | A descriptive prompt to confirm or cancel the action. |
|
||||
| Method | Select from **POST**, **PUT**, or **GET**. |
|
||||
| URL | The request URL.</p><p>To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. |
|
||||
| Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. |
|
||||
| Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.</p><p>Click the **+** icon to add as many key/value pairs as you need. |
|
||||
| Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. |
|
||||
| Body | The body of the request. |
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Data links and actions options
|
||||
comments: |
|
||||
This file is used in the following visualizations: table
|
||||
---
|
||||
|
||||
_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls.
|
||||
In both cases, you can carry out these tasks while maintaining the context of the source panel.
|
||||
|
||||
For each data link, set the following options:
|
||||
|
||||
- **Title**
|
||||
- **URL**
|
||||
- **Open in new tab**
|
||||
|
||||
Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality.
|
||||
If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links.
|
||||
|
||||
For each action, define the following API call settings:
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ----------------------------------------------------------------- |
|
||||
| Title | A human-readable label for the action that's displayed in the UI. |
|
||||
| Confirmation message | A descriptive prompt to confirm or cancel the action. |
|
||||
| Method | Select from **POST**, **PUT**, or **GET**. |
|
||||
| URL | The request URL.</p><p>To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. |
|
||||
| Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. |
|
||||
| Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.</p><p>Click the **+** icon to add as many key/value pairs as you need. |
|
||||
| Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. |
|
||||
| Body | The body of the request. |
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
|
||||
@@ -1,10 +1,15 @@
|
||||
---
|
||||
title: Data link options
|
||||
title: Data links and actions options
|
||||
comments: |
|
||||
This file is used in the following visualizations: geomap, heatmap, histogram
|
||||
---
|
||||
|
||||
Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor.
|
||||
Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel.
|
||||
You can create links that include the series name or even the value under the cursor.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Actions are not supported for this visualization.
|
||||
{{< /admonition >}}
|
||||
|
||||
For each data link, set the following options:
|
||||
|
||||
@@ -12,4 +17,4 @@ For each data link, set the following options:
|
||||
- **URL**
|
||||
- **Open in new tab**
|
||||
|
||||
To learn more, refer to [Configure data links](../../configure-data-links/).
|
||||
To learn more, refer to [Configure data links and actions](../../configure-data-links/).
|
||||
@@ -36,7 +36,6 @@ export interface FeatureToggles {
|
||||
autoMigrateStatPanel?: boolean;
|
||||
disableAngular?: boolean;
|
||||
canvasPanelNesting?: boolean;
|
||||
vizActions?: boolean;
|
||||
disableSecretsCompatibility?: boolean;
|
||||
logRequestsInstrumentedAsUnknown?: boolean;
|
||||
grpcServer?: boolean;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DataLinksContextMenu } from './DataLinksContextMenu';
|
||||
|
||||
const fakeAriaLabel = 'fake aria label';
|
||||
describe('DataLinksContextMenu', () => {
|
||||
it('renders context menu when there are more than one data links or actions', () => {
|
||||
it('renders context menu when there are more than one data links', () => {
|
||||
render(
|
||||
<DataLinksContextMenu
|
||||
links={() => [
|
||||
@@ -23,7 +23,6 @@ describe('DataLinksContextMenu', () => {
|
||||
origin: {},
|
||||
},
|
||||
]}
|
||||
actions={[{ title: 'Action1', onClick: () => {} }]}
|
||||
>
|
||||
{() => {
|
||||
return <div aria-label="fake aria label" />;
|
||||
@@ -35,43 +34,7 @@ describe('DataLinksContextMenu', () => {
|
||||
expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders context menu when there are actions and one data link', () => {
|
||||
render(
|
||||
<DataLinksContextMenu
|
||||
links={() => [
|
||||
{
|
||||
href: '/link1',
|
||||
title: 'Link1',
|
||||
target: '_blank',
|
||||
origin: {},
|
||||
},
|
||||
]}
|
||||
actions={[{ title: 'Action1', onClick: () => {} }]}
|
||||
>
|
||||
{() => {
|
||||
return <div aria-label="fake aria label" />;
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument();
|
||||
expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders context menu when there are only actions', () => {
|
||||
render(
|
||||
<DataLinksContextMenu links={() => []} actions={[{ title: 'Action1', onClick: () => {} }]}>
|
||||
{() => {
|
||||
return <div aria-label="fake aria label" />;
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument();
|
||||
expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders link when there is a single data link and no actions', () => {
|
||||
it('renders link when there is a single data link', () => {
|
||||
render(
|
||||
<DataLinksContextMenu
|
||||
links={() => [
|
||||
|
||||
@@ -2,11 +2,11 @@ import { css } from '@emotion/css';
|
||||
import { CSSProperties } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { actionModelToContextMenuItems, linkModelToContextMenuItems } from '../../utils/dataLinks';
|
||||
import { linkModelToContextMenuItems } from '../../utils/dataLinks';
|
||||
import { WithContextMenu } from '../ContextMenu/WithContextMenu';
|
||||
import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup';
|
||||
import { MenuItem } from '../Menu/MenuItem';
|
||||
@@ -15,7 +15,6 @@ export interface DataLinksContextMenuProps {
|
||||
children: (props: DataLinksContextMenuApi) => JSX.Element;
|
||||
links: () => LinkModel[];
|
||||
style?: CSSProperties;
|
||||
actions?: ActionModel[];
|
||||
}
|
||||
|
||||
export interface DataLinksContextMenuApi {
|
||||
@@ -23,16 +22,12 @@ export interface DataLinksContextMenuApi {
|
||||
targetClassName?: string;
|
||||
}
|
||||
|
||||
export const DataLinksContextMenu = ({ children, links, actions, style }: DataLinksContextMenuProps) => {
|
||||
export const DataLinksContextMenu = ({ children, links, style }: DataLinksContextMenuProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const itemsGroup: MenuItemsGroup[] = [
|
||||
{ items: linkModelToContextMenuItems(links), label: Boolean(links().length) ? 'Data links' : '' },
|
||||
];
|
||||
const hasActions = Boolean(actions?.length);
|
||||
if (hasActions) {
|
||||
itemsGroup.push({ items: actionModelToContextMenuItems(actions!), label: 'Actions' });
|
||||
}
|
||||
|
||||
const linksCounter = itemsGroup[0].items.length;
|
||||
const renderMenuGroupItems = () => {
|
||||
@@ -59,7 +54,7 @@ export const DataLinksContextMenu = ({ children, links, actions, style }: DataLi
|
||||
cursor: 'context-menu',
|
||||
});
|
||||
|
||||
if (linksCounter > 1 || hasActions) {
|
||||
if (linksCounter > 1) {
|
||||
return (
|
||||
<WithContextMenu renderMenuItems={renderMenuGroupItems}>
|
||||
{({ openMenu }) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const defaultScale: ThresholdsConfig = {
|
||||
};
|
||||
|
||||
export const BarGaugeCell = (props: TableCellProps) => {
|
||||
const { field, innerWidth, tableStyles, cell, cellProps, row, actions } = props;
|
||||
const { field, innerWidth, tableStyles, cell, cellProps, row } = props;
|
||||
const displayValue = field.display!(cell.value);
|
||||
const cellOptions = getCellOptions(field);
|
||||
|
||||
@@ -56,7 +56,6 @@ export const BarGaugeCell = (props: TableCellProps) => {
|
||||
};
|
||||
|
||||
const hasLinks = Boolean(getLinks().length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
const alignmentFactors = getAlignmentFactor(field, displayValue, cell.row.index);
|
||||
|
||||
const renderComponent = (menuProps: DataLinksContextMenuApi) => {
|
||||
@@ -85,8 +84,8 @@ export const BarGaugeCell = (props: TableCellProps) => {
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
{hasLinks || hasActions ? (
|
||||
<DataLinksContextMenu links={getLinks} actions={actions} style={{ display: 'flex', width: '100%' }}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu links={getLinks} style={{ display: 'flex', width: '100%' }}>
|
||||
{(api) => renderComponent(api)}
|
||||
</DataLinksContextMenu>
|
||||
) : (
|
||||
|
||||
@@ -17,8 +17,7 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ
|
||||
import { getCellColors, getCellOptions } from './utils';
|
||||
|
||||
export const DefaultCell = (props: TableCellProps) => {
|
||||
const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height, actions } =
|
||||
props;
|
||||
const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height } = props;
|
||||
const inspectEnabled = Boolean(field.config.custom?.inspect);
|
||||
const displayValue = field.display!(cell.value);
|
||||
|
||||
@@ -26,7 +25,6 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
const showActions = (showFilters && cell.value !== undefined) || inspectEnabled;
|
||||
const cellOptions = getCellOptions(field);
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
let value: string | ReactElement;
|
||||
|
||||
@@ -81,8 +79,8 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
|
||||
return (
|
||||
<div key={key} {...rest} className={cellStyle}>
|
||||
{hasLinks || hasActions ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
|
||||
@@ -9,13 +9,12 @@ import { getCellOptions } from './utils';
|
||||
const DATALINKS_HEIGHT_OFFSET = 10;
|
||||
|
||||
export const ImageCell = (props: TableCellProps) => {
|
||||
const { field, cell, tableStyles, row, cellProps, actions } = props;
|
||||
const { field, cell, tableStyles, row, cellProps } = props;
|
||||
const cellOptions = getCellOptions(field);
|
||||
const { title, alt } =
|
||||
cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { title: undefined, alt: undefined };
|
||||
const displayValue = field.display!(cell.value);
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
|
||||
// The image element
|
||||
const img = (
|
||||
@@ -32,11 +31,10 @@ export const ImageCell = (props: TableCellProps) => {
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
{/* If there are data links/actions, we render them with image */}
|
||||
{/* Otherwise we simply render the image */}
|
||||
{hasLinks || hasActions ? (
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu
|
||||
style={{ height: tableStyles.cellHeight - DATALINKS_HEIGHT_OFFSET, width: 'auto' }}
|
||||
links={() => getCellLinks(field, row) || []}
|
||||
actions={actions}
|
||||
>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TableCellInspectorMode } from './TableCellInspector';
|
||||
import { TableCellProps } from './types';
|
||||
|
||||
export function JSONViewCell(props: TableCellProps): JSX.Element {
|
||||
const { cell, tableStyles, cellProps, field, row, actions } = props;
|
||||
const { cell, tableStyles, cellProps, field, row } = props;
|
||||
const inspectEnabled = Boolean(field.config.custom?.inspect);
|
||||
const txt = css({
|
||||
cursor: 'pointer',
|
||||
@@ -30,14 +30,13 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
|
||||
}
|
||||
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={inspectEnabled ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer}>
|
||||
<div className={cx(tableStyles.cellText, txt)}>
|
||||
{hasLinks || hasActions ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionModel, LinkModel } from '@grafana/data';
|
||||
import { LinkModel } from '@grafana/data';
|
||||
|
||||
import { MenuItemProps } from '../components/Menu/MenuItem';
|
||||
|
||||
@@ -18,14 +18,3 @@ export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItem
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const actionModelToContextMenuItems: (actions: ActionModel[]) => MenuItemProps[] = (actions) => {
|
||||
return actions.map((action) => {
|
||||
return {
|
||||
label: action.title,
|
||||
ariaLabel: action.title,
|
||||
icon: 'record-audio',
|
||||
onClick: action.onClick,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -66,9 +66,6 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
{
|
||||
Action: ac.ActionDatasourcesExplore,
|
||||
},
|
||||
{
|
||||
Action: ac.ActionDatasourcesDrilldown,
|
||||
},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleEditor)},
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
}
|
||||
|
||||
r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index)
|
||||
r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesDrilldown)), hs.Index)
|
||||
r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, hs.Index)
|
||||
r.Get("/playlists/*", reqSignedIn, hs.Index)
|
||||
|
||||
@@ -386,8 +386,7 @@ const (
|
||||
ActionSettingsWrite = "settings:write"
|
||||
|
||||
// Datasources actions
|
||||
ActionDatasourcesExplore = "datasources:explore"
|
||||
ActionDatasourcesDrilldown = "datasources:drilldown"
|
||||
ActionDatasourcesExplore = "datasources:explore"
|
||||
|
||||
// Global Scopes
|
||||
ScopeGlobalUsersAll = "global.users:*"
|
||||
|
||||
@@ -33,8 +33,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
)
|
||||
|
||||
// `authzService` is hardcoded in authz-service
|
||||
const authzServiceAudience = "authzService"
|
||||
// AuthzServiceAudience is the audience for the authz service.
|
||||
const AuthzServiceAudience = "authzService"
|
||||
|
||||
// ProvideAuthZClient provides an AuthZ client and creates the AuthZ service.
|
||||
func ProvideAuthZClient(
|
||||
@@ -123,7 +123,7 @@ func newRemoteRBACClient(clientCfg *authzClientSettings, tracer tracing.Tracer)
|
||||
clientCfg.remoteAddress,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithPerRPCCredentials(
|
||||
newGRPCTokenAuth(authzServiceAudience, clientCfg.tokenNamespace, tokenClient),
|
||||
NewGRPCTokenAuth(AuthzServiceAudience, clientCfg.tokenNamespace, tokenClient),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
"github.com/grafana/authlib/authn"
|
||||
)
|
||||
|
||||
func newGRPCTokenAuth(audience, namespace string, tc authn.TokenExchanger) *tokenAuth {
|
||||
// TODO: move this to authlib
|
||||
func NewGRPCTokenAuth(audience, namespace string, tc authn.TokenExchanger) *tokenAuth {
|
||||
return &tokenAuth{audience, namespace, tc}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features
|
||||
// TODO: add TLS support
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithPerRPCCredentials(
|
||||
newGRPCTokenAuth(authzServiceAudience, fmt.Sprintf("stacks-%s", cfg.StackID), tokenClient),
|
||||
NewGRPCTokenAuth(AuthzServiceAudience, fmt.Sprintf("stacks-%s", cfg.StackID), tokenClient),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func (z *Zanzana) start(ctx context.Context) error {
|
||||
|
||||
authenticator := authnlib.NewAccessTokenAuthenticator(
|
||||
authnlib.NewAccessTokenVerifier(
|
||||
authnlib.VerifierConfig{AllowedAudiences: []string{authzServiceAudience}},
|
||||
authnlib.VerifierConfig{AllowedAudiences: []string{AuthzServiceAudience}},
|
||||
authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{
|
||||
SigningKeysURL: z.cfg.ZanzanaServer.SigningKeysURL,
|
||||
}),
|
||||
|
||||
@@ -150,14 +150,6 @@ var (
|
||||
Owner: grafanaDatavizSquad,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "vizActions",
|
||||
Description: "Allow actions in visualizations",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDatavizSquad,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "disableSecretsCompatibility",
|
||||
Description: "Disable duplicated secret storage in legacy tables",
|
||||
|
||||
@@ -17,7 +17,6 @@ autoMigrateWorldmapPanel,preview,@grafana/dataviz-squad,false,false,true
|
||||
autoMigrateStatPanel,preview,@grafana/dataviz-squad,false,false,true
|
||||
disableAngular,preview,@grafana/dataviz-squad,false,false,true
|
||||
canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true
|
||||
vizActions,experimental,@grafana/dataviz-squad,false,false,true
|
||||
disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,true,false
|
||||
logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false
|
||||
grpcServer,preview,@grafana/search-and-storage,false,false,false
|
||||
|
||||
|
@@ -79,10 +79,6 @@ const (
|
||||
// Allow elements nesting
|
||||
FlagCanvasPanelNesting = "canvasPanelNesting"
|
||||
|
||||
// FlagVizActions
|
||||
// Allow actions in visualizations
|
||||
FlagVizActions = "vizActions"
|
||||
|
||||
// FlagDisableSecretsCompatibility
|
||||
// Disable duplicated secret storage in legacy tables
|
||||
FlagDisableSecretsCompatibility = "disableSecretsCompatibility"
|
||||
|
||||
@@ -4202,7 +4202,8 @@
|
||||
"metadata": {
|
||||
"name": "vizActions",
|
||||
"resourceVersion": "1722461779830",
|
||||
"creationTimestamp": "2024-09-09T14:11:55Z"
|
||||
"creationTimestamp": "2024-09-09T14:11:55Z",
|
||||
"deletionTimestamp": "2025-02-07T18:50:26Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Allow actions in visualizations",
|
||||
|
||||
@@ -130,7 +130,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere
|
||||
})
|
||||
}
|
||||
|
||||
if hasAccess(ac.EvalPermission(ac.ActionDatasourcesDrilldown)) {
|
||||
if hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) {
|
||||
drilldownChildNavLinks := s.buildDrilldownNavLinks(c)
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Text: "Drilldown",
|
||||
|
||||
@@ -37,7 +37,7 @@ type Rule interface {
|
||||
// It has no effect if the rule has not yet been Run, or if the rule is Stopped.
|
||||
Eval(eval *Evaluation) (bool, *Evaluation)
|
||||
// Update sends a singal to change the definition of the rule.
|
||||
Update(lastVersion RuleVersionAndPauseStatus) bool
|
||||
Update(eval *Evaluation) bool
|
||||
// Type gives the type of the rule.
|
||||
Type() ngmodels.RuleType
|
||||
// Status indicates the status of the evaluating rule.
|
||||
@@ -59,7 +59,6 @@ func newRuleFactory(
|
||||
sender AlertsSender,
|
||||
stateManager *state.Manager,
|
||||
evalFactory eval.EvaluatorFactory,
|
||||
ruleProvider ruleProvider,
|
||||
clock clock.Clock,
|
||||
rrCfg setting.RecordingRuleSettings,
|
||||
met *metrics.Scheduler,
|
||||
@@ -95,7 +94,6 @@ func newRuleFactory(
|
||||
sender,
|
||||
stateManager,
|
||||
evalFactory,
|
||||
ruleProvider,
|
||||
clock,
|
||||
met,
|
||||
logger,
|
||||
@@ -109,15 +107,11 @@ func newRuleFactory(
|
||||
type evalAppliedFunc = func(ngmodels.AlertRuleKey, time.Time)
|
||||
type stopAppliedFunc = func(ngmodels.AlertRuleKey)
|
||||
|
||||
type ruleProvider interface {
|
||||
get(ngmodels.AlertRuleKey) *ngmodels.AlertRule
|
||||
}
|
||||
|
||||
type alertRule struct {
|
||||
key ngmodels.AlertRuleKeyWithGroup
|
||||
|
||||
evalCh chan *Evaluation
|
||||
updateCh chan RuleVersionAndPauseStatus
|
||||
updateCh chan *Evaluation
|
||||
ctx context.Context
|
||||
stopFn util.CancelCauseFunc
|
||||
|
||||
@@ -129,7 +123,6 @@ type alertRule struct {
|
||||
sender AlertsSender
|
||||
stateManager *state.Manager
|
||||
evalFactory eval.EvaluatorFactory
|
||||
ruleProvider ruleProvider
|
||||
|
||||
// Event hooks that are only used in tests.
|
||||
evalAppliedHook evalAppliedFunc
|
||||
@@ -149,7 +142,6 @@ func newAlertRule(
|
||||
sender AlertsSender,
|
||||
stateManager *state.Manager,
|
||||
evalFactory eval.EvaluatorFactory,
|
||||
ruleProvider ruleProvider,
|
||||
clock clock.Clock,
|
||||
met *metrics.Scheduler,
|
||||
logger log.Logger,
|
||||
@@ -161,7 +153,7 @@ func newAlertRule(
|
||||
return &alertRule{
|
||||
key: key,
|
||||
evalCh: make(chan *Evaluation),
|
||||
updateCh: make(chan RuleVersionAndPauseStatus),
|
||||
updateCh: make(chan *Evaluation),
|
||||
ctx: ctx,
|
||||
stopFn: stop,
|
||||
appURL: appURL,
|
||||
@@ -171,7 +163,6 @@ func newAlertRule(
|
||||
sender: sender,
|
||||
stateManager: stateManager,
|
||||
evalFactory: evalFactory,
|
||||
ruleProvider: ruleProvider,
|
||||
evalAppliedHook: evalAppliedHook,
|
||||
stopAppliedHook: stopAppliedHook,
|
||||
metrics: met,
|
||||
@@ -220,7 +211,7 @@ func (a *alertRule) Eval(eval *Evaluation) (bool, *Evaluation) {
|
||||
}
|
||||
|
||||
// update sends an instruction to the rule evaluation routine to update the scheduled rule to the specified version. The specified version must be later than the current version, otherwise no update will happen.
|
||||
func (a *alertRule) Update(lastVersion RuleVersionAndPauseStatus) bool {
|
||||
func (a *alertRule) Update(eval *Evaluation) bool {
|
||||
// check if the channel is not empty.
|
||||
select {
|
||||
case <-a.updateCh:
|
||||
@@ -230,7 +221,7 @@ func (a *alertRule) Update(lastVersion RuleVersionAndPauseStatus) bool {
|
||||
}
|
||||
|
||||
select {
|
||||
case a.updateCh <- lastVersion:
|
||||
case a.updateCh <- eval:
|
||||
return true
|
||||
case <-a.ctx.Done():
|
||||
return false
|
||||
@@ -254,15 +245,16 @@ func (a *alertRule) Run() error {
|
||||
select {
|
||||
// used by external services (API) to notify that rule is updated.
|
||||
case ctx := <-a.updateCh:
|
||||
if currentFingerprint == ctx.Fingerprint {
|
||||
fp := ctx.Fingerprint()
|
||||
if currentFingerprint == fp {
|
||||
a.logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint)
|
||||
continue
|
||||
}
|
||||
|
||||
a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.IsPaused, "fingerprint", ctx.Fingerprint)
|
||||
a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.rule.IsPaused, "fingerprint", fp)
|
||||
// clear the state. So the next evaluation will start from the scratch.
|
||||
a.resetState(grafanaCtx, ctx.IsPaused)
|
||||
currentFingerprint = ctx.Fingerprint
|
||||
a.resetState(grafanaCtx, ctx.rule, ctx.rule.IsPaused)
|
||||
currentFingerprint = fp
|
||||
// evalCh - used by the scheduler to signal that evaluation is needed.
|
||||
case ctx, ok := <-a.evalCh:
|
||||
if !ok {
|
||||
@@ -298,7 +290,7 @@ func (a *alertRule) Run() error {
|
||||
// lingers in DB and won't be cleaned up until next alert rule update.
|
||||
needReset = needReset || (currentFingerprint == 0 && isPaused)
|
||||
if needReset {
|
||||
a.resetState(grafanaCtx, isPaused)
|
||||
a.resetState(grafanaCtx, ctx.rule, isPaused)
|
||||
}
|
||||
currentFingerprint = f
|
||||
if isPaused {
|
||||
@@ -494,8 +486,7 @@ func (a *alertRule) expireAndSend(ctx context.Context, states []state.StateTrans
|
||||
}
|
||||
}
|
||||
|
||||
func (a *alertRule) resetState(ctx context.Context, isPaused bool) {
|
||||
rule := a.ruleProvider.get(a.key.AlertRuleKey)
|
||||
func (a *alertRule) resetState(ctx context.Context, rule *ngmodels.AlertRule, isPaused bool) {
|
||||
reason := ngmodels.StateReasonUpdated
|
||||
if isPaused {
|
||||
reason = ngmodels.StateReasonPaused
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestAlertRule(t *testing.T) {
|
||||
r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1))
|
||||
resultCh := make(chan bool)
|
||||
go func() {
|
||||
resultCh <- r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})
|
||||
resultCh <- r.Update(&Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()})
|
||||
}()
|
||||
select {
|
||||
case <-r.updateCh:
|
||||
@@ -54,8 +54,8 @@ func TestAlertRule(t *testing.T) {
|
||||
})
|
||||
t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) {
|
||||
r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1))
|
||||
version1 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}
|
||||
version2 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}
|
||||
version1 := &Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()}
|
||||
version2 := &Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
@@ -178,7 +178,7 @@ func TestAlertRule(t *testing.T) {
|
||||
r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1))
|
||||
r.Stop(errRuleDeleted)
|
||||
require.ErrorIs(t, r.ctx.Err(), errRuleDeleted)
|
||||
require.False(t, r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}))
|
||||
require.False(t, r.Update(&Evaluation{rule: gen.GenerateRef()}))
|
||||
})
|
||||
t.Run("eval should do nothing", func(t *testing.T) {
|
||||
ruleSpec := gen.GenerateRef()
|
||||
@@ -234,7 +234,7 @@ func TestAlertRule(t *testing.T) {
|
||||
}
|
||||
switch rand.Intn(max) + 1 {
|
||||
case 1:
|
||||
r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})
|
||||
r.Update(&Evaluation{rule: gen.GenerateRef()})
|
||||
case 2:
|
||||
r.Eval(&Evaluation{
|
||||
scheduledAt: time.Now(),
|
||||
@@ -284,7 +284,7 @@ func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *a
|
||||
Log: log.NewNopLogger(),
|
||||
}
|
||||
st := state.NewManager(managerCfg, state.NewNoopPersister())
|
||||
return newAlertRule(ctx, key, nil, false, 0, nil, st, nil, nil, nil, nil, log.NewNopLogger(), nil, nil, nil)
|
||||
return newAlertRule(ctx, key, nil, false, 0, nil, st, nil, nil, nil, log.NewNopLogger(), nil, nil, nil)
|
||||
}
|
||||
|
||||
func TestRuleRoutine(t *testing.T) {
|
||||
@@ -572,7 +572,6 @@ func TestRuleRoutine(t *testing.T) {
|
||||
t.Run("when a message is sent to update channel", func(t *testing.T) {
|
||||
rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef()
|
||||
folderTitle := "folderName"
|
||||
ruleFp := ruleWithFolder{rule, folderTitle}.Fingerprint()
|
||||
|
||||
evalAppliedChan := make(chan time.Time)
|
||||
|
||||
@@ -628,8 +627,8 @@ func TestRuleRoutine(t *testing.T) {
|
||||
require.Greaterf(t, expectedToBeSent, 0, "State manager was expected to return at least one state that can be expired")
|
||||
|
||||
t.Run("should do nothing if version in channel is the same", func(t *testing.T) {
|
||||
ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false})
|
||||
ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false}) // second time just to make sure that previous messages were handled
|
||||
ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle})
|
||||
ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle}) // second time just to make sure that previous messages were handled
|
||||
|
||||
actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)
|
||||
require.Len(t, actualStates, len(states))
|
||||
@@ -638,7 +637,7 @@ func TestRuleRoutine(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should clear the state and expire firing alerts if version in channel is greater", func(t *testing.T) {
|
||||
ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp + 1, false})
|
||||
ruleInfo.Update(&Evaluation{rule: models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID())), folderTitle: folderTitle})
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return len(sender.Calls()) > 0
|
||||
@@ -905,7 +904,7 @@ func TestRuleRoutine(t *testing.T) {
|
||||
}
|
||||
|
||||
func ruleFactoryFromScheduler(sch *schedule) ruleFactory {
|
||||
return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, &sch.schedulableAlertRules, sch.clock, sch.rrCfg, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc)
|
||||
return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, sch.clock, sch.rrCfg, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc)
|
||||
}
|
||||
|
||||
func stateForRule(rule *models.AlertRule, ts time.Time, evalState eval.State) *state.State {
|
||||
|
||||
@@ -113,7 +113,7 @@ func (r *recordingRule) Eval(eval *Evaluation) (bool, *Evaluation) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recordingRule) Update(lastVersion RuleVersionAndPauseStatus) bool {
|
||||
func (r *recordingRule) Update(_ *Evaluation) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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/services/ngalert/metrics"
|
||||
@@ -116,7 +117,10 @@ func TestRecordingRule(t *testing.T) {
|
||||
}
|
||||
switch rand.Intn(max) + 1 {
|
||||
case 1:
|
||||
r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})
|
||||
r.Update(&Evaluation{
|
||||
rule: gen.GenerateRef(),
|
||||
folderTitle: util.GenerateShortUID(),
|
||||
})
|
||||
case 2:
|
||||
r.Eval(&Evaluation{
|
||||
scheduledAt: time.Now(),
|
||||
@@ -492,7 +496,7 @@ func TestRecordingRule_Integration(t *testing.T) {
|
||||
t.Run("status shows evaluation", func(t *testing.T) {
|
||||
status := process.(*recordingRule).Status()
|
||||
|
||||
//TODO: assert "error" to fix test, update to "nodata" in the future
|
||||
// TODO: assert "error" to fix test, update to "nodata" in the future
|
||||
require.Equal(t, "error", status.Health)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -87,11 +87,6 @@ func (r *ruleRegistry) keyMap() map[models.AlertRuleKey]struct{} {
|
||||
return definitionsIDs
|
||||
}
|
||||
|
||||
type RuleVersionAndPauseStatus struct {
|
||||
Fingerprint fingerprint
|
||||
IsPaused bool
|
||||
}
|
||||
|
||||
type Evaluation struct {
|
||||
scheduledAt time.Time
|
||||
rule *models.AlertRule
|
||||
|
||||
@@ -302,7 +302,6 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.
|
||||
sch.alertsSender,
|
||||
sch.stateManager,
|
||||
sch.evaluatorFactory,
|
||||
&sch.schedulableAlertRules,
|
||||
sch.clock,
|
||||
sch.rrCfg,
|
||||
sch.metrics,
|
||||
@@ -372,9 +371,9 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.
|
||||
// if we do not need to eval the rule, check the whether rule was just updated and if it was, notify evaluation routine about that
|
||||
logger.Debug("Rule has been updated. Notifying evaluation routine")
|
||||
go func(routine Rule, rule *ngmodels.AlertRule) {
|
||||
routine.Update(RuleVersionAndPauseStatus{
|
||||
Fingerprint: ruleWithFolder{rule: rule, folderTitle: folderTitle}.Fingerprint(),
|
||||
IsPaused: rule.IsPaused,
|
||||
routine.Update(&Evaluation{
|
||||
rule: rule,
|
||||
folderTitle: folderTitle,
|
||||
})
|
||||
}(ruleRoutine, item)
|
||||
updatedRules = append(updatedRules, ngmodels.AlertRuleKeyWithVersion{
|
||||
|
||||
@@ -40,6 +40,9 @@ type BackendOptions struct {
|
||||
PollingInterval time.Duration
|
||||
WatchBufferSize int
|
||||
IsHA bool
|
||||
|
||||
// testing
|
||||
SimulatedNetworkLatency time.Duration // slows down the create transactions by a fixed amount
|
||||
}
|
||||
|
||||
func NewBackend(opts BackendOptions) (Backend, error) {
|
||||
@@ -58,15 +61,16 @@ func NewBackend(opts BackendOptions) (Backend, error) {
|
||||
opts.WatchBufferSize = defaultWatchBufferSize
|
||||
}
|
||||
return &backend{
|
||||
isHA: opts.IsHA,
|
||||
done: ctx.Done(),
|
||||
cancel: cancel,
|
||||
log: log.New("sql-resource-server"),
|
||||
tracer: opts.Tracer,
|
||||
dbProvider: opts.DBProvider,
|
||||
pollingInterval: opts.PollingInterval,
|
||||
watchBufferSize: opts.WatchBufferSize,
|
||||
batchLock: &batchLock{running: make(map[string]bool)},
|
||||
isHA: opts.IsHA,
|
||||
done: ctx.Done(),
|
||||
cancel: cancel,
|
||||
log: log.New("sql-resource-server"),
|
||||
tracer: opts.Tracer,
|
||||
dbProvider: opts.DBProvider,
|
||||
pollingInterval: opts.PollingInterval,
|
||||
watchBufferSize: opts.WatchBufferSize,
|
||||
batchLock: &batchLock{running: make(map[string]bool)},
|
||||
simulatedNetworkLatency: opts.SimulatedNetworkLatency,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -95,6 +99,9 @@ type backend struct {
|
||||
pollingInterval time.Duration
|
||||
watchBufferSize int
|
||||
notifier eventNotifier
|
||||
|
||||
// testing
|
||||
simulatedNetworkLatency time.Duration
|
||||
}
|
||||
|
||||
func (b *backend) Init(ctx context.Context) error {
|
||||
@@ -249,7 +256,9 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64,
|
||||
return fmt.Errorf("update resource rv: %w", err)
|
||||
}
|
||||
newVersion = rv
|
||||
|
||||
if b.simulatedNetworkLatency > 0 {
|
||||
time.Sleep(b.simulatedNetworkLatency)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
|
||||
test "github.com/grafana/grafana/pkg/storage/unified/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestBackend(b *testing.B) resource.StorageBackend {
|
||||
dbstore := infraDB.InitTestDB(b)
|
||||
eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil)
|
||||
require.NoError(b, err)
|
||||
require.NotNil(b, eDB)
|
||||
|
||||
backend, err := sql.NewBackend(sql.BackendOptions{
|
||||
DBProvider: eDB,
|
||||
IsHA: true,
|
||||
SimulatedNetworkLatency: 5 * time.Millisecond, // to simulate some network latency
|
||||
})
|
||||
require.NoError(b, err)
|
||||
require.NotNil(b, backend)
|
||||
err = backend.Init(context.Background())
|
||||
require.NoError(b, err)
|
||||
return backend
|
||||
}
|
||||
|
||||
func BenchmarkSQLStorageBackend(b *testing.B) {
|
||||
opts := test.DefaultBenchmarkOptions()
|
||||
if infraDB.IsTestDbSQLite() {
|
||||
opts.Concurrency = 1 // to avoid SQLite database is locked error
|
||||
}
|
||||
test.BenchmarkStorageBackend(b, newTestBackend(b), opts)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// BenchmarkOptions configures the benchmark parameters
|
||||
type BenchmarkOptions struct {
|
||||
NumResources int // total number of resources to write
|
||||
Concurrency int // number of concurrent writers
|
||||
NumNamespaces int // number of different namespaces
|
||||
NumGroups int // number of different groups
|
||||
NumResourceTypes int // number of different resource types
|
||||
}
|
||||
|
||||
// DefaultBenchmarkOptions returns the default benchmark options
|
||||
func DefaultBenchmarkOptions() *BenchmarkOptions {
|
||||
return &BenchmarkOptions{
|
||||
NumResources: 1000,
|
||||
Concurrency: 20,
|
||||
NumNamespaces: 1,
|
||||
NumGroups: 1,
|
||||
NumResourceTypes: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkResult contains the benchmark metrics
|
||||
type BenchmarkResult struct {
|
||||
TotalDuration time.Duration
|
||||
WriteCount int
|
||||
Throughput float64 // writes per second
|
||||
P50Latency time.Duration
|
||||
P90Latency time.Duration
|
||||
P99Latency time.Duration
|
||||
}
|
||||
|
||||
// runStorageBackendBenchmark runs a write throughput benchmark
|
||||
func runStorageBackendBenchmark(ctx context.Context, backend resource.StorageBackend, opts *BenchmarkOptions) (*BenchmarkResult, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultBenchmarkOptions()
|
||||
}
|
||||
|
||||
// Create channels for workers
|
||||
jobs := make(chan int, opts.NumResources)
|
||||
results := make(chan time.Duration, opts.NumResources)
|
||||
errors := make(chan error, opts.NumResources)
|
||||
|
||||
// Fill the jobs channel
|
||||
for i := 0; i < opts.NumResources; i++ {
|
||||
jobs <- i
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize each group and resource type combination in the init namespace
|
||||
namespace := "ns-init"
|
||||
for g := 0; g < opts.NumGroups; g++ {
|
||||
group := fmt.Sprintf("group-%d", g)
|
||||
for r := 0; r < opts.NumResourceTypes; r++ {
|
||||
resourceType := fmt.Sprintf("resource-%d", r)
|
||||
_, err := writeEvent(ctx, backend, "init", resource.WatchEvent_ADDED,
|
||||
WithNamespace(namespace),
|
||||
WithGroup(group),
|
||||
WithResource(resourceType),
|
||||
WithValue([]byte("init")))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize backend: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start workers
|
||||
startTime := time.Now()
|
||||
for workerID := 0; workerID < opts.Concurrency; workerID++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for jobID := range jobs {
|
||||
// Calculate a unique ID for this job that's guaranteed to be unique across all workers
|
||||
uniqueID := jobID
|
||||
|
||||
// Generate deterministic and unique resource details
|
||||
namespace := fmt.Sprintf("ns-%d", uniqueID%opts.NumNamespaces)
|
||||
group := fmt.Sprintf("group-%d", uniqueID%opts.NumGroups)
|
||||
resourceType := fmt.Sprintf("resource-%d", uniqueID%opts.NumResourceTypes)
|
||||
// Ensure name is unique by using the global uniqueID
|
||||
name := fmt.Sprintf("item-%d", uniqueID)
|
||||
|
||||
writeStart := time.Now()
|
||||
_, err := writeEvent(ctx, backend, name, resource.WatchEvent_ADDED,
|
||||
WithNamespace(namespace),
|
||||
WithGroup(group),
|
||||
WithResource(resourceType),
|
||||
WithValue([]byte(strings.Repeat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 20)))) // ~1.21 KiB
|
||||
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
results <- time.Since(writeStart)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all workers to complete
|
||||
wg.Wait()
|
||||
close(results)
|
||||
close(errors)
|
||||
|
||||
// Check for errors
|
||||
if len(errors) > 0 {
|
||||
return nil, <-errors // Return the first error encountered
|
||||
}
|
||||
|
||||
// Collect all latencies
|
||||
latencies := make([]time.Duration, 0, opts.NumResources)
|
||||
for latency := range results {
|
||||
latencies = append(latencies, latency)
|
||||
}
|
||||
|
||||
// Sort latencies for percentile calculation
|
||||
sort.Slice(latencies, func(i, j int) bool {
|
||||
return latencies[i] < latencies[j]
|
||||
})
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
throughput := float64(opts.NumResources) / totalDuration.Seconds()
|
||||
|
||||
return &BenchmarkResult{
|
||||
TotalDuration: totalDuration,
|
||||
WriteCount: opts.NumResources,
|
||||
Throughput: throughput,
|
||||
P50Latency: latencies[len(latencies)*50/100],
|
||||
P90Latency: latencies[len(latencies)*90/100],
|
||||
P99Latency: latencies[len(latencies)*99/100],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BenchmarkStorageBackend runs a benchmark test for a storage backend implementation
|
||||
func BenchmarkStorageBackend(b *testing.B, backend resource.StorageBackend, opts *BenchmarkOptions) {
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := runStorageBackendBenchmark(ctx, backend, opts)
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ReportMetric(result.Throughput, "writes/sec")
|
||||
b.ReportMetric(float64(result.P50Latency.Milliseconds()), "p50-latency-ms")
|
||||
b.ReportMetric(float64(result.P90Latency.Milliseconds()), "p90-latency-ms")
|
||||
b.ReportMetric(float64(result.P99Latency.Milliseconds()), "p99-latency-ms")
|
||||
|
||||
// Also log the results for better visibility
|
||||
b.Logf("Benchmark Configuration: Workers=%d, Resources=%d, Namespaces=%d, Groups=%d, Resource Types=%d", opts.Concurrency, opts.NumResources, opts.NumNamespaces, opts.NumGroups, opts.NumResourceTypes)
|
||||
b.Logf("")
|
||||
b.Logf("Benchmark Results:")
|
||||
b.Logf("Total Duration: %v", result.TotalDuration)
|
||||
b.Logf("Write Count: %d", result.WriteCount)
|
||||
b.Logf("Throughput: %.2f writes/sec", result.Throughput)
|
||||
b.Logf("P50 Latency: %v", result.P50Latency)
|
||||
b.Logf("P90 Latency: %v", result.P90Latency)
|
||||
b.Logf("P99 Latency: %v", result.P99Latency)
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
DataLinksFieldConfigSettings,
|
||||
} from '@grafana/data';
|
||||
import { actionsOverrideProcessor } from '@grafana/data/src/field/overrides/processors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { FieldConfig } from '@grafana/schema';
|
||||
import { RadioButtonGroup, TimeZonePicker, Switch } from '@grafana/ui';
|
||||
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||
@@ -349,7 +348,7 @@ export const getAllStandardFieldConfigs = () => {
|
||||
category,
|
||||
};
|
||||
|
||||
const dataLinksCategory = config.featureToggles.vizActions ? 'Data links and actions' : 'Data links';
|
||||
const dataLinksCategory = 'Data links and actions';
|
||||
|
||||
const links: FieldConfigPropertyItem<FieldConfig, DataLink[], DataLinksFieldConfigSettings> = {
|
||||
id: 'links',
|
||||
@@ -379,7 +378,6 @@ export const getAllStandardFieldConfigs = () => {
|
||||
shouldApply: () => true,
|
||||
category: [dataLinksCategory],
|
||||
getItemsCount: (value) => (value ? value.length : 0),
|
||||
showIf: () => config.featureToggles.vizActions,
|
||||
hideFromDefaults: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { Action, GrafanaTheme2, httpMethodOptions, HttpRequestMethod, VariableSuggestion } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Switch } from '@grafana/ui/';
|
||||
import { Field } from '@grafana/ui/src/components/Forms/Field';
|
||||
import { InlineField } from '@grafana/ui/src/components/Forms/InlineField';
|
||||
@@ -108,8 +107,6 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne
|
||||
value.fetch.method !== HttpRequestMethod.GET &&
|
||||
value.fetch.headers?.some(([name, value]) => name === 'Content-Type' && value === 'application/json');
|
||||
|
||||
const action = config.featureToggles.vizActions ? 'or action' : '';
|
||||
|
||||
return (
|
||||
<div className={styles.listItem}>
|
||||
<Field label={t('grafana-ui.action-editor.modal.action-title', 'Title')} className={styles.inputField}>
|
||||
@@ -147,8 +144,7 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne
|
||||
label={t('grafana-ui.data-link-inline-editor.one-click', 'One click')}
|
||||
description={t(
|
||||
'grafana-ui.action-editor.modal.one-click-description',
|
||||
'Only one link {{ action }} can have one click enabled at a time',
|
||||
{ action }
|
||||
'Only one link or action can have one click enabled at a time'
|
||||
)}
|
||||
>
|
||||
<Switch value={value.oneClick || false} onChange={onOneClickChanged} />
|
||||
|
||||
+7
-11
@@ -5,7 +5,7 @@ import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, TimeRange, dateTime } from '@grafana/data';
|
||||
import { Alert, Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
||||
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
||||
@@ -103,15 +103,15 @@ const LokiStateHistory = ({ ruleUID }: Props) => {
|
||||
<input type="submit" hidden />
|
||||
</form>
|
||||
{!isEmpty(commonLabels) && (
|
||||
<div className={styles.commonLabels}>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<strong>Common labels</strong>
|
||||
<Stack gap={1} alignItems="center" wrap="wrap">
|
||||
<Stack gap={0.5} alignItems="center" minWidth="fit-content">
|
||||
<Text variant="bodySmall">Common labels</Text>
|
||||
<Tooltip content="Common labels are the ones attached to all of the alert instances">
|
||||
<Icon name="info-circle" />
|
||||
<Icon name="info-circle" size="sm" />
|
||||
</Tooltip>
|
||||
<AlertLabels labels={fromPairs(commonLabels)} size="sm" />
|
||||
</Stack>
|
||||
</div>
|
||||
<AlertLabels labels={fromPairs(commonLabels)} size="sm" />
|
||||
</Stack>
|
||||
)}
|
||||
{isEmpty(frameSubset) ? (
|
||||
<>
|
||||
@@ -254,10 +254,6 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
color: theme.colors.warning.text,
|
||||
padding: theme.spacing(),
|
||||
}),
|
||||
commonLabels: css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'max-content auto',
|
||||
}),
|
||||
// we need !important here to override the list item default styles
|
||||
highlightedLogRecord: css({
|
||||
background: `${theme.colors.primary.transparent} !important`,
|
||||
|
||||
@@ -61,7 +61,6 @@ export interface Props {
|
||||
permalinkedRowId?: string;
|
||||
scrollIntoView?: (element: HTMLElement) => void;
|
||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
pinnedRowId?: string;
|
||||
pinnedLogs?: string[];
|
||||
/**
|
||||
* If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` for performance reasons.
|
||||
@@ -305,7 +304,7 @@ export const LogRows = memo(
|
||||
onPinLine={props.onPinLine}
|
||||
onUnpinLine={props.onUnpinLine}
|
||||
pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle}
|
||||
pinned={props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)}
|
||||
pinned={pinnedLogs?.some((logId) => logId === row.rowId || logId === row.uid)}
|
||||
isFilterLabelActive={props.isFilterLabelActive}
|
||||
handleTextSelection={handleSelection}
|
||||
enableLogDetails={enableLogDetails}
|
||||
|
||||
@@ -561,7 +561,7 @@ export const LogRowContextModal: React.FunctionComponent<LogRowContextModalProps
|
||||
onClickHideField={hideField}
|
||||
onUnpinLine={() => setSticky(false)}
|
||||
onPinLine={() => setSticky(true)}
|
||||
pinnedRowId={sticky ? row.uid : undefined}
|
||||
pinnedLogs={sticky ? [row.uid] : undefined}
|
||||
overflowingContent={true}
|
||||
scrollElement={null}
|
||||
/>
|
||||
|
||||
@@ -177,6 +177,46 @@ describe('VersionInstallButton', () => {
|
||||
);
|
||||
expect(screen.getByText('Downgrade')).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should show the installation button if invalid semver version is provided', () => {
|
||||
const version: Version = {
|
||||
version: '1.0.a',
|
||||
createdAt: '',
|
||||
isCompatible: false,
|
||||
grafanaDependency: null,
|
||||
};
|
||||
const installedVersion = '1.0.1';
|
||||
renderWithStore(
|
||||
<VersionInstallButton
|
||||
installedVersion={installedVersion}
|
||||
pluginId={'test'}
|
||||
version={version}
|
||||
disabled={false}
|
||||
onConfirmInstallation={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Install')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the installation button if invalid semver installed version is provided', () => {
|
||||
const version: Version = {
|
||||
version: '1.0.0',
|
||||
createdAt: '',
|
||||
isCompatible: false,
|
||||
grafanaDependency: null,
|
||||
};
|
||||
const installedVersion = '1.0.a';
|
||||
renderWithStore(
|
||||
<VersionInstallButton
|
||||
installedVersion={installedVersion}
|
||||
pluginId={'test'}
|
||||
version={version}
|
||||
disabled={false}
|
||||
onConfirmInstallation={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Install')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function renderWithStore(component: JSX.Element) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { gt } from 'semver';
|
||||
import { gt, valid } from 'semver';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
@@ -14,6 +14,12 @@ import { Version } from '../types';
|
||||
const PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME = 'plugins_upgrade_clicked';
|
||||
const PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME = 'plugins_downgrade_clicked';
|
||||
|
||||
enum InstallState {
|
||||
INSTALL = 'Install',
|
||||
UPGRADE = 'Upgrade',
|
||||
DOWNGRADE = 'Downgrade',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
version: Version;
|
||||
@@ -38,7 +44,7 @@ export const VersionInstallButton = ({
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const isDowngrade = installedVersion && gt(installedVersion, version.version);
|
||||
const installState = getInstallState(installedVersion, version.version);
|
||||
|
||||
useEffect(() => {
|
||||
if (installedVersion === version.version) {
|
||||
@@ -61,7 +67,7 @@ export const VersionInstallButton = ({
|
||||
schema_version: '1.0.0',
|
||||
};
|
||||
|
||||
if (!installedVersion || gt(version.version, installedVersion)) {
|
||||
if (installState === InstallState.UPGRADE) {
|
||||
reportInteraction(PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME, trackProps);
|
||||
} else {
|
||||
reportInteraction(PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME, {
|
||||
@@ -76,7 +82,7 @@ export const VersionInstallButton = ({
|
||||
};
|
||||
|
||||
const onInstallClick = () => {
|
||||
if (isDowngrade) {
|
||||
if (installState === InstallState.DOWNGRADE) {
|
||||
setIsModalOpen(true);
|
||||
} else {
|
||||
performInstallation();
|
||||
@@ -91,24 +97,9 @@ export const VersionInstallButton = ({
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
let label = 'Downgrade';
|
||||
let hidden = false;
|
||||
const isPreinstalled = isPreinstalledPlugin(pluginId);
|
||||
|
||||
if (!installedVersion) {
|
||||
label = 'Install';
|
||||
} else if (gt(version.version, installedVersion)) {
|
||||
label = 'Upgrade';
|
||||
if (isPreinstalled.withVersion) {
|
||||
// Hide button if the plugin is preinstalled with a specific version
|
||||
hidden = true;
|
||||
}
|
||||
} else {
|
||||
if (isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate)) {
|
||||
// Hide the downgrade button if the plugin is preinstalled since it will be auto-updated
|
||||
hidden = true;
|
||||
}
|
||||
}
|
||||
const hidden = getButtonHiddenState(installState, isPreinstalled);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -124,7 +115,7 @@ export const VersionInstallButton = ({
|
||||
tooltip={tooltip}
|
||||
tooltipPlacement="bottom-start"
|
||||
>
|
||||
{label} {isInstalling ? <Spinner className={styles.spinner} inline size="sm" /> : getIcon(label)}
|
||||
{installState} {isInstalling ? <Spinner className={styles.spinner} inline size="sm" /> : getIcon(installState)}
|
||||
</Button>
|
||||
<ConfirmModal
|
||||
isOpen={isModalOpen}
|
||||
@@ -140,16 +131,38 @@ export const VersionInstallButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getIcon(label: string) {
|
||||
if (label === 'Downgrade') {
|
||||
function getIcon(installState: InstallState) {
|
||||
if (installState === InstallState.DOWNGRADE) {
|
||||
return <Icon name="arrow-down" />;
|
||||
}
|
||||
if (label === 'Upgrade') {
|
||||
if (installState === InstallState.UPGRADE) {
|
||||
return <Icon name="arrow-up" />;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getInstallState(installedVersion?: string, version?: string): InstallState {
|
||||
if (!installedVersion || !version || !valid(installedVersion) || !valid(version)) {
|
||||
return InstallState.INSTALL;
|
||||
}
|
||||
return gt(installedVersion, version) ? InstallState.DOWNGRADE : InstallState.UPGRADE;
|
||||
}
|
||||
|
||||
function getButtonHiddenState(installState: InstallState, isPreinstalled: { found: boolean; withVersion: boolean }) {
|
||||
// Default state for initial install
|
||||
if (installState === InstallState.INSTALL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle downgrade case
|
||||
if (installState === InstallState.DOWNGRADE) {
|
||||
return isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate);
|
||||
}
|
||||
|
||||
// Handle upgrade case
|
||||
return isPreinstalled.withVersion;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
spinner: css({
|
||||
marginLeft: theme.spacing(1),
|
||||
|
||||
@@ -49,7 +49,7 @@ export const CanvasTooltip = ({ scene }: Props) => {
|
||||
}
|
||||
|
||||
// Retrieve timestamp of the last data point if available
|
||||
const timeField = scene.data?.series[0].fields?.find((field) => field.type === FieldType.time);
|
||||
const timeField = scene.data?.series[0]?.fields?.find((field) => field.type === FieldType.time);
|
||||
const lastTimeValue = timeField?.values[timeField.values.length - 1];
|
||||
const shouldDisplayTimeContentItem =
|
||||
timeField && lastTimeValue && element.data.field && getFieldDisplayName(timeField) !== element.data.field;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { get as lodashGet } from 'lodash';
|
||||
|
||||
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CanvasElementOptions } from 'app/features/canvas/element';
|
||||
import {
|
||||
canvasElementRegistry,
|
||||
@@ -67,8 +66,6 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
|
||||
const current = options?.type ? options.type : DEFAULT_CANVAS_ELEMENT_CONFIG.type;
|
||||
const layerTypes = getElementTypes(opts.scene.shouldShowAdvancedTypes, current).options;
|
||||
|
||||
const actionsEnabled = config.featureToggles.vizActions;
|
||||
|
||||
const isUnsupported =
|
||||
!opts.scene.shouldShowAdvancedTypes && !defaultElementItems.filter((item) => item.id === options?.type).length;
|
||||
|
||||
@@ -123,9 +120,7 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
|
||||
}
|
||||
|
||||
optionBuilder.addDataLinks(builder, ctx);
|
||||
if (actionsEnabled) {
|
||||
optionBuilder.addActions(builder, ctx);
|
||||
}
|
||||
optionBuilder.addActions(builder, ctx);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { capitalize } from 'lodash';
|
||||
|
||||
import { FieldType } from '@grafana/data';
|
||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { ConnectionDirection } from 'app/features/canvas/element';
|
||||
import { SVGElements } from 'app/features/canvas/runtime/element';
|
||||
import { ColorDimensionEditor, ResourceDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors';
|
||||
@@ -213,7 +212,7 @@ export const optionBuilder: OptionSuppliers = {
|
||||
|
||||
addDataLinks: (builder, context) => {
|
||||
builder.addCustomEditor({
|
||||
category: config.featureToggles.vizActions ? ['Data links and actions'] : ['Data links'],
|
||||
category: ['Data links and actions'],
|
||||
id: 'dataLinks',
|
||||
path: 'links',
|
||||
name: 'Links',
|
||||
@@ -230,7 +229,6 @@ export const optionBuilder: OptionSuppliers = {
|
||||
name: 'Actions',
|
||||
editor: ActionsEditor,
|
||||
settings: context.options,
|
||||
showIf: () => config.featureToggles.vizActions,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ActionModel, Field, InterpolateFunction, LinkModel } from '@grafana/data';
|
||||
import { DataFrame } from '@grafana/data/';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getActions } from 'app/features/actions/utils';
|
||||
|
||||
export const getDataLinks = (field: Field, rowIdx: number) => {
|
||||
@@ -32,10 +31,6 @@ export const getFieldActions = (
|
||||
replaceVars: InterpolateFunction,
|
||||
rowIndex: number
|
||||
) => {
|
||||
if (!config.featureToggles?.vizActions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions: Array<ActionModel<Field>> = [];
|
||||
const actionLookup = new Set<string>();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { merge } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { TableCellOptions } from '@grafana/schema';
|
||||
import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui';
|
||||
|
||||
@@ -91,15 +90,9 @@ let cellDisplayModeOptions: Array<SelectableValue<TableCellOptions>> = [
|
||||
{ value: { type: TableCellDisplayMode.DataLinks }, label: 'Data links' },
|
||||
{ value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' },
|
||||
{ value: { type: TableCellDisplayMode.Image }, label: 'Image' },
|
||||
{ value: { type: TableCellDisplayMode.Actions }, label: 'Actions' },
|
||||
];
|
||||
|
||||
if (config.featureToggles.vizActions) {
|
||||
cellDisplayModeOptions = [
|
||||
...cellDisplayModeOptions,
|
||||
{ value: { type: TableCellDisplayMode.Actions }, label: 'Actions' },
|
||||
];
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
fixBottomMargin: css({
|
||||
marginBottom: theme.spacing(-2),
|
||||
|
||||
@@ -152,10 +152,6 @@ const getCellActions = (
|
||||
rowIndex: number,
|
||||
replaceVariables: InterpolateFunction | undefined
|
||||
) => {
|
||||
if (!config.featureToggles?.vizActions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions: Array<ActionModel<Field>> = [];
|
||||
const actionLookup = new Set<string>();
|
||||
|
||||
|
||||
@@ -1703,7 +1703,7 @@
|
||||
"action-query-params": "Query parameters",
|
||||
"action-title": "Title",
|
||||
"action-title-placeholder": "Action title",
|
||||
"one-click-description": "Only one link {{ action }} can have one click enabled at a time"
|
||||
"one-click-description": "Only one link or action can have one click enabled at a time"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -1703,7 +1703,7 @@
|
||||
"action-query-params": "Qūęřy päřämęŧęřş",
|
||||
"action-title": "Ŧįŧľę",
|
||||
"action-title-placeholder": "Åčŧįőʼn ŧįŧľę",
|
||||
"one-click-description": "Øʼnľy őʼnę ľįʼnĸ {{ action }} čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę"
|
||||
"one-click-description": "Øʼnľy őʼnę ľįʼnĸ őř äčŧįőʼn čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
Reference in new issue
Block a user