Merge branch 'main' into drew08t/canvas-add-zoom

This commit is contained in:
drew08t
2023-11-02 20:59:12 -07:00
215 changed files with 7582 additions and 3982 deletions
+72 -39
View File
@@ -256,6 +256,17 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-data/src/transformations/transformers/reduce.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@@ -796,40 +807,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/components/Graph/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/components/GraphNG/GraphNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"]
],
"packages/grafana-ui/src/components/GraphNG/hooks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-ui/src/components/InfoBox/InfoBox.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@@ -981,11 +958,6 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/TimeSeries/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
@@ -1032,6 +1004,45 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/graveyard/Graph/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"]
],
"packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-ui/src/graveyard/TimeSeries/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"packages/grafana-ui/src/options/builder/axis.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -1177,6 +1188,23 @@ exports[`better eslint`] = {
"public/app/core/components/ForgottenPassword/ForgottenPassword.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/core/components/GraphNG/GraphNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"]
],
"public/app/core/components/GraphNG/hooks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/core/components/Layers/LayerDragDropList.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
@@ -1411,6 +1439,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/core/components/TimeSeries/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/core/components/TraceToLogs/TagMappingInput.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
+5 -3
View File
@@ -312,9 +312,6 @@
/packages/grafana-ui/src/components/Table/ @grafana/grafana-bi-squad
/packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/GraphNG/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/Graph/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/TimeSeries/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/uPlot/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/ValuePicker/ @grafana/dataviz-squad
@@ -322,6 +319,9 @@
/packages/grafana-ui/src/components/VizLegend/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad
/packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad
/packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad
/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad
/packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend
/packages/grafana-data/src/transformations/ @grafana/grafana-bi-squad
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
@@ -366,6 +366,8 @@ cypress.config.js @grafana/grafana-frontend-platform
/public/app/core/components/TimePicker/ @grafana/grafana-frontend-platform
/public/app/core/components/Layers/ @grafana/dataviz-squad
/public/app/core/components/TraceToLogs @grafana/observability-traces-and-profiling
/public/app/core/components/GraphNG/ @grafana/dataviz-squad
/public/app/core/components/TimeSeries/ @grafana/dataviz-squad
/public/app/features/all.ts @grafana/grafana-frontend-platform
/public/app/features/admin/ @grafana/grafana-authnz-team
/public/app/features/auth-config/ @grafana/grafana-authnz-team
@@ -48,7 +48,7 @@ To follow these instructions, you need at least one of the following:
To create an API, complete the following steps:
1. Sign in to Grafana.
1. Click **Administration** in the left-side menu and select **API Keys**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**.
1. Click **Add API key**.
1. Enter a unique name for the key.
1. In the **Role** field, select one of the following access levels you want to assign to the key.
@@ -101,7 +101,7 @@ For more information about permissions, refer to [Roles and permissions]({{< rel
To migrate all API keys to service accounts, complete the following steps:
1. Sign in to Grafana, point to **Configuration** (the gear icon), and click **API Keys**.
1. Sign in to Grafana, point to **Administration**, **Users and access**, and click **API Keys**.
1. In the top of the page, find the section which says **Switch from API keys to service accounts**
1. Click **Migrate to service accounts now**.
1. A confirmation window will appear, asking to confirm the migration. Click **Yes, migrate now** if you are willing to continue.
@@ -110,7 +110,7 @@ To migrate all API keys to service accounts, complete the following steps:
To migrate a single API key to a service account, complete the following steps:
1. Sign in to Grafana.
1. Click **Administration** in the left-side menu and select **API Keys**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**.
1. Find the API Key you want to migrate.
1. Click **Migrate to service account**.
@@ -52,7 +52,7 @@ There is more than one way to add the license to a Grafana instance:
This is the preferred option for single instance installations of Grafana Enterprise.
1. Sign in as a Grafana server administrator.
1. Click **Administration > Stats and license** in the side navigation menu.
1. Click **Administration > General > Stats and license** in the side navigation menu.
1. Click **Upload a new token**.
1. Select your license file, and upload it.
@@ -207,6 +207,8 @@ To determine the number of active users:
1. Click **Administration** in the side navigation menu.
1. Click **General**.
1. Click **Stats and license**.
1. Review the utilization count on the **Utilization** panel.
@@ -112,6 +112,6 @@ In this task you configure Grafana Enterprise to validate the license with AWS i
### Task 4: Start or restart Grafana
1. To restart Grafana and activate your license, update the service running Grafana to use the latest revision of the task definition that you created.
1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > Stats and license** to validate that your license is active.
1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > General > Stats and license** to validate that your license is active.
For more information about validating that your license is active, refer to [Grafana Enterprise license restrictions]({{< relref "../../#grafana-enterprise-license-restrictions" >}}).
@@ -123,7 +123,7 @@ To restart Grafana on a Kubernetes cluster,
1. Run the command `kubectl rollout restart deployment my-release`.
1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > Stats and license** to validate that your license is active.
1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to **Administration > General > Stats and license** to validate that your license is active.
For more information about restarting Grafana, refer to [Restart Grafana]({{< relref "../../../../setup-grafana/start-restart-grafana/" >}}).
@@ -36,7 +36,7 @@ You can use AWS Marketplace to make the following modifications to your Grafana
1. Sign in to Grafana as a Server Administrator.
1. Click **Administration** in the side navigation menu, and then **Stats and license**.
1. Click **Administration** in the side navigation menu, **General**, and then **Stats and license**.
1. In the **Token** section under **Enterprise License**, click **Renew token**.
@@ -59,7 +59,7 @@ Complete this task when you want to view a list of existing organizations.
**To view a list of organizations:**
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Organizations**.
1. Click **Administration** in the left-side menu, **General**, and then **Organizations**.
## Create an organization
@@ -72,7 +72,7 @@ Create an organization when you want to isolate dashboards and other resources f
**To create an organization:**
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Organizations**.
1. Click **Administration** in the left-side menu, **General**, and then **Organizations**.
1. Click **+ New org**.
1. Enter the name of the new organization and click **Create**.
@@ -100,7 +100,7 @@ Deleting the organization also deletes all teams and dashboards associated the o
**To delete an organization:**
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Organizations**.
1. Click **Administration** in the left-side menu, **General**, and then **Organizations**.
1. Click the red **X** next to the organization that you want to delete.
1. Click **Delete**.
@@ -115,6 +115,6 @@ Edit an organization when you want to change its name.
**To edit an organization:**
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Organizations**.
1. Click **Administration** in the left-side menu, **General**, and then **Organizations**.
1. Click the organization you want to edit.
1. Update the organization name and click **Update**.
@@ -44,6 +44,7 @@ Grafana server administrators and organization administrators can change organiz
Follow these instructions if you are a Grafana Server Admin.
1. Click **Administration** in the left-side menu.
1. Click **General**.
1. Click **Organizations**.
1. In the organization list, click the name of the organization that you want to change.
1. In **Name**, enter the new organization name.
@@ -54,6 +55,7 @@ Follow these instructions if you are a Grafana Server Admin.
If you are an Organization Admin, follow these steps:
1. Click **Administration** in the left-side menu.
1. Click **General**.
1. Click **Default preferences**.
1. In **Organization name**, enter the new name.
1. Click **Update organization name**.
@@ -63,7 +65,7 @@ If you are an Organization Admin, follow these steps:
Organization administrators and team administrators can change team names and email addresses.
To change the team name or email, follow these steps:
1. Click **Administration** in the left-side menu and select **Team**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Team**.
1. In the team list, click the name of the team that you want to change.
1. Click the **Settings** tab.
1. In the Team details section, you can edit the following:
@@ -112,6 +114,7 @@ To see what the current settings are, refer to [View server settings]({{< relref
Organization administrators can change the UI theme for all users in an organization.
1. Click **Administration** in the left-side menu.
1. Click **General**.
1. Click **Default preferences**.
1. In the Preferences section, select the UI theme.
1. Click **Save**.
@@ -120,7 +123,7 @@ Organization administrators can change the UI theme for all users in an organiza
Organization and team administrators can change the UI theme for all users on a team.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click the team for which you want to change the UI theme.
1. Click the **Settings** tab.
1. In the Preferences section, select the UI theme.
@@ -149,6 +152,7 @@ Grafana server administrators can choose a default timezone for all users on the
Organization administrators can choose a default timezone for their organization.
1. Click **Administration** in the left-side menu.
1. Click **General**.
1. Click **Default preferences**.
1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level.
1. Click **Save**.
@@ -157,7 +161,7 @@ Organization administrators can choose a default timezone for their organization
Organization administrators and team administrators can choose a default timezone for all users on a team.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click the team for which you want to change the timezone.
1. Click the **Settings** tab.
1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level.
@@ -209,6 +213,7 @@ Organization administrators can choose a default home dashboard for their organi
1. Navigate to the dashboard you want to set as the home dashboard.
1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already.
1. Click **Administration** in the left-side menu.
1. Click **General**.
1. Click **Default preferences**.
1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards.
1. Click **Save**.
@@ -219,7 +224,7 @@ Organization administrators and Team Admins can set a default home dashboard for
1. Navigate to the dashboard you want to set as the home dashboard.
1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click the team for which you want to change the home dashboard.
1. Click the **Settings** tab.
1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards.
@@ -246,6 +251,7 @@ Grafana server administrators can change the default Grafana UI language for all
Organization administrators can change the language for all users in an organization.
1. Click **Administration** in the left-side menu.
1. Click **General**.
1. Click **Default preferences**.
1. In the Preferences section, select an option in the **Language** dropdown.
1. Click **Save**.
@@ -254,7 +260,7 @@ Organization administrators can change the language for all users in an organiza
Organization and team administrators can set a default language for all users on a team.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click the team for which you want to change the language.
1. Click the **Settings** tab.
1. In the Preferences section, select an option in the **Language** dropdown.
@@ -85,13 +85,13 @@ Before following the steps below, make sure you are logged in as a Grafana admin
<a id="#plugin-catalog-entry"></a>
Administrators can find the Plugin catalog at **Administration > Plugins**.
Administrators can find the Plugin catalog at **Administration > Plugins and data > Plugins**.
### Browse plugins
To browse for available plugins:
1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins.
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins.
1. Click the **All** filter to browse all available plugins.
1. Click the **Data sources**, **Panels**, or **Applications** buttons to filter by plugin type.
@@ -99,7 +99,7 @@ To browse for available plugins:
To install a plugin:
1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins.
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins.
1. Click the **All** filter to browse all available plugins.
1. Browse and find a plugin.
1. Click on the plugin logo.
@@ -111,7 +111,7 @@ When the update is complete, you see a confirmation message that the installatio
To update a plugin:
1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins.
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins.
1. Click on the plugin logo.
1. Click **Update**.
@@ -121,7 +121,7 @@ When the update is complete, you see a confirmation message that the update was
To uninstall a plugin:
1. In Grafana, click **Administration > Plugins** in the side navigation menu to view installed plugins.
1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view installed plugins.
1. Click on the plugin logo.
1. Click **Uninstall**.
@@ -157,6 +157,8 @@ datasources:
password:
# <string> Sets the basic authorization password.
basicAuthPassword:
# <int> Sets the version. Used to compare versions when
# updating. Ignored when creating a new data source.
version: 1
# <bool> Allows users to edit data sources from the
# Grafana UI.
@@ -54,14 +54,14 @@ In both cases, the assignment applies only to the user, team or service account
For more information about switching organizations, refer to [Switch organizations]({{< relref "../../../user-management/user-preferences/_index.md#switch-organizations" >}}).
3. In the left-side menu, click **Administration** and then **Users**, **Teams**, or **Service accounts**.
3. In the left-side menu, click **Administration**, **Users and access**, and then **Users**, **Teams**, or **Service accounts**.
4. In the **Role** column, select the fixed role that you want to assign to the user, team, or service account.
5. Click **Update**.
**To assign a fixed role as a server administrator:**
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
1. In the Organizations section, click **Change role**.
1. Select a role within an organization that you want to assign to the user.
@@ -34,7 +34,7 @@ If you are a Grafana server administrator, use the Settings tab to view the sett
### View server settings
1. Log in to your Grafana server with an account that has the Grafana Admin flag set.
1. Click **Administration** in the left-side menu, and then **Settings**.
1. Click **Administration** in the left-side menu, **General**, and then **Settings**.
### Available settings
@@ -51,7 +51,7 @@ If you are a Grafana server admin, then you can view useful statistics about you
### View server stats
1. Log in to your Grafana server with an account that has the Grafana Admin flag set.
1. Click **Administration** in the left-side menu, and then **Stats and license**.
1. Click **Administration** in the left-side menu, **General**, and then **Stats and license**.
### Available stats
@@ -45,7 +45,7 @@ A user can belong to multiple teams.
To create a team:
1. Sign in to Grafana as an organization administrator or team administrator.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click **New Team**.
1. Complete the fields and click **Create**.
1. Click **Add member**.
@@ -59,7 +59,7 @@ Add a team member to an existing team whenever you want to provide access to tea
To add a team member:
1. Sign in to Grafana as an organization administrator.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click the name of the team to which you want to add members, and click **Add member**.
1. Locate and select a user.
1. Choose if you want to add the user as a team Member or an Admin.
@@ -72,7 +72,7 @@ Complete this task when you want to add or modify team member permissions.
To grant team member permissions:
1. Sign in to Grafana as an organization administrator or a team administrator.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click the name of the team for which you want to add or modify team member permissions.
1. In the team member list, find and click the user that you want to change. You can use the search field to filter the list if necessary.
1. In the Permission column, select the new user permission level.
@@ -84,7 +84,7 @@ You can remove a team member when you no longer want to apply team permissions t
To remove a team member:
1. Sign in to Grafana as an organization administrator or team administrator.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click a team from which you want to remove a user.
1. Click the **X** next to the name of the user.
@@ -95,7 +95,7 @@ Delete a team when you no longer need it. This action permanently deletes the te
To delete a team:
1. Sign in to Grafana as an organization administrator.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
1. Click the **X** next to the name of the team.
1. Click **Delete**.
@@ -106,7 +106,7 @@ See the complete list of teams in your Grafana organization.
To view a list of teams:
1. Sign in to Grafana as an organization administrator or a team administrator.
1. Click **Administration** in the left-side menu and select **Teams**.
1. Click **Administration** in the left-side menu, **Users and access**, and select **Teams**.
The role you use to sign in to Grafana determines how you see team lists.
@@ -38,7 +38,7 @@ You can see a list of users with accounts in your Grafana organization. If neces
**To view a list of organization users**:
1. Sign in to Grafana as an organization administrator.
1. Navigate to **Administration > Users**.
1. Navigate to **Administration > Users and access > Users**.
{{% admonition type="note" %}}
If you have [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can also [view a global list of users]({{< relref "../server-user-management#view-a-list-of-users" >}}) in the Server Admin section of Grafana.
@@ -59,7 +59,7 @@ Organization roles sync from the authentication provider on user sign-in. To pre
**To change the organization role of a user**:
1. Sign in to Grafana as an organization administrator.
1. Navigate to **Administration > Users**.
1. Navigate to **Administration > Users and access > Users**.
1. Find the user account for which you want to change the role.
If necessary, use the search field to filter the list.
@@ -96,7 +96,7 @@ If you have [server administrator]({{< relref "../../roles-and-permissions/#graf
> **Note**: It might be that you are currently in the proper organization and don't need to switch organizations.
1. Navigate to **Administration > Users**.
1. Navigate to **Administration > Users and access > Users**.
1. Click **Organization users**.
1. Click **Invite**.
1. Enter the following information:
@@ -127,7 +127,7 @@ The **Pending Invites** button is only visible if there are unanswered invitatio
**To manage a pending invitation**:
1. Sign in to Grafana as an organization administrator.
1. Navigate to **Administration > Users**.
1. Navigate to **Administration > Users and access > Users**.
1. Click **Pending Invites**.
The **Pending Invites** button appears only when there are unaccepted invitations.
@@ -149,7 +149,7 @@ This action does not remove the user account from the Grafana server.
**To remove a user from an organization**:
1. Sign in to Grafana as an organization administrator.
1. Navigate to **Administration > Users**.
1. Navigate to **Administration > Users and access > Users**.
1. Find the user account that you want to remove from the organization.
Use the search field to filter the list, if necessary.
@@ -39,7 +39,7 @@ You can see a list of users with accounts on your Grafana server. This action mi
**To view a list of users**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
{{% admonition type="note" %}}
If you have [organization administrator]({{< relref "../../roles-and-permissions/#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can still [view of list of users in a given organization]({{< relref "../manage-org-users/#view-a-list-of-organization-users" >}}).
@@ -56,7 +56,7 @@ View user details when you want to see login, and organizations and permissions
**To view user details**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
A user account contains the following sections.
@@ -88,7 +88,7 @@ Edit a user account when you want to modify user login credentials, or delete, d
**To edit a user account**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
1. Complete any of the following actions, as necessary.
@@ -115,7 +115,7 @@ When you configure advanced authentication using Oauth, SAML, LDAP, or the Auth
**To add a user**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click **New user**.
1. Complete the fields and click **Create user**.
@@ -136,7 +136,7 @@ The force logout action can apply to one device that is logged in to Grafana, or
- Ensure you have Grafana server administrator privileges
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
1. Scroll down to the Sessions section.
1. Perform one of the following actions:
@@ -31,7 +31,7 @@ You are required to specify an Admin role for each organization. The first user
**To add a user to an organization**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
1. In the Organizations section, click **Add user to organization**.
1. Select an organization and a role.
@@ -57,7 +57,7 @@ Remove a user from an organization when they no longer require access to the das
**To remove a user from an organization**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
1. In the Organization section, click **Remove from organization** next to the organization from which you want to remove the user.
1. Click **Confirm removal**.
@@ -27,7 +27,7 @@ Server administrators are "super-admins" with full permissions to create, read,
**To assign or remove Grafana administrator privileges**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
1. In the Permissions section, next to Grafana Admin, click **Change**.
1. Click **Yes** or **No**, depending on whether or not you want this user to have the Grafana server administrator role.
@@ -21,7 +21,7 @@ Update organization permissions when you want to enhance or restrict a user's ac
**To change a user's organization permissions**:
1. Sign in to Grafana as a server administrator.
1. Click **Administration** in the left-side menu, and then **Users**.
1. Click **Administration** in the left-side menu, **Users and access**, and then **Users**.
1. Click a user.
1. In the Organizations section, click **Change role** for the role you want to change
1. Select another role.
@@ -202,7 +202,7 @@ Create alerts from any panel type. This means you can reuse the queries in the p
1. Navigate to a dashboard in the **Dashboards** section.
2. In the top right corner of the panel, click on the three dots (ellipses).
3. From the dropdown menu, select **More...** and then choose **Create alert**.
3. From the dropdown menu, select **More...** and then choose **New alert rule**.
This will open the alert rule form, allowing you to configure and create your alert based on the current panel's query.
@@ -59,7 +59,6 @@ Some features are enabled by default. You can disable these feature by setting t
| Feature toggle name | Description |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trimDefaults` | Use cue schema to remove values that will be applied automatically |
| `panelTitleSearch` | Search for dashboards using panel title |
| `migrationLocking` | Lock database during migrations |
| `correlations` | Correlations page |
@@ -162,6 +161,8 @@ Experimental features might be changed or removed without prior notice.
| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. |
| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. |
| `annotationPermissionUpdate` | Separate annotation permissions from dashboard permissions to allow for more granular control. |
| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe |
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles |
## Development feature toggles
@@ -44,7 +44,7 @@ In these steps you'll create an alert and define an expression to evaluate. Thes
### Create a Grafana-managed alert
1. Navigate in Grafana to **Alerting**, then to **Alert Rules** and click **+ Create alert rule**.
1. Navigate in Grafana to **Alerting**, then to **Alert Rules** and click **+ New alert rule**.
1. Choose **Grafana Managed Alert** to create an alert that uses expressions.
1. Select your Loki datasource from the drop-down.
1. Enter the alert query in the query editor, switch to **code** mode in the top right corner of the editor to paste the query below:
@@ -328,7 +328,7 @@ We have now created a dummy webhook endpoint and created a new Alerting Contact
Now that Grafana knows how to notify us, it's time to set up an alert rule:
1. In the sidebar, click **Alert rules**.
1. Click **Create alert rule**.
1. Click **New alert rule**.
1. In section **1**, name the rule `fundamentals-test`.
1. For section **2**, select **Grafana Managed Alert** as the rule type. Next, find query box **A**. Choose your Prometheus datasource and enter the same query that we used in our earlier panel: `sum(rate(tns_request_duration_seconds_count[5m])) by(route)`. Click **Run queries**. You should see some data in the graph.
1. Now scroll down to query box **B**. Change the operation from **Reduce** to **Classic condition**. [You can read more about classic and multi-dimensional conditions here](/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule). For conditions enter the following: `WHEN last() OF A IS ABOVE 0.2`. Delete query **C**.
@@ -25,10 +25,7 @@ Grafana 2.0 shipped with a great feature that enables it to render any graph or
No matter what data source you are using, the PNG image of the Graph will look the same as it does in your browser.
This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will take you to the graph.
> _Amazon S3 Required_: The hubot-grafana script will upload the rendered graphs to Amazon S3. This
> is so Hipchat and Slack can show them reliably (they require the image to be publicly available).
This guide shows you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell Hubot to render any dashboard or graph right from a channel in Slack, Basecamp, or any other supported Hubot adapter. The bot will respond with an image of the graph and a link that will take you to the graph.
{{< figure src="/static/img/docs/tutorials/hubot_grafana.png" max-width="800px" >}}
@@ -40,7 +37,7 @@ This guide will show you how to install and configure the [Hubot-Grafana](https:
Hubot is very easy to install and host. If you do not already have a bot up and running please read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide.
## Install Hubot-Grafana script
## Install the Hubot-Grafana script
In your Hubot project repo install the Grafana plugin using `npm`:
@@ -56,18 +53,15 @@ Edit the file external-scripts.json, and add hubot-grafana to the list of plugin
## Configure
The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly.
The Hubot-Grafana plugin requires two environment variables to be set in order to work properly.
```bash
export HUBOT_GRAFANA_HOST=https://play.grafana.org
export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234
export HUBOT_GRAFANA_S3_BUCKET=mybucket
export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ
export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234
export HUBOT_GRAFANA_S3_PREFIX=graphs
export HUBOT_GRAFANA_S3_REGION=us-standard
```
There are [additional environment variables](https://github.com/stephenyeargin/hubot-grafana?tab=readme-ov-file#general-settings) that you can set to control the appearance of the graphs.
### Grafana server side rendering
The hubot plugin will take advantage of the Grafana server side rendering feature that can render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only).
@@ -80,9 +74,9 @@ To verify that this feature works try the `Direct link to rendered image` link i
You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown.
### Amazon S3
### Image uploading
The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered panel to `S3` and it will use that URL when it posts to Slack or Hipchat.
There are several approaches to uploading the rendered graphs. If you are using Slack, Rocket.Chat, or Telegram, the adapter's native uploader will take care of sending it through their respective API. If your Hubot is hosted on a platform that doesn't support uploads (such as IRC), you can use the [built-in S3 uploader](https://github.com/stephenyeargin/hubot-grafana/wiki/Amazon-S3-Image-Hosting). Note if you configure S3, it will not use the adapter's upload features.
## Hubot commands
@@ -229,6 +229,17 @@ The Grafana Assume Role authentication provider lets Grafana Cloud users of the
To learn more, refer to the [CloudWatch authentication documentation](/docs/grafana/next/datasources/aws-cloudwatch/aws-authentication).
## Create alerts from panels
<!-- Brenda Muir -->
<!-- OSS, Enterprise from 10.3-->
October 23, 2023
_Generally available in Grafana Cloud_
Create alerts from dashboard panels. You can reuse the panel queries and create alerts based on them.
## No basic role
<!-- Eric Leijonmarck -->
+1 -1
View File
@@ -65,7 +65,7 @@ require (
github.com/grafana/cuetsy v0.1.10 // @grafana/grafana-as-code
github.com/grafana/grafana-aws-sdk v0.19.1 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go v1.9.0 // @grafana/backend-platform
github.com/grafana/grafana-plugin-sdk-go v0.187.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.189.0 // @grafana/plugins-platform-backend
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform
github.com/hashicorp/go-hclog v1.5.0 // @grafana/plugins-platform-backend
github.com/hashicorp/go-plugin v1.4.9 // @grafana/plugins-platform-backend
+2
View File
@@ -1841,6 +1841,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW3
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/grafana-plugin-sdk-go v0.187.0 h1:lOwoFbbTs27KqR3F32GvOX9Et3Ek8p8qsFw+SUJtAAM=
github.com/grafana/grafana-plugin-sdk-go v0.187.0/go.mod h1:PHK8eQOz3ES28RmImdTHNOTxBZaH6mb/ytJGxk7VVJc=
github.com/grafana/grafana-plugin-sdk-go v0.189.0 h1:30n0dtehLT0Z0DdRfatk1INwXOKfCb9LKaSFl8zBj9g=
github.com/grafana/grafana-plugin-sdk-go v0.189.0/go.mod h1:nctofuR6fyhx3Stbnh5ha6setroeqqBgO0Rj9s4t86o=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758 h1:ATUhvJSJwzdzhnmzUI92fxVFqyqmcnzJ47wtHTK3LW4=
+1 -1
View File
@@ -252,7 +252,7 @@
"@grafana/flamegraph": "workspace:*",
"@grafana/google-sdk": "0.1.1",
"@grafana/lezer-logql": "0.2.1",
"@grafana/lezer-traceql": "0.0.8",
"@grafana/lezer-traceql": "0.0.9",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^1.20.1",
@@ -159,7 +159,7 @@ export function calculateFieldDisplayName(field: Field, frame?: DataFrame, allFr
return displayName;
}
function getUniqueFieldName(field: Field, frame?: DataFrame) {
export function getUniqueFieldName(field: Field, frame?: DataFrame) {
let dupeCount = 0;
let foundSelf = false;
+1 -1
View File
@@ -14,5 +14,5 @@ export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export { sortThresholds, getActiveThreshold } from './thresholds';
export { applyFieldOverrides, validateFieldConfig, applyRawFieldOverrides, useFieldOverrides } from './fieldOverrides';
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
export { getFieldDisplayName, getFrameDisplayName, cacheFieldDisplayNames } from './fieldState';
export { getFieldDisplayName, getFrameDisplayName, cacheFieldDisplayNames, getUniqueFieldName } from './fieldState';
export { getScaleCalculator, getFieldConfigWithMinMax, getMinMaxAndDelta } from './scale';
@@ -20,3 +20,6 @@ export type { RenameByRegexTransformerOptions } from './transformers/renameByReg
export { joinDataFrames as outerJoinDataFrames, isLikelyAscendingVector } from './transformers/joinDataFrames';
export * from './transformers/histogram';
export { ensureTimeField } from './transformers/convertFieldType';
// Required for Sparklines util to work in @grafana/data, but ideally kept internal
export { applyNullInsertThreshold } from './transformers/nulls/nullInsertThreshold';
@@ -0,0 +1,186 @@
import { DataFrame, FieldType } from '../../../types';
type InsertMode = (prev: number, next: number, threshold: number) => number;
const INSERT_MODES = {
threshold: (prev: number, next: number, threshold: number) => prev + threshold,
midpoint: (prev: number, next: number, threshold: number) => (prev + next) / 2,
// previous time + 1ms to prevent StateTimeline from forward-interpolating prior state
plusone: (prev: number, next: number, threshold: number) => prev + 1,
};
interface NullInsertOptions {
frame: DataFrame;
refFieldName?: string | null;
refFieldPseudoMax?: number;
refFieldPseudoMin?: number;
insertMode?: InsertMode;
}
function getRefField(frame: DataFrame, refFieldName?: string | null) {
return frame.fields.find((field) => {
// note: getFieldDisplayName() would require full DF[]
return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time;
});
}
/** @internal */
export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame {
if (opts.frame.length === 0) {
return opts.frame;
}
let thorough = true;
let { frame, refFieldName, refFieldPseudoMax, refFieldPseudoMin, insertMode } = opts;
if (!insertMode) {
insertMode = INSERT_MODES.threshold;
}
const refField = getRefField(frame, refFieldName);
if (refField == null) {
return frame;
}
refField.state = {
...refField.state,
nullThresholdApplied: true,
};
const thresholds = frame.fields.map((field) => field.config.custom?.insertNulls || refField.config.interval || null);
const uniqueThresholds = new Set<number>(thresholds);
uniqueThresholds.delete(null as any);
if (uniqueThresholds.size === 0) {
return frame;
}
if (uniqueThresholds.size === 1) {
const threshold = uniqueThresholds.values().next().value;
if (threshold <= 0) {
return frame;
}
const refValues = refField.values;
const frameValues = frame.fields.map((field) => field.values);
const filledFieldValues = nullInsertThreshold(
refValues,
frameValues,
threshold,
refFieldPseudoMin,
refFieldPseudoMax,
insertMode,
thorough
);
if (filledFieldValues === frameValues) {
return frame;
}
return {
...frame,
length: filledFieldValues[0].length,
fields: frame.fields.map((field, i) => ({
...field,
values: filledFieldValues[i],
})),
};
}
// TODO: unique threshold-per-field (via overrides) is unimplemented
// should be done by processing each (refField + thresholdA-field1 + thresholdA-field2...)
// as a separate nullInsertThreshold() dataset, then re-join into single dataset via join()
return frame;
}
function nullInsertThreshold(
refValues: number[],
frameValues: any[][],
threshold: number,
refFieldPseudoMin: number | null = null,
// will insert a trailing null when refFieldPseudoMax > last datapoint + threshold
refFieldPseudoMax: number | null = null,
getInsertValue: InsertMode,
// will insert the value at every missing interval
thorough: boolean
) {
const len = refValues.length;
const refValuesNew: number[] = [];
// Continuously subtract the threshold from the first data point, filling in insert values accordingly
if (refFieldPseudoMin != null && refFieldPseudoMin < refValues[0]) {
let preFillCount = Math.ceil((refValues[0] - refFieldPseudoMin) / threshold);
// this will be 0 or 1 threshold increment left of visible range
let prevSlot = refValues[0] - preFillCount * threshold;
while (prevSlot < refValues[0]) {
// (prevSlot - threshold) is used to simulate the previous 'real' data point, as getInsertValue expects
refValuesNew.push(getInsertValue(prevSlot - threshold, prevSlot, threshold));
prevSlot += threshold;
}
}
// Insert initial value
refValuesNew.push(refValues[0]);
let prevValue: number = refValues[0];
// Fill nulls when a value is greater than the threshold value
for (let i = 1; i < len; i++) {
const curValue = refValues[i];
while (curValue - prevValue > threshold) {
refValuesNew.push(getInsertValue(prevValue, curValue, threshold));
prevValue += threshold;
if (!thorough) {
break;
}
}
refValuesNew.push(curValue);
prevValue = curValue;
}
// At the end of the sequence
if (refFieldPseudoMax != null && refFieldPseudoMax > prevValue) {
while (prevValue + threshold < refFieldPseudoMax) {
refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold));
prevValue += threshold;
}
}
const filledLen = refValuesNew.length;
if (filledLen === len) {
return frameValues;
}
const filledFieldValues: any[][] = [];
for (let fieldValues of frameValues) {
let filledValues;
if (fieldValues !== refValues) {
filledValues = Array(filledLen);
for (let i = 0, j = 0; i < filledLen; i++) {
filledValues[i] = refValues[j] === refValuesNew[i] ? fieldValues[j++] : null;
}
} else {
filledValues = refValuesNew;
}
filledFieldValues.push(filledValues);
}
return filledFieldValues;
}
@@ -0,0 +1,27 @@
import { DataFrame } from '../../../types';
export function nullToValue(frame: DataFrame) {
return {
...frame,
fields: frame.fields.map((field) => {
const noValue = +field.config?.noValue!;
if (!Number.isNaN(noValue)) {
const transformedVals = field.values.slice();
for (let i = 0; i < transformedVals.length; i++) {
if (transformedVals[i] === null) {
transformedVals[i] = noValue;
}
}
return {
...field,
values: transformedVals,
};
} else {
return field;
}
}),
};
}
@@ -33,10 +33,13 @@ export interface ExplorePanelsState extends Partial<Record<PreferredVisualisatio
/**
* Keep a list of vars the correlations editor / helper in explore will use
*
* vars can be modified by transformation variables, origVars is so we can rebuild the original list
*/
/** @internal */
export interface ExploreCorrelationHelperData {
resultField: string;
origVars: Record<string, string>;
vars: Record<string, string>;
}
@@ -18,7 +18,6 @@
* @public
*/
export interface FeatureToggles {
trimDefaults?: boolean;
disableEnvelopeEncryption?: boolean;
['live-service-web-worker']?: boolean;
queryOverLive?: boolean;
@@ -156,4 +155,6 @@ export interface FeatureToggles {
alertmanagerRemotePrimary?: boolean;
alertmanagerRemoteOnly?: boolean;
annotationPermissionUpdate?: boolean;
extractFieldsNameDeduplication?: boolean;
dashboardSceneForViewers?: boolean;
}
@@ -72,7 +72,7 @@ describe('valueFormats', () => {
${'dtdurationms'} | ${undefined} | ${100000} | ${'1 minute'}
${'dtdurationms'} | ${undefined} | ${150000} | ${'2 minutes'}
`(
'With format=$format decimals=$decimals and value=$value then result shoudl be = $expected',
'With format=$format decimals=$decimals and value=$value then result should be = $expected',
async ({ format, value, decimals, expected }) => {
const result = getValueFormat(format)(value, decimals, undefined, undefined);
const full = formattedValueToString(result);
@@ -1,11 +1,9 @@
import { css, cx } from '@emotion/css';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { Icon } from '../Icon/Icon';
export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => {
const [showScrollTopIndicator, setShowTopScrollIndicator] = useState(false);
@@ -39,9 +37,7 @@ export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => {
className={cx(styles.scrollIndicator, styles.scrollTopIndicator, {
[styles.scrollIndicatorVisible]: showScrollTopIndicator,
})}
>
<Icon className={classNames(styles.scrollIcon, styles.scrollTopIcon)} name="angle-up" />
</div>
/>
<div className={styles.scrollContent}>
<div ref={scrollTopMarker} />
{children}
@@ -51,9 +47,7 @@ export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => {
className={cx(styles.scrollIndicator, styles.scrollBottomIndicator, {
[styles.scrollIndicatorVisible]: showScrollBottomIndicator,
})}
>
<Icon className={classNames(styles.scrollIcon, styles.scrollBottomIcon)} name="angle-down" />
</div>
/>
</>
);
};
@@ -85,16 +79,5 @@ const getStyles = (theme: GrafanaTheme2) => {
scrollIndicatorVisible: css({
opacity: 1,
}),
scrollIcon: css({
left: '50%',
position: 'absolute',
transform: 'translateX(-50%)',
}),
scrollTopIcon: css({
top: 0,
}),
scrollBottomIcon: css({
bottom: 0,
}),
};
};
@@ -5,11 +5,10 @@ import {
FieldType,
isLikelyAscendingVector,
sortDataFrame,
applyNullInsertThreshold,
} from '@grafana/data';
import { GraphFieldConfig } from '@grafana/schema';
import { applyNullInsertThreshold } from '../GraphNG/nullInsertThreshold';
/** @internal
* Given a sparkline config returns a DataFrame ready to be turned into Plot data set
**/
@@ -1,12 +1,11 @@
import { css } from '@emotion/css';
import React, { createElement, CSSProperties, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import ReactDomServer from 'react-dom/server';
import React, { createElement, CSSProperties } from 'react';
import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { Tooltip } from '../Tooltip/Tooltip';
import { TruncatedText } from './TruncatedText';
import { customWeight, customColor, customVariant } from './utils';
export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
@@ -30,70 +29,35 @@ export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'clas
export const Text = React.forwardRef<HTMLElement, TextProps>(
({ element = 'span', variant, weight, color, truncate, italic, textAlignment, children, ...restProps }, ref) => {
const styles = useStyles2(getTextStyles, element, variant, color, weight, truncate, italic, textAlignment);
const [isOverflowing, setIsOverflowing] = useState(false);
const internalRef = useRef<HTMLElement>(null);
// wire up the forwarded ref to the internal ref
useImperativeHandle<HTMLElement | null, HTMLElement | null>(ref, () => internalRef.current);
const childElement = createElement(
element,
{
...restProps,
style: undefined, // remove style prop to avoid overriding the styles
className: styles,
// when overflowing, the internalRef is passed to the tooltip which forwards it on to the child element
ref: isOverflowing ? undefined : internalRef,
},
children
);
const resizeObserver = useMemo(
() =>
new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target.clientWidth && entry.target.scrollWidth) {
if (entry.target.scrollWidth > entry.target.clientWidth) {
setIsOverflowing(true);
}
if (entry.target.scrollWidth <= entry.target.clientWidth) {
setIsOverflowing(false);
}
}
}
}),
[]
);
useEffect(() => {
const { current } = internalRef;
if (current && truncate) {
resizeObserver.observe(current);
}
return () => {
resizeObserver.disconnect();
};
}, [isOverflowing, resizeObserver, truncate]);
const getTooltipText = (children: NonNullable<React.ReactNode>) => {
if (typeof children === 'string') {
return children;
}
const html = ReactDomServer.renderToStaticMarkup(<>{children}</>);
const getRidOfTags = html.replace(/(<([^>]+)>)/gi, '');
return getRidOfTags;
};
// A 'span' is an inline element therefore it can't be truncated
// and it should be wrapped in a parent element that is the one that will show the tooltip
if (truncate && isOverflowing && element !== 'span') {
return (
<Tooltip ref={internalRef} content={getTooltipText(children)}>
{childElement}
</Tooltip>
const childElement = (ref: React.ForwardedRef<HTMLElement> | undefined) => {
return createElement(
element,
{
...restProps,
style: undefined, // Remove the style prop to avoid overriding the styles
className: styles,
// When overflowing, the internalRef is passed to the tooltip, which forwards it to the child element
ref,
},
children
);
} else {
return childElement;
};
// A 'span' is an inline element, so it can't be truncated
// and it should be wrapped in a parent element that will show the tooltip
if (!truncate || element === 'span') {
return childElement(undefined);
}
return (
<TruncatedText
childElement={childElement}
// eslint-disable-next-line react/no-children-prop
children={children}
ref={ref}
/>
);
}
);
@@ -0,0 +1,64 @@
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import ReactDOMServer from 'react-dom/server';
import { Tooltip } from '../Tooltip/Tooltip';
interface TruncatedTextProps {
childElement: (ref: React.ForwardedRef<HTMLElement> | undefined) => React.ReactElement;
children: NonNullable<React.ReactNode>;
}
export const TruncatedText = React.forwardRef<HTMLElement, TruncatedTextProps>(({ childElement, children }, ref) => {
const [isOverflowing, setIsOverflowing] = useState(false);
const internalRef = useRef<HTMLElement>(null);
// Wire up the forwarded ref to the internal ref
useImperativeHandle<HTMLElement | null, HTMLElement | null>(ref, () => internalRef.current);
const resizeObserver = useMemo(
() =>
new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target.clientWidth && entry.target.scrollWidth) {
if (entry.target.scrollWidth > entry.target.clientWidth) {
setIsOverflowing(true);
}
if (entry.target.scrollWidth <= entry.target.clientWidth) {
setIsOverflowing(false);
}
}
}
}),
[]
);
useEffect(() => {
const { current } = internalRef;
if (current) {
resizeObserver.observe(current);
}
return () => {
resizeObserver.disconnect();
};
}, [setIsOverflowing, resizeObserver]);
const getTooltipText = (children: NonNullable<React.ReactNode>) => {
if (typeof children === 'string') {
return children;
}
const html = ReactDOMServer.renderToStaticMarkup(<>{children}</>);
return html.replace(/(<([^>]+)>)/gi, '');
};
if (isOverflowing) {
return (
<Tooltip ref={internalRef} content={getTooltipText(children)}>
{childElement(undefined)}
</Tooltip>
);
} else {
return childElement(internalRef);
}
});
TruncatedText.displayName = 'TruncatedText';
+19 -11
View File
@@ -117,9 +117,6 @@ export {
export { Sparkline } from './Sparkline/Sparkline';
export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
export { GraphWithLegend } from './Graph/GraphWithLegend';
export { GraphContextMenu, GraphContextMenuHeader } from './Graph/GraphContextMenu';
export { BarGauge } from './BarGauge/BarGauge';
export {
VizTooltip,
@@ -130,7 +127,6 @@ export {
type SeriesTableRowProps,
} from './VizTooltip';
export { VizRepeater, type VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
export { graphTimeFormat, graphTickFormatter } from './Graph/utils';
export {
PanelChrome,
type PanelChromeProps,
@@ -154,7 +150,7 @@ export { VizLegend } from './VizLegend/VizLegend';
export { VizLegendListItem } from './VizLegend/VizLegendListItem';
export { Alert, type AlertVariant } from './Alert/Alert';
export { GraphSeriesToggler, type GraphSeriesTogglerAPI } from './Graph/GraphSeriesToggler';
export { GraphSeriesToggler, type GraphSeriesTogglerAPI } from '../graveyard/Graph/GraphSeriesToggler';
export { Collapse, ControlledCollapse } from './Collapse/Collapse';
export { CollapsableSection } from './Collapse/CollapsableSection';
export { DataLinkButton } from './DataLinks/DataLinkButton';
@@ -215,6 +211,7 @@ export { Link } from './Link/Link';
export { TextLink } from './Link/TextLink';
export { Text } from './Text/Text';
export { Box } from './Layout/Box/Box';
export { Stack } from './Layout/Stack/Stack';
export { Label } from './Forms/Label';
export { Field, type FieldProps } from './Forms/Field';
@@ -289,13 +286,24 @@ export { UPlotChart } from './uPlot/Plot';
export { PlotLegend } from './uPlot/PlotLegend';
export * from './uPlot/geometries';
export * from './uPlot/plugins';
export { type PlotTooltipInterpolator, type PlotSelection } from './uPlot/types';
export { type PlotTooltipInterpolator, type PlotSelection, FIXED_UNIT } from './uPlot/types';
export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder';
export { GraphNG, type GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG';
export { TimeSeries } from './TimeSeries/TimeSeries';
export { useGraphNGContext } from './GraphNG/hooks';
export { preparePlotFrame, buildScaleKey } from './GraphNG/utils';
export { type GraphNGLegendEvent } from './GraphNG/types';
export * from './PanelChrome/types';
export { Label as BrowserLabel } from './BrowserLabel/Label';
export { PanelContainer } from './PanelContainer/PanelContainer';
// -----------------------------------------------------
// Graveyard: exported, but no longer used internally
// These will be removed in the future
// -----------------------------------------------------
export { Graph } from '../graveyard/Graph/Graph';
export { GraphWithLegend } from '../graveyard/Graph/GraphWithLegend';
export { GraphContextMenu, GraphContextMenuHeader } from '../graveyard/Graph/GraphContextMenu';
export { graphTimeFormat, graphTickFormatter } from '../graveyard/Graph/utils';
export { GraphNG, type GraphNGProps } from '../graveyard/GraphNG/GraphNG';
export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries';
export { useGraphNGContext } from '../graveyard/GraphNG/hooks';
export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils';
export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types';
@@ -0,0 +1,40 @@
import { FieldConfig, FieldType } from '@grafana/data';
import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
import { FIXED_UNIT } from './types';
/**
* @internal -- not a public API
*/
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>, fieldType: FieldType) {
const defaultPart = 'na';
const scaleRange = `${config.min !== undefined ? config.min : defaultPart}-${
config.max !== undefined ? config.max : defaultPart
}`;
const scaleSoftRange = `${config.custom?.axisSoftMin !== undefined ? config.custom.axisSoftMin : defaultPart}-${
config.custom?.axisSoftMax !== undefined ? config.custom.axisSoftMax : defaultPart
}`;
const scalePlacement = `${
config.custom?.axisPlacement !== undefined ? config.custom?.axisPlacement : AxisPlacement.Auto
}`;
const scaleUnit = config.unit ?? FIXED_UNIT;
const scaleDistribution = config.custom?.scaleDistribution
? getScaleDistributionPart(config.custom.scaleDistribution)
: ScaleDistribution.Linear;
const scaleLabel = Boolean(config.custom?.axisLabel) ? config.custom!.axisLabel : defaultPart;
return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}/${fieldType}`;
}
function getScaleDistributionPart(config: ScaleDistributionConfig) {
if (config.type === ScaleDistribution.Log) {
return `${config.type}${config.log}`;
}
return config.type;
}
@@ -3,6 +3,11 @@ import uPlot, { Options, AlignedData } from 'uplot';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
/**
* @internal -- not a public API
*/
export const FIXED_UNIT = '__fixed';
export type PlotConfig = Pick<
Options,
'mode' | 'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select' | 'tzDate' | 'padding'
@@ -1,7 +1,7 @@
import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data';
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
import { preparePlotFrame } from '../GraphNG/utils';
import { preparePlotFrame } from '../../../../../public/app/core/components/GraphNG/utils';
import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils';
@@ -5,7 +5,8 @@ import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, Stacki
import { attachDebugger } from '../../utils';
import { createLogger } from '../../utils/logger';
import { buildScaleKey } from '../GraphNG/utils';
import { buildScaleKey } from './internal';
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
@@ -5,7 +5,7 @@ import React from 'react';
import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId, DisplayProcessor } from '@grafana/data';
import { TooltipDisplayMode } from '@grafana/schema';
import { VizTooltip } from '../VizTooltip';
import { VizTooltip } from '../../components/VizTooltip';
import Graph from './Graph';
@@ -7,8 +7,8 @@ import React, { PureComponent } from 'react';
import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/data';
import { TooltipDisplayMode } from '@grafana/schema';
import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../VizTooltip';
import { FlotPosition } from '../VizTooltip/VizTooltip';
import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../../components/VizTooltip';
import { FlotPosition } from '../../components/VizTooltip/VizTooltip';
import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu';
import { GraphTooltip } from './GraphTooltip/GraphTooltip';
@@ -11,13 +11,13 @@ import {
GrafanaTheme2,
} from '@grafana/data';
import { ContextMenu, ContextMenuProps } from '../../components/ContextMenu/ContextMenu';
import { FormattedValueDisplay } from '../../components/FormattedValueDisplay/FormattedValueDisplay';
import { HorizontalGroup } from '../../components/Layout/Layout';
import { MenuGroup, MenuGroupProps } from '../../components/Menu/MenuGroup';
import { MenuItem } from '../../components/Menu/MenuItem';
import { SeriesIcon } from '../../components/VizLegend/SeriesIcon';
import { useStyles2 } from '../../themes';
import { ContextMenu, ContextMenuProps } from '../ContextMenu/ContextMenu';
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
import { HorizontalGroup } from '../Layout/Layout';
import { MenuGroup, MenuGroupProps } from '../Menu/MenuGroup';
import { MenuItem } from '../Menu/MenuItem';
import { SeriesIcon } from '../VizLegend/SeriesIcon';
import { GraphDimensions } from './GraphTooltip/types';
@@ -2,7 +2,7 @@ import React from 'react';
import { TooltipDisplayMode } from '@grafana/schema';
import { VizTooltipContentProps } from '../../VizTooltip';
import { VizTooltipContentProps } from '../../../components/VizTooltip';
import { MultiModeGraphTooltip } from './MultiModeGraphTooltip';
import { SingleModeGraphTooltip } from './SingleModeGraphTooltip';
@@ -3,7 +3,7 @@ import React from 'react';
import { createDimension, createTheme, FieldType, DisplayProcessor } from '@grafana/data';
import { ActiveDimensions } from '../../VizTooltip';
import { ActiveDimensions } from '../../../components/VizTooltip';
import { MultiModeGraphTooltip } from './MultiModeGraphTooltip';
import { GraphDimensions } from './types';
@@ -2,8 +2,8 @@ import React from 'react';
import { getValueFromDimension } from '@grafana/data';
import { SeriesTable } from '../../VizTooltip';
import { FlotPosition } from '../../VizTooltip/VizTooltip';
import { SeriesTable } from '../../../components/VizTooltip';
import { FlotPosition } from '../../../components/VizTooltip/VizTooltip';
import { getMultiSeriesGraphHoverInfo } from '../utils';
import { GraphTooltipContentProps } from './types';
@@ -7,7 +7,7 @@ import {
getFieldDisplayName,
} from '@grafana/data';
import { SeriesTable } from '../../VizTooltip';
import { SeriesTable } from '../../../components/VizTooltip';
import { GraphTooltipContentProps } from './types';
@@ -1,6 +1,6 @@
import { Dimension, Dimensions, TimeZone } from '@grafana/data';
import { ActiveDimensions } from '../../VizTooltip';
import { ActiveDimensions } from '../../../components/VizTooltip';
/** @deprecated */
export interface GraphDimensions extends Dimensions {
@@ -6,10 +6,10 @@ import React from 'react';
import { GraphSeriesValue } from '@grafana/data';
import { LegendDisplayMode, LegendPlacement } from '@grafana/schema';
import { CustomScrollbar } from '../../components/CustomScrollbar/CustomScrollbar';
import { VizLegend } from '../../components/VizLegend/VizLegend';
import { VizLegendItem } from '../../components/VizLegend/types';
import { stylesFactory } from '../../themes';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
import { VizLegend } from '../VizLegend/VizLegend';
import { VizLegendItem } from '../VizLegend/types';
import { Graph, GraphProps } from './Graph';
@@ -0,0 +1,275 @@
import React, { Component } from 'react';
import { Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import uPlot, { AlignedData } from 'uplot';
import {
DataFrame,
DataHoverClearEvent,
DataHoverEvent,
Field,
FieldMatcherID,
fieldMatchers,
FieldType,
LegacyGraphHoverEvent,
TimeRange,
TimeZone,
} from '@grafana/data';
import { VizLegendOptions } from '@grafana/schema';
import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext';
import { VizLayout } from '../../components/VizLayout/VizLayout';
import { UPlotChart } from '../../components/uPlot/Plot';
import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder';
import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
import { ScaleProps } from '../../components/uPlot/config/UPlotScaleBuilder';
import { findMidPointYPosition, pluginLog } from '../../components/uPlot/utils';
import { Themeable2 } from '../../types';
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
/**
* @deprecated
* @internal -- not a public API
*/
export type PropDiffFn<T extends any = any> = (prev: T, next: T) => boolean;
/** @deprecated */
export interface GraphNGProps extends Themeable2 {
frames: DataFrame[];
structureRev?: number; // a number that will change when the frames[] structure changes
width: number;
height: number;
timeRange: TimeRange;
timeZone: TimeZone[] | TimeZone;
legend: VizLegendOptions;
fields?: XYFieldMatchers; // default will assume timeseries data
renderers?: Renderers;
tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps;
tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps;
onLegendClick?: (event: GraphNGLegendEvent) => void;
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
propsToDiff?: Array<string | PropDiffFn>;
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
/**
* needed for propsToDiff to re-init the plot & config
* this is a generic approach to plot re-init, without having to specify which panel-level options
* should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in
* similar to structureRev. then we can drop propsToDiff entirely.
*/
options?: Record<string, any>;
}
function sameProps(prevProps: any, nextProps: any, propsToDiff: Array<string | PropDiffFn> = []) {
for (const propName of propsToDiff) {
if (typeof propName === 'function') {
if (!propName(prevProps, nextProps)) {
return false;
}
} else if (nextProps[propName] !== prevProps[propName]) {
return false;
}
}
return true;
}
/**
* @internal -- not a public API
* @deprecated
*/
export interface GraphNGState {
alignedFrame: DataFrame;
alignedData?: AlignedData;
config?: UPlotConfigBuilder;
}
/**
* "Time as X" core component, expects ascending x
* @deprecated
*/
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
private plotInstance: React.RefObject<uPlot>;
private subscription = new Subscription();
constructor(props: GraphNGProps) {
super(props);
let state = this.prepState(props);
state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData;
this.state = state;
this.plotInstance = React.createRef();
}
getTimeRange = () => this.props.timeRange;
prepState(props: GraphNGProps, withConfig = true) {
let state: GraphNGState = null as any;
const { frames, fields, preparePlotFrame } = props;
const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame;
const alignedFrame = preparePlotFrameFn(
frames,
fields || {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])),
},
props.timeRange
);
pluginLog('GraphNG', false, 'data aligned', alignedFrame);
if (alignedFrame) {
let config = this.state?.config;
if (withConfig) {
config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange);
pluginLog('GraphNG', false, 'config prepared', config);
}
state = {
alignedFrame,
config,
};
pluginLog('GraphNG', false, 'data prepared', state.alignedData);
}
return state;
}
handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) {
const time = evt.payload?.point?.time;
const u = this.plotInstance.current;
if (u && time) {
// Try finding left position on time axis
const left = u.valToPos(time, 'x');
let top;
if (left) {
// find midpoint between points at current idx
top = findMidPointYPosition(u, u.posToIdx(left));
}
if (!top || !left) {
return;
}
u.setCursor({
left,
top,
});
}
}
componentDidMount() {
this.panelContext = this.context as PanelContext;
const { eventBus } = this.panelContext;
this.subscription.add(
eventBus
.getStream(DataHoverEvent)
.pipe(throttleTime(50))
.subscribe({
next: (evt) => {
if (eventBus === evt.origin) {
return;
}
this.handleCursorUpdate(evt);
},
})
);
// Legacy events (from flot graph)
this.subscription.add(
eventBus
.getStream(LegacyGraphHoverEvent)
.pipe(throttleTime(50))
.subscribe({
next: (evt) => this.handleCursorUpdate(evt),
})
);
this.subscription.add(
eventBus
.getStream(DataHoverClearEvent)
.pipe(throttleTime(50))
.subscribe({
next: () => {
const u = this.plotInstance?.current;
// @ts-ignore
if (u && !u.cursor._lock) {
u.setCursor({
left: -10,
top: -10,
});
}
},
})
);
}
componentDidUpdate(prevProps: GraphNGProps) {
const { frames, structureRev, timeZone, propsToDiff } = this.props;
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) {
let newState = this.prepState(this.props, false);
if (newState) {
const shouldReconfig =
this.state.config === undefined ||
timeZone !== prevProps.timeZone ||
structureRev !== prevProps.structureRev ||
!structureRev ||
propsChanged;
if (shouldReconfig) {
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
pluginLog('GraphNG', false, 'config recreated', newState.config);
}
newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData;
this.setState(newState);
}
}
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
const { width, height, children, renderLegend } = this.props;
const { config, alignedFrame, alignedData } = this.state;
if (!config) {
return null;
}
return (
<VizLayout width={width} height={height} legend={renderLegend(config)}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart
config={config}
data={alignedData!}
width={vizWidth}
height={vizHeight}
plotRef={(u) => ((this.plotInstance as React.MutableRefObject<uPlot>).current = u)}
>
{children ? children(config, alignedFrame) : null}
</UPlotChart>
)}
</VizLayout>
);
}
}
@@ -0,0 +1,41 @@
import React, { useCallback, useContext } from 'react';
import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data';
import { XYFieldMatchers } from './types';
/** @deprecated */
interface GraphNGContextType {
mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex;
dimFields: XYFieldMatchers;
data: DataFrame;
}
/** @deprecated */
export const GraphNGContext = React.createContext<GraphNGContextType>({} as GraphNGContextType);
/** @deprecated */
export const useGraphNGContext = () => {
const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext<GraphNGContextType>(GraphNGContext);
const getXAxisField = useCallback(() => {
const xFieldMatcher = dimFields.x;
let xField: Field | null = null;
for (let j = 0; j < data.fields.length; j++) {
if (xFieldMatcher(data.fields[j], data, [data])) {
xField = data.fields[j];
break;
}
}
return xField;
}, [data, dimFields]);
return {
dimFields,
mapSeriesIndexToDataFrameFieldIndex,
getXAxisField,
alignedData: data,
};
};
@@ -0,0 +1,334 @@
import { FieldType, createDataFrame } from '@grafana/data';
import { applyNullInsertThreshold } from './nullInsertThreshold';
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function genFrame() {
let fieldCount = 10;
let valueCount = 3000;
let step = 1000;
let skipProb = 0.5;
let skipSteps = [1, 5]; // min, max
let allValues = Array(fieldCount);
allValues[0] = Array(valueCount);
for (let i = 0, curStep = Date.now(); i < valueCount; i++) {
curStep = allValues[0][i] = curStep + step * (Math.random() < skipProb ? randInt(skipSteps[0], skipSteps[1]) : 1);
}
for (let fi = 1; fi < fieldCount; fi++) {
let values = Array(valueCount);
for (let i = 0; i < valueCount; i++) {
values[i] = Math.random() * 100;
}
allValues[fi] = values;
}
return {
length: valueCount,
fields: allValues.map((values, i) => {
return {
name: 'A-' + i,
type: i === 0 ? FieldType.time : FieldType.number,
config: {
interval: i === 0 ? step : null,
},
values: values,
};
}),
};
}
describe('nullInsertThreshold Transformer', () => {
test('should insert nulls at +threshold between adjacent > threshold: 1', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']);
});
test('should insert nulls at +threshold between adjacent > threshold: 2', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [5, 7, 11] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 2 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 2 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([5, 7, 9, 11]);
expect(result.fields[1].values).toEqual([4, 6, null, 8]);
expect(result.fields[2].values).toEqual(['a', 'b', null, 'c']);
});
test('should insert nulls at +interval between adjacent > interval: 1', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']);
});
test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [4, 6, 13] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({
frame: df,
refFieldName: null,
refFieldPseudoMin: -0.5,
refFieldPseudoMax: 13,
});
expect(result.fields[0].values).toEqual([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
expect(result.fields[1].values).toEqual([
null,
null,
null,
null,
null,
4,
null,
6,
null,
null,
null,
null,
null,
null,
8,
]);
expect(result.fields[2].values).toEqual([
null,
null,
null,
null,
null,
'a',
null,
'b',
null,
null,
null,
null,
null,
null,
'c',
]);
});
// this tests that intervals at 24hr but starting not at 12am UTC are not always snapped to 12am UTC
test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold 11PM UTC', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: { interval: 86400000 },
values: [1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000],
},
{ name: 'One', type: FieldType.number, values: [0, 1, 2, 3, 4] },
],
});
const result = applyNullInsertThreshold({
frame: df,
refFieldName: null,
refFieldPseudoMin: 1679320395828,
refFieldPseudoMax: 1679815217157,
});
expect(result.fields[0].values).toEqual([
1679266800000, 1679353200000, 1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000,
]);
expect(result.fields[1].values).toEqual([null, null, 0, 1, 2, 3, 4]);
});
test('should insert trailing null at end +interval when timeRange.to.valueOf() exceeds threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 13 });
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8, null, null]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c', null, null]);
// should work for frames with 1 datapoint
const df2 = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] },
{ name: 'One', type: FieldType.number, values: [1] },
{ name: 'Two', type: FieldType.string, values: ['a'] },
],
});
// Max is 2.5 as opposed to the above 13 otherwise
// we get 12 nulls instead of the additional 1
const result2 = applyNullInsertThreshold({ frame: df2, refFieldName: null, refFieldPseudoMax: 2.5 });
expect(result2.fields[0].values).toEqual([1, 2]);
expect(result2.fields[1].values).toEqual([1, null]);
expect(result2.fields[2].values).toEqual(['a', null]);
});
test('should not insert trailing null at end +interval when timeRange.to.valueOf() equals threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] },
{ name: 'One', type: FieldType.number, values: [1] },
{ name: 'Two', type: FieldType.string, values: ['a'] },
],
});
const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 2 });
expect(result.fields[0].values).toEqual([1]);
expect(result.fields[1].values).toEqual([1]);
expect(result.fields[2].values).toEqual(['a']);
});
// TODO: make this work
test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([5, 6, 7, 8, 11]);
expect(result.fields[1].values).toEqual([4, null, 6, null, 8]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, 'c']);
});
test('should noop on 0 datapoints', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [] },
{ name: 'Value', type: FieldType.number, values: [] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop on invalid threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 2, 4] },
{ name: 'Value', type: FieldType.number, config: { custom: { insertNulls: -1 } }, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop on invalid interval', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: -1 }, values: [1, 2, 4] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop when no missing steps', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop when refFieldName not found', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 5] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df, refFieldName: 'Time2' });
expect(result).toBe(df);
});
// Leave this test skipped - it should be run manually
test.skip('perf stress test should be <= 10ms', () => {
// 10 fields x 3,000 values with 50% skip (output = 10 fields x 6,000 values)
let bigFrameA = genFrame();
// eslint-disable-next-line no-console
console.time('insertValues-10x3k');
applyNullInsertThreshold({ frame: bigFrameA });
// eslint-disable-next-line no-console
console.timeEnd('insertValues-10x3k');
});
});
@@ -19,6 +19,7 @@ interface NullInsertOptions {
insertMode?: InsertMode;
}
/** @deprecated */
export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame {
if (opts.frame.length === 0) {
return opts.frame;
@@ -0,0 +1,33 @@
/**
* mutates all nulls -> undefineds in the fieldValues array for value-less refValues ranges below maxThreshold
* refValues is typically a time array and maxThreshold is the allowable distance between in time
* @deprecated
*/
export function nullToUndefThreshold(refValues: number[], fieldValues: any[], maxThreshold: number): any[] {
let prevRef;
let nullIdx;
for (let i = 0; i < fieldValues.length; i++) {
let fieldVal = fieldValues[i];
if (fieldVal == null) {
if (nullIdx == null && prevRef != null) {
nullIdx = i;
}
} else {
if (nullIdx != null) {
if (refValues[i] - (prevRef as number) < maxThreshold) {
while (nullIdx < i) {
fieldValues[nullIdx++] = undefined;
}
}
nullIdx = null;
}
prevRef = refValues[i];
}
}
return fieldValues;
}
@@ -0,0 +1,94 @@
import { FieldType, createDataFrame } from '@grafana/data';
import { applyNullInsertThreshold } from './nullInsertThreshold';
import { nullToValue } from './nullToValue';
describe('nullToValue Transformer', () => {
test('should change all nulls to configured zero value', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 3, 10] },
{
name: 'One',
type: FieldType.number,
config: { custom: { insertNulls: 1 }, noValue: '0' },
values: [4, 6, 8],
},
{
name: 'Two',
type: FieldType.string,
config: { custom: { insertNulls: 1 }, noValue: '0' },
values: ['a', 'b', 'c'],
},
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df }));
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, 0, 6, 0, 0, 0, 0, 0, 0, 8]);
expect(result.fields[2].values).toEqual(['a', 0, 'b', 0, 0, 0, 0, 0, 0, 'c']);
});
test('should change all nulls to configured positive value', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [5, 7, 11] },
{
name: 'One',
type: FieldType.number,
config: { custom: { insertNulls: 2 }, noValue: '1' },
values: [4, 6, 8],
},
{
name: 'Two',
type: FieldType.string,
config: { custom: { insertNulls: 2 }, noValue: '1' },
values: ['a', 'b', 'c'],
},
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df }));
expect(result.fields[0].values).toEqual([5, 7, 9, 11]);
expect(result.fields[1].values).toEqual([4, 6, 1, 8]);
expect(result.fields[2].values).toEqual(['a', 'b', 1, 'c']);
});
test('should change all nulls to configured negative value', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, config: { noValue: '-1' }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { noValue: '-1' }, values: ['a', 'b', 'c'] },
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df }));
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, -1, 6, -1, -1, -1, -1, -1, -1, 8]);
expect(result.fields[2].values).toEqual(['a', -1, 'b', -1, -1, -1, -1, -1, -1, 'c']);
});
test('should have no effect without nulls', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df, refFieldName: null }));
expect(result.fields[0].values).toEqual([1, 2, 3]);
expect(result.fields[1].values).toEqual([4, 6, 8]);
expect(result.fields[2].values).toEqual(['a', 'b', 'c']);
});
});
@@ -1,5 +1,6 @@
import { DataFrame } from '@grafana/data';
/** @deprecated */
export function nullToValue(frame: DataFrame) {
return {
...frame,
@@ -0,0 +1,18 @@
import { DataFrameFieldIndex, FieldMatcher } from '@grafana/data';
import { SeriesVisibilityChangeMode } from '../../components/PanelChrome';
/**
* Event being triggered when the user interact with the Graph legend.
* @deprecated
*/
export interface GraphNGLegendEvent {
fieldIndex: DataFrameFieldIndex;
mode: SeriesVisibilityChangeMode;
}
/** @deprecated */
export interface XYFieldMatchers {
x: FieldMatcher; // first match
y: FieldMatcher;
}
@@ -7,7 +7,8 @@ import {
ScaleDistributionConfig,
} from '@grafana/schema';
import { FIXED_UNIT } from './GraphNG';
import { FIXED_UNIT } from '../../components/uPlot/types';
import { applyNullInsertThreshold } from './nullInsertThreshold';
import { nullToUndefThreshold } from './nullToUndefThreshold';
import { XYFieldMatchers } from './types';
@@ -0,0 +1 @@
Items in this folder are all deprecated and will be removed in the future
@@ -2,11 +2,11 @@ import React, { Component } from 'react';
import { DataFrame, TimeRange } from '@grafana/data';
import { PanelContextRoot } from '../../components/PanelChrome/PanelContext';
import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend';
import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
import { withTheme2 } from '../../themes/ThemeContext';
import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG';
import { PanelContextRoot } from '../PanelChrome/PanelContext';
import { hasVisibleLegendSeries, PlotLegend } from '../uPlot/PlotLegend';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { preparePlotConfigBuilder } from './utils';
@@ -61,10 +61,10 @@ for (let i = 0; i < BIN_INCRS.length; i++) {
BIN_INCRS[i] = 2 ** i;
}
import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../../components/uPlot/config/UPlotConfigBuilder';
import { getScaleGradientFn } from '../../components/uPlot/config/gradientFills';
import { getStackingGroups, preparePlotData2 } from '../../components/uPlot/utils';
import { buildScaleKey } from '../GraphNG/utils';
import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../uPlot/config/UPlotConfigBuilder';
import { getScaleGradientFn } from '../uPlot/config/gradientFills';
import { getStackingGroups, preparePlotData2 } from '../uPlot/utils';
const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals));
-1
View File
@@ -10,4 +10,3 @@
*/
export { Grid } from './components/Layout/Grid/Grid';
export { Stack } from './components/Layout/Stack/Stack';
-1
View File
@@ -471,7 +471,6 @@ func (hs *HTTPServer) registerRoutes() {
dashboardRoute.Post("/calculate-diff", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
dashboardRoute.Post("/validate", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.ValidateDashboard))
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
dashboardRoute.Post("/db", authorize(ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard))
dashboardRoute.Get("/home", routing.Wrap(hs.GetHomeDashboard))
-39
View File
@@ -68,32 +68,6 @@ func dashboardGuardianResponse(err error) response.Response {
return response.Error(http.StatusForbidden, "Access denied to this dashboard", nil)
}
// swagger:route POST /dashboards/trim dashboards trimDashboard
//
// Trim defaults from dashboard.
//
// Responses:
// 200: trimDashboardResponse
// 401: unauthorisedError
// 500: internalServerError
func (hs *HTTPServer) TrimDashboard(c *contextmodel.ReqContext) response.Response {
cmd := dashboards.TrimDashboardCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
dash := cmd.Dashboard
meta := cmd.Meta
// TODO temporarily just return the input as a no-op while we convert to thema calls
dto := dtos.TrimDashboardFullWithMeta{
Dashboard: dash,
Meta: meta,
}
c.TimeRequest(metrics.MApiDashboardGet)
return response.JSON(http.StatusOK, dto)
}
// swagger:route GET /dashboards/uid/{uid} dashboards getDashboardByUID
//
// Get dashboard by uid.
@@ -1233,13 +1207,6 @@ type CalcDashboardDiffParams struct {
}
}
// swagger:parameters trimDashboard
type TrimDashboardParams struct {
// in:body
// required:true
Body dashboards.TrimDashboardCommand
}
// swagger:response dashboardResponse
type DashboardResponse struct {
// The response message
@@ -1315,12 +1282,6 @@ type CalculateDashboardDiffResponse struct {
Body []byte `json:"body"`
}
// swagger:response trimDashboardResponse
type TrimDashboardResponse struct {
// in: body
Body dtos.TrimDashboardFullWithMeta `json:"body"`
}
// swagger:response getHomeDashboardResponse
type GetHomeDashboardResponse struct {
// in: body
-5
View File
@@ -51,11 +51,6 @@ type DashboardFullWithMeta struct {
Dashboard *simplejson.Json `json:"dashboard"`
}
type TrimDashboardFullWithMeta struct {
Meta *simplejson.Json `json:"meta"`
Dashboard *simplejson.Json `json:"dashboard"`
}
type DashboardRedirect struct {
RedirectUri string `json:"redirectUri"`
}
+5
View File
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
@@ -79,6 +80,10 @@ func (s *Service) Get(ctx context.Context, p *plugins.Plugin) []string {
func (s *Service) GetConfigMap(ctx context.Context, _ string, _ *auth.ExternalService) map[string]string {
m := make(map[string]string)
if s.cfg.GrafanaAppURL != "" {
m[backend.AppURL] = s.cfg.GrafanaAppURL
}
// TODO add support via plugin SDK
//if externalService != nil {
// m[oauthtokenretriever.AppURL] = s.cfg.GrafanaAppURL
+11
View File
@@ -500,3 +500,14 @@ func TestService_GetConfigMap_featureToggles(t *testing.T) {
}
})
}
func TestService_GetConfigMap_appURL(t *testing.T) {
t.Run("Uses the configured app URL", func(t *testing.T) {
s := &Service{
cfg: &config.Cfg{
GrafanaAppURL: "https://myorg.com/",
},
}
require.Equal(t, map[string]string{"GF_APP_URL": "https://myorg.com/"}, s.GetConfigMap(context.Background(), "", nil))
})
}
+7 -3
View File
@@ -57,7 +57,11 @@ func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.
continue
}
paths, err := l.getAbsPluginJSONPaths(path)
followDistFolder := true
if src.PluginClass(ctx) == plugins.ClassCore {
followDistFolder = false
}
paths, err := l.getAbsPluginJSONPaths(path, followDistFolder)
if err != nil {
return nil, err
}
@@ -154,7 +158,7 @@ func (l *Local) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error)
return plugin, nil
}
func (l *Local) getAbsPluginJSONPaths(path string) ([]string, error) {
func (l *Local) getAbsPluginJSONPaths(path string, followDistFolder bool) ([]string, error) {
var pluginJSONPaths []string
var err error
@@ -163,7 +167,7 @@ func (l *Local) getAbsPluginJSONPaths(path string) ([]string, error) {
return []string{}, err
}
if err = walk(path, true, true,
if err = walk(path, true, true, followDistFolder,
func(currentPath string, fi os.FileInfo, err error) error {
if err != nil {
if errors.Is(err, os.ErrNotExist) {
@@ -274,7 +274,7 @@ func TestFinder_Find(t *testing.T) {
func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
t.Run("When scanning a folder that doesn't exists shouldn't return an error", func(t *testing.T) {
origWalk := walk
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error {
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error {
return walkFn(path, nil, os.ErrNotExist)
}
t.Cleanup(func() {
@@ -282,14 +282,14 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
})
finder := NewLocalFinder(false)
paths, err := finder.getAbsPluginJSONPaths("test")
paths, err := finder.getAbsPluginJSONPaths("test", true)
require.NoError(t, err)
require.Empty(t, paths)
})
t.Run("When scanning a folder that lacks permission shouldn't return an error", func(t *testing.T) {
origWalk := walk
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error {
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error {
return walkFn(path, nil, os.ErrPermission)
}
t.Cleanup(func() {
@@ -297,14 +297,14 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
})
finder := NewLocalFinder(false)
paths, err := finder.getAbsPluginJSONPaths("test")
paths, err := finder.getAbsPluginJSONPaths("test", true)
require.NoError(t, err)
require.Empty(t, paths)
})
t.Run("When scanning a folder that returns a non-handled error should return that error", func(t *testing.T) {
origWalk := walk
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error {
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error {
return walkFn(path, nil, errors.New("random error"))
}
t.Cleanup(func() {
@@ -312,10 +312,28 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
})
finder := NewLocalFinder(false)
paths, err := finder.getAbsPluginJSONPaths("test")
paths, err := finder.getAbsPluginJSONPaths("test", true)
require.Error(t, err)
require.Empty(t, paths)
})
t.Run("should forward if the dist folder should be evaluated", func(t *testing.T) {
origWalk := walk
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error {
if followDistFolder {
return walkFn(path, nil, errors.New("unexpected followDistFolder"))
}
return walkFn(path, nil, filepath.SkipDir)
}
t.Cleanup(func() {
walk = origWalk
})
finder := NewLocalFinder(false)
paths, err := finder.getAbsPluginJSONPaths("test", false)
require.ErrorIs(t, err, filepath.SkipDir)
require.Empty(t, paths)
})
}
var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool {
+14 -6
View File
@@ -9,12 +9,6 @@ package featuremgmt
var (
// Register each toggle here
standardFeatureFlags = []FeatureFlag{
{
Name: "trimDefaults",
Description: "Use cue schema to remove values that will be applied automatically",
Stage: FeatureStagePublicPreview,
Owner: grafanaAsCodeSquad,
},
{
Name: "disableEnvelopeEncryption",
Description: "Disable envelope encryption (emergency only)",
@@ -966,5 +960,19 @@ var (
RequiresDevMode: false,
Owner: grafanaAuthnzSquad,
},
{
Name: "extractFieldsNameDeduplication",
Description: "Make sure extracted field names are unique in the dataframe",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaBiSquad,
},
{
Name: "dashboardSceneForViewers",
Description: "Enables dashboard rendering using Scenes for viewer roles",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
}
)
+2 -1
View File
@@ -1,5 +1,4 @@
Name,Stage,Owner,requiresDevMode,RequiresLicense,RequiresRestart,FrontendOnly
trimDefaults,preview,@grafana/grafana-as-code,false,false,false,false
disableEnvelopeEncryption,GA,@grafana/grafana-as-code,false,false,false,false
live-service-web-worker,experimental,@grafana/grafana-app-platform-squad,false,false,false,true
queryOverLive,experimental,@grafana/grafana-app-platform-squad,false,false,false,true
@@ -137,3 +136,5 @@ alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,fal
alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false,false
alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false,false
annotationPermissionUpdate,experimental,@grafana/grafana-authnz-team,false,false,false,false
extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,false,false,false,true
dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,false,true
1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
trimDefaults preview @grafana/grafana-as-code false false false false
2 disableEnvelopeEncryption GA @grafana/grafana-as-code false false false false
3 live-service-web-worker experimental @grafana/grafana-app-platform-squad false false false true
4 queryOverLive experimental @grafana/grafana-app-platform-squad false false false true
136 alertmanagerRemotePrimary experimental @grafana/alerting-squad false false false false
137 alertmanagerRemoteOnly experimental @grafana/alerting-squad false false false false
138 annotationPermissionUpdate experimental @grafana/grafana-authnz-team false false false false
139 extractFieldsNameDeduplication experimental @grafana/grafana-bi-squad false false false true
140 dashboardSceneForViewers experimental @grafana/dashboards-squad false false false true
+8 -4
View File
@@ -7,10 +7,6 @@
package featuremgmt
const (
// FlagTrimDefaults
// Use cue schema to remove values that will be applied automatically
FlagTrimDefaults = "trimDefaults"
// FlagDisableEnvelopeEncryption
// Disable envelope encryption (emergency only)
FlagDisableEnvelopeEncryption = "disableEnvelopeEncryption"
@@ -558,4 +554,12 @@ const (
// FlagAnnotationPermissionUpdate
// Separate annotation permissions from dashboard permissions to allow for more granular control.
FlagAnnotationPermissionUpdate = "annotationPermissionUpdate"
// FlagExtractFieldsNameDeduplication
// Make sure extracted field names are unique in the dataframe
FlagExtractFieldsNameDeduplication = "extractFieldsNameDeduplication"
// FlagDashboardSceneForViewers
// Enables dashboard rendering using Scenes for viewer roles
FlagDashboardSceneForViewers = "dashboardSceneForViewers"
)
@@ -9,7 +9,6 @@ import (
func TestFeatureUsageStats(t *testing.T) {
featureManagerWithAllFeatures := WithFeatures(
"trimDefaults",
"database_metrics",
"live-config",
"UPPER_SNAKE_CASE",
@@ -17,7 +16,6 @@ func TestFeatureUsageStats(t *testing.T) {
)
require.Equal(t, map[string]any{
"stats.features.trim_defaults.count": 1,
"stats.features.database_metrics.count": 1,
"stats.features.live_config.count": 1,
"stats.features.upper_snake_case.count": 1,
@@ -403,7 +403,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv)
require.NoError(t, err)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac)
@@ -480,7 +480,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, ac, dashSrv)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv)
require.NoError(t, err)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac)
@@ -618,7 +618,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, ac, dashSrv)
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv)
require.NoError(t, err)
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
+10 -5
View File
@@ -35,7 +35,7 @@ func (t Token) String() string {
} else if t.IsVariable() {
return t.Variable
} else {
panic("empty token")
return ""
}
}
@@ -108,7 +108,7 @@ func tokenizeVariable(in []rune) (Token, int, error) {
)
if !startVariable(in) {
panic("tokenizeVariable called with input that doesn't start with delimiter")
return Token{}, pos, fmt.Errorf("expected '${', got '%s'", string(in[:2]))
}
pos += 2 // seek past opening delimiter
@@ -118,11 +118,11 @@ func tokenizeVariable(in []rune) (Token, int, error) {
r = in[pos]
if unicode.IsSpace(r) && r != ' ' {
return Token{}, pos, fmt.Errorf("unexpected whitespace")
return Token{}, pos, errors.New("unexpected whitespace")
}
if startVariable(in[pos:]) {
return Token{}, pos, fmt.Errorf("ambiguous delimiter")
return Token{}, pos, errors.New("ambiguous delimiter")
}
if r == '}' {
@@ -139,7 +139,12 @@ func tokenizeVariable(in []rune) (Token, int, error) {
return Token{}, pos, fmt.Errorf("expected '}', got '%c'", r)
}
return Token{Variable: string(runes)}, pos, nil
token := Token{Variable: string(runes)}
if !token.IsVariable() {
return Token{}, pos, errors.New("empty variable")
}
return token, pos, nil
}
func startVariable(in []rune) bool {
@@ -303,6 +303,12 @@ func TestMigrateTmpl(t *testing.T) {
expected: withDeduplicateMap("{{$mergedLabels.instance}}{{` is down ${`}}{{$mergedLabels.nestedVar}}}"),
vars: true,
},
{
name: "edge cases",
input: "Test test 123 \n$(metric)\n${.}\n${}\n${Condition[0]}",
expected: withDeduplicateMap("Test test 123 \n$(metric)\n{{index $mergedLabels \".\"}}\n${}\n{{index $mergedLabels \"Condition[0]\"}}"),
vars: true,
},
}
for _, tc := range cases {
@@ -0,0 +1,85 @@
package remote
import (
"context"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
)
type RemotePrimaryForkedAlertmanager struct {
internal notifier.Alertmanager
remote notifier.Alertmanager
}
func NewRemotePrimaryForkedAlertmanager(internal, remote notifier.Alertmanager) *RemotePrimaryForkedAlertmanager {
return &RemotePrimaryForkedAlertmanager{
internal: internal,
remote: remote,
}
}
func (fam *RemotePrimaryForkedAlertmanager) ApplyConfig(ctx context.Context, config *models.AlertConfiguration) error {
return nil
}
func (fam *RemotePrimaryForkedAlertmanager) SaveAndApplyConfig(ctx context.Context, config *apimodels.PostableUserConfig) error {
return nil
}
func (fam *RemotePrimaryForkedAlertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error {
return nil
}
func (fam *RemotePrimaryForkedAlertmanager) GetStatus() apimodels.GettableStatus {
return apimodels.GettableStatus{}
}
func (fam *RemotePrimaryForkedAlertmanager) CreateSilence(ctx context.Context, silence *apimodels.PostableSilence) (string, error) {
return "", nil
}
func (fam *RemotePrimaryForkedAlertmanager) DeleteSilence(ctx context.Context, id string) error {
return nil
}
func (fam *RemotePrimaryForkedAlertmanager) GetSilence(ctx context.Context, id string) (apimodels.GettableSilence, error) {
return apimodels.GettableSilence{}, nil
}
func (fam *RemotePrimaryForkedAlertmanager) ListSilences(ctx context.Context, filter []string) (apimodels.GettableSilences, error) {
return apimodels.GettableSilences{}, nil
}
func (fam *RemotePrimaryForkedAlertmanager) GetAlerts(ctx context.Context, active, silenced, inhibited bool, filter []string, receiver string) (apimodels.GettableAlerts, error) {
return apimodels.GettableAlerts{}, nil
}
func (fam *RemotePrimaryForkedAlertmanager) GetAlertGroups(ctx context.Context, active, silenced, inhibited bool, filter []string, receiver string) (apimodels.AlertGroups, error) {
return apimodels.AlertGroups{}, nil
}
func (fam *RemotePrimaryForkedAlertmanager) PutAlerts(ctx context.Context, alerts apimodels.PostableAlerts) error {
return nil
}
func (fam *RemotePrimaryForkedAlertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) {
return []apimodels.Receiver{}, nil
}
func (fam *RemotePrimaryForkedAlertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*notifier.TestReceiversResult, error) {
return &notifier.TestReceiversResult{}, nil
}
func (fam *RemotePrimaryForkedAlertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
return &notifier.TestTemplatesResults{}, nil
}
func (fam *RemotePrimaryForkedAlertmanager) CleanUp() {}
func (fam *RemotePrimaryForkedAlertmanager) StopAndWait() {}
func (fam *RemotePrimaryForkedAlertmanager) Ready() bool {
return false
}
@@ -0,0 +1,85 @@
package remote
import (
"context"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
)
type RemoteSecondaryForkedAlertmanager struct {
internal notifier.Alertmanager
remote notifier.Alertmanager
}
func NewRemoteSecondaryForkedAlertmanager(internal, remote notifier.Alertmanager) *RemoteSecondaryForkedAlertmanager {
return &RemoteSecondaryForkedAlertmanager{
internal: internal,
remote: remote,
}
}
func (fam *RemoteSecondaryForkedAlertmanager) ApplyConfig(ctx context.Context, config *models.AlertConfiguration) error {
return nil
}
func (fam *RemoteSecondaryForkedAlertmanager) SaveAndApplyConfig(ctx context.Context, config *apimodels.PostableUserConfig) error {
return nil
}
func (fam *RemoteSecondaryForkedAlertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error {
return nil
}
func (fam *RemoteSecondaryForkedAlertmanager) GetStatus() apimodels.GettableStatus {
return apimodels.GettableStatus{}
}
func (fam *RemoteSecondaryForkedAlertmanager) CreateSilence(ctx context.Context, silence *apimodels.PostableSilence) (string, error) {
return "", nil
}
func (fam *RemoteSecondaryForkedAlertmanager) DeleteSilence(ctx context.Context, id string) error {
return nil
}
func (fam *RemoteSecondaryForkedAlertmanager) GetSilence(ctx context.Context, id string) (apimodels.GettableSilence, error) {
return apimodels.GettableSilence{}, nil
}
func (fam *RemoteSecondaryForkedAlertmanager) ListSilences(ctx context.Context, filter []string) (apimodels.GettableSilences, error) {
return apimodels.GettableSilences{}, nil
}
func (fam *RemoteSecondaryForkedAlertmanager) GetAlerts(ctx context.Context, active, silenced, inhibited bool, filter []string, receiver string) (apimodels.GettableAlerts, error) {
return apimodels.GettableAlerts{}, nil
}
func (fam *RemoteSecondaryForkedAlertmanager) GetAlertGroups(ctx context.Context, active, silenced, inhibited bool, filter []string, receiver string) (apimodels.AlertGroups, error) {
return apimodels.AlertGroups{}, nil
}
func (fam *RemoteSecondaryForkedAlertmanager) PutAlerts(ctx context.Context, alerts apimodels.PostableAlerts) error {
return nil
}
func (fam *RemoteSecondaryForkedAlertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) {
return []apimodels.Receiver{}, nil
}
func (fam *RemoteSecondaryForkedAlertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*notifier.TestReceiversResult, error) {
return &notifier.TestReceiversResult{}, nil
}
func (fam *RemoteSecondaryForkedAlertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
return &notifier.TestTemplatesResults{}, nil
}
func (fam *RemoteSecondaryForkedAlertmanager) CleanUp() {}
func (fam *RemoteSecondaryForkedAlertmanager) StopAndWait() {}
func (fam *RemoteSecondaryForkedAlertmanager) Ready() bool {
return false
}
+1 -5
View File
@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
@@ -39,20 +38,17 @@ type DBstore struct {
SQLStore db.DB
Logger log.Logger
FolderService folder.Service
AccessControl accesscontrol.AccessControl
DashboardService dashboards.DashboardService
}
func ProvideDBStore(
cfg *setting.Cfg, featureToggles featuremgmt.FeatureToggles, sqlstore db.DB, folderService folder.Service,
access accesscontrol.AccessControl, dashboards dashboards.DashboardService) (*DBstore, error) {
cfg *setting.Cfg, featureToggles featuremgmt.FeatureToggles, sqlstore db.DB, folderService folder.Service, dashboards dashboards.DashboardService) (*DBstore, error) {
store := DBstore{
Cfg: cfg.UnifiedAlerting,
FeatureToggles: featureToggles,
SQLStore: sqlstore,
Logger: log.New("ngalert.dbstore"),
FolderService: folderService,
AccessControl: access,
DashboardService: dashboards,
}
if err := folderService.RegisterService(store); err != nil {
+1 -2
View File
@@ -63,8 +63,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG,
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg)
folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus)
ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService,
ac, &dashboards.FakeDashboardService{})
ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService, &dashboards.FakeDashboardService{})
require.NoError(tb, err)
ng, err := ngalert.ProvideService(
cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil),
@@ -80,7 +80,11 @@ func (dc *DatasourceProvisioner) provisionDataSources(ctx context.Context, cfg *
updateCmd := createUpdateCommand(ds, dataSource.ID)
dc.log.Debug("updating datasource from configuration", "name", updateCmd.Name, "uid", updateCmd.UID)
if _, err := dc.store.UpdateDataSource(ctx, updateCmd); err != nil {
return err
if errors.Is(err, datasources.ErrDataSourceUpdatingOldVersion) {
dc.log.Debug("ignoring old version of datasource", "name", updateCmd.Name, "uid", updateCmd.UID)
} else {
return err
}
}
}
}
@@ -243,6 +243,7 @@ func createUpdateCommand(ds *upsertDataSourceFromConfig, id int64) *datasources.
return &datasources.UpdateDataSourceCommand{
ID: id,
Version: ds.Version,
UID: ds.UID,
OrgID: ds.OrgID,
Name: ds.Name,
@@ -13,3 +13,11 @@ func TestUIDFromNames(t *testing.T) {
require.Equal(t, safeUIDFromName("AAA"), "PCB1AD2119D8FAFB6")
})
}
func TestCreateUpdateCommand(t *testing.T) {
t.Run("includes the version in the command", func(t *testing.T) {
ds := &upsertDataSourceFromConfig{OrgID: 1, Version: 1, Name: "test"}
cmd := createUpdateCommand(ds, 1)
require.Equal(t, 1, cmd.Version)
})
}
@@ -267,7 +267,6 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
SQLStore: ps.SQLStore,
Logger: ps.log,
FolderService: nil, // we don't use it yet
AccessControl: ps.ac,
DashboardService: ps.dashboardService,
}
ruleService := provisioning.NewAlertRuleService(

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