Merge branch 'main' into grafana-git-ui-sync

This commit is contained in:
Clarity-89 committed 2025-02-27 10:49:36 +02:00
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 -12
View File
@@ -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,
};
});
};
-3
View File
@@ -66,9 +66,6 @@ func (hs *HTTPServer) declareFixedRoles() error {
{
Action: ac.ActionDatasourcesExplore,
},
{
Action: ac.ActionDatasourcesDrilldown,
},
},
},
Grants: []string{string(org.RoleEditor)},
+1 -1
View File
@@ -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)
+1 -2
View File
@@ -386,8 +386,7 @@ const (
ActionSettingsWrite = "settings:write"
// Datasources actions
ActionDatasourcesExplore = "datasources:explore"
ActionDatasourcesDrilldown = "datasources:drilldown"
ActionDatasourcesExplore = "datasources:explore"
// Global Scopes
ScopeGlobalUsersAll = "global.users:*"
+3 -3
View File
@@ -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 {
+2 -1
View File
@@ -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}
}
+2 -2
View File
@@ -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,
}),
-8
View File
@@ -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",
-1
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
17 autoMigrateStatPanel preview @grafana/dataviz-squad false false true
18 disableAngular preview @grafana/dataviz-squad false false true
19 canvasPanelNesting experimental @grafana/dataviz-squad false false true
vizActions experimental @grafana/dataviz-squad false false true
20 disableSecretsCompatibility experimental @grafana/hosted-grafana-team false true false
21 logRequestsInstrumentedAsUnknown experimental @grafana/hosted-grafana-team false false false
22 grpcServer preview @grafana/search-and-storage false false false
-4
View File
@@ -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"
+2 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+12 -21
View File
@@ -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
+3 -4
View File
@@ -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{
+19 -10
View File
@@ -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)
}
+171
View File
@@ -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,
};
+1 -5
View File
@@ -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} />
@@ -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>();
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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": {