Merge remote-tracking branch 'origin/main' into resource-store

This commit is contained in:
Ryan McKinley 2024-06-24 20:55:15 +03:00
commit 44a134f72b
161 changed files with 5314 additions and 3756 deletions

View File

@ -593,6 +593,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-dashboards-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/dashboards-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite
depends_on:
@ -601,6 +609,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-smoke-tests-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/smoke-tests-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite panels-suite
depends_on:
@ -609,6 +625,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-panels-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/panels-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite various-suite
depends_on:
@ -617,6 +641,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-various-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/various-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/various-suite
- commands:
- cd /
- ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH}
@ -1908,6 +1940,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-dashboards-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/dashboards-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite
depends_on:
@ -1916,6 +1956,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-smoke-tests-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/smoke-tests-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite panels-suite
depends_on:
@ -1924,6 +1972,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-panels-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/panels-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite various-suite
depends_on:
@ -1932,6 +1988,14 @@ steps:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-various-suite
- commands:
- ./bin/build e2e-tests --port 3001 --suite scenes/various-suite
depends_on:
- grafana-server
environment:
HOST: grafana-server
image: cypress/included:13.10.0
name: end-to-end-tests-scenes/various-suite
- commands:
- cd /
- ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH}
@ -4996,6 +5060,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 06f574902baa67d8885abb48e48987f675d7637e30d4b783b3bb84e51b46cdaf
hmac: a916fba452c568a0e1d392702db3423fa52652d8d60f65f7b7aa43a7c1e952f4
...

24
.github/CODEOWNERS vendored
View File

@ -152,7 +152,7 @@
/pkg/tests/apis/ @grafana/grafana-app-platform-squad
/pkg/tests/api/correlations/ @grafana/explore-squad
/pkg/tsdb/grafanads/ @grafana/grafana-backend-group
/pkg/tsdb/opentsdb/ @grafana/grafana-backend-group
/pkg/tsdb/opentsdb/ @grafana/partner-datasources
/pkg/util/ @grafana/grafana-backend-group
/pkg/web/ @grafana/grafana-backend-group
@ -182,11 +182,11 @@
/devenv/docker/blocks/collectd/ @grafana/observability-metrics
/devenv/docker/blocks/etcd @grafana/grafana-app-platform-squad
/devenv/docker/blocks/grafana/ @grafana/grafana-as-code
/devenv/docker/blocks/graphite/ @grafana/observability-metrics
/devenv/docker/blocks/graphite09/ @grafana/observability-metrics
/devenv/docker/blocks/graphite1/ @grafana/observability-metrics
/devenv/docker/blocks/influxdb/ @grafana/observability-metrics
/devenv/docker/blocks/influxdb1/ @grafana/observability-metrics
/devenv/docker/blocks/graphite/ @grafana/partner-datasources
/devenv/docker/blocks/graphite09/ @grafana/partner-datasources
/devenv/docker/blocks/graphite1/ @grafana/partner-datasources
/devenv/docker/blocks/influxdb/ @grafana/partner-datasources
/devenv/docker/blocks/influxdb1/ @grafana/partner-datasources
/devenv/docker/blocks/jaeger/ @grafana/observability-traces-and-profiling
/devenv/docker/blocks/maildev/ @grafana/alerting-frontend
/devenv/docker/blocks/mariadb/ @grafana/oss-big-tent
@ -200,7 +200,7 @@
/devenv/docker/blocks/mysql_exporter/ @grafana/oss-big-tent
/devenv/docker/blocks/mysql_opendata/ @grafana/oss-big-tent
/devenv/docker/blocks/mysql_tests/ @grafana/oss-big-tent
/devenv/docker/blocks/opentsdb/ @grafana/observability-metrics
/devenv/docker/blocks/opentsdb/ @grafana/partner-datasources
/devenv/docker/blocks/postgres/ @grafana/oss-big-tent
/devenv/docker/blocks/postgres_tests/ @grafana/oss-big-tent
/devenv/docker/blocks/prometheus/ @grafana/observability-metrics
@ -253,9 +253,7 @@
# Observability backend code
/pkg/tsdb/prometheus/ @grafana/observability-metrics
/pkg/tsdb/influxdb/ @grafana/observability-metrics
/pkg/tsdb/elasticsearch/ @grafana/observability-logs
/pkg/tsdb/graphite/ @grafana/observability-metrics
/pkg/tsdb/loki/ @grafana/observability-logs
/pkg/tsdb/tempo/ @grafana/observability-traces-and-profiling
/pkg/tsdb/grafana-pyroscope-datasource/ @grafana/observability-traces-and-profiling
@ -267,6 +265,8 @@
# Partner Datasources backend code
/pkg/tsdb/mssql/ @grafana/partner-datasources
/pkg/tsdb/influxdb/ @grafana/partner-datasources
/pkg/tsdb/graphite/ @grafana/partner-datasources
# Database migrations
/pkg/services/sqlstore/migrations/ @grafana/grafana-search-and-storage
@ -573,14 +573,14 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/plugins/datasource/grafana/ @grafana/grafana-frontend-platform
/public/app/plugins/datasource/grafana-testdata-datasource/ @grafana/plugins-platform-frontend
/public/app/plugins/datasource/azuremonitor/ @grafana/partner-datasources
/public/app/plugins/datasource/graphite/ @grafana/observability-metrics
/public/app/plugins/datasource/influxdb/ @grafana/observability-metrics
/public/app/plugins/datasource/graphite/ @grafana/partner-datasources
/public/app/plugins/datasource/influxdb/ @grafana/partner-datasources
/public/app/plugins/datasource/jaeger/ @grafana/observability-traces-and-profiling
/public/app/plugins/datasource/loki/ @grafana/observability-logs
/public/app/plugins/datasource/mixed/ @grafana/dashboards-squad
/public/app/plugins/datasource/mssql/ @grafana/partner-datasources
/public/app/plugins/datasource/mysql/ @grafana/oss-big-tent
/public/app/plugins/datasource/opentsdb/ @grafana/observability-metrics
/public/app/plugins/datasource/opentsdb/ @grafana/partner-datasources
/public/app/plugins/datasource/grafana-postgresql-datasource/ @grafana/oss-big-tent
/public/app/plugins/datasource/prometheus/ @grafana/observability-metrics
/public/app/plugins/datasource/cloud-monitoring/ @grafana/partner-datasources

View File

@ -1,11 +1,4 @@
[
{
"type": "check-milestone",
"title": "Milestone Check",
"targetUrl": "https://github.com/grafana/grafana/blob/main/contribute/merge-pull-request.md#assign-a-milestone",
"success": "Milestone set",
"failure": "Milestone not set"
},
{
"type": "check-changelog",
"title": "Changelog Check",

View File

@ -1,39 +1,26 @@
name: Auto-milestone
on:
pull_request:
pull_request_target:
types:
- opened
- reopened
- closed
- ready_for_review
permissions:
pull-requests: write
# Note: this action runs with write permissions on GITHUB_TOKEN even from forks
# so it must not run untrusted code (such as checking out the pull request)
jobs:
config:
runs-on: "ubuntu-latest"
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
- name: "Check for secrets"
id: check
shell: bash
run: |
if [ -n "${{ (secrets.GRAFANA_DELIVERY_BOT_APP_ID != '' && secrets.GRAFANA_DELIVERY_BOT_APP_PEM != '') || '' }}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
main:
needs: config
if: needs.config.outputs.has-secrets
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: "Generate token"
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
# Note: Github will not trigger other actions from this because it uses
# the GITHUB_TOKEN token
- name: Run auto-milestone
uses: grafana/grafana-github-actions-go/auto-milestone@main
with:
pr: ${{ github.event.pull_request.number }}
token: ${{ steps.generate_token.outputs.token }}
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -18,6 +18,14 @@ jobs:
with:
ref: ${{ github.head_ref }}
- name: Generate token
if: steps.crowdin-download.outputs.pull_request_url
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_ID }}
private_key: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_PEM }}
- name: Download sources
id: crowdin-download
uses: crowdin/github-action@v1
@ -53,18 +61,10 @@ jobs:
github_user_name: "github-actions[bot]"
github_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Generate token
if: steps.crowdin-download.outputs.pull_request_url
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_ID }}
private_key: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_PEM }}
- name: Get pull request ID
if: steps.crowdin-download.outputs.pull_request_url
shell: bash
@ -74,7 +74,7 @@ jobs:
pr_id=$(gh pr view ${{ steps.crowdin-download.outputs.pull_request_url }} --json id -q .id)
echo "PULL_REQUEST_ID=$pr_id" >> "$GITHUB_ENV"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: Get project board ID
uses: octokit/graphql-action@v2.x
@ -119,4 +119,4 @@ jobs:
if: steps.crowdin-download.outputs.pull_request_url
with:
pr: ${{ steps.crowdin-download.outputs.pull_request_number }}
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}

View File

@ -519,6 +519,9 @@ user_invite_max_lifetime_duration = 24h
# The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour).
verification_email_max_lifetime_duration = 1h
# Frequency of updating a user's last seen time. The minimum supported duration is 5m (5 minutes). The maximum supported duration is 1h (1 hour)
last_seen_update_interval = 15m
# Enter a comma-separated list of usernames to hide them in the Grafana UI. These users are shown to Grafana admins and to themselves.
hidden_users =

View File

@ -520,6 +520,9 @@
# The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour).
;verification_email_max_lifetime_duration = 1h
# Frequency of updating a user's last seen time. The minimum supported duration is 5m (5 minutes). The maximum supported duration is 1h (1 hour).
;last_seen_update_interval = 15m
# Enter a comma-separated list of users login to hide them in the Grafana UI. These users are shown to Grafana admins and themselves.
; hidden_users =

View File

@ -0,0 +1,16 @@
route:
group_by: ['alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 1h
receiver: 'web.hook'
receivers:
- name: 'web.hook'
webhook_configs:
- url: 'http://127.0.0.1:5001/'
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'dev', 'instance']

View File

@ -25,7 +25,11 @@
FD_DATASOURCE: prom
alertmanager:
image: quay.io/prometheus/alertmanager
image: prom/alertmanager
volumes:
- ${PWD}/docker/blocks/prometheus/alertmanager.yml:/etc/alertmanager/alertmanager.yml
command: >
--config.file=/etc/alertmanager/alertmanager.yml
ports:
- "9093:9093"

View File

@ -1,11 +1,5 @@
---
draft: true
aliases:
- ../administration/reports/
- ../enterprise/export-pdf/
- ../enterprise/reporting/
- ../panels/create-reports/
- reporting/
keywords:
- grafana
- announcement
@ -15,34 +9,30 @@ labels:
- enterprise
menuTitle: Announcement banner
title: Create and configure announcement banner
description: Creat a banner to show important updates and information at the top of on every page
refs:
rbac:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/
description: How to create an announcement banner to show important updates and information at the top of every Grafana page.
---
# Create and configure announcement banner
# Create an announcement banner
Announcement banner allows you to show important updates and information at the top of every page in Grafana. You can use the announcement banner to communicate important information to your users, such as maintenance windows, new features, or other important updates.
An announcement banner shows at the top of every page in Grafana. You can use the announcement banner to communicate information to your users, such as maintenance windows, new features, or other important updates.
## Create or update an announcement banner
Only organization administrators can create announcement banner by default. You can customize who can create announcement banner with [Role-based access control](ref:rbac).
By default, only organization administrators can create announcement banners. You can customize who can create announcement banners with [Role-based access control](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/).
To create or update an announcement banner, follow these steps:
1. Click **Administration > General > Announcement banner** in the side navigation menu.
The Announcement banner page allows you to view, create and update the settings for a notification banner. Only one banner can be created at a time.
The **Announcement banner** page allows you to view, create, and update the settings for an announcement banner.
Only one banner can be active at a time.
2. Toggle the **Enable** switch on to enable the announcement banner. It can be toggled off at any time to disable the banner.
1. Toggle the **Enable** switch on to enable the announcement banner.
You can disable the banner at any time with this toggle.
1. Enter the **Message** for the announcement banner.
The message field supports Markdown.
3. Enter the **Message** for the announcement banner.
The message field supports Markdown. To add a header, use the following syntax:
To add a header, use the following syntax:
```markdown
### Header
@ -54,26 +44,18 @@ To create or update an announcement banner, follow these steps:
[link text](https://www.example.com)
```
The preview of the configured banner will appear on top of the form, under the **Preview** section.
4. Select the banner's start date and time in the **Starts** field.
By default, the banner starts being displayed immediately. You can set a future date and time for the banner to start displaying.
5. Select the banner's end date and time in the **Ends** field.
By default, the banner is displayed indefinitely. You can set a future date and time for the banner to stop displaying.
6. Select the banner's visibility.
The preview of the configured banner appears on top of the form, under the **Preview** section.
1. Select the banner's start date and time in the **Starts at** field.
By default, the banner starts being displayed immediately.
You can set a future date and time for the banner to start displaying.
1. Select the banner's end date and time in the **Ends at** field.
By default, the banner is displayed indefinitely.
You can set a date and time for the banner to stop displaying.
1. Select the banner's visibility.
**Everyone** - The banner is visible to all users, including on login page.
**Authenticated users** - The banner is visible to only authenticated users.
7. Select the type of banner in the **Variant** field.
This will determine the color of the banner's background.
8. Click **Save** to save the banner settings.
The banner will now be displayed at the top of every page in Grafana.
1. Select the type of banner in the **Variant** field.
This determines the color of the banner's background.
1. Click **Save** to save the banner settings.
The banner displays at the top of every page in Grafana between the start and end dates.

View File

@ -49,7 +49,7 @@ To follow these instructions, you need at least one of the following:
### Steps
To create an API, complete the following steps:
To create an API key, complete the following steps:
1. Sign in to Grafana.
1. Click **Administration** in the left-side menu, **Users and access**, and select **API Keys**.

View File

@ -91,7 +91,7 @@ roles:
# <bool> force deletion revoking all grants of the role.
force: true
- uid: 'basic_editor'
# <bool> always apply the specified changes to the role, regardless of the role version in the data base
# <bool> always apply the specified changes to the role, regardless of the role version in the database
overrideRole: true
global: true
# <list> list of roles to copy permissions from.

View File

@ -39,52 +39,75 @@ You can control permissions for library panels using [role-based access control
When you create a library panel, the panel on the source dashboard is converted to a library panel as well. You need to save the original dashboard once a panel is converted.
1. Open a panel in edit mode.
1. In the panel display options, click the down arrow option to bring changes to the visualization.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-create-lib-panel-from-edit-9-5.png" class="docs-image--no-shadow" max-width= "800px" alt="Library panels tab of the panel editor pane" >}}
1. Click **Library panels**, and then click **+ Create library panel** to open the create dialog.
1. Click **Edit** in the top-right corner of the dashboard.
1. On the panel you want to update, hover over any part of the panel to display the menu icon on the top-right corner.
1. Click the menu icon and select **More > Create library panel**.
1. In **Library panel name**, enter the name.
1. In **Save in folder**, select the folder to save the library panel.
1. Click **Create library panel** to save your changes.
1. Save the dashboard.
1. Click **Create library panel**.
1. Click **Save dashboard** and **Exit edit**.
Once created, you can modify the library panel using any dashboard on which it appears. After you save the changes, all instances of the library panel reflect these modifications.
You can also create a library panel directly from the edit menu of any panel.
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-create-from-more-9-5.png" class="docs-image--no-shadow" max-width= "900px" alt="Create library panel option in the panel menu" >}}
## Add a library panel to a dashboard
Add a Grafana library panel to a dashboard when you want to provide visualizations to other dashboard users.
1. Click **Dashboards** in the left-side menu.
1. Click **Dashboards** in the main menu.
1. Click **New** and select **New Dashboard** in the dropdown.
1. On the empty dashboard, click **+ Import library panel**.
1. On the empty dashboard, click **+ Add library panel**.
You will see a list of your library panels.
You'll see a list of your library panels.
1. Filter the list or search to find the panel you want to add.
1. Click a panel to add it to the dashboard.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
## Unlink a library panel
Unlink a library panel when you want to make a change to the panel and not affect other instances of the library panel.
1. Click **Dashboards** in the left-side menu.
1. Click **Dashboards** in the main menu.
1. Click **Library panels**.
1. Select a library panel that is being used in dashboards.
1. Click the panel you want to unlink.
1. In the dialog box, select the dashboard from which you want to unlink the panel.
1. Click **View panel in \<dashboard name\>**.
1. Click **Edit** in the top-right corner of the dashboard.
1. Hover over any part of the panel you want to unlink to display the menu icon on the top-right corner.
1. Click the menu icon and select **More > Unlink library panel**.
1. Click **Yes, unlink**.
1. Click **Save dashboard** and **Exit edit**.
Alternatively, if you know where the library panel is being used, you can go directly to that dashboard and start at step 7.
## Replace a library panel
To replace a library panel with a different one, follow these steps:
1. Click **Dashboards** in the main menu.
1. Click **Library panels**.
1. Select a library panel that is being used in different dashboards.
1. Select the panel you want to unlink.
1. Hover over any part of the panel to display the actions menu on the top right corner.
1. Click the menu and select **Edit**.
1. Click **Unlink** on the top right corner of the page.
1. Click **Yes, unlink**.
1. Click the panel you want to unlink.
1. In the dialog box, select the dashboard from which you want to unlink the panel.
1. Click **View panel in \<dashboard name\>**.
1. Click **Edit** in the top-right corner of the dashboard.
1. Hover over any part of the panel you want to unlink to display the menu icon on the top-right corner.
1. Click the menu icon and select **More > Replace library panel**.
1. Select the replacement library panel.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save** and **Exit edit**.
Alternatively, if you know where the library panel that you want to replace is being used, you can go directly to that dashboard and start at step 7.
## View a list of library panels
You can view a list of available library panels and search for a library panel.
You can view a list of available library panels and see where those panels are being used.
1. Click **Dashboards** in the left-side menu.
1. Click **Dashboards** in the main menu.
1. Click **Library panels**.
You can see a list of previously defined library panels.
@ -94,10 +117,14 @@ You can view a list of available library panels and search for a library panel.
You can also filter the panels by folder or type.
1. Click the panel to see if it's being used in any dashboards.
1. (Optional) If the library panel is in use, select one of the dashboards using it.
1. (Optional) Click **View panel in \<dashboard name\>** to see the panel in context.
## Delete a library panel
Delete a library panel when you no longer need it.
1. Click **Dashboards** in the left-side menu.
1. Click **Dashboards** in the main menu.
1. Click **Library panels**.
1. Click the delete icon next to the library panel name.

View File

@ -26,7 +26,7 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/
destination: /docs/grafana-cloud/visualizations/dashboards/assess-dashboard-usage/
generative-ai-features:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards
@ -36,12 +36,37 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/modify-dashboard-settings/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/modify-dashboard-settings/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/modify-dashboard-settings/
repeating-rows:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
dashboard-folders:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/manage-dashboards/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/manage-dashboards/
sharing:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/share-dashboards-panels/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/share-dashboards-panels/
dashboard-links:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/manage-dashboard-links/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/manage-dashboard-links/
panel-overview:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/panel-overview/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-overview/
---
# Use dashboards
@ -56,32 +81,30 @@ The dashboard user interface provides a number of features that you can use to c
The following image and descriptions highlight all dashboard features.
{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-annotated-9-5-0.png" width="700px" alt="An annotated image of a dashboard" >}}
![An annotated image of a dashboard](/media/docs/grafana/dashboards/screenshot-dashboard-annotated-11.2.png)
- (1) **Grafana home**: Click **Home** in the breadcrumb to be redirected to the home page configured in the Grafana instance.
- (2) **Dashboard title**: When you click the dashboard title, you can search for dashboards contained in the current folder. You can create your own dashboard titles or have Grafana create them for you using [generative AI features](ref:generative-ai-features).
- (3) **Share dashboard or panel**: Use this option to share the current dashboard or panel using a link or snapshot. You can also export the dashboard definition from the share modal.
- (4) **Add**: Use this option to add a panel, dashboard row, or library panel to the current dashboard.
- (5) **Save dashboard**: Click to save changes to your dashboard.
- (6) **Dashboard insights**: Click to view analytics about your dashboard including information about users, activity, query counts. Learn more about [dashboard analytics](ref:dashboard-analytics).
- (7) **Dashboard settings**: Use this option to change dashboard name, folder, and tags and manage variables and annotation queries. Learn more about [dashboard settings](ref:dashboard-settings).
- (8) **Time picker dropdown**: Click to select relative time range options and set custom absolute time ranges.
- You can change the **Timezone** and **fiscal year** settings from the time range controls by clicking the **Change time settings** button.
- Time settings are saved on a per-dashboard basis.
- (9) **Zoom out time range**: Click to zoom out the time range. Learn more about how to use [common time range controls](#common-time-range-controls).
- (10) **Refresh dashboard**: Click to immediately trigger queries and refresh dashboard data.
- (11) **Refresh dashboard time interval**: Click to select a dashboard auto refresh time interval.
- (12) **View mode**: Click to display the dashboard on a large screen such as a TV or a kiosk. View mode hides irrelevant information such as navigation menus. Learn more about view mode in our [How to Create Kiosks to Display Dashboards on a TV blog post](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/).
- (13) **Dashboard panel**: The primary building block of a dashboard is the panel. To add a new panel, dashboard row, or library panel, click **Add panel**.
- Library panels can be shared among many dashboards.
- To move a panel, drag the panel header to another location.
- To resize a panel, click and drag the lower right corner of the panel.
- Use [generative AI features](ref:generative-ai-features) to create panel titles and descriptions.
- (14) **Graph legend**: Change series colors, y-axis and series visibility directly from the legend.
- (15) **Dashboard row**: A dashboard row is a logical divider within a dashboard that groups panels together.
- Rows can be collapsed or expanded allowing you to hide parts of the dashboard.
- Panels inside a collapsed row do not issue queries.
- Use [repeating rows](ref:repeating-rows) to dynamically create rows based on a template variable.
1. **Grafana home** - Click **Home** in the breadcrumb to go to the home page configured in the Grafana instance.
1. **Dashboard folder** - When you click the dashboard folder name, you can search for other dashboards contained in the folder and perform other [folder management tasks](ref:dashboard-folders).
1. **Dashboard title** - You can create your own dashboard titles or have Grafana create them for you using [generative AI features](ref:generative-ai-features).
1. **Mark as favorite** - Mark the dashboard as one of your favorites so it's included in your list of **Starred** dashboards in the main menu.
1. **Dashboard insights** - Click to view analytics about your dashboard including information about users, activity, query counts. Learn more about [dashboard analytics](ref:dashboard-analytics).
1. **Share dashboard** - Access several [dashboard sharing](ref:sharing) options.
1. **Edit** - Click to leave view-only mode and enter edit mode, where you can make changes directly to the dashboard and access dashboard settings, as well as several panel editing functions.
1. **Kiosk mode** - Click to display the dashboard on a large screen such as a TV or a kiosk. Kiosk mode hides elements such as navigation menus. Learn more about kiosk mode in our [How to Create Kiosks to Display Dashboards on a TV blog post](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/). Press `Enter` to leave kiosk mode.
1. **Variables** - Use [variables](ref:variables) to create more interactive and dynamic dashboards.
1. **Dashboard links** - Link to other dashboards, panels, and external websites. Learn more about [dashboard links](ref:dashboard-links).
1. **Current dashboard time range and time picker** - Click to select [relative time range](#relative-time-range) options and set custom [absolute time ranges](#absolute-time-range).
- You can change the **Timezone** and **Fiscal year** settings from the time range controls by clicking the **Change time settings** button.
- Time settings are saved on a per-dashboard basis.
1. **Time range zoom out** - Click to zoom out the time range. Learn more about how to use [common time range controls](#common-time-range-controls).
1. **Refresh dashboard** - Click to immediately trigger queries and refresh dashboard data.
1. **Auto refresh control** - Click to select a dashboard auto refresh time interval.
1. **Dashboard row** - A dashboard row is a logical divider within a dashboard that groups panels together.
- Rows can be collapsed or expanded allowing you to hide parts of the dashboard.
- Panels inside a collapsed row do not issue queries.
- Use [repeating rows](ref:repeating-rows) to dynamically create rows based on a template variable.
1. **Dashboard panel** - The [panel](ref:panel-overview) is the primary building block of a dashboard.
1. **Panel legend** - Change series colors as well as y-axis and series visibility directly from the legend.
## Keyboard shortcuts
@ -148,7 +171,7 @@ Grafana Alerting does not support the following syntaxes at this time:
The dashboard and panel time controls have a common UI.
<img class="no-shadow" src="/static/img/docs/time-range-controls/common-time-controls-7-0.png" max-width="700px">
![Common time controls](/media/docs/grafana/dashboards/screenshot-common-time-controls-11.2.png)
The following sections define common time range controls.
@ -158,11 +181,11 @@ The current time range, also called the _time picker_, shows the time range curr
Hover your cursor over the field to see the exact time stamps in the range and their source (such as the local browser).
<img class="no-shadow" src="/static/img/docs/time-range-controls/time-picker-7-0.png" max-width="300px">
![Time picker](/media/docs/grafana/dashboards/screenshot-time-picker-11.2.png)
Click the current time range to change it. You can change the current time using a _relative time range_, such as the last 15 minutes, or an _absolute time range_, such as `2020-05-14 00:00:00 to 2020-05-15 23:59:59`.
<img class="no-shadow" src="/media/docs/grafana/dashboards/screenshot-change-current-time-range-10.3.png" max-width="900px">
![Current time range](/media/docs/grafana/dashboards/screenshot-current-time-range-11.2.png)
#### Relative time range

View File

@ -135,9 +135,12 @@ Query expressions are different for each data source. For more information, refe
- **On Dashboard Load:** Queries the data source every time the dashboard loads. This slows down dashboard loading, because the variable query needs to be completed before dashboard can be initialized.
- **On Time Range Change:** Queries the data source every time the dashboard loads and when the dashboard time range changes. Use this option if your variable options query contains a time range filter or is dependent on the dashboard time range.
1. In the **Query** field, enter a query.
- The query field varies according to your data source. Some data sources have custom query editors.
- Make sure that the query returns values named `__text` and `__value` as appropriate in your query syntax. For example, in SQL, you can use a query such as `SELECT hostname AS __text, id AS __value FROM MyTable`. Queries for other languages will vary depending on syntax.
- Each data source defines how the variable values are extracted. The typical implementation uses every string value returned from the data source response as a variable value. Make sure to double-check the documentation for the data source.
- Some data sources let you provide custom "display names" for the values. For instance, the PostgreSQL, MySQL, and Microsoft SQL Server plugins handle this by looking for fields named `__text` and `__value` in the result. Other data sources may look for `text` and `value` or use a different approach. Always remember to double-check the documentation for the data source.
- If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand.
1. (Optional) In the **Regex** field, type a regex expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with regex](#filter-variables-with-regex).
1. In the **Sort** list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query will be used.
1. (Optional) Enter [Selection Options](#configure-variable-selection-options).

View File

@ -28,20 +28,22 @@ weight: 200
# Manage and inspect variables
The variables page lets you [add](ref:add) variables and [manage](#manage-variables) existing variables. It also allows you to [inspect](#inspect-variables) variables and identify whether a variable is being referenced (or used) in other variables or dashboard.
In the **Variables** tab, you can [add](ref:add) variables and [manage](#manage-variables) existing variables. You can also [inspect](#inspect-variables) variables to identify any dependencies between them. <!--whether a variable is being referenced (or used) in other variables or dashboard.-->
## Manage variables
You can take the following actions on the variables page:
You can take the following actions in the **Variables** tab:
**Move:** You can move a variable up or down the list using drag and drop.
**Clone:** To clone a variable, click the clone icon from the set of icons on the right. This creates a copy of the variable with the name of the original variable prefixed with `copy_of_`.
**Delete:** To delete a variable, click the trash icon from the set of icons on the right.
- **Move** - Move a variable up or down the list using drag and drop.
- **Clone** - Clone a variable by clicking the clone icon in the set of icons on the right. This creates a copy of the variable with the name of the original variable prefixed with `copy_of_`.
- **Delete** - Delete a variable by clicking the trash icon in the set of icons on the right.
## Inspect variables
In addition to [managing variables](#manage-variables), the **Variables** tab lets you easily identify whether variables have any dependencies. To check, click **Show dependencies** at the bottom of the list, which opens the dependencies diagram:
<!-- Update and comment this back in when the reference functionality is working again
The variables page lets you easily identify whether a variable is being referenced (or used) in other variables or dashboard. In addition, you can also [add](ref:add) and [manage variables](#manage-variables) on this page.
{{% admonition type="note" %}}
@ -54,6 +56,10 @@ Any variable that is referenced or used has a green check mark next to it, while
![Variables list](/static/img/docs/variables-templates/variable-not-referenced-7-4.png)
In addition, all referenced variables have a dependency icon next to the green check mark. You can click on the icon to view the dependency map. The dependency map can be moved. You can zoom in out with mouse wheel or track pad equivalent.
In addition, all referenced variables have a dependency icon next to the green check mark. You can click on the icon to view the dependency map. The dependency map can be moved. You can zoom in out with mouse wheel or track pad equivalent.-->
![Variables list](/static/img/docs/variables-templates/dependancy-map-7-4.png)
{{% admonition type="note" %}}
This feature is available in Grafana 7.4 and later versions.
{{% /admonition %}}

View File

@ -892,6 +892,12 @@ The duration in time a verification email, used to update the email address of a
This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week).
Default is 1h (1 hour).
### last_seen_update_interval
The frequency of updating a user's last seen time.
This setting should be expressed as a duration. Examples: 1h (hour), 15m (minutes)
Default is `15m` (15 minutes). The minimum supported duration is `5m` (5 minutes). The maximum supported duration is `1h` (1 hour).
### hidden_users
This is a comma-separated list of usernames. Users specified here are hidden in the Grafana UI. They are still visible to Grafana administrators and to themselves.

View File

@ -27,7 +27,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `publicDashboards` | [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. | Yes |
| `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes |
| `exploreContentOutline` | Content outline sidebar | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `nestedFolders` | Enable folder nesting | Yes |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |

View File

@ -17,8 +17,6 @@ weight: 50
# Get started with Grafana Alerting - Part 2
## Introduction
This is part 2 of the [Get Started with Grafana Alerting tutorial](http://grafana.com/tutorials/alerting-get-started/).
In this guide, we dig into more complex yet equally fundamental elements of Grafana Alerting: **alert instances** and **notification policies**.
@ -65,7 +63,7 @@ Create a notification policy if you want to handle metrics returned by alert rul
1. In the field **Label** enter `device`, and in the field **Value** enter `desktop`.
1. From the **Contact point** drop-down, choose **Webhook**.
{{< admonition type="note" >}}
If you dont have any contact points, add a [Contact point](http://localhost:3002/docs/grafana/latest/tutorials/alerting-get-started/#create-a-contact-point).
If you dont have any contact points, add a [Contact point](https://grafana.com/tutorials/alerting-get-started/#create-a-contact-point).
{{</ admonition >}}
1. Click **Save Policy**.

View File

@ -25,6 +25,10 @@ In this tutorial you will:
- Set up an alert rule.
- Receive firing and resolved alert notifications in a public webhook.
{{< admonition type="tip" >}}
Check out [Part 2](http://grafana.com/tutorials/alerting-get-started-pt2/) if you want to learn more about alerts and notification routing.
{{< /admonition >}}
## Before you begin
### Grafana Cloud users
@ -184,6 +188,10 @@ To edit the Alert rule:
By incrementing the threshold, the condition is no longer met, and after the evaluation interval has concluded (1 minute approx.), you should receive an alert notification with status **“Resolved”**.
## Learn more
Your learning journey continues in [Part 2](http://grafana.com/tutorials/alerting-get-started-pt2/) where you will learn about alert instances and notification routing.
## Summary
In this tutorial, you have learned how to set up a contact point, create an alert, and send alert notifications to a public Webhook. By following these steps, youve gained a foundational understanding of how to leverage Grafana Alerting capabilities to monitor and respond to events of interest in your data.

View File

@ -17,7 +17,7 @@ describe('Public dashboards', () => {
e2e.pages.Dashboard.DashNav.shareButton().click();
// Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e.components.Tab.title('Public dashboard').click();
// Create button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled');
@ -78,7 +78,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
@ -118,7 +118,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard');
// save url before disabling public dashboard

View File

@ -13,7 +13,7 @@ describe('Create a public dashboard with template variables shows a template var
e2e.pages.Dashboard.DashNav.shareButton().click();
// Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e.components.Tab.title('Public dashboard').click();
// Warning Alert dashboard cannot be made public because it has template variables
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');

View File

@ -0,0 +1,39 @@
import * as e2e from '@grafana/e2e-selectors';
import { expect, test } from '@grafana/plugin-e2e';
test('should evaluate to false if entire request returns 500', async ({ page, alertRuleEditPage, selectors }) => {
await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule');
// remove the default query
const queryA = alertRuleEditPage.getAlertRuleQueryRow('A');
await alertRuleEditPage
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Remove query'), {
root: queryA.locator,
})
.click();
await expect(alertRuleEditPage.evaluate()).not.toBeOK();
});
test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({
page,
alertRuleEditPage,
}) => {
await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule');
//add working query
const queryA = alertRuleEditPage.getAlertRuleQueryRow('A');
await queryA.datasource.set('gdev-prometheus');
await queryA.locator.getByLabel('Code').click();
await page.waitForFunction(() => window.monaco);
await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click();
await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))');
//add broken query
const newQuery = await alertRuleEditPage.clickAddQueryRow();
await newQuery.datasource.set('gdev-prometheus');
await newQuery.locator.getByLabel('Code').click();
await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click();
await page.keyboard.insertText('topk(5,');
await expect(alertRuleEditPage.evaluate()).not.toBeOK();
});

View File

@ -106,6 +106,11 @@ case "$1" in
;;
esac
;;
"scenes/"*)
cypressConfig[specPattern]=./e2e/"${args[0]}"/$testFilesForSingleSuite
cypressConfig[video]=${args[1]}
env[SCENES]=true
;;
"enterprise-smtp")
env[SMTP_PLUGIN_ENABLED]=true
cypressConfig[specPattern]=./e2e/extensions/enterprise/smtp-suite/$testFilesForSingleSuite

View File

@ -1,7 +1,7 @@
import testDashboard from '../dashboards/TestDashboard.json';
import { e2e } from '../utils';
describe('Dashboard browse', () => {
// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-browse.spec.ts
describe.skip('Dashboard browse', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});

View File

@ -1,6 +1,6 @@
import { e2e } from '../utils';
describe('Public dashboards', () => {
// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-public-create.spec.ts
describe.skip('Public dashboards', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
@ -17,7 +17,7 @@ describe('Public dashboards', () => {
e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
e2e.components.Tab.title('Public dashboard').click();
// Create button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled');
@ -78,7 +78,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
@ -118,7 +118,7 @@ describe('Public dashboards', () => {
// Select public dashboards tab
cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
e2e.components.Tab.title('Public dashboard').click();
cy.wait('@query-public-dashboard');
// save url before disabling public dashboard

View File

@ -13,7 +13,7 @@ describe('Create a public dashboard with template variables shows a template var
e2e.components.NavToolbar.shareDashboard().click();
// Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboardScene.Tab().click();
e2e.components.Tab.title('Public Dashboard').click();
// Warning Alert dashboard cannot be made public because it has template variables
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');

View File

@ -25,7 +25,7 @@ describe('Snapshots', () => {
e2e.components.NavToolbar.shareDashboard().click();
// Select the snapshot tab
e2e.pages.ShareDashboardModal.SnapshotScene.Tab().click();
e2e.components.Tab.title('Snapshot').click();
// Publish snapshot
cy.intercept('POST', '/api/snapshots').as('save');

View File

@ -2,8 +2,8 @@ import panelSandboxDashboard from '../../dashboards/PanelSandboxDashboard.json';
import { e2e } from '../../utils';
const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950';
describe('Panel sandbox', () => {
// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts
describe.skip('Panel sandbox', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true);

View File

@ -17,8 +17,8 @@ const addDataSource = () => {
},
});
};
describe('Exemplars', () => {
// Skipping due to race conditions with same old arch test e2e/various-suite/exemplars.spec.ts
describe.skip('Exemplars', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));

View File

@ -1,7 +1,8 @@
import { e2e } from '../utils';
const DASHBOARD_ID = 'ed155665';
describe('Annotations filtering', () => {
// Skipping due to race conditions with same old arch test e2e/various-suite/filter-annotations.spec.ts
describe.skip('Annotations filtering', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});

View File

@ -1,7 +1,8 @@
import { e2e } from '../utils';
import { fromBaseUrl } from '../utils/support/url';
describe('Keyboard shortcuts', () => {
// Skipping due to race conditions with same old arch test e2e/various-suite/keybinds.spec.ts
describe.skip('Keyboard shortcuts', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));

View File

@ -110,7 +110,8 @@ const lokiQueryResult = {
},
};
describe('Loki Query Editor', () => {
// Skipping due to race conditions with same old arch test e2e/various-suite/loki-table-explore-to-dash.spec.ts
describe.skip('Loki Query Editor', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});

View File

@ -77,8 +77,8 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) {
// do nothing
}
}
describe('Prometheus variable query editor', () => {
// Skipping due to race conditions with same old arch test e2e/various-suite/prometheus-variable-editor.spec.ts
describe.skip('Prometheus variable query editor', () => {
beforeEach(() => {
createPromDS(DATASOURCE_ID, DATASOURCE_NAME);
});

View File

@ -479,6 +479,7 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH
github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4=
github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4=
github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM=
@ -885,6 +886,7 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=

View File

@ -76,7 +76,7 @@
"@emotion/eslint-plugin": "11.11.0",
"@grafana/eslint-config": "7.0.0",
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules",
"@grafana/plugin-e2e": "1.3.2",
"@grafana/plugin-e2e": "1.4.0",
"@grafana/tsconfig": "^1.3.0-rc1",
"@manypkg/get-packages": "^2.2.0",
"@playwright/test": "1.44.1",

View File

@ -29,7 +29,6 @@ export interface FeatureToggles {
featureHighlights?: boolean;
storage?: boolean;
correlations?: boolean;
exploreContentOutline?: boolean;
datasourceQueryMultiStatus?: boolean;
autoMigrateOldPanels?: boolean;
autoMigrateGraphPanel?: boolean;

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { store } from '../store';
import { store } from './store';
export interface Props<T> {
storageKey: string;

View File

@ -13,6 +13,8 @@ export * from './binaryOperators';
export * from './unaryOperators';
export * from './nodeGraph';
export * from './selectUtils';
export * from './store';
export * from './LocalStorageValueProvider';
export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders';
export { arrayUtils };
export { getFlotPairs, getFlotPairsConstant } from './flotPairs';

View File

@ -266,7 +266,7 @@ export const Components = {
},
},
Tab: {
title: (title: string) => `Tab ${title}`,
title: (title: string) => `data-testid Tab ${title}`,
active: () => '[class*="-activeTabStyle"]',
},
RefreshPicker: {

View File

@ -66,6 +66,7 @@ export const Pages = {
container: 'data-testid new share button menu',
shareInternally: 'data-testid new share button share internally',
shareExternally: 'data-testid new share button share externally',
shareSnapshot: 'data-testid new share button share snapshot',
},
},
playlistControls: {
@ -103,7 +104,7 @@ export const Pages = {
* @deprecated use components.TimeZonePicker.containerV2 from Grafana 8.3 instead
*/
timezone: 'Time zone picker select container',
title: 'Tab General',
title: 'General',
},
Annotations: {
List: {
@ -245,7 +246,6 @@ export const Pages = {
},
ShareDashboardModal: {
PublicDashboard: {
Tab: 'Tab Public dashboard',
WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox',
LimitedDSCheckbox: 'data-testid public dashboard limited datasources checkbox',
CostIncreaseCheckbox: 'data-testid public dashboard cost may increase checkbox',
@ -270,12 +270,8 @@ export const Pages = {
ReshareLink: 'data-testid public dashboard reshare link button',
},
},
PublicDashboardScene: {
Tab: 'Tab Public Dashboard',
},
SnapshotScene: {
url: (key: string) => `/dashboard/snapshot/${key}`,
Tab: 'Tab Snapshot',
PublishSnapshot: 'data-testid publish snapshot button',
CopyUrlButton: 'data-testid snapshot copy url button',
CopyUrlInput: 'data-testid snapshot copy url input',
@ -287,6 +283,9 @@ export const Pages = {
copyUrlButton: 'data-testid share externally copy url button',
shareTypeSelect: 'data-testid share externally share type select',
},
ShareSnapshot: {
container: 'data-testid share snapshot drawer container',
},
},
PublicDashboard: {
page: 'public-dashboard-page',

View File

@ -13,6 +13,4 @@ export * from './TraceToLogs/TraceToLogsSettings';
export * from './TraceToMetrics/TraceToMetricsSettings';
export * from './TraceToProfiles/TraceToProfilesSettings';
export * from './utils';
export * from './store';
export * from './LocalStorageValueProvider/LocalStorageValueProvider';
export * from './combineResponses';

View File

@ -1,6 +0,0 @@
// reset font file paths so storybook loads them based on
// staticDirs defined in packages/grafana-ui/.storybook/main.ts
$font-file-path: './public/fonts';
$fa-font-path: $font-file-path;
@import '../../../public/sass/grafana.dark.scss';

View File

@ -1,6 +0,0 @@
// reset font file paths so storybook loads them based on
// staticDirs defined in packages/grafana-ui/.storybook/main.ts
$font-file-path: './public/fonts';
$fa-font-path: $font-file-path;
@import '../../../public/sass/grafana.light.scss';

View File

@ -17,9 +17,9 @@ import { withTimeZone } from '../src/utils/storybook/withTimeZone';
import { ThemedDocsContainer } from '../src/utils/storybook/ThemedDocsContainer';
// @ts-ignore
import lightTheme from './grafana.light.scss';
import lightTheme from '../../../public/sass/grafana.light.scss';
// @ts-ignore
import darkTheme from './grafana.dark.scss';
import darkTheme from '../../../public/sass/grafana.dark.scss';
import { GrafanaDark, GrafanaLight } from './storybookTheme';
const handleThemeChange = (theme: any) => {

View File

@ -43,9 +43,9 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
const commonProps = {
className: linkClass,
'data-testid': selectors.components.Tab.title(label),
...otherProps,
onClick: onChangeTab,
'aria-label': otherProps['aria-label'] || selectors.components.Tab.title(label),
role: 'tab',
'aria-selected': active,
};

View File

@ -231,6 +231,7 @@ export { FieldArray } from './Forms/FieldArray';
// Select
export { default as resetSelectStyles } from './Select/resetSelectStyles';
export * from './Select/Select';
export { SelectMenuOptions } from './Select/SelectMenu';
export { getSelectStyles } from './Select/getSelectStyles';
export * from './Select/types';

View File

@ -9,6 +9,7 @@ import { getCardStyles } from './card';
import { getCodeStyles } from './code';
import { getElementStyles } from './elements';
import { getExtraStyles } from './extra';
import { getFontAwesomeStyles } from './fontAwesome';
import { getFontStyles } from './fonts';
import { getFormElementStyles } from './forms';
import { getJsonFormatterStyles } from './jsonFormatter';
@ -31,6 +32,7 @@ export function GlobalStyles() {
getCodeStyles(theme),
getElementStyles(theme),
getExtraStyles(theme),
getFontAwesomeStyles(theme),
getFontStyles(theme),
getFormElementStyles(theme),
getJsonFormatterStyles(theme),

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,9 @@ import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
export function getFontStyles(theme: GrafanaTheme2) {
const grafanaPublicPath = typeof window !== 'undefined' && window.__grafana_public_path__;
const fontRoot = grafanaPublicPath ? `${grafanaPublicPath}fonts/` : 'public/fonts/';
return css([
{
/* latin */
@ -11,7 +14,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal',
fontWeight: 400,
fontDisplay: 'swap',
src: "url('./public/fonts/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')",
src: `url('${fontRoot}roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')`,
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
@ -23,7 +26,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal',
fontWeight: 500,
fontDisplay: 'swap',
src: "url('./public/fonts/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')",
src: `url('${fontRoot}roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')`,
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
@ -42,7 +45,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal',
fontWeight: 400,
fontDisplay: 'swap',
src: "url('./public/fonts/inter/Inter-Regular.woff2') format('woff2')",
src: `url('${fontRoot}inter/Inter-Regular.woff2') format('woff2')`,
},
},
{
@ -51,7 +54,7 @@ export function getFontStyles(theme: GrafanaTheme2) {
fontStyle: 'normal',
fontWeight: 500,
fontDisplay: 'swap',
src: "url('./public/fonts/inter/Inter-Medium.woff2') format('woff2')",
src: `url('${fontRoot}inter/Inter-Medium.woff2') format('woff2')`,
},
},
]);

View File

@ -373,13 +373,18 @@ func (proxy *DataSourceProxy) logRequest() {
panelPluginId := proxy.ctx.Req.Header.Get("X-Panel-Plugin-Id")
uri, err := util.SanitizeURI(proxy.ctx.Req.RequestURI)
if err == nil {
proxy.ctx.Logger.Error("Could not sanitize RequestURI", "error", err)
}
ctxLogger := logger.FromContext(proxy.ctx.Req.Context())
ctxLogger.Info("Proxying incoming request",
"userid", proxy.ctx.UserID,
"orgid", proxy.ctx.OrgID,
"username", proxy.ctx.Login,
"datasource", proxy.ds.Type,
"uri", proxy.ctx.Req.RequestURI,
"uri", uri,
"method", proxy.ctx.Req.Method,
"panelPluginId", panelPluginId,
"body", body)

View File

@ -54,7 +54,7 @@ func (d *DualWriterMode1) Create(ctx context.Context, original runtime.Object, c
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage create timeout"))
defer cancel()
if err := enrichLegacyObject(original, createdCopy, true); err != nil {
if err := enrichLegacyObject(original, createdCopy); err != nil {
cancel()
}
@ -201,7 +201,7 @@ func (d *DualWriterMode1) Update(ctx context.Context, name string, objInfo rest.
// if the object is found, create a new updateWrapper with the object found
if foundObj != nil {
if err := enrichLegacyObject(foundObj, resCopy, false); err != nil {
if err := enrichLegacyObject(foundObj, resCopy); err != nil {
log.Error(err, "could not enrich object")
cancel()
}

View File

@ -50,7 +50,7 @@ func (d *DualWriterMode2) Create(ctx context.Context, original runtime.Object, c
}
d.recordLegacyDuration(false, mode2Str, options.Kind, method, startLegacy)
if err := enrichLegacyObject(original, created, true); err != nil {
if err := enrichLegacyObject(original, created); err != nil {
return created, err
}
@ -261,7 +261,7 @@ func (d *DualWriterMode2) Update(ctx context.Context, name string, objInfo rest.
// if the object is found, create a new updateWrapper with the object found
if foundObj != nil {
err = enrichLegacyObject(foundObj, obj, false)
err = enrichLegacyObject(foundObj, obj)
if err != nil {
return obj, false, err
}
@ -328,7 +328,7 @@ func parseList(legacyList []runtime.Object) (metainternalversion.ListOptions, ma
return options, indexMap, nil
}
func enrichLegacyObject(originalObj, returnedObj runtime.Object, created bool) error {
func enrichLegacyObject(originalObj, returnedObj runtime.Object) error {
accessorReturned, err := meta.Accessor(returnedObj)
if err != nil {
return err
@ -350,13 +350,6 @@ func enrichLegacyObject(originalObj, returnedObj runtime.Object, created bool) e
}
accessorReturned.SetAnnotations(ac)
// if the object is created, we need to reset the resource version and UID
// create method expects an empty resource version
if created {
accessorReturned.SetResourceVersion("")
accessorReturned.SetUID("")
return nil
}
// otherwise, we propagate the original RV and UID
accessorReturned.SetResourceVersion(accessorOriginal.GetResourceVersion())
accessorReturned.SetUID(accessorOriginal.GetUID())

View File

@ -79,7 +79,7 @@ func TestMode2_Create(t *testing.T) {
assert.Equal(t, exampleObj, obj)
accessor, err := meta.Accessor(obj)
assert.NoError(t, err)
assert.Equal(t, accessor.GetResourceVersion(), "")
assert.Equal(t, accessor.GetResourceVersion(), "1")
})
}
}
@ -493,83 +493,7 @@ func TestEnrichReturnedObject(t *testing.T) {
wantErr bool
}{
{
name: "create: original object does not have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5")},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID("")},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "create: returned object does not have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6")},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID(""), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "create: both objects have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6"), Labels: map[string]string{"label2": "2"}, Annotations: map[string]string{"annotation2": "2"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID(""), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "create: both objects have labels and annotations with duplicated keys",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
inputReturned: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "2", UID: types.UID("6"), Labels: map[string]string{"label1": "11"}, Annotations: map[string]string{"annotation1": "11"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
isCreated: true,
expectedObject: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", UID: types.UID(""), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
Spec: example.PodSpec{}, Status: example.PodStatus{},
},
},
{
name: "update: original object does not have labels and annotations",
name: "original object does not have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5")},
@ -587,7 +511,7 @@ func TestEnrichReturnedObject(t *testing.T) {
},
},
{
name: "update: returned object does not have labels and annotations",
name: "returned object does not have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
@ -605,7 +529,7 @@ func TestEnrichReturnedObject(t *testing.T) {
},
},
{
name: "update: both objects have labels and annotations",
name: "both objects have labels and annotations",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
@ -623,7 +547,7 @@ func TestEnrichReturnedObject(t *testing.T) {
},
},
{
name: "update: both objects have labels and annotations with duplicated keys",
name: "both objects have labels and annotations with duplicated keys",
inputOriginal: &example.Pod{
TypeMeta: metav1.TypeMeta{Kind: "foo"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", UID: types.UID("5"), Labels: map[string]string{"label1": "1"}, Annotations: map[string]string{"annotation1": "1"}},
@ -658,7 +582,7 @@ func TestEnrichReturnedObject(t *testing.T) {
for _, tt := range testCase {
t.Run(tt.name, func(t *testing.T) {
err := enrichLegacyObject(tt.inputOriginal, tt.inputReturned, tt.isCreated)
err := enrichLegacyObject(tt.inputOriginal, tt.inputReturned)
if tt.wantErr {
assert.Error(t, err)
return

View File

@ -19,11 +19,11 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
@ -113,7 +113,7 @@ func (l *loggerImpl) prepareLogParams(c *contextmodel.ReqContext, duration time.
"size", rw.Size(),
}
referer, err := SanitizeURL(r.Referer())
referer, err := util.SanitizeURI(r.Referer())
// We add an empty referer when there's a parsing error, hence this is before the err check.
logParams = append(logParams, "referer", referer)
if err != nil {
@ -153,27 +153,3 @@ func errorLogParams(err error) []any {
"error", gfErr.LogMessage,
}
}
var sensitiveQueryStrings = [...]string{
"auth_token",
}
func SanitizeURL(s string) (string, error) {
if s == "" {
return s, nil
}
u, err := url.ParseRequestURI(s)
if err != nil {
return "", fmt.Errorf("failed to sanitize URL")
}
// strip out sensitive query strings
values := u.Query()
for _, query := range sensitiveQueryStrings {
values.Del(query)
}
u.RawQuery = values.Encode()
return u.String(), nil
}

View File

@ -16,43 +16,6 @@ import (
"github.com/grafana/grafana/pkg/web"
)
func Test_sanitizeURL(t *testing.T) {
tests := []struct {
name string
input string
want string
expectError bool
}{
{
name: "Receiving empty string should return it",
input: "",
want: "",
},
{
name: "Receiving valid URL string should return it parsed",
input: "https://grafana.com/",
want: "https://grafana.com/",
},
{
name: "Receiving invalid URL string should return empty string",
input: "this is not a valid URL",
want: "",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url, err := SanitizeURL(tt.input)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equalf(t, tt.want, url, "SanitizeURL(%v)", tt.input)
})
}
}
func Test_prepareLog(t *testing.T) {
type opts struct {
Features []any

View File

@ -17,6 +17,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
@ -77,7 +78,7 @@ func (r *queryREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable
}
func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Object, incomingResponder rest.Responder) (http.Handler, error) {
func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.Object, incomingResponder rest.Responder) (http.Handler, error) {
// See: /pkg/apiserver/builder/helper.go#L34
// The name is set with a rewriter hack
if name != "name" {
@ -88,6 +89,7 @@ func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Objec
return http.HandlerFunc(func(w http.ResponseWriter, httpreq *http.Request) {
ctx, span := b.tracer.Start(httpreq.Context(), "QueryService.Query")
defer span.End()
ctx = request.WithNamespace(ctx, request.NamespaceValue(connectCtx))
responder := newResponderWrapper(incomingResponder,
func(statusCode int, obj runtime.Object) {
@ -116,7 +118,6 @@ func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Objec
responder.Error(err)
return
}
// Parses the request and splits it into multiple sub queries (if necessary)
req, err := b.parser.parseRequest(ctx, raw)
if err != nil {

View File

@ -99,15 +99,6 @@ var (
Expression: "true", // enabled by default
AllowSelfServe: true,
},
{
Name: "exploreContentOutline",
Description: "Content outline sidebar",
Stage: FeatureStageGeneralAvailability,
Owner: grafanaExploreSquad,
Expression: "true", // enabled by default
FrontendOnly: true,
AllowSelfServe: true,
},
{
Name: "datasourceQueryMultiStatus",
Description: "Introduce HTTP 207 Multi Status for api/ds/query",

View File

@ -10,7 +10,6 @@ lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,f
featureHighlights,GA,@grafana/grafana-as-code,false,false,false
storage,experimental,@grafana/grafana-app-platform-squad,false,false,false
correlations,GA,@grafana/explore-squad,false,false,false
exploreContentOutline,GA,@grafana/explore-squad,false,false,true
datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false
autoMigrateOldPanels,preview,@grafana/dataviz-squad,false,false,true
autoMigrateGraphPanel,preview,@grafana/dataviz-squad,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
10 featureHighlights GA @grafana/grafana-as-code false false false
11 storage experimental @grafana/grafana-app-platform-squad false false false
12 correlations GA @grafana/explore-squad false false false
exploreContentOutline GA @grafana/explore-squad false false true
13 datasourceQueryMultiStatus experimental @grafana/plugins-platform-backend false false false
14 autoMigrateOldPanels preview @grafana/dataviz-squad false false true
15 autoMigrateGraphPanel preview @grafana/dataviz-squad false false true

View File

@ -51,10 +51,6 @@ const (
// Correlations page
FlagCorrelations = "correlations"
// FlagExploreContentOutline
// Content outline sidebar
FlagExploreContentOutline = "exploreContentOutline"
// FlagDatasourceQueryMultiStatus
// Introduce HTTP 207 Multi Status for api/ds/query
FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus"

View File

@ -841,8 +841,9 @@
{
"metadata": {
"name": "exploreContentOutline",
"resourceVersion": "1718727528075",
"creationTimestamp": "2023-10-13T16:57:13Z"
"resourceVersion": "1717578796182",
"creationTimestamp": "2023-10-13T16:57:13Z",
"deletionTimestamp": "2024-06-17T09:45:00Z"
},
"spec": {
"description": "Content outline sidebar",

View File

@ -292,7 +292,7 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"},
"grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"},
"grafana-oncall-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "OnCall"},
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"},
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incident"},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"},
"grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4},
"grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3},

View File

@ -301,15 +301,15 @@ func (s *Service) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLast
return err
}
if !shouldUpdateLastSeen(u.LastSeenAt) {
if !s.shouldUpdateLastSeen(u.LastSeenAt) {
return user.ErrLastSeenUpToDate
}
return s.store.UpdateLastSeenAt(ctx, cmd)
}
func shouldUpdateLastSeen(t time.Time) bool {
return time.Since(t) > time.Minute*15
func (s *Service) shouldUpdateLastSeen(t time.Time) bool {
return time.Since(t) > s.cfg.UserLastSeenUpdateInterval
}
func (s *Service) GetSignedInUser(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {

View File

@ -221,6 +221,7 @@ func TestUpdateLastSeenAt(t *testing.T) {
tracer: tracing.InitializeTracerForTest(),
}
userService.cfg = setting.NewCfg()
userService.cfg.UserLastSeenUpdateInterval = 5 * time.Minute
t.Run("update last seen at", func(t *testing.T) {
userStore.ExpectedSignedInUser = &user.SignedInUser{UserID: 1, OrgID: 1, Email: "email", Login: "login", Name: "name", LastSeenAt: time.Now().Add(-20 * time.Minute)}

View File

@ -306,6 +306,7 @@ type Cfg struct {
UserInviteMaxLifetime time.Duration
HiddenUsers map[string]struct{}
CaseInsensitiveLogin bool // Login and Email will be considered case insensitive
UserLastSeenUpdateInterval time.Duration
VerificationEmailMaxLifetime time.Duration
// Service Accounts
@ -1695,6 +1696,19 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
return errors.New("the minimum supported value for the `user_invite_max_lifetime_duration` configuration is 15m (15 minutes)")
}
cfg.UserLastSeenUpdateInterval, err = gtime.ParseDuration(valueAsString(users, "last_seen_update_interval", "15m"))
if err != nil {
return err
}
if cfg.UserLastSeenUpdateInterval < time.Minute*5 {
cfg.Logger.Warn("the minimum supported value for the `last_seen_update_interval` configuration is 5m (5 minutes)")
cfg.UserLastSeenUpdateInterval = time.Minute * 5
} else if cfg.UserLastSeenUpdateInterval > time.Hour*1 {
cfg.Logger.Warn("the maximum supported value for the `last_seen_update_interval` configuration is 1h (1 hour)")
cfg.UserLastSeenUpdateInterval = time.Hour * 1
}
cfg.HiddenUsers = make(map[string]struct{})
hiddenUsers := users.Key("hidden_users").MustString("")
for _, user := range strings.Split(hiddenUsers, ",") {

54
pkg/util/uri_sanitize.go Normal file
View File

@ -0,0 +1,54 @@
package util
import (
"fmt"
"net/url"
"strings"
)
const masking = "hidden"
var sensitiveQueryChecks = map[string]func(key string, urlValues url.Values) bool{
"auth_token": func(key string, urlValues url.Values) bool {
return true
},
"x-amz-signature": func(key string, urlValues url.Values) bool {
return true
},
"x-goog-signature": func(key string, urlValues url.Values) bool {
return true
},
"sig": func(key string, urlValues url.Values) bool {
for k := range urlValues {
if strings.ToLower(k) == "sv" {
return true
}
}
return false
},
}
func SanitizeURI(s string) (string, error) {
if s == "" {
return s, nil
}
u, err := url.ParseRequestURI(s)
if err != nil {
return "", fmt.Errorf("failed to sanitize URL")
}
// strip out sensitive query strings
urlValues := u.Query()
for key := range urlValues {
lk := strings.ToLower(key)
if checker, ok := sensitiveQueryChecks[lk]; ok {
if checker(key, urlValues) {
urlValues.Set(key, masking)
}
}
}
u.RawQuery = urlValues.Encode()
return u.String(), nil
}

View File

@ -0,0 +1,74 @@
package util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_sanitizeURI(t *testing.T) {
tests := []struct {
name string
input string
want string
expectError bool
}{
{
name: "Receiving empty string should return it",
input: "",
want: "",
},
{
name: "Receiving URL with auth_token should remove it",
input: "https://grafana.com/?auth_token=secret-token&q=1234",
want: "https://grafana.com/?auth_token=hidden&q=1234",
},
{
name: "Receiving presigned URL from AWS should remove signature",
input: "https://s3.amazonaws.com/finance-department-bucket/2022/tax-certificate.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3SGQVQG7FGA6KKA6%2F20221104%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221104T140227Z&X-Amz-Expires=3600&X-Amz-Signature=b22&X-Amz-SignedHeaders=host",
want: "https://s3.amazonaws.com/finance-department-bucket/2022/tax-certificate.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3SGQVQG7FGA6KKA6%2F20221104%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221104T140227Z&X-Amz-Expires=3600&X-Amz-Signature=hidden&X-Amz-SignedHeaders=host",
},
{
name: "Receiving presigned URL from GCP should remove signature",
input: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&X-Goog-Signature=247a&X-Goog-SignedHeaders=host",
want: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&X-Goog-Signature=hidden&X-Goog-SignedHeaders=host",
},
{
name: "Receiving presigned URL with lower case query params from GCP should remove signature",
input: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&x-goog-signature=247a&X-Goog-SignedHeaders=host",
want: "https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com%2F20181026%2Fus-central1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T181309Z&X-Goog-Expires=900&X-Goog-SignedHeaders=host&x-goog-signature=hidden",
},
{
name: "Receiving presigned URL from Azure should remove signature",
input: "https://myaccount.queue.core.windows.net/myqueue/messages?se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&sig=jDrr6cna7JPwIaxWfdH0tT5v9dc%3d&sp=p&st=2015-07-01T08%3A49Z&sv=2015-02-21&visibilitytimeout=120",
want: "https://myaccount.queue.core.windows.net/myqueue/messages?se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&sig=hidden&sp=p&st=2015-07-01T08%3A49Z&sv=2015-02-21&visibilitytimeout=120",
},
{
name: "Receiving presigned URL from Azure with upper case query values should remove signature",
input: "https://myaccount.queue.core.windows.net/myqueue/messages?se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&SIG=jDrr6cna7JPwIaxWfdH0tT5v9dc%3d&sp=p&st=2015-07-01T08%3A49Z&SV=2015-02-21&visibilitytimeout=120",
want: "https://myaccount.queue.core.windows.net/myqueue/messages?SIG=hidden&SV=2015-02-21&se=2015-07-02T08%3A49Z&si=YWJjZGVmZw%3D%3D&sp=p&st=2015-07-01T08%3A49Z&visibilitytimeout=120",
},
{
name: "Receiving valid URL string should return it parsed",
input: "https://grafana.com/?sig=testing-a-generic-parameter",
want: "https://grafana.com/?sig=testing-a-generic-parameter",
},
{
name: "Receiving invalid URL string should return empty string",
input: "this is not a valid URL",
want: "",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url, err := SanitizeURI(tt.input)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equalf(t, tt.want, url, "SanitizeURI(%v)", tt.input)
})
}
}

View File

@ -137,7 +137,7 @@ export function getNavTitle(navId: string | undefined) {
case 'testing-and-synthetics':
return t('nav.testing-and-synthetics.title', 'Testing & synthetics');
case 'plugin-page-grafana-incident-app':
return t('nav.incidents.title', 'Incidents');
return t('nav.incidents.title', 'Incident');
case 'plugin-page-grafana-ml-app':
return t('nav.machine-learning.title', 'Machine learning');
case 'plugin-page-grafana-slo-app':

View File

@ -689,7 +689,7 @@ describe('RuleList', () => {
expect(alertsInReorder).toHaveLength(2);
});
describe('pausing rules', () => {
describe.skip('pausing rules', () => {
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingRuleRead,

View File

@ -8,11 +8,9 @@ import {
Annotations,
GrafanaAlertStateDecision,
Labels,
PostableRuleGrafanaRuleDTO,
PostableRulerRuleGroupDTO,
PromRulesResponse,
RulerAlertingRuleDTO,
RulerGrafanaRuleDTO,
RulerRecordingRuleDTO,
RulerRuleGroupDTO,
RulerRulesConfigDTO,
} from 'app/types/unified-alerting-dto';
@ -77,14 +75,7 @@ interface ExportRulesParams {
ruleUid?: string;
}
export interface ModifyExportPayload {
rules: Array<RulerAlertingRuleDTO | RulerRecordingRuleDTO | PostableRuleGrafanaRuleDTO>;
name: string;
interval?: string | undefined;
source_tenants?: string[] | undefined;
}
export interface AlertRuleUpdated {
export interface AlertGroupUpdated {
message: string;
/**
* UIDs of rules updated from this request
@ -220,7 +211,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
}),
// TODO This should be probably a separate ruler API file
rulerRuleGroup: build.query<
getRuleGroupForNamespace: build.query<
RulerRuleGroupDTO,
{ rulerConfig: RulerDataSourceConfig; namespace: string; group: string }
>({
@ -231,6 +222,17 @@ export const alertRuleApi = alertingApi.injectEndpoints({
providesTags: ['CombinedAlertRule'],
}),
deleteRuleGroupFromNamespace: build.mutation<
RulerRuleGroupDTO,
{ rulerConfig: RulerDataSourceConfig; namespace: string; group: string }
>({
query: ({ rulerConfig, namespace, group }) => {
const { path, params } = rulerUrlBuilder(rulerConfig).namespaceGroup(namespace, group);
return { url: path, params, method: 'DELETE' };
},
invalidatesTags: ['CombinedAlertRule'],
}),
getAlertRule: build.query<RulerGrafanaRuleDTO, { uid: string }>({
// TODO: In future, if supported in other rulers, parametrize ruler source name
// For now, to make the consumption of this hook clearer, only support Grafana ruler
@ -272,7 +274,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
}),
exportModifiedRuleGroup: build.mutation<
string,
{ payload: ModifyExportPayload; format: ExportFormats; nameSpaceUID: string }
{ payload: PostableRulerRuleGroupDTO; format: ExportFormats; nameSpaceUID: string }
>({
query: ({ payload, format, nameSpaceUID }) => ({
url: `/api/ruler/grafana/api/v1/rules/${nameSpaceUID}/export/`,
@ -298,13 +300,20 @@ export const alertRuleApi = alertingApi.injectEndpoints({
}),
keepUnusedDataFor: 0,
}),
updateRuleGroupForNamespace: build.mutation<
AlertGroupUpdated,
{ rulerConfig: RulerDataSourceConfig; namespace: string; payload: PostableRulerRuleGroupDTO }
>({
query: ({ payload, namespace, rulerConfig }) => {
const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace);
updateRule: build.mutation<AlertRuleUpdated, { nameSpaceUID: string; payload: ModifyExportPayload }>({
query: ({ payload, nameSpaceUID }) => ({
url: `/api/ruler/grafana/api/v1/rules/${nameSpaceUID}/`,
data: payload,
method: 'POST',
}),
return {
url: path,
params,
data: payload,
method: 'POST',
};
},
invalidatesTags: ['CombinedAlertRule'],
}),
}),

View File

@ -1,13 +1,16 @@
import { produce } from 'immer';
import React from 'react';
import { Menu } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from 'app/features/alerting/unified/utils/rules';
import {
isGrafanaRulerRule,
isGrafanaRulerRulePaused,
getRuleGroupLocationFromCombinedRule,
} from 'app/features/alerting/unified/utils/rules';
import { CombinedRule } from 'app/types/unified-alerting';
import { grafanaRulerConfig } from '../hooks/useCombinedRule';
import { usePauseRuleInGroup } from '../hooks/useProduceNewRuleGroup';
import { stringifyErrorLike } from '../utils/misc';
interface Props {
rule: CombinedRule;
@ -22,12 +25,9 @@ interface Props {
* and triggering API call to do so
*/
const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
// we need to fetch the group again, as maybe the group has been filtered
const [getGroup] = alertRuleApi.endpoints.rulerRuleGroup.useLazyQuery();
const notifyApp = useAppNotification();
const [pauseRule, updateState] = usePauseRuleInGroup();
// Add any dependencies here
const [updateRule] = alertRuleApi.endpoints.updateRule.useMutation();
const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule);
const icon = isPaused ? 'play' : 'pause';
const title = isPaused ? 'Resume evaluation' : 'Pause evaluation';
@ -39,41 +39,17 @@ const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
if (!isGrafanaRulerRule(rule.rulerRule)) {
return;
}
const ruleUid = rule.rulerRule.grafana_alert.uid;
const targetGroup = await getGroup({
rulerConfig: grafanaRulerConfig,
namespace: rule.namespace.uid || rule.rulerRule.grafana_alert.namespace_uid,
group: rule.group.name,
}).unwrap();
if (!targetGroup) {
notifyApp.error(
`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule. Could not get the target group to update the rule.`
);
try {
const ruleGroupId = getRuleGroupLocationFromCombinedRule(rule);
const ruleUID = rule.rulerRule.grafana_alert.uid;
await pauseRule(ruleGroupId, ruleUID, newIsPaused);
} catch (error) {
notifyApp.error(`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule: ${stringifyErrorLike(error)}`);
return;
}
// Parse the rules into correct format for API
const modifiedRules = targetGroup.rules.map((groupRule) => {
if (!(isGrafanaRulerRule(groupRule) && groupRule.grafana_alert.uid === ruleUid)) {
return groupRule;
}
return produce(groupRule, (updatedGroupRule) => {
updatedGroupRule.grafana_alert.is_paused = newIsPaused;
});
});
const payload = {
interval: targetGroup.interval!,
name: targetGroup.name,
rules: modifiedRules,
};
await updateRule({
nameSpaceUID: rule.namespace.uid || rule.rulerRule.grafana_alert.namespace_uid,
payload,
}).unwrap();
onPauseChange?.();
};
@ -81,6 +57,7 @@ const MenuItemPauseRule = ({ rule, onPauseChange }: Props) => {
<Menu.Item
label={title}
icon={icon}
disabled={updateState.isLoading}
onClick={() => {
setRulePause(!isPaused);
}}

View File

@ -171,7 +171,7 @@ describe('contact points', () => {
}
// check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' });
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' });
await userEvent.click(notificationTemplatesTab);
expect(screen.getByRole('link', { name: 'Add notification template' })).toHaveAttribute('aria-disabled', 'true');
});
@ -388,7 +388,7 @@ describe('contact points', () => {
expect(viewProvisioned).not.toBeDisabled();
// check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' });
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' });
await userEvent.click(notificationTemplatesTab);
expect(screen.queryByRole('link', { name: 'Add notification template' })).not.toBeInTheDocument();
});

View File

@ -4,7 +4,7 @@ import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-h
import { Link, useParams } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { config, locationService } from '@grafana/runtime';
import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { useAppNotification } from 'app/core/copy/appNotification';
@ -12,7 +12,11 @@ import { contextSrv } from 'app/core/core';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
import { isGrafanaRulerRule, isGrafanaRulerRulePaused } from 'app/features/alerting/unified/utils/rules';
import {
getRuleGroupLocationFromRuleWithLocation,
isGrafanaRulerRule,
isGrafanaRulerRulePaused,
} from 'app/features/alerting/unified/utils/rules';
import { useDispatch } from 'app/types';
import { RuleWithLocation } from 'app/types/unified-alerting';
@ -23,8 +27,9 @@ import {
trackAlertRuleFormCancelled,
trackAlertRuleFormSaved,
} from '../../../Analytics';
import { useDeleteRuleFromGroup } from '../../../hooks/useProduceNewRuleGroup';
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions';
import { saveRuleFormAction } from '../../../state/actions';
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
import { initialAsyncRequestState } from '../../../utils/redux';
import {
@ -36,7 +41,6 @@ import {
ignoreHiddenQueries,
normalizeDefaultAnnotations,
} from '../../../utils/rule-form';
import * as ruleId from '../../../utils/rule-id';
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
import { AlertRuleNameInput } from '../AlertRuleNameInput';
import AnnotationsStep from '../AnnotationsStep';
@ -60,6 +64,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
const [queryParams] = useQueryParams();
const [showEditYaml, setShowEditYaml] = useState(false);
const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
const [deleteRuleFromGroup, _deleteRuleState] = useDeleteRuleFromGroup();
const routeParams = useParams<{ type: string; id: string }>();
const ruleType = translateRouteParamToRuleType(routeParams.type);
@ -151,16 +156,12 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
);
};
const deleteRule = () => {
const deleteRule = async () => {
if (existing) {
const identifier = ruleId.fromRulerRule(
existing.ruleSourceName,
existing.namespace,
existing.group.name,
existing.rule
);
const ruleGroupIdentifier = getRuleGroupLocationFromRuleWithLocation(existing);
dispatch(deleteRuleAction(identifier, { navigateTo: '/alerting/list' }));
await deleteRuleFromGroup(ruleGroupIdentifier, existing.rule);
locationService.replace(returnTo);
}
};

View File

@ -7,8 +7,12 @@ import { useAppNotification } from 'app/core/copy/appNotification';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AppChromeUpdate } from '../../../../../../core/components/AppChrome/AppChromeUpdate';
import { RulerRuleDTO, RulerRuleGroupDTO } from '../../../../../../types/unified-alerting-dto';
import { alertRuleApi, ModifyExportPayload } from '../../../api/alertRuleApi';
import {
PostableRulerRuleGroupDTO,
RulerRuleDTO,
RulerRuleGroupDTO,
} from '../../../../../../types/unified-alerting-dto';
import { alertRuleApi } from '../../../api/alertRuleApi';
import { fetchRulerRulesGroup } from '../../../api/ruler';
import { useDataSourceFeatures } from '../../../hooks/useCombinedRule';
import { RuleFormValues } from '../../../types/rule-form';
@ -133,7 +137,7 @@ export const getPayloadToExport = (
uid: string,
formValues: RuleFormValues,
existingGroup: RulerRuleGroupDTO<RulerRuleDTO> | null | undefined
): ModifyExportPayload => {
): PostableRulerRuleGroupDTO => {
const grafanaRuleDto = formValuesToRulerGrafanaRuleDTO(formValues);
const updatedRule = { ...grafanaRuleDto, grafana_alert: { ...grafanaRuleDto.grafana_alert, uid: uid } };
@ -167,7 +171,7 @@ export const getPayloadToExport = (
const useGetPayloadToExport = (values: RuleFormValues, uid: string) => {
const rulerGroupDto = useGetGroup(values.folder?.uid ?? '', values.group);
const payload: ModifyExportPayload = useMemo(() => {
const payload: PostableRulerRuleGroupDTO = useMemo(() => {
return getPayloadToExport(uid, values, rulerGroupDto?.value);
}, [uid, rulerGroupDto, values]);
return { payload, loadingGroup: rulerGroupDto.loading };

View File

@ -1,17 +1,19 @@
import React, { useState, useCallback, useMemo } from 'react';
import { locationService } from '@grafana/runtime';
import { ConfirmModal } from '@grafana/ui';
import { dispatch } from 'app/store/store';
import { CombinedRule } from 'app/types/unified-alerting';
import { deleteRuleAction } from '../../state/actions';
import { getRulesSourceName } from '../../utils/datasource';
import { fromRulerRule } from '../../utils/rule-id';
import { useDeleteRuleFromGroup } from '../../hooks/useProduceNewRuleGroup';
import { fetchPromAndRulerRulesAction } from '../../state/actions';
import { getRuleGroupLocationFromCombinedRule } from '../../utils/rules';
type DeleteModalHook = [JSX.Element, (rule: CombinedRule) => void, () => void];
export const useDeleteModal = (): DeleteModalHook => {
export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule | undefined>();
const [deleteRuleFromGroup, _deleteState] = useDeleteRuleFromGroup();
const dismissModal = useCallback(() => {
setRuleToDelete(undefined);
@ -22,20 +24,25 @@ export const useDeleteModal = (): DeleteModalHook => {
}, []);
const deleteRule = useCallback(
(ruleToDelete?: CombinedRule) => {
if (ruleToDelete && ruleToDelete.rulerRule) {
const identifier = fromRulerRule(
getRulesSourceName(ruleToDelete.namespace.rulesSource),
ruleToDelete.namespace.name,
ruleToDelete.group.name,
ruleToDelete.rulerRule
);
async (rule?: CombinedRule) => {
if (!rule?.rulerRule) {
return;
}
dispatch(deleteRuleAction(identifier, { navigateTo: '/alerting/list' }));
dismissModal();
const location = getRuleGroupLocationFromCombinedRule(rule);
await deleteRuleFromGroup(location, rule.rulerRule);
// refetch rules for this rules source
// @TODO remove this when we moved everything to RTKQ then the endpoint will simply invalidate the tags
dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: location.dataSourceName }));
dismissModal();
if (redirectToListView) {
locationService.replace('/alerting/list');
}
},
[dismissModal]
[deleteRuleFromGroup, dismissModal, redirectToListView]
);
const modal = useMemo(

View File

@ -1,7 +1,7 @@
import { produce } from 'immer';
import React from 'react';
import { render, screen, userEvent } from 'test/test-utils';
import { byLabelText } from 'testing-library-selector';
import { byLabelText, byRole } from 'testing-library-selector';
import { config, setPluginExtensionsHook } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
@ -24,7 +24,9 @@ jest.mock('app/core/services/context_srv');
const mockContextSrv = jest.mocked(contextSrv);
const ui = {
menu: byRole('menu'),
moreButton: byLabelText(/More/),
pauseButton: byRole('menuitem', { name: /Pause evaluation/ }),
};
const grantAllPermissions = () => {
@ -76,6 +78,19 @@ describe('RuleActionsButtons', () => {
expect(await getMenuContents()).toMatchSnapshot();
});
it('should be able to pause a Grafana rule', async () => {
const user = userEvent.setup();
grantAllPermissions();
const mockRule = getGrafanaRule();
render(<RuleActionsButtons rule={mockRule} rulesSource="grafana" />);
await user.click(await ui.moreButton.find());
await user.click(await ui.pauseButton.find());
expect(ui.menu.query()).not.toBeInTheDocument();
});
it('renders correct options for Cloud rule', async () => {
const user = userEvent.setup();
grantAllPermissions();

View File

@ -44,7 +44,9 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton
const dispatch = useDispatch();
const location = useLocation();
const style = useStyles2(getStyles);
const [deleteModal, showDeleteModal] = useDeleteModal();
const redirectToListView = compact ? false : true;
const [deleteModal, showDeleteModal] = useDeleteModal(redirectToListView);
const [showSilenceDrawer, setShowSilenceDrawer] = useState<boolean>(false);

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