Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2024-12-16 19:07:54 +03:00
commit e4837d452e
95 files changed
+6686 -5513

No files matched your search

+1 -173
View File
@@ -1021,18 +1021,6 @@ services:
volumes:
- name: postgres
path: /var/lib/postgresql/data/pgdata
- commands:
- docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_DATABASE: grafana_tests
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
image: mysql:5.7.39
name: mysql57
volumes:
- name: mysql57
path: /var/lib/mysql
- commands:
- docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password
environment:
@@ -1161,27 +1149,6 @@ steps:
POSTGRES_HOST: postgres
image: golang:1.23.1-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql57:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
name: wait-for-mysql-5.7
- commands:
- apk add --update build-base
- apk add --update mysql-client
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql57 -P 3306 -u root
-prootpass
- go clean -testcache
- go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find
./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+'
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
- wait-for-mysql-5.7
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.23.1-alpine
name: mysql-5.7-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
@@ -1274,9 +1241,6 @@ volumes:
- name: postgres
temp:
medium: memory
- name: mysql57
temp:
medium: memory
- name: mysql80
temp:
medium: memory
@@ -1499,18 +1463,6 @@ services:
volumes:
- name: postgres
path: /var/lib/postgresql/data/pgdata
- commands:
- docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_DATABASE: grafana_tests
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
image: mysql:5.7.39
name: mysql57
volumes:
- name: mysql57
path: /var/lib/mysql
- commands:
- docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password
environment:
@@ -1627,17 +1579,6 @@ steps:
POSTGRES_HOST: postgres
image: golang:1.23.1-alpine
name: postgres-benchmark-integration-tests
- commands:
- apk add --update build-base
- if [ -z ${GO_PACKAGES} ]; then echo 'missing GO_PACKAGES'; false; fi
- go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES}
depends_on:
- wire-install
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.23.1-alpine
name: mysql-5.7-benchmark-integration-tests
- commands:
- apk add --update build-base
- if [ -z ${GO_PACKAGES} ]; then echo 'missing GO_PACKAGES'; false; fi
@@ -1662,9 +1603,6 @@ volumes:
- name: postgres
temp:
medium: memory
- name: mysql57
temp:
medium: memory
- name: mysql80
temp:
medium: memory
@@ -2617,18 +2555,6 @@ services:
volumes:
- name: postgres
path: /var/lib/postgresql/data/pgdata
- commands:
- docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_DATABASE: grafana_tests
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
image: mysql:5.7.39
name: mysql57
volumes:
- name: mysql57
path: /var/lib/mysql
- commands:
- docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password
environment:
@@ -2718,27 +2644,6 @@ steps:
POSTGRES_HOST: postgres
image: golang:1.23.1-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql57:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
name: wait-for-mysql-5.7
- commands:
- apk add --update build-base
- apk add --update mysql-client
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql57 -P 3306 -u root
-prootpass
- go clean -testcache
- go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find
./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+'
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
- wait-for-mysql-5.7
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.23.1-alpine
name: mysql-5.7-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
@@ -2827,9 +2732,6 @@ volumes:
- name: postgres
temp:
medium: memory
- name: mysql57
temp:
medium: memory
- name: mysql80
temp:
medium: memory
@@ -3201,18 +3103,6 @@ services:
volumes:
- name: postgres
path: /var/lib/postgresql/data/pgdata
- commands:
- docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_DATABASE: grafana_tests
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
image: mysql:5.7.39
name: mysql57
volumes:
- name: mysql57
path: /var/lib/mysql
- commands:
- docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password
environment:
@@ -3302,27 +3192,6 @@ steps:
POSTGRES_HOST: postgres
image: golang:1.23.1-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql57:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
name: wait-for-mysql-5.7
- commands:
- apk add --update build-base
- apk add --update mysql-client
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql57 -P 3306 -u root
-prootpass
- go clean -testcache
- go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find
./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+'
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
- wait-for-mysql-5.7
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.23.1-alpine
name: mysql-5.7-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
@@ -3409,9 +3278,6 @@ volumes:
- name: postgres
temp:
medium: memory
- name: mysql57
temp:
medium: memory
- name: mysql80
temp:
medium: memory
@@ -4954,18 +4820,6 @@ services:
volumes:
- name: postgres
path: /var/lib/postgresql/data/pgdata
- commands:
- docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_DATABASE: grafana_tests
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
image: mysql:5.7.39
name: mysql57
volumes:
- name: mysql57
path: /var/lib/mysql
- commands:
- docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password
environment:
@@ -5048,27 +4902,6 @@ steps:
POSTGRES_HOST: postgres
image: golang:1.23.1-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql57:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
name: wait-for-mysql-5.7
- commands:
- apk add --update build-base
- apk add --update mysql-client
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql57 -P 3306 -u root
-prootpass
- go clean -testcache
- go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find
./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+'
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
- wait-for-mysql-5.7
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.23.1-alpine
name: mysql-5.7-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
image: jwilder/dockerize:0.6.1
@@ -5150,9 +4983,6 @@ volumes:
- name: postgres
temp:
medium: memory
- name: mysql57
temp:
medium: memory
- name: mysql80
temp:
medium: memory
@@ -5454,7 +5284,6 @@ steps:
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r316-55f47f8
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM memcached:1.6.9-alpine
@@ -5493,7 +5322,6 @@ steps:
- trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8
- trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r316-55f47f8
- trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39
- trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32
- trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL memcached:1.6.9-alpine
@@ -5751,6 +5579,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 08edb24b8ec53e0ce09f525cf6fdf4ea5dc67710fe64c3ea93ceaf3af42b8305
hmac: 341dbe9d2f9c6d2ee70026e495921ea4670550729350047ec0c680094497cc44
...
+2 -1
View File
@@ -11,6 +11,7 @@ on:
jobs:
deploy-pr-preview:
if: github.repository == 'grafana/grafana'
uses: grafana/writers-toolkit/.github/workflows/deploy-preview.yml@main
with:
sha: ${{ github.event.pull_request.head.sha }}
@@ -20,4 +21,4 @@ jobs:
repo: grafana
website_directory: content/docs/grafana/latest
relative_prefix: /docs/grafana/latest/
index_file: true
index_file: true
+3
View File
@@ -1361,6 +1361,9 @@ max_state_save_concurrency = 1
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
state_periodic_save_interval = 5m
# If the feature flag 'alertingSaveStatePeriodic' is enabled, this is the size of the batch that is saved to the database at once.
state_periodic_save_batch_size = 1
# Disables the smoothing of alert evaluations across their evaluation window.
# Rules will evaluate in sync.
disable_jitter = false
+3
View File
@@ -1345,6 +1345,9 @@
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
;state_periodic_save_interval = 5m
# If the feature flag 'alertingSaveStatePeriodic' is enabled, this is the size of the batch that is saved to the database at once.
;state_periodic_save_batch_size = 1
# Disables the smoothing of alert evaluations across their evaluation window.
# Rules will evaluate in sync.
;disable_jitter = false
@@ -1,8 +1,9 @@
---
description: Label based data access for Loki given Teams
description: Label based data access for Teams
keywords:
- grafana
- loki
- mimir
- lbac
labels:
products:
@@ -14,17 +15,26 @@ weight: 100
# Label Based Access Control (LBAC) for data sources
Label Based Access Control (LBAC) simplifies and streamlines data source access management based on team memberships.
Label Based Access Control (LBAC) for data sources simplifies and streamlines data source access management based on team memberships.
Label-Based Access Control (LBAC) allows fine-grained access control to data sources by filtering logs or metrics based on labels. It lets administrators configure access rules for teams, ensuring that users only query data relevant to their assigned permissions.
## Supported Data Sources
LBAC for data sources is currently available for `Loki, Prometheus` with basic authentication. Support for additional data sources may be added in future updates.
**LBAC for data sources offers:**
- Team-based access control using `LogQL` rules.
- Simplified data source management by consolidating multiple sources into one.
- Dashboard reuse across teams with tailored access.
{{< admonition type="note" >}}
LBAC rules is available for preview for logs with Loki in Grafana Cloud.
LBAC rules is available for **private preview** in Grafana Cloud.
Report any unexpected behavior to the Grafana Support team.
To use LBAC rules you must enable the `teamHttpHeaders` feature toggle because the feature uses HTTP headers for the LBAC rules requests.
{{< /admonition >}}
You can configure user access based upon team memberships using LogQL.
LBAC for data sources controls access to logs depending on the rules set for each team.
You can configure user access based upon team memberships using `LogQL`.
LBAC for data sources controls access to logs or metrics depending on the rules set for each team.
This feature addresses two common challenges faced by Grafana users:
@@ -33,18 +43,32 @@ This feature addresses two common challenges faced by Grafana users:
1. Using the same dashboard across multiple teams.
LBAC for data sources lets Grafana Teams use the same dashboard with different access control rules.
To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for data sources](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/).
To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for Loki Data Source](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/).
To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for Prometheus Data Source](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/).
## Logs and Metrics Filtering with LBAC
LBAC for data sources enables you to filter access for both logs and metrics. By defining rules with label selectors, you can specify:
- **Logs**: Control access to log lines using LogQL queries with labels such as `namespace` or `cluster`.
- **Metrics**: Control access to metric data points using LogQL with labels such as `job` or `region` and access for metrics `__name__`.
This flexibility allows teams to use the same data source for multiple use cases while maintaining secure access boundaries.
## Before you begin
To be able to use LBAC for data sources, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance.
## Limitations
- There is a set number of rules to be configured within a data source, depending on the size of the rules.
- Around ~500-600 rules is the upper limit.
- If there are no LBAC for data sources rules for a user's team, that user can query all logs.
- If there are no LBAC for data sources rules for a user's team, that user can query all logs or metrics.
- If an administrator is part of a team with LBAC for data sources rules, those rules are applied to the administrator requests.
- Cloud Access Policy (CAP) LBAC rules override LBAC for data sources rules.
CAP are the access controls from Grafana Cloud.
You must remove any label selectors from your Cloud Access Policy that is configured for the Loki data source, otherwise the CAP label selectors override the LBAC for data sources rules. For more information about CAP label selectors, refer to [Use label-based access control (LBAC) with access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/label-access-policies/).
You must remove any label selectors from your Cloud Access Policy that is configured for the data source, otherwise the CAP label selectors override the LBAC for data sources rules. For more information about CAP label selectors, refer to [Use label-based access control (LBAC) with access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/label-access-policies/).
## Data source permissions
@@ -54,17 +78,17 @@ You must remove any label selectors from your Cloud Access Policy that is config
## Recommended setup
It's recommended that you create a single Loki data source for using LBAC for data sources rules so you have a clear separation of data sources using LBAC for data sources and those that aren't.
It's recommended that you create a single data source for using LBAC for data sources rules so you have a clear separation of data sources using LBAC for data sources and those that aren't.
All teams should have with only teams having `query` permission.
You should create another Loki data source configured without LBAC for data sources for full access to the logs.
You should create another data source configured without LBAC for data sources for full access.
## LBAC rules
Grafana adds LBAC for data sources rules to the HTTP request via the Loki data source.
Grafana adds LBAC for data sources rules to the HTTP request via the data source.
If you configure multiple rules for a team, each rule is evaluated separately.
Query results include lines that match any of the rules.
Only users with data source `Admin` permissions can edit LBAC for data sources rules in the **Data source permissions** tab because changing LBAC rules requires the same access level as editing data source permissions.
To set up LBAC for data sources for a Loki data source, refer to [Configure LBAC for data sources](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/).
To set up LBAC for data sources for a data source, refer to [Configure LBAC for data sources](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/).
@@ -0,0 +1,92 @@
---
description: Configure LBAC for data sources for Prometheus data source
keywords:
- Prometheus
- datasource
- team
labels:
products:
- cloud
- enterprise
title: Configure LBAC for data sources for Prometheus
weight: 250
---
# Configure LBAC for data sources for Prometheus data source
## Grafana Cloud
LBAC for data sources is available in private preview on Grafana Cloud for Prometheus created with basic authentication. Prometheus data sources for LBAC for data sources can only be created, provisioning is currently not available.
You cannot configure LBAC rules for Grafana-provisioned data sources from the UI. Alternatively, you can replicate the setting of the provisioned data source in a new data source as described in [LBAC Configuration for New Prometheus Data Source](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/configure-teamlbac-for-Prometheus/#task-1-lbac-configuration-for-new-Prometheus-data-source) and then add the LBAC configuration to the new data source.
## Before you begin
To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you.
- Be sure that you have the permission setup to create a Prometheus tenant in Grafana Cloud
- Be sure that you have admin data source permissions for Grafana.
### Permissions
We recommend that you remove all permissions for roles and teams that are not required to access the data source. This will help to ensure that only the required teams have access to the data source. The recommended permissions are `Admin` permission and only add the teams `Query` permissions that you want to add LBAC for data sources rules for.
## Task 1: LBAC Configuration for New Prometheus Data Source
1. Access Prometheus data sources details for your stack through grafana.com
1. Copy Prometheus details and create a CAP
- Copy the details of your Prometheus setup.
- Create a Cloud Access Policy (CAP) for the Prometheus data source in grafana.com.
- Ensure the CAP includes `metrics:read` permissions.
- Ensure the CAP does not include `labels` rules.
1. Create a new Prometheus data source
- In Grafana, proceed to add a new data source and select Prometheus as the type.
1. Navigate back to the Prometheus data source
- Set up the Prometheus data source using basic authentication. Use the `userID` as the username. Use the generated CAP `token` as the password.
- Save and connect.
1. Navigate to data source permissions
- Go to the permissions tab of the newly created Prometheus data source. Here, you'll find the LBAC for data sources rules section.
For more information on how to setup LBAC for data sources rules for a Prometheus data source, refer to [Create LBAC for data sources rules for the Prometheus data source](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/create-teamlbac-rules/).
## Grafana Enterprise
LBAC for data sources is available in Grafana Enterprise for Prometheus created with basic authentication. Prometheus data sources for LBAC for data sources can only be created.
You cannot configure LBAC rules for Grafana-provisioned data sources from the UI. Alternatively, you can replicate the setting of the provisioned data source in a new data source as described in [LBAC Configuration for New Prometheus Data Source](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/configure-teamlbac-for-Prometheus/#task-1-lbac-configuration-for-new-Prometheus-data-source) and then add the LBAC configuration to the new data source.
## Before you begin
To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you.
- Be sure that you have the permission setup to create a cluster in your Grafana
- Be sure that you have admin plugins permissions for Grafana.
- Be sure that you have admin data source permissions for Grafana.
### Permissions
We recommend that you remove all permissions for roles and teams that are not required to access the data source. This will help to ensure that only the required teams have access to the data source. The recommended permissions are `Admin` permission and only add the teams `Query` permissions that you want to add LBAC for data sources rules for.
## Task 0: Setup Grafana Enterprise Metrics tenant and access policies
1. Access the plugins page and install Grafana Enterprise Metrics plugins
1. Connect your plugin and use app as the cluster
1. Access the app Grafana Enterprise Metrics and configure a tenant
1. Store the `uid` of the tenant to be used as the username for the basic authentication
1. Access the policies page inside of the app and create a AP
- Create a Access Policy (CAP) for the Prometheus data source.
- Ensure the CAP includes `metrics:read` permissions.
- Ensure the CAP does not include `labels` rules.
- Store the `token` to be used as password for authentication.
## Task 1: LBAC Configuration for New Prometheus Data Source
1. Create a new Prometheus data source
- In Grafana, proceed to add a new data source and select Prometheus as the type.
1. Navigate back to the Prometheus data source
- Set up the Prometheus data source using basic authentication. Use the `uid` as the username. Use the generated `token` as the password.
- Save and connect.
1. Navigate to data source permissions
- Go to the permissions tab of the newly created Prometheus data source. Here, you'll find the LBAC for data sources rules section.
For more information on how to setup LBAC for data sources rules for a Prometheus data source, refer to [Create LBAC for data sources rules for the Prometheus data source](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/create-teamlbac-rules/).
@@ -1,5 +1,5 @@
---
description: Learn how to create LBAC for data sources rules for the Loki data source.
description: Learn how to create LBAC for data sources rules for a supported data source.
keywords:
- loki
- lbac
@@ -7,13 +7,13 @@ keywords:
labels:
products:
- cloud
title: Create LBAC for data sources rules for the Loki data source
title: Create LBAC for data sources rules for a supported data source
weight: 250
---
# Create LBAC for data sources rules for the Loki data source
# Create LBAC for data source rule
LBAC for data sources is available on Cloud for Loki data sources created with basic authentication. Managed/Provisioned Loki data source can **NOT** be configured with LBAC for data sources as of now.
LBAC for data sources is available for LBAC-supported data sources created with basic authentication. As of today, managed/provisioned data source can **NOT** be configured with LBAC rules.
## Before you begin
@@ -23,19 +23,24 @@ To be able to use LBAC for data sources rules, you need to enable the feature to
- Be sure that you have admin data source permissions for Grafana.
- Be sure that you have a team setup in Grafana.
### Create a LBAC for data sources Rule for a team
### Create a LBAC for data sources rule for a team
1. Navigate to your Loki data source
1. Navigate to your data source
1. Navigate to the permissions tab
- Here, you'll find the LBAC for data sources rules section.
1. Add a LBAC for data sources Rule
- Add a new rule for the team in the LBAC for data sources rules section.
1. Define a label selector for the rule
- Add a label selector to the rule. Refer to Loki query documentation for guidance on the types of log selections you can specify.
- Add a label selector to the rule. Refer to documentation for guidance on the types of log selections you can specify.
### LBAC rule
A LBAC rule is a `logql` query that runs as a query to the Loki instance for your logs. Each rule operates independently as its own filter, separate from other rules within a team. For example, you can create a label policy that includes all log lines with a specific label.
An LBAC rule is a `logql` query that filters logs or metrics based on labels. Each rule operates independently as its own filter, separate from other rules within a team.
For example:
- For logs: `{namespace="dev", cluster="us-west-0"}` filters log lines matching both `namespace="dev"` and `cluster="us-west-0"`.
- For metrics: `{job="api-server", region="europe"}` filters metric data points matching `job="api-server"` and `region="europe"`.
One rule `{namespace="dev", cluster="us-west-0"}` created with multiple namespaces will be seen as `namespace="dev"` **AND** `cluster="us-west-0"`.
Two rules `{namespace="dev"}`, `{cluster="us-west-0"}` created for a team will be seen as `namespace="dev"` **OR** `cluster="us-west-0"`.
@@ -46,39 +51,35 @@ We recommend you only add `query` permissions for teams that should use the data
We recommend for a first setup, setting up as few rules as possible for each team and make them additive for simplicity.
To validate the rules, we recommend testing the rules in the Loki Explore view. This will allow you to see the logs that would be returned for the rule.
To validate the rules, we recommend testing the rules in the Explore view. This will allow you to see the metrics or logs that would be returned for the rule.
#### Tasks
### Task 1: One rule set up for each team
### Task 1: One rule setup for each team
One common use case for creating an LBAC policy is to have specific access to logs that have a specific label. For example, you can create a label policy that includes all log lines with the label.
One common use case for creating an LBAC policy is to grant access to logs or metrics with a specific label. For example, you can create a label policy that includes all log lines or metrics with the label `namespace`.
We have two teams, Team A and Team B with `Query` permissions. Loki access is setup with `Admin` roles to have `Admin` permission only.
We have two teams, Team A and Team B with `Query` permissions. Data source access is set up with `Admin` roles to have `Admin` permission only.
- Team A has a rule `namespace="dev"`.
- Team B has a rule `namespace="prod"`.
A user that is part of Team A will have access to logs that match `namespace="dev"`.
A user that is part of Team B will have access to logs that match `namespace="prod"`.
A user that is part of Team A and Team B will have access to logs that match `namespace="dev"` OR `namespace="prod"`.
A user that is part of Team A will have access to logs or metrics matching `namespace="dev"`. A user in both Team A and Team B will have access to data matching `namespace="dev"` OR `namespace="prod"`.
### Task 2: Set up a rule to exclude a label for a team
One common use case for creating an LBAC policy is to exclude logs that have a specific label. For example, you can create a label policy that excludes all log lines with the label `secret=true` by adding a selector with `secret!="true"` when you create an access policy:
One common use case for creating an LBAC policy is to exclude logs or metrics that have a specific label. For example, you can create a label policy that excludes all log lines with the label `secret=true` by adding a selector with `secret!="true"` when you create an access policy:
We have one team, Team A `Query` permissions. Loki access is setup with `Admin` roles to have `Admin` permission only.
We have one team, Team A `Query` permissions. Data source access is setup with `Admin` roles to have `Admin` permission only.
- Team A has a rule `secret!="true"`.
A user that is part of Team A will **NOT** have access to logs that match `secret!="true"`.
A user that is part of Team A will **NOT** have access to logs or metrics that match `secret!="true"`.
### Task 3: Set up multiple rules for a team
We have two teams, Team A and Team B with `Query` permissions. Loki access is setup with `Admin` roles having `Admin` permission.
We have two teams, Team A and Team B with `Query` permissions. Data Source access is setup with `Admin` roles having `Admin` permission.
- Team A has rule `cluster="us-west-0", namespace=~"dev|prod"` configured.
@@ -114,7 +115,7 @@ A user in Team B will have access to logs that match `namespace!="dev"`.
### Task 5: Single rule setup for a team
We have two teams, Team A and Team B. Loki access is setup with `Editor`, `Viewer` roles to have `Query` permission.
We have two teams, Team A and Team B. Data Source access is setup with `Editor`, `Viewer` roles to have `Query` permission.
- Team A has a rule `namespace="dev"` configured.
@@ -131,8 +132,8 @@ A user that is not part of Team A and part of Team B, that is `Editor` or `Viewe
We have team B, user A is part of Team B and has an `Admin` basic role.
- Team B has no roles assigned
- Team B has Query permissions to data source Loki
- Team B has Query permissions to data source
- Team B has a rule `{ project_id="project-dev" }`
User A may only access logs for data source Loki that match `{ project_id="project-dev" }` and no other logs on the data source.
User A may only access logs or metrics for a data source that match `{ project_id="project-dev" }`.
@@ -129,7 +129,7 @@ Use [alert rule evaluation](ref:alert-rule-evaluation) to determine how frequent
If you are creating a new evaluation group, specify the interval for the group.
All rules within the same group are evaluated sequentially over the same time interval.
All rules within the same group are evaluated sequentially over the same time interval. You can reorder them from the **Alert rules** page.
1. Enter a pending period.
@@ -66,7 +66,9 @@ Select your data source and enter a query. The queries used in data source-manag
1. From the **Group** dropdown, select an existing group within the selected namespace or add a new one.
Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time.
Rules within a group are run sequentially at a regular interval, with the same evaluation time.
Newly created rules are appended to the end of the group, and you can reorder them from the **Alert rules** page.
## Add labels
@@ -63,7 +63,9 @@ transition of an alert instance is saved in the database.
This can be prevented by writing to the database periodically. For this the feature flag `alertingSaveStatePeriodic` needs
to be enabled. By default, it saves the states every 5 minutes to the database and on each shutdown. The periodic interval
can also be configured using the `state_periodic_save_interval` configuration flag.
can also be configured using the `state_periodic_save_interval` configuration flag. During this process, Grafana deletes all existing alert instances from the
database and then writes the entire current set of instances back in batches in a single transacton.
Configure the size of each batch using the `state_periodic_save_batch_size` configuration option.
The time it takes to write to the database periodically can be monitored using the `state_full_sync_duration_seconds` metric
that is exposed by Grafana.
@@ -317,7 +317,7 @@ concurrent_render_limit = 4
# Set the scale factor for rendering images. 2 is enough for monitor resolutions
# 4 would be better for printed material. Setting a higher value affects performance and memory
image_scale_factor = 2
# Set the maximum file size in megabytes for the CSV attachments
# Set the maximum file size in megabytes for the report email attachments
max_attachment_size_mb = 10
# Path to the directory containing font files
fonts_path =
@@ -152,7 +152,7 @@ Scale factor for rendering images. Value `2` is enough for monitor resolutions,
### max_attachment_size_mb
Set the maximum file size in megabytes for the CSV attachments.
Set the maximum file size in megabytes for the report email attachments.
### fonts_path
@@ -239,14 +239,10 @@ Payload:
```json
{
...
"info": {
...
"groups": [
"engineer",
"admin",
],
...
},
"groups": [
"engineer",
"admin",
],
...
}
```
@@ -254,7 +250,7 @@ Payload:
Config:
```bash
role_attribute_path = contains(info.groups[*], 'admin') && 'Admin' || contains(info.groups[*], 'editor') && 'Editor' || 'Viewer'
role_attribute_path = contains(groups[*], 'admin') && 'Admin' || contains(groups[*], 'editor') && 'Editor' || 'Viewer'
```
##### Map server administrator role
@@ -266,13 +262,9 @@ Payload:
```json
{
...
"info": {
...
"roles": [
"admin",
],
...
},
"roles": [
"admin",
],
...
}
```
@@ -280,7 +272,7 @@ Payload:
Config:
```ini
role_attribute_path = contains(info.roles[*], 'admin') && 'GrafanaAdmin' || contains(info.roles[*], 'editor') && 'Editor' || 'Viewer'
role_attribute_path = contains(roles[*], 'admin') && 'GrafanaAdmin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'
allow_assign_grafana_admin = true
```
@@ -305,26 +297,16 @@ Payload:
```json
{
...
"info": {
...
"roles": [
"org_foo",
"org_bar",
"another_org"
],
...
},
...
"roles": ["org_foo", "org_bar", "another_org"]
}
```
Config:
```ini
role_attribute_path = contains(info.roles[*], 'admin') && 'GrafanaAdmin' || 'None'
role_attribute_path = contains(roles[*], 'admin') && 'GrafanaAdmin' || 'None'
allow_assign_grafana_admin = true
org_attribute_path = info.roles
org_attribute_path = roles
org_mapping = org_foo:org_foo:Viewer org_bar:org_bar:Editor *:org_baz:Editor
```
@@ -347,7 +329,7 @@ To learn more about group synchronization, refer to [Configure team sync](https:
Configuration:
```bash
groups_attribute_path = info.groups
groups_attribute_path = groups
```
Payload:
@@ -355,14 +337,10 @@ Payload:
```json
{
...
"info": {
...
"groups": [
"engineers",
"analysts",
],
...
},
"groups": [
"engineers",
"analysts",
],
...
}
```
+10 -10
View File
@@ -175,12 +175,12 @@
"esbuild": "0.24.0",
"esbuild-loader": "4.2.2",
"esbuild-plugin-browserslist": "^0.15.0",
"eslint": "9.16.0",
"eslint": "9.17.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "28.9.0",
"eslint-plugin-jest-dom": "^5.4.0",
"eslint-plugin-jsdoc": "50.5.0",
"eslint-plugin-jsdoc": "50.6.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-lodash": "8.0.0",
"eslint-plugin-no-barrel-files": "^1.1.1",
@@ -211,7 +211,7 @@
"knip": "^5.10.0",
"lerna": "8.1.8",
"mini-css-extract-plugin": "2.9.2",
"msw": "2.6.8",
"msw": "2.6.9",
"mutationobserver-shim": "0.3.7",
"ngtemplate-loader": "2.1.0",
"node-notifier": "10.0.1",
@@ -220,7 +220,7 @@
"postcss-loader": "8.1.1",
"postcss-reporter": "7.1.0",
"postcss-scss": "4.0.9",
"prettier": "3.3.3",
"prettier": "3.4.2",
"pseudoizer": "^0.1.0",
"react-refresh": "0.14.0",
"react-select-event": "5.5.1",
@@ -256,7 +256,7 @@
"@emotion/css": "11.13.5",
"@emotion/react": "11.14.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@floating-ui/react": "0.27.0",
"@floating-ui/react": "0.27.2",
"@formatjs/intl-durationformat": "^0.7.0",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/aws-sdk": "0.5.0",
@@ -365,7 +365,7 @@
"prismjs": "1.29.0",
"rc-slider": "11.1.7",
"rc-time-picker": "3.7.3",
"rc-tree": "5.10.1",
"rc-tree": "5.11.0",
"re-resizable": "6.10.3",
"react": "18.2.0",
"react-diff-viewer-continued": "^3.4.0",
@@ -379,17 +379,17 @@
"react-inlinesvg": "4.1.5",
"react-loading-skeleton": "3.5.0",
"react-moveable": "0.56.0",
"react-redux": "9.1.2",
"react-redux": "9.2.0",
"react-resizable": "3.0.5",
"react-responsive-carousel": "^3.2.23",
"react-router": "5.3.3",
"react-router-dom": "5.3.3",
"react-router-dom-v5-compat": "^6.26.1",
"react-select": "5.8.3",
"react-select": "5.9.0",
"react-split-pane": "0.1.92",
"react-table": "7.8.0",
"react-transition-group": "4.4.5",
"react-use": "17.5.1",
"react-use": "17.6.0",
"react-virtual": "2.10.4",
"react-virtualized-auto-sizer": "1.0.24",
"react-window": "1.8.10",
@@ -452,7 +452,7 @@
},
"packageManager": "yarn@4.5.3",
"dependenciesMeta": {
"prettier@3.3.3": {
"prettier@3.4.2": {
"unplugged": true
}
},
+1 -1
View File
@@ -52,7 +52,7 @@
"moment-timezone": "0.5.46",
"ol": "7.4.0",
"papaparse": "5.4.1",
"react-use": "17.5.1",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"string-hash": "^1.1.3",
"tinycolor2": "1.6.0",
@@ -5,6 +5,7 @@ import {
standardFieldConfigEditorRegistry,
} from '../field/standardFieldConfigEditorRegistry';
import { FieldConfigProperty, FieldConfigPropertyItem } from '../types/fieldOverrides';
import { PanelMigrationModel } from '../types/panel';
import { PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
import { PanelPlugin } from './PanelPlugin';
@@ -308,4 +309,178 @@ describe('PanelPlugin', () => {
});
});
});
describe('setMigrationHandler', () => {
it('should handle synchronous migrations', () => {
const panel = new PanelPlugin(() => <div>Panel</div>);
const mockMigration = () => ({
newOption: 'migrated',
});
panel.setMigrationHandler(mockMigration);
const migrationModel: PanelMigrationModel = {
id: 1,
type: 'test-panel',
options: { oldOption: 'value' },
fieldConfig: { defaults: {}, overrides: [] },
pluginVersion: '1.0.0',
};
expect(panel.onPanelMigration).toBeDefined();
expect(panel.onPanelMigration!(migrationModel)).toEqual({
newOption: 'migrated',
});
});
it('should handle async migrations', async () => {
const panel = new PanelPlugin(() => <div>Panel</div>);
const mockAsyncMigration = async () => {
return Promise.resolve({
newOption: 'async-migrated',
});
};
panel.setMigrationHandler(mockAsyncMigration);
const migrationModel: PanelMigrationModel = {
id: 1,
type: 'test-panel',
options: { oldOption: 'value' },
fieldConfig: { defaults: {}, overrides: [] },
};
const result = await panel.onPanelMigration!(migrationModel);
expect(result).toEqual({
newOption: 'async-migrated',
});
});
it('should handle complex panel migrations with advanced transformations', () => {
const panel = new PanelPlugin(() => <div>Panel</div>);
const mockMigration = (model: PanelMigrationModel) => {
const { options, fieldConfig, title, id, type, pluginVersion } = model;
//notice many of these migrations don't make sense in real code but are here
//to make sure the attributes of the PanelMigrationModel are used and tested
const baseMigration = {
...options,
display: {
...options.display,
mode: options.display?.type === 'legacy' ? 'modern' : options.display?.mode,
title: title?.toLowerCase() ?? 'untitled',
panelId: `${type}-${id}`,
},
thresholds: options.thresholds?.map((t: { value: string | number; color: string }) => ({
...t,
value: typeof t.value === 'string' ? parseInt(t.value, 10) : t.value,
// Use fieldConfig defaults for threshold colors if available
color: fieldConfig.defaults?.color ?? t.color,
})),
metadata: {
migrationVersion: pluginVersion ? `${pluginVersion} -> 2.0.0` : '2.0.0',
migratedFields: Object.keys(fieldConfig.defaults ?? {}),
overrideCount: fieldConfig.overrides?.length ?? 0,
},
// Merge custom field defaults into options
customDefaults: fieldConfig.defaults?.custom ?? {},
// Transform overrides into a map
overrideMap: fieldConfig.overrides?.reduce(
(acc, override) => ({
...acc,
[override.matcher.id]: override.properties,
}),
{}
),
};
// Apply panel type specific migrations
if (type.includes('visualization')) {
return {
...baseMigration,
visualizationSpecific: {
enhanced: true,
legacyFormat: false,
},
};
}
return baseMigration;
};
panel.setMigrationHandler(mockMigration);
const complexModel: PanelMigrationModel = {
id: 123,
type: 'visualization-panel',
title: 'Complex METRICS',
pluginVersion: '1.0.0',
options: {
display: {
type: 'legacy',
showHeader: true,
},
thresholds: [
{ value: '90', color: 'red' },
{ value: '50', color: 'yellow' },
],
queries: ['A', 'B'],
},
fieldConfig: {
defaults: {
color: { mode: 'thresholds' },
custom: {
lineWidth: 1,
fillOpacity: 0.5,
},
mappings: [],
},
overrides: [
{
matcher: { id: 'byName', options: 'cpu' },
properties: [{ id: 'color', value: 'red' }],
},
{
matcher: { id: 'byValue', options: 'memory' },
properties: [{ id: 'unit', value: 'bytes' }],
},
],
},
};
const result = panel.onPanelMigration!(complexModel);
expect(result).toMatchObject({
display: {
mode: 'modern',
showHeader: true,
title: 'complex metrics',
panelId: 'visualization-panel-123',
},
thresholds: [
{ value: 90, color: { mode: 'thresholds' } },
{ value: 50, color: { mode: 'thresholds' } },
],
queries: ['A', 'B'],
metadata: {
migrationVersion: '1.0.0 -> 2.0.0',
migratedFields: ['color', 'custom', 'mappings'],
overrideCount: 2,
},
customDefaults: {
lineWidth: 1,
fillOpacity: 0.5,
},
overrideMap: {
byName: [{ id: 'color', value: 'red' }],
byValue: [{ id: 'unit', value: 'bytes' }],
},
visualizationSpecific: {
enhanced: true,
legacyFormat: false,
},
});
});
});
});
@@ -557,6 +557,7 @@ export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
rangeRaw?: RawTimeRange;
timeInfo?: string; // The query time description (blue text in the upper right)
panelId?: number;
panelName?: string;
panelPluginId?: string;
dashboardUID?: string;
headers?: Record<string, string>;
+21 -1
View File
@@ -134,12 +134,32 @@ export interface PanelEditorProps<T = any> {
data?: PanelData;
}
/**
* This type mirrors the required properties from PanelModel<TOptions> needed for migration handlers.
*
* By maintaining a separate type definition, we ensure that changes to PanelModel
* that would break third-party migration handlers are caught at compile time,
* rather than failing silently when third-party code attempts to use an incompatible panel.
*
* TOptions must be any to follow the same pattern as PanelModel<TOptions>
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface PanelMigrationModel<TOptions = any> {
id: number;
type: string;
title?: string;
options: TOptions;
fieldConfig: PanelModel<TOptions>['fieldConfig'];
pluginVersion?: string;
targets?: PanelModel<TOptions>['targets'];
}
/**
* Called when a panel is first loaded with current panel model to migrate panel options if needed.
* Can return panel options, or a Promise that resolves to panel options for async migrations
*/
export type PanelMigrationHandler<TOptions = any> = (
panel: PanelModel<TOptions>
panel: PanelMigrationModel<TOptions>
) => Partial<TOptions> | Promise<Partial<TOptions>>;
/**
@@ -86,6 +86,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
secureJsonData?: KeyValue;
secureJsonFields?: KeyValue<boolean>;
enabled?: boolean;
autoEnabled?: boolean;
defaultNavUrl?: string;
hasUpdate?: boolean;
enterprise?: boolean;
+1 -1
View File
@@ -18,7 +18,7 @@
},
"devDependencies": {
"@typescript-eslint/types": "^8.9.0",
"eslint": "9.16.0",
"eslint": "9.17.0",
"tslib": "2.7.0"
},
"private": true
+1 -1
View File
@@ -50,7 +50,7 @@
"d3": "^7.8.5",
"lodash": "4.17.21",
"react": "18.2.0",
"react-use": "17.5.1",
"react-use": "17.6.0",
"react-virtualized-auto-sizer": "1.0.24",
"tinycolor2": "1.6.0",
"tslib": "2.7.0"
+1 -1
View File
@@ -49,7 +49,7 @@
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"esbuild": "0.24.0",
"prettier": "3.3.3",
"prettier": "3.4.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"rimraf": "6.0.1",
@@ -24,8 +24,8 @@
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0"
},
+1 -1
View File
@@ -12,7 +12,7 @@
"@types/eslint": "9.6.1",
"@types/webpack-bundle-analyzer": "^4.7.0",
"copy-webpack-plugin": "12.0.2",
"eslint": "9.16.0",
"eslint": "9.17.0",
"eslint-webpack-plugin": "4.2.0",
"fork-ts-checker-webpack-plugin": "9.0.2",
"glob": "11.0.0",
+6 -6
View File
@@ -37,7 +37,7 @@
},
"dependencies": {
"@emotion/css": "11.13.5",
"@floating-ui/react": "0.27.0",
"@floating-ui/react": "0.27.2",
"@grafana/data": "11.5.0-pre",
"@grafana/experimental": "2.1.5",
"@grafana/faro-web-sdk": "1.12.2",
@@ -65,8 +65,8 @@
"pluralize": "8.0.0",
"prismjs": "1.29.0",
"react-highlight-words": "0.20.0",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"react-window": "1.8.10",
"rxjs": "7.8.1",
"semver": "7.6.3",
@@ -106,11 +106,11 @@
"copy-webpack-plugin": "12.0.2",
"css-loader": "7.1.2",
"esbuild": "0.24.0",
"eslint": "9.16.0",
"eslint": "9.17.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "28.9.0",
"eslint-plugin-jsdoc": "50.5.0",
"eslint-plugin-jsdoc": "50.6.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-lodash": "8.0.0",
"eslint-plugin-react": "7.37.2",
@@ -121,7 +121,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-matcher-utils": "29.7.0",
"prettier": "3.3.3",
"prettier": "3.4.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select-event": "5.5.1",
+1 -1
View File
@@ -44,7 +44,7 @@
"@grafana/ui": "11.5.0-pre",
"history": "4.10.1",
"lodash": "4.17.21",
"react-use": "17.5.1",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0"
},
+2 -2
View File
@@ -25,8 +25,8 @@
"lodash": "4.17.21",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"react-virtualized-auto-sizer": "1.0.24",
"rxjs": "7.8.1",
"sql-formatter-plus": "^1.3.6",
+5 -5
View File
@@ -49,7 +49,7 @@
"@emotion/css": "11.13.5",
"@emotion/react": "11.14.0",
"@emotion/serialize": "1.3.3",
"@floating-ui/react": "0.27.0",
"@floating-ui/react": "0.27.2",
"@grafana/data": "11.5.0-pre",
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/faro-web-sdk": "^1.3.6",
@@ -83,11 +83,11 @@
"monaco-editor": "0.34.1",
"ol": "7.4.0",
"prismjs": "1.29.0",
"rc-cascader": "3.30.0",
"rc-cascader": "3.31.0",
"rc-drawer": "7.2.0",
"rc-slider": "11.1.7",
"rc-time-picker": "^3.7.3",
"rc-tooltip": "6.2.1",
"rc-tooltip": "6.3.1",
"react-calendar": "^5.1.0",
"react-colorful": "5.6.1",
"react-custom-scrollbars-2": "4.5.0",
@@ -98,10 +98,10 @@
"react-inlinesvg": "4.1.5",
"react-loading-skeleton": "3.5.0",
"react-router-dom-v5-compat": "^6.26.1",
"react-select": "5.8.3",
"react-select": "5.9.0",
"react-table": "7.8.0",
"react-transition-group": "4.4.5",
"react-use": "17.5.1",
"react-use": "17.6.0",
"react-window": "1.8.10",
"rxjs": "7.8.1",
"slate": "0.47.9",
@@ -222,8 +222,9 @@ const getStyles = (theme: GrafanaTheme2) => {
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'center',
padding: theme.spacing(0.5, 2),
padding: theme.spacing(0.5, 1.5),
minHeight: theme.spacing(4),
borderRadius: theme.shape.radius.default,
margin: 0,
border: 'none',
width: '100%',
+1
View File
@@ -12,6 +12,7 @@ type PluginSetting struct {
Id string `json:"id"`
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
AutoEnabled bool `json:"autoEnabled"`
Module string `json:"module"`
BaseUrl string `json:"baseUrl"`
Info plugins.Info `json:"info"`
+48 -4
View File
@@ -50,8 +50,6 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
@@ -66,6 +64,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
folderUidRoute.Put("/", handler.updateFolder)
folderUidRoute.Delete("/", handler.deleteFolder)
folderUidRoute.Get("/", handler.getFolder)
folderUidRoute.Get("/counts", handler.countFolderContent)
})
} else {
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
@@ -74,6 +73,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
})
}
})
@@ -747,6 +747,29 @@ func (fk8s *folderK8sHandler) getFolders(c *contextmodel.ReqContext) {
c.JSON(http.StatusOK, hits)
}
func (fk8s *folderK8sHandler) countFolderContent(c *contextmodel.ReqContext) {
client, ok := fk8s.getClient(c)
if !ok {
return
}
uid := web.Params(c.Req)[":uid"]
counts, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}, "counts")
if err != nil {
fk8s.writeError(c, err)
return
}
out, err := toFolderLegacyCounts(counts)
if err != nil {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, out)
}
func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
client, ok := fk8s.getClient(c)
if !ok {
@@ -988,7 +1011,7 @@ func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f
return nil, nil
}
folderIDs, err := fk8s.getParents(f)
folderIDs, err := getParents(f)
if err != nil {
return nil, err
}
@@ -1004,7 +1027,7 @@ func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f
return metadata, nil
}
func (fk8s *folderK8sHandler) getParents(f *folder.Folder) (map[string]bool, error) {
func getParents(f *folder.Folder) (map[string]bool, error) {
folderIDs := map[string]bool{f.UID: true}
if (f.UID == accesscontrol.GeneralFolderUID) || (f.UID == folder.SharedWithMeFolderUID) {
return folderIDs, nil
@@ -1022,3 +1045,24 @@ func (fk8s *folderK8sHandler) getParents(f *folder.Folder) (map[string]bool, err
return folderIDs, nil
}
func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCounts, error) {
ds, err := folderalpha1.UnstructuredToDescendantCounts(u)
if err != nil {
return nil, err
}
var out = make(folder.DescendantCounts)
for _, v := range ds.Counts {
// if stats come from unified storage, we will use them
if v.Group != "sql-fallback" {
out[v.Resource] = v.Count
continue
}
// if stats are from single tenant DB and they are not in unified storage, we will use them
if _, ok := out[v.Resource]; !ok {
out[v.Resource] = v.Count
}
}
return &out, nil
}
+116
View File
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
clientrest "k8s.io/client-go/rest"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -752,3 +753,118 @@ func TestUpdateFolderLegacyAndUnifiedStorage(t *testing.T) {
}
})
}
func TestToFolderCounts(t *testing.T) {
var tests = []struct {
name string
input *unstructured.Unstructured
expected *folder.DescendantCounts
expectError bool
}{
{
name: "with only counts from unified storage",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": []interface{}{
map[string]interface{}{
"group": "alpha",
"resource": "folders",
"count": int64(1),
},
map[string]interface{}{
"group": "alpha",
"resource": "dashboards",
"count": int64(3),
},
map[string]interface{}{
"group": "alpha",
"resource": "alertRules",
"count": int64(0),
},
},
},
},
expected: &folder.DescendantCounts{
"folders": 1,
"dashboards": 3,
"alertRules": 0,
},
},
{
name: "with counts from both storages",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": []interface{}{
map[string]interface{}{
"group": "alpha",
"resource": "folders",
"count": int64(1),
},
map[string]interface{}{
"group": "alpha",
"resource": "dashboards",
"count": int64(3),
},
map[string]interface{}{
"group": "sql-fallback",
"resource": "folders",
"count": int64(0),
},
},
},
},
expected: &folder.DescendantCounts{
"folders": 1,
"dashboards": 3,
},
},
{
name: "it uses the values from sql-fallaback if not found in unified storage",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": []interface{}{
map[string]interface{}{
"group": "alpha",
"resource": "dashboards",
"count": int64(3),
},
map[string]interface{}{
"group": "sql-fallback",
"resource": "folders",
"count": int64(2),
},
},
},
},
expected: &folder.DescendantCounts{
"folders": 2,
"dashboards": 3,
},
},
{
name: "malformed input",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": map[string]interface{}{},
},
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual, err := toFolderLegacyCounts(tc.input)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
+1
View File
@@ -721,6 +721,7 @@ func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[stri
OrgID: orgID,
Enabled: plugin.AutoEnabled,
Pinned: plugin.AutoEnabled,
AutoEnabled: plugin.AutoEnabled,
PluginVersion: plugin.Info.Version,
}
+6 -1
View File
@@ -216,6 +216,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
if plugin.IsApp() {
dto.Enabled = plugin.AutoEnabled
dto.Pinned = plugin.AutoEnabled
dto.AutoEnabled = plugin.AutoEnabled
}
ps, err := hs.PluginSettings.GetPluginSettingByPluginID(c.Req.Context(), &pluginsettings.GetByPluginIDArgs{
@@ -254,9 +255,13 @@ func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.R
}
pluginID := web.Params(c.Req)[":pluginId"]
if _, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID); !exists {
p, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
}
if p.AutoEnabled && !cmd.Enabled {
return response.Error(http.StatusBadRequest, "Cannot disable auto-enabled plugin", nil)
}
cmd.OrgId = c.SignedInUser.GetOrgID()
cmd.PluginId = pluginID
+40
View File
@@ -871,3 +871,43 @@ func Test_PluginsSettings(t *testing.T) {
})
}
}
func Test_UpdatePluginSetting(t *testing.T) {
pID := "test-app"
p1 := createPlugin(plugins.JSONData{
ID: pID, Type: "app", Name: pID,
Info: plugins.Info{
Version: "1.0.0",
},
AutoEnabled: true,
}, plugins.ClassExternal, plugins.NewFakeFS(),
)
pluginRegistry := &fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
},
}
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
pID: {ID: 0, OrgID: 1, PluginID: pID, PluginVersion: "1.0.0", Enabled: true},
}}
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.PluginSettings = &pluginSettings
hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{})
hs.pluginFileStore = filestore.ProvideService(pluginRegistry)
hs.managedPluginsService = managedplugins.NewNoop()
hs.log = log.NewNopLogger()
})
input := strings.NewReader(`{"enabled": false}`)
endpoint := fmt.Sprintf("/api/plugins/%s/settings", pID)
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(1, []ac.Permission{{Action: pluginaccesscontrol.ActionWrite, Scope: "plugins:id:test-app"}}))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
+8
View File
@@ -2,6 +2,8 @@ package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -77,3 +79,9 @@ type ResourceStats struct {
Resource string `json:"resource"`
Count int64 `json:"count"`
}
func UnstructuredToDescendantCounts(obj *unstructured.Unstructured) (*DescendantCounts, error) {
var res DescendantCounts
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &res)
return &res, err
}
+41 -6
View File
@@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/apimachinery/utils"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
@@ -41,15 +42,29 @@ func (g *genericStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.S
return fields
}
func (g *genericStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {}
func (g *genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
oldMeta, err := utils.MetaAccessor(old)
func (g *genericStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
meta, err := utils.MetaAccessor(obj)
if err != nil {
return
}
meta.SetGeneration(1)
objCopy := obj.DeepCopyObject()
err = runtime.SetZeroValue(objCopy)
if err != nil {
return
}
metaCopy, err := utils.MetaAccessor(objCopy)
if err != nil {
return
}
status, err := metaCopy.GetStatus()
if err == nil {
_ = meta.SetStatus(status)
}
}
status, err := oldMeta.GetStatus()
func (g *genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
oldMeta, err := utils.MetaAccessor(old)
if err != nil {
return
}
@@ -59,7 +74,27 @@ func (g *genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime
return
}
_ = newMeta.SetStatus(status)
// update shouldn't change the status
status, err := oldMeta.GetStatus()
if err != nil {
_ = newMeta.SetStatus(nil)
} else {
_ = newMeta.SetStatus(status)
}
spec, err := newMeta.GetSpec()
if err != nil {
return
}
oldSpec, err := oldMeta.GetSpec()
if err != nil {
return
}
if !apiequality.Semantic.DeepEqual(spec, oldSpec) {
newMeta.SetGeneration(oldMeta.GetGeneration() + 1)
}
}
func (g *genericStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
@@ -0,0 +1,134 @@
package generic_test
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/apis/example"
)
func TestPrepareForUpdate(t *testing.T) {
ctx := context.TODO()
gv := schema.GroupVersion{Group: "test", Version: "v1"}
strategy := generic.NewStrategy(runtime.NewScheme(), gv)
oldObj := &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Generation: 1,
},
Spec: example.PodSpec{
NodeSelector: map[string]string{"foo": "bar"},
},
Status: example.PodStatus{
Phase: example.PodPhase("Running"),
},
}
testCases := []struct {
name string
newObj *example.Pod
oldObj *example.Pod
expectedGen int64
expectedObj *example.Pod
}{
{
name: "ignore status updates",
newObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Generation: 1,
},
Spec: example.PodSpec{
NodeSelector: map[string]string{"foo": "bar"},
},
Status: example.PodStatus{
Phase: example.PodPhase("Stopped"),
},
},
oldObj: oldObj.DeepCopy(),
expectedGen: 2,
expectedObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Generation: 1,
},
Spec: example.PodSpec{
NodeSelector: map[string]string{"foo": "bar"},
},
Status: example.PodStatus{
Phase: example.PodPhase("Running"),
},
},
},
{
name: "increment generation if spec changes",
newObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Generation: 1,
},
Spec: example.PodSpec{
NodeSelector: map[string]string{"foo": "baz"},
},
Status: example.PodStatus{
Phase: example.PodPhase("Running"),
},
},
oldObj: oldObj.DeepCopy(),
expectedGen: 2,
expectedObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Generation: 2,
},
Spec: example.PodSpec{
NodeSelector: map[string]string{"foo": "baz"},
},
Status: example.PodStatus{
Phase: example.PodPhase("Running"),
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
strategy.PrepareForUpdate(ctx, tc.newObj, tc.oldObj)
require.Equal(t, tc.expectedObj, tc.newObj)
})
}
}
func TestPrepareForCreate(t *testing.T) {
ctx := context.TODO()
gv := schema.GroupVersion{Group: "test", Version: "v1"}
strategy := generic.NewStrategy(runtime.NewScheme(), gv)
obj := &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: example.PodSpec{
NodeSelector: map[string]string{"foo": "bar"},
},
Status: example.PodStatus{
Phase: example.PodPhase("Running"),
},
}
strategy.PrepareForCreate(ctx, obj)
require.Equal(t, int64(1), obj.Generation)
require.Equal(t, example.PodStatus{}, obj.Status)
}
+1 -1
View File
@@ -128,7 +128,7 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API
storage[resourceInfo.StoragePath()] = legacyStore
storage[resourceInfo.StoragePath("parents")] = &subParentsREST{b.folderSvc}
storage[resourceInfo.StoragePath("access")] = &subAccessREST{b.folderSvc}
storage[resourceInfo.StoragePath("count")] = &subCountREST{searcher: b.searcher}
storage[resourceInfo.StoragePath("counts")] = &subCountREST{searcher: b.searcher}
// enable dual writer
if optsGetter != nil && dualWriteBuilder != nil {
@@ -98,6 +98,11 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Ident
return nil, nil
}
if errors.Is(refreshErr, oauthtoken.ErrRetriesExhausted) {
ctxLogger.Warn("Retries have been exhausted for locking the DB for OAuth token refresh", "id", id.ID, "error", refreshErr)
return nil, refreshErr
}
ctxLogger.Error("Failed to refresh OAuth access token", "id", id.ID, "error", refreshErr)
// log the user out
+1
View File
@@ -409,6 +409,7 @@ func (ng *AlertNG) init() error {
DoNotSaveNormalState: ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingNoNormalState),
ApplyNoDataAndErrorToAllStates: ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingNoDataErrorExecution),
MaxStateSaveConcurrency: ng.Cfg.UnifiedAlerting.MaxStateSaveConcurrency,
StatePeriodicSaveBatchSize: ng.Cfg.UnifiedAlerting.StatePeriodicSaveBatchSize,
RulesPerRuleGroupLimit: ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit,
Tracer: ng.tracer,
Log: log.New("ngalert.state.manager"),
+3
View File
@@ -71,6 +71,9 @@ type ManagerCfg struct {
DoNotSaveNormalState bool
// MaxStateSaveConcurrency controls the number of goroutines (per rule) that can save alert state in parallel.
MaxStateSaveConcurrency int
// StatePeriodicSaveBatchSize controls the size of the alert instance batch that is saved periodically when the
// alertingSaveStatePeriodic feature flag is enabled.
StatePeriodicSaveBatchSize int
// ApplyNoDataAndErrorToAllStates makes state manager to apply exceptional results (NoData and Error)
// to all states when corresponding execution in the rule definition is set to either `Alerting` or `OK`
ApplyNoDataAndErrorToAllStates bool
+1 -1
View File
@@ -16,7 +16,7 @@ type InstanceStore interface {
// SaveAlertInstancesForRule overwrites the state for the given rule.
SaveAlertInstancesForRule(ctx context.Context, key models.AlertRuleKeyWithGroup, instances []models.AlertInstance) error
DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKeyWithGroup) error
FullSync(ctx context.Context, instances []models.AlertInstance) error
FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int) error
}
// InstanceReader provides methods to fetch alert instances.
@@ -20,6 +20,7 @@ type AsyncStatePersister struct {
log log.Logger
// doNotSaveNormalState controls whether eval.Normal state is persisted to the database and returned by get methods.
doNotSaveNormalState bool
batchSize int
store InstanceStore
ticker *clock.Ticker
metrics *metrics.State
@@ -31,6 +32,7 @@ func NewAsyncStatePersister(log log.Logger, ticker *clock.Ticker, cfg ManagerCfg
store: cfg.InstanceStore,
ticker: ticker,
doNotSaveNormalState: cfg.DoNotSaveNormalState,
batchSize: cfg.StatePeriodicSaveBatchSize,
metrics: cfg.Metrics,
}
}
@@ -58,11 +60,11 @@ func (a *AsyncStatePersister) fullSync(ctx context.Context, instancesProvider Al
startTime := time.Now()
a.log.Debug("Full state sync start")
instances := instancesProvider.GetAlertInstances(a.doNotSaveNormalState)
if err := a.store.FullSync(ctx, instances); err != nil {
if err := a.store.FullSync(ctx, instances, a.batchSize); err != nil {
a.log.Error("Full state sync failed", "duration", time.Since(startTime), "instances", len(instances))
return err
}
a.log.Debug("Full state sync done", "duration", time.Since(startTime), "instances", len(instances))
a.log.Debug("Full state sync done", "duration", time.Since(startTime), "instances", len(instances), "batchSize", a.batchSize)
if a.metrics != nil {
a.metrics.StateFullSyncDuration.Observe(time.Since(startTime).Seconds())
}
+1 -1
View File
@@ -64,7 +64,7 @@ func (f *FakeInstanceStore) DeleteAlertInstancesByRule(ctx context.Context, key
return nil
}
func (f *FakeInstanceStore) FullSync(ctx context.Context, instances []models.AlertInstance) error {
func (f *FakeInstanceStore) FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int) error {
f.mtx.Lock()
defer f.mtx.Unlock()
f.recordedOps = []any{}
+83 -26
View File
@@ -224,51 +224,108 @@ func (st DBstore) DeleteAlertInstancesByRule(ctx context.Context, key models.Ale
})
}
func (st DBstore) FullSync(ctx context.Context, instances []models.AlertInstance) error {
// FullSync performs a full synchronization of the given alert instances to the database.
//
// This method will delete all existing alert instances and insert the given instances in a single transaction.
//
// The batchSize parameter controls how many instances are inserted per batch. Increasing batchSize can improve
// performance for large datasets, but can also increase load on the database.
func (st DBstore) FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int) error {
if len(instances) == 0 {
return nil
}
if batchSize <= 0 {
batchSize = 1
}
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// First we delete all records from the table
if _, err := sess.Exec("DELETE FROM alert_instance"); err != nil {
return fmt.Errorf("failed to delete alert_instance table: %w", err)
}
for _, alertInstance := range instances {
if err := models.ValidateAlertInstance(alertInstance); err != nil {
st.Logger.Warn("Failed to validate alert instance, skipping", "err", err, "rule_uid", alertInstance.RuleUID)
continue
}
labelTupleJSON, err := alertInstance.Labels.StringKey()
if err != nil {
st.Logger.Warn("Failed to generate alert instance labels key, skipping", "err", err, "rule_uid", alertInstance.RuleUID)
continue
total := len(instances)
for start := 0; start < total; start += batchSize {
end := start + batchSize
if end > total {
end = total
}
_, err = sess.Exec(
"INSERT INTO alert_instance (rule_org_id, rule_uid, labels, labels_hash, current_state, current_reason, current_state_since, current_state_end, last_eval_time, resolved_at, last_sent_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
alertInstance.RuleOrgID,
alertInstance.RuleUID,
labelTupleJSON,
alertInstance.LabelsHash,
alertInstance.CurrentState,
alertInstance.CurrentReason,
alertInstance.CurrentStateSince.Unix(),
alertInstance.CurrentStateEnd.Unix(),
alertInstance.LastEvalTime.Unix(),
nullableTimeToUnix(alertInstance.ResolvedAt),
nullableTimeToUnix(alertInstance.LastSentAt),
)
if err != nil {
return fmt.Errorf("failed to insert into alert_instance table: %w", err)
batch := instances[start:end]
if err := st.insertInstancesBatch(sess, batch); err != nil {
return fmt.Errorf("failed to insert batch [%d:%d]: %w", start, end, err)
}
}
if err := sess.Commit(); err != nil {
return fmt.Errorf("failed to commit alert_instance table: %w", err)
}
return nil
})
}
func (st DBstore) insertInstancesBatch(sess *sqlstore.DBSession, batch []models.AlertInstance) error {
// If the batch is empty, nothing to insert.
if len(batch) == 0 {
return nil
}
query := strings.Builder{}
placeholders := make([]string, 0, len(batch))
args := make([]any, 0, len(batch)*11)
query.WriteString("INSERT INTO alert_instance ")
query.WriteString("(rule_org_id, rule_uid, labels, labels_hash, current_state, current_reason, current_state_since, current_state_end, last_eval_time, resolved_at, last_sent_at) VALUES ")
for _, instance := range batch {
if err := models.ValidateAlertInstance(instance); err != nil {
st.Logger.Warn("Skipping invalid alert instance", "err", err, "rule_uid", instance.RuleUID)
continue
}
labelTupleJSON, err := instance.Labels.StringKey()
if err != nil {
st.Logger.Warn("Skipping instance with invalid labels key", "err", err, "rule_uid", instance.RuleUID)
continue
}
placeholders = append(placeholders, "(?,?,?,?,?,?,?,?,?,?,?)")
args = append(args,
instance.RuleOrgID,
instance.RuleUID,
labelTupleJSON,
instance.LabelsHash,
instance.CurrentState,
instance.CurrentReason,
instance.CurrentStateSince.Unix(),
instance.CurrentStateEnd.Unix(),
instance.LastEvalTime.Unix(),
nullableTimeToUnix(instance.ResolvedAt),
nullableTimeToUnix(instance.LastSentAt),
)
}
// If no valid instances were found in this batch, skip insertion.
if len(placeholders) == 0 {
return nil
}
query.WriteString(strings.Join(placeholders, ","))
execArgs := make([]any, 0, len(args)+1)
execArgs = append(execArgs, query.String())
execArgs = append(execArgs, args...)
if _, err := sess.Exec(execArgs...); err != nil {
return fmt.Errorf("failed to insert instances: %w", err)
}
return nil
}
// nullableTimeToUnix converts a nullable time.Time to nil, if it is nil, otherwise it converts the time.Time to a unix timestamp.
func nullableTimeToUnix(t *time.Time) *int64 {
if t == nil {
@@ -297,6 +297,8 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
}
func TestIntegrationFullSync(t *testing.T) {
batchSize := 1
ctx := context.Background()
_, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds)
@@ -310,7 +312,7 @@ func TestIntegrationFullSync(t *testing.T) {
}
t.Run("Should do a proper full sync", func(t *testing.T) {
err := dbstore.FullSync(ctx, instances)
err := dbstore.FullSync(ctx, instances, batchSize)
require.NoError(t, err)
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
@@ -331,8 +333,9 @@ func TestIntegrationFullSync(t *testing.T) {
}
}
})
t.Run("Should remove non existing entries on sync", func(t *testing.T) {
err := dbstore.FullSync(ctx, instances[1:])
err := dbstore.FullSync(ctx, instances[1:], batchSize)
require.NoError(t, err)
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
@@ -346,9 +349,10 @@ func TestIntegrationFullSync(t *testing.T) {
}
}
})
t.Run("Should add new entries on sync", func(t *testing.T) {
newRuleUID := "y"
err := dbstore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)))
err := dbstore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize)
require.NoError(t, err)
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
@@ -369,6 +373,134 @@ func TestIntegrationFullSync(t *testing.T) {
}
}
})
t.Run("Should save all instances when batch size is bigger than 1", func(t *testing.T) {
batchSize = 2
newRuleUID := "y"
err := dbstore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize)
require.NoError(t, err)
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
RuleOrgID: orgID,
})
require.NoError(t, err)
require.Len(t, res, len(instances)+1)
for _, ruleUID := range append(ruleUIDs, newRuleUID) {
found := false
for _, instance := range res {
if instance.RuleUID == ruleUID {
found = true
continue
}
}
if !found {
t.Errorf("Instance with RuleUID '%s' not found", ruleUID)
}
}
})
t.Run("Should not fail when the instances are empty", func(t *testing.T) {
// First, insert some data into the table.
initialInstances := []models.AlertInstance{
generateTestAlertInstance(orgID, "preexisting-1"),
generateTestAlertInstance(orgID, "preexisting-2"),
}
err := dbstore.FullSync(ctx, initialInstances, 5)
require.NoError(t, err)
// Now call FullSync with no instances. According to the code, this should return nil
// and should not delete anything in the table.
err = dbstore.FullSync(ctx, []models.AlertInstance{}, 5)
require.NoError(t, err)
// Check that the previously inserted instances are still present.
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
RuleOrgID: orgID,
})
require.NoError(t, err)
require.Len(t, res, 2, "Expected the preexisting instances to remain since empty sync does nothing")
found1, found2 := false, false
for _, r := range res {
if r.RuleUID == "preexisting-1" {
found1 = true
}
if r.RuleUID == "preexisting-2" {
found2 = true
}
}
require.True(t, found1, "Expected preexisting-1 to remain")
require.True(t, found2, "Expected preexisting-2 to remain")
})
t.Run("Should handle invalid instances by skipping them", func(t *testing.T) {
// Create a batch with one valid and one invalid instance
validInstance := generateTestAlertInstance(orgID, "valid")
invalidInstance := generateTestAlertInstance(orgID, "")
// Make the invalid instance actually invalid
invalidInstance.AlertInstanceKey.RuleUID = ""
err := dbstore.FullSync(ctx, []models.AlertInstance{validInstance, invalidInstance}, 2)
require.NoError(t, err)
// Only the valid instance should be saved.
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
RuleOrgID: orgID,
})
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, "valid", res[0].RuleUID)
})
t.Run("Should handle batchSize larger than the number of instances", func(t *testing.T) {
// Insert a small number of instances but use a large batchSize
smallSet := []models.AlertInstance{
generateTestAlertInstance(orgID, "batch-test1"),
generateTestAlertInstance(orgID, "batch-test2"),
}
err := dbstore.FullSync(ctx, smallSet, 100)
require.NoError(t, err)
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
RuleOrgID: orgID,
})
require.NoError(t, err)
require.Len(t, res, len(smallSet))
found1, found2 := false, false
for _, r := range res {
if r.RuleUID == "batch-test1" {
found1 = true
}
if r.RuleUID == "batch-test2" {
found2 = true
}
}
require.True(t, found1)
require.True(t, found2)
})
t.Run("Should handle a large set of instances with a moderate batchSize", func(t *testing.T) {
// Clear everything first.
err := dbstore.FullSync(ctx, []models.AlertInstance{}, 1)
require.NoError(t, err)
largeCount := 300
largeSet := make([]models.AlertInstance, largeCount)
for i := 0; i < largeCount; i++ {
largeSet[i] = generateTestAlertInstance(orgID, fmt.Sprintf("large-%d", i))
}
err = dbstore.FullSync(ctx, largeSet, 50)
require.NoError(t, err)
res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{
RuleOrgID: orgID,
})
require.NoError(t, err)
require.Len(t, res, largeCount)
})
}
func generateTestAlertInstance(orgID int64, ruleID string) models.AlertInstance {
+2 -1
View File
@@ -35,6 +35,7 @@ var (
ErrNoRefreshTokenFound = errors.New("no refresh token found")
ErrNotAnOAuthProvider = errors.New("not an oauth provider")
ErrCouldntRefreshToken = errors.New("could not refresh token")
ErrRetriesExhausted = errors.New("retries exhausted")
)
type Service struct {
@@ -270,7 +271,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, s
if attempts < 5 {
return nil
}
return ErrCouldntRefreshToken
return ErrRetriesExhausted
}
var newToken *oauth2.Token
+53 -2
View File
@@ -62,7 +62,11 @@ func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *login.Delet
return f.ExpectedError
}
func TestService_TryTokenRefresh(t *testing.T) {
func TestIntegration_TryTokenRefresh(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
unexpiredToken := &oauth2.Token{
AccessToken: "testaccess",
RefreshToken: "testrefresh",
@@ -86,6 +90,7 @@ func TestService_TryTokenRefresh(t *testing.T) {
socialConnector *socialtest.MockSocialConnector
socialService *socialtest.FakeSocialService
store db.DB
service *Service
}
@@ -251,6 +256,30 @@ func TestService_TryTokenRefresh(t *testing.T) {
},
expectedToken: unexpiredTokenWithIDToken,
},
{
desc: "should return ErrRetriesExhausted when lock cannot be acquired",
identity: &authn.Identity{ID: "1234", Type: claims.TypeUser, AuthenticatedBy: login.GenericOAuthModule},
setup: func(env *environment) {
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
UseRefreshToken: true,
}
env.authInfoService.ExpectedUserAuth = &login.UserAuth{
AuthModule: login.GenericOAuthModule,
AuthId: "subject",
UserId: 1234,
OAuthAccessToken: unexpiredTokenWithIDToken.AccessToken,
OAuthRefreshToken: unexpiredTokenWithIDToken.RefreshToken,
OAuthExpiry: unexpiredTokenWithIDToken.Expiry,
OAuthTokenType: unexpiredTokenWithIDToken.TokenType,
OAuthIdToken: EXPIRED_ID_TOKEN,
}
_ = env.store.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Exec(`INSERT INTO server_lock (operation_uid, last_execution, version) VALUES (?, ?, ?)`, "oauth-refresh-token-1234", time.Now().Add(2*time.Second).Unix(), 0)
return err
})
},
expectedErr: ErrRetriesExhausted,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
@@ -265,6 +294,7 @@ func TestService_TryTokenRefresh(t *testing.T) {
socialService: &socialtest.FakeSocialService{
ExpectedConnector: socialConnector,
},
store: store,
}
if tt.setup != nil {
@@ -308,7 +338,11 @@ func TestService_TryTokenRefresh(t *testing.T) {
}
}
func TestService_TryTokenRefresh_WithExternalSessions(t *testing.T) {
func TestIntegration_TryTokenRefresh_WithExternalSessions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
unexpiredToken := &oauth2.Token{
AccessToken: "testaccess",
RefreshToken: "testrefresh",
@@ -338,6 +372,7 @@ func TestService_TryTokenRefresh_WithExternalSessions(t *testing.T) {
socialConnector *socialtest.MockSocialConnector
socialService *socialtest.FakeSocialService
store db.DB
service *Service
}
@@ -509,6 +544,21 @@ func TestService_TryTokenRefresh_WithExternalSessions(t *testing.T) {
},
expectedToken: unexpiredTokenWithIDToken,
},
{
desc: "should return ErrRetriesExhausted when lock cannot be acquired",
identity: &authn.Identity{ID: "1234", Type: claims.TypeUser, AuthenticatedBy: login.GenericOAuthModule},
setup: func(env *environment) {
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
UseRefreshToken: true,
}
_ = env.store.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Exec(`INSERT INTO server_lock (operation_uid, last_execution, version) VALUES (?, ?, ?)`, "oauth-refresh-token-1234-1", time.Now().Add(2*time.Second).Unix(), 0)
return err
})
},
expectedErr: ErrRetriesExhausted,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
@@ -523,6 +573,7 @@ func TestService_TryTokenRefresh_WithExternalSessions(t *testing.T) {
socialService: &socialtest.FakeSocialService{
ExpectedConnector: socialConnector,
},
store: store,
}
if tt.setup != nil {
@@ -27,6 +27,7 @@ type InfoDTO struct {
Enabled bool
Pinned bool
PluginVersion string
AutoEnabled bool
}
type UpdateArgs struct {
@@ -19,6 +19,7 @@ func Provision(ctx context.Context, configDirectory string, pluginStore pluginst
cfgProvider: newConfigReader(logger, pluginStore),
pluginSettings: pluginSettings,
orgService: orgService,
pluginStore: pluginStore,
}
return ap.applyChanges(ctx, configDirectory)
}
@@ -30,6 +31,7 @@ type PluginProvisioner struct {
cfgProvider configReader
pluginSettings pluginsettings.Service
orgService org.Service
pluginStore pluginstore.Store
}
func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) error {
@@ -45,6 +47,14 @@ func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) er
app.OrgID = 1
}
p, found := ap.pluginStore.Plugin(ctx, app.PluginID)
if !found {
return errors.New("plugin not found")
}
if p.AutoEnabled && !app.Enabled {
return errors.New("plugin is auto enabled and cannot be disabled")
}
ps, err := ap.pluginSettings.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{
OrgID: app.OrgID,
PluginID: app.PluginID,
@@ -8,9 +8,11 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
func TestPluginProvisioner(t *testing.T) {
@@ -37,7 +39,16 @@ func TestPluginProvisioner(t *testing.T) {
store := &mockStore{}
orgMock := orgtest.NewOrgServiceFake()
orgMock.ExpectedOrg = &org.Org{ID: 4}
ap := PluginProvisioner{log: log.New("test"), cfgProvider: reader, pluginSettings: store, orgService: orgMock}
ap := PluginProvisioner{
log: log.New("test"),
cfgProvider: reader,
pluginSettings: store,
orgService: orgMock,
pluginStore: pluginstore.NewFakePluginStore(
pluginstore.Plugin{JSONData: plugins.JSONData{ID: "test-plugin"}},
pluginstore.Plugin{JSONData: plugins.JSONData{ID: "test-plugin-2"}},
),
}
err := ap.applyChanges(context.Background(), "")
require.NoError(t, err)
@@ -68,6 +79,30 @@ func TestPluginProvisioner(t *testing.T) {
require.Equal(t, tc.ExpectedSecureJSONData, cmd.SecureJSONData)
}
})
t.Run("Should return error trying to disable an auto-enabled plugin", func(t *testing.T) {
cfg := []*pluginsAsConfig{
{
Apps: []*appFromConfig{
{PluginID: "test-plugin", OrgID: 2, Enabled: false},
},
},
}
reader := &testConfigReader{result: cfg}
store := &mockStore{}
ap := PluginProvisioner{
log: log.New("test"),
cfgProvider: reader,
pluginSettings: store,
pluginStore: pluginstore.NewFakePluginStore(
pluginstore.Plugin{JSONData: plugins.JSONData{ID: "test-plugin", AutoEnabled: true}},
),
}
err := ap.applyChanges(context.Background(), "")
require.Error(t, err)
require.Contains(t, err.Error(), "plugin is auto enabled and cannot be disabled")
})
}
type testConfigReader struct {
+6 -3
View File
@@ -114,9 +114,10 @@ type UnifiedAlertingSettings struct {
RecordingRules RecordingRuleSettings
// MaxStateSaveConcurrency controls the number of goroutines (per rule) that can save alert state in parallel.
MaxStateSaveConcurrency int
StatePeriodicSaveInterval time.Duration
RulesPerRuleGroupLimit int64
MaxStateSaveConcurrency int
StatePeriodicSaveInterval time.Duration
StatePeriodicSaveBatchSize int
RulesPerRuleGroupLimit int64
// Retention period for Alertmanager notification log entries.
NotificationLogRetention time.Duration
@@ -457,6 +458,8 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error {
return err
}
uaCfg.StatePeriodicSaveBatchSize = ua.Key("state_periodic_save_batch_size").MustInt(1)
uaCfg.NotificationLogRetention, err = gtime.ParseDuration(valueAsString(ua, "notification_log_retention", (5 * 24 * time.Hour).String()))
if err != nil {
return err
+1 -1
View File
@@ -93,7 +93,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
]
},
{
"name": "folders/count",
"name": "folders/counts",
"singularName": "",
"namespaced": true,
"kind": "DescendantCounts",
+3
View File
@@ -257,6 +257,9 @@ func (c *K8sResourceClient) sanitizeObject(v *unstructured.Unstructured, replace
meta, ok := copy["metadata"].(map[string]any)
require.True(c.t, ok)
// remove generation
delete(meta, "generation")
replaceMeta = append(replaceMeta, "creationTimestamp", "resourceVersion", "uid")
for _, key := range replaceMeta {
old, ok := meta[key]
@@ -5,7 +5,11 @@ import * as faroWebSdkModule from '@grafana/faro-web-sdk';
import { BrowserConfig, FetchTransport } from '@grafana/faro-web-sdk';
import { EchoSrvTransport } from './EchoSrvTransport';
import { GrafanaJavascriptAgentBackend, GrafanaJavascriptAgentBackendOptions } from './GrafanaJavascriptAgentBackend';
import {
GrafanaJavascriptAgentBackend,
GrafanaJavascriptAgentBackendOptions,
TRACKING_URLS,
} from './GrafanaJavascriptAgentBackend';
describe('GrafanaJavascriptAgentEchoBackend', () => {
let mockedSetUser: jest.Mock;
@@ -93,8 +97,7 @@ describe('GrafanaJavascriptAgentEchoBackend', () => {
expect(initializeFaroMock.mock.calls[0][0].transports?.[0]).toBeInstanceOf(EchoSrvTransport);
expect(initializeFaroMock.mock.calls[0][0].transports?.[0].getIgnoreUrls()).toEqual([
/.*\/log-grafana-javascript-agent.*/,
/.*.google-analytics.com*.*/,
/.*.googletagmanager.com*.*/,
/\.(google-analytics|googletagmanager)\.com/,
/frontend-metrics/,
/\/collect(?:\/[\w]*)?$/,
]);
@@ -116,6 +119,17 @@ describe('GrafanaJavascriptAgentEchoBackend', () => {
});
});
test('will ensure the performance of TRACKING_URLS', async () => {
// 10e6 is based on true events
const longString = Array.from({ length: 10e6 }, () => Math.random().toString(36)[2]).join('');
const maxExecutionTime = 500;
const start = performance.now();
TRACKING_URLS.some((u) => u && longString.match(u) !== null);
const end = performance.now();
expect(end - start).toBeLessThanOrEqual(maxExecutionTime);
});
//@FIXME - make integration test work
// it('integration test with EchoSrv and GrafanaJavascriptAgent', async () => {
@@ -37,9 +37,8 @@ export interface GrafanaJavascriptAgentBackendOptions extends BrowserConfig {
ignoreUrls: RegExp[];
}
const TRACKING_URLS = [
/.*.google-analytics.com*.*/,
/.*.googletagmanager.com*.*/,
export const TRACKING_URLS = [
/\.(google-analytics|googletagmanager)\.com/,
/frontend-metrics/,
/\/collect(?:\/[\w]*)?$/,
];
@@ -0,0 +1,27 @@
import { mockFolder } from '../mocks';
import { stringifyFolder } from './useFolder';
describe('with slashes', () => {
it('should correctly stringify a folder', () => {
const folder = mockFolder({ title: 'my/folder' });
expect(stringifyFolder(folder)).toEqual('my\\/folder');
});
it('should correctly stringify a nested folder', () => {
const folder = mockFolder({ title: 'my/folder', parents: [mockFolder({ title: 'parent/slash' })] });
expect(stringifyFolder(folder)).toEqual('parent\\/slash/my\\/folder');
});
});
describe('without slashes', () => {
it('should correctly stringify a folder', () => {
const folder = mockFolder({ title: 'my folder' });
expect(stringifyFolder(folder)).toEqual('my folder');
});
it('should correctly stringify a nested folder', () => {
const folder = mockFolder({ title: 'my folder', parents: [mockFolder({ title: 'my parent' })] });
expect(stringifyFolder(folder)).toEqual('my parent/my folder');
});
});
@@ -34,5 +34,11 @@ export function useFolder(uid?: string): ReturnBag {
}
export function stringifyFolder({ title, parents }: FolderDTO) {
return parents && parents?.length ? [...parents.map((p) => p.title), title].join('/') : title;
return parents && parents?.length
? [...parents.map((p) => p.title), title].map(encodeTitle).join('/')
: encodeTitle(title);
}
export function encodeTitle(title: string): string {
return title.replaceAll('/', '\\/');
}
+1 -1
View File
@@ -473,7 +473,7 @@ export function fieldMap(provider: string): Record<string, FieldData> {
label: 'Organization attribute path',
description: 'JMESPath expression to use for organization lookup.',
type: 'text',
hidden: !['generic_oauth', 'okta'].includes(provider),
hidden: !(['generic_oauth', 'okta'].includes(provider) && contextSrv.isGrafanaAdmin),
},
defineAllowedGroups: {
label: 'Define allowed groups',
@@ -638,6 +638,7 @@ describe('DashboardScene', () => {
app: CoreApp.Dashboard,
dashboardUID: 'dash-1',
panelId: 1,
panelName: 'Panel A',
panelPluginId: 'table',
});
});
@@ -653,6 +654,7 @@ describe('DashboardScene', () => {
app: CoreApp.Dashboard,
dashboardUID: 'dash-1',
panelId: 1,
panelName: 'Panel A',
panelPluginId: 'table',
});
});
@@ -629,6 +629,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
app: CoreApp.Dashboard,
dashboardUID: this.state.uid,
panelId,
panelName: panel?.state?.title,
panelPluginId: panel?.state.pluginId,
scopes: this._scopesFacade?.value,
};
@@ -380,6 +380,7 @@ export class PanelModel implements DataConfigSource, IPanelModel {
datasource: this.datasource,
queries: this.targets,
panelId: this.id,
panelName: this.title,
panelPluginId: this.type,
dashboardUID: dashboardUID,
timezone: dashboardTimezone,
+3 -3
View File
@@ -25,10 +25,10 @@ export class PlaylistSrv extends StateManagerBase<PlaylistSrvState> {
private nextTimeoutId: ReturnType<typeof setTimeout> | undefined;
private urls: string[] = []; // the URLs we need to load
private index = 0;
private declare interval: number;
private declare startUrl: string;
declare private interval: number;
declare private startUrl: string;
private numberOfLoops = 0;
private declare validPlaylistUrl: string;
declare private validPlaylistUrl: string;
private locationListenerUnsub?: () => void;
private api: PlaylistAPI;
@@ -0,0 +1,98 @@
import { render, screen } from '@testing-library/react';
import { PluginSignatureStatus } from '@grafana/data';
import { contextSrv } from 'app/core/core';
import { usePluginConfig } from '../../hooks/usePluginConfig';
import { CatalogPlugin } from '../../types';
import { GetStartedWithApp } from './GetStartedWithApp';
jest.mock('app/core/core', () => ({
contextSrv: {
hasPermission: jest.fn(),
},
}));
jest.mock('../../api', () => ({
updatePluginSettings: jest.fn(),
}));
jest.mock('../../hooks/usePluginConfig', () => ({
usePluginConfig: jest.fn(),
}));
const mockPlugin: CatalogPlugin = {
id: 'test-plugin',
name: 'Test Plugin',
description: 'Test Plugin Description',
downloads: 0,
hasUpdate: false,
info: {
logos: {
large: 'https://grafana.com/assets/img/brand/grafana_icon.svg',
small: 'https://grafana.com/assets/img/brand/grafana_icon.svg',
},
keywords: [],
},
isDev: false,
isCore: false,
isEnterprise: false,
isInstalled: false,
isDisabled: false,
isDeprecated: false,
isManaged: false,
isPreinstalled: { found: false, withVersion: false },
isPublished: false,
orgName: 'Test Org',
signature: PluginSignatureStatus.valid,
popularity: 0,
publishedAt: '2021-01-01',
updatedAt: '2021-01-01',
};
describe('GetStartedWithApp', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders null if pluginConfig is not available', () => {
(usePluginConfig as jest.Mock).mockReturnValue({ value: null });
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
const { container } = render(<GetStartedWithApp plugin={mockPlugin} />);
expect(container.firstChild).toBeNull();
});
it('renders null if user does not have permission', () => {
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: {} } });
(contextSrv.hasPermission as jest.Mock).mockReturnValue(false);
const { container } = render(<GetStartedWithApp plugin={mockPlugin} />);
expect(container.firstChild).toBeNull();
});
it('renders enable button if plugin is not enabled', () => {
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: { enabled: false } } });
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
render(<GetStartedWithApp plugin={mockPlugin} />);
expect(screen.getByText('Enable')).toBeInTheDocument();
});
it('renders disable button if plugin is enabled and not autoEnabled', () => {
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: { enabled: true, autoEnabled: false } } });
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
render(<GetStartedWithApp plugin={mockPlugin} />);
expect(screen.getByText('Disable')).toBeInTheDocument();
});
it('does not render disable button if plugin is enabled and autoEnabled', () => {
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: { enabled: true, autoEnabled: true } } });
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
render(<GetStartedWithApp plugin={mockPlugin} />);
expect(screen.queryByText('Disable')).not.toBeInTheDocument();
});
});
@@ -25,7 +25,7 @@ export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null
return null;
}
const { enabled, jsonData } = pluginConfig?.meta;
const { enabled, autoEnabled, jsonData } = pluginConfig?.meta;
const enable = () => {
reportInteraction('plugins_detail_enable_clicked', {
@@ -63,7 +63,7 @@ export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null
</Button>
)}
{enabled && (
{enabled && !autoEnabled && (
<Button variant="destructive" onClick={disable}>
Disable
</Button>
@@ -8,7 +8,6 @@ import * as useQueryParams from 'app/core/hooks/useQueryParams';
import { TestProvider } from '../../../test/helpers/TestProvider';
import { backendSrv } from '../../core/services/backend_srv';
import { TeamPermissionLevel } from '../../types';
import { getMockTeam } from '../teams/__mocks__/teamMocks';
import { Props, UserProfileEditPage } from './UserProfileEditPage';
@@ -45,7 +44,6 @@ const defaultProps: Props = {
email: 'team.one@test.com',
avatarUrl: '/avatar/07d881f402480a2a511a9a15b5fa82c0',
memberCount: 2000,
permission: TeamPermissionLevel.Admin,
}),
],
orgs: [
@@ -1,5 +1,5 @@
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { OrgRole, TeamPermissionLevel } from '../../../types';
import { OrgRole } from '../../../types';
import { getMockTeam } from '../../teams/__mocks__/teamMocks';
import {
@@ -92,13 +92,13 @@ describe('userReducer', () => {
.givenReducer(userReducer, { ...initialUserState, teamsAreLoading: true })
.whenActionIsDispatched(
teamsLoaded({
teams: [getMockTeam(1, 'aaaaaa', { permission: TeamPermissionLevel.Admin })],
teams: [getMockTeam(1, 'aaaaaa')],
})
)
.thenStateShouldEqual({
...initialUserState,
teamsAreLoading: false,
teams: [getMockTeam(1, 'aaaaaa', { permission: TeamPermissionLevel.Admin })],
teams: [getMockTeam(1, 'aaaaaa')],
});
});
});
@@ -160,6 +160,7 @@ function describeQueryRunnerScenario(
raw: { from: '1d', to: 'now' },
},
panelId: 1,
panelName: 'PanelName',
queries: [{ refId: 'A' }],
} as QueryRunnerOptions;
@@ -198,6 +199,11 @@ describe('PanelQueryRunner', () => {
expect(ctx.queryCalledWith?.scopedVars.__interval!.text).toBe('5m');
expect(ctx.queryCalledWith?.scopedVars.__interval_ms!.text).toBe('300000');
});
it('should pass the panel id and name', async () => {
expect(ctx.queryCalledWith?.panelId).toBe(1);
expect(ctx.queryCalledWith?.panelName).toBe('PanelName');
});
});
describeQueryRunnerScenario('with maxDataPoints', (ctx) => {
@@ -50,6 +50,7 @@ export interface QueryRunnerOptions<
datasource: DataSourceRef | DataSourceApi<TQuery, TOptions> | null;
queries: TQuery[];
panelId?: number;
panelName?: string;
panelPluginId?: string;
dashboardUID?: string;
timezone: TimeZone;
@@ -258,6 +259,7 @@ export class PanelQueryRunner {
timezone,
datasource,
panelId,
panelName,
panelPluginId,
dashboardUID,
timeRange,
@@ -283,6 +285,7 @@ export class PanelQueryRunner {
requestId: getNextRequestId(),
timezone,
panelId,
panelName,
panelPluginId,
dashboardUID,
range: timeRange,
@@ -328,6 +331,9 @@ export class PanelQueryRunner {
request.intervalMs = norm.intervalMs;
request.filters = this.templateSrv.getAdhocFilters(ds.name, true);
request.panelId = panelId;
request.panelName = panelName;
this.lastRequest = request;
this.pipeToSubject(runRequest(ds, request), panelId, false, addErroDSVariable);
@@ -118,6 +118,7 @@ describe('emitDataRequestEvent', () => {
it('Should report meta analytics', () => {
const data = getTestData({
panelId: 2,
panelName: 'Panel Name2',
});
emitDataRequestEvent(datasource)(data);
@@ -130,6 +131,7 @@ describe('emitDataRequestEvent', () => {
datasourceType: datasource.type,
source: CoreApp.Dashboard,
panelId: 2,
panelName: 'Panel Name2',
dashboardUid: 'test', // from dashboard srv
dataSize: 0,
duration: 1,
@@ -144,6 +146,7 @@ describe('emitDataRequestEvent', () => {
const data = getTestData(
{
panelId: 2,
panelName: 'Panel Name2',
},
partiallyCachedSeries
);
@@ -158,6 +161,7 @@ describe('emitDataRequestEvent', () => {
datasourceType: datasource.type,
source: CoreApp.Dashboard,
panelId: 2,
panelName: 'Panel Name2',
dashboardUid: 'test',
dataSize: 2,
duration: 1,
@@ -172,6 +176,7 @@ describe('emitDataRequestEvent', () => {
const data = getTestData(
{
panelId: 2,
panelName: 'Panel Name2',
},
multipleDataframesWithSameRefId
);
@@ -186,6 +191,7 @@ describe('emitDataRequestEvent', () => {
datasourceType: datasource.type,
source: CoreApp.Dashboard,
panelId: 2,
panelName: 'Panel Name2',
dashboardUid: 'test', // from dashboard srv
dataSize: 2,
duration: 1,
@@ -31,6 +31,8 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
panelId: 0,
panelPluginId: data.request?.panelPluginId,
duration: data.request.endTime! - data.request.startTime,
...(data?.request?.panelId && Number.isInteger(data.request.panelId) && { panelId: data.request.panelId }),
...(data?.request?.panelName && { panelName: data.request.panelName }),
};
enrichWithInfo(eventData, data);
@@ -62,9 +64,6 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
eventData.totalQueries = Object.keys(queryCacheStatus).length;
eventData.cachedQueries = Object.values(queryCacheStatus).filter((val) => val === true).length;
if (data.request && Number.isInteger(data.request.panelId)) {
eventData.panelId = data.request.panelId;
}
const dashboard = getDashboardSrv().getCurrent();
if (dashboard) {
+4 -5
View File
@@ -23,13 +23,13 @@ import { Page } from 'app/core/components/Page/Page';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
import { Trans, t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction, Role, StoreState, Team } from 'app/types';
import { AccessControlAction, Role, StoreState, TeamWithRoles } from 'app/types';
import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker';
import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions';
type Cell<T extends keyof Team = keyof Team> = CellProps<Team, Team[T]>;
type Cell<T extends keyof TeamWithRoles = keyof TeamWithRoles> = CellProps<TeamWithRoles, TeamWithRoles[T]>;
export interface OwnProps {}
export interface State {
@@ -37,13 +37,12 @@ export interface State {
}
// this is dummy data to pass to the table while the real data is loading
const skeletonData: Team[] = new Array(3).fill(null).map((_, index) => ({
const skeletonData: TeamWithRoles[] = new Array(3).fill(null).map((_, index) => ({
id: index,
uid: '',
memberCount: 0,
name: '',
orgId: 0,
permission: 0,
}));
export const TeamList = ({
@@ -76,7 +75,7 @@ export const TeamList = ({
const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate);
const displayRolePicker = shouldDisplayRolePicker();
const columns: Array<Column<Team>> = useMemo(
const columns: Array<Column<TeamWithRoles>> = useMemo(
() => [
{
id: 'avatarUrl',
@@ -24,7 +24,6 @@ export const getMockTeam = (i = 1, uid = 'aaaaaa', overrides = {}): Team => {
avatarUrl: 'some/url/',
email: `test-${uid}@test.com`,
memberCount: i,
permission: TeamPermissionLevel.Member,
accessControl: { isEditor: false },
orgId: 0,
...overrides,
+3 -3
View File
@@ -5,7 +5,7 @@ import { FetchDataArgs } from '@grafana/ui';
import { updateNavIndex } from 'app/core/actions';
import { contextSrv } from 'app/core/core';
import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { AccessControlAction, Team, TeamMember, ThunkResult } from 'app/types';
import { AccessControlAction, TeamWithRoles, TeamMember, ThunkResult, Team } from 'app/types';
import { buildNavModel } from './navModel';
import {
@@ -46,9 +46,9 @@ export function loadTeams(initial = false): ThunkResult<void> {
contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList)
) {
dispatch(rolesFetchBegin());
const teamIds = response?.teams.map((t: Team) => t.id);
const teamIds = response?.teams.map((t: TeamWithRoles) => t.id);
const roles = await getBackendSrv().post(`/api/access-control/teams/roles/search`, { teamIds });
response.teams.forEach((t: Team) => {
response.teams.forEach((t: TeamWithRoles) => {
t.roles = roles ? roles[t.id] || [] : [];
});
dispatch(rolesFetchEnd());
+6 -1
View File
@@ -33,6 +33,7 @@ import {
sceneUtils,
SceneVariable,
SceneVariableSet,
UrlSyncContextProvider,
UrlSyncManager,
VariableDependencyConfig,
VariableValueSelectors,
@@ -668,7 +669,11 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
<settings.Component model={settings} />
</div>
)}
<div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div>
{topScene && (
<UrlSyncContextProvider scene={topScene}>
<div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div>
</UrlSyncContextProvider>
)}
</div>
);
};
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -19,8 +19,8 @@
"prismjs": "1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0"
},
@@ -20,8 +20,8 @@
"prismjs": "1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0"
},
@@ -15,7 +15,7 @@
"prismjs": "1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-use": "17.5.1",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0"
},
@@ -15,8 +15,8 @@
"micro-memoize": "^4.1.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0",
"uuid": "9.0.1"
@@ -15,7 +15,7 @@
"logfmt": "^1.3.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "5.8.3",
"react-select": "5.9.0",
"react-window": "1.8.10",
"rxjs": "7.8.1",
"stream-browserify": "3.0.0",
@@ -13,7 +13,7 @@
"monaco-editor": "0.34.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-use": "17.5.1",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0"
},
@@ -29,8 +29,8 @@
"prismjs": "1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"semver": "7.6.3",
"stream-browserify": "3.0.0",
@@ -14,8 +14,8 @@
"lodash": "4.17.21",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-select": "5.8.3",
"react-use": "17.5.1",
"react-select": "5.9.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
"tslib": "2.7.0"
},
-2
View File
@@ -2,9 +2,7 @@ import { OrgRole } from '@grafana/data';
export enum TeamPermissionLevel {
Admin = 4,
Editor = 2,
Member = 0,
Viewer = 1,
}
export { OrgRole as OrgRole };
+13 -11
View File
@@ -1,5 +1,6 @@
import { WithAccessControlMetadata } from '@grafana/data';
import { Role } from './accessControl';
import { TeamPermissionLevel } from './acl';
export interface TeamDTO {
/**
@@ -13,14 +14,16 @@ export interface TeamDTO {
}
// This is the team resource with permissions and metadata expanded
export interface Team {
id: number; // TODO switch to UUID
uid: string; // Prefer UUID
export interface Team extends WithAccessControlMetadata {
/**
* AccessControl metadata associated with a given resource.
* Internal id of team
* @deprecated use uid instead
*/
accessControl?: Record<string, boolean>;
id: number;
/**
* A unique identifier for the team.
*/
uid: string; // Prefer UUID
/**
* AvatarUrl is the team's avatar URL.
*/
@@ -41,10 +44,9 @@ export interface Team {
* OrgId is the ID of an organisation the team belongs to.
*/
orgId: number;
/**
* TODO - it seems it's a team_member.permission, unlikely it should belong to the team kind
*/
permission: TeamPermissionLevel;
}
export interface TeamWithRoles extends Team {
/**
* RBAC roles assigned to the team.
*/
-1
View File
@@ -273,7 +273,6 @@ def integration_test_pipelines():
pipelines = []
volumes = integration_test_services_volumes()
integration_test_steps = postgres_integration_tests_steps() + \
mysql_integration_tests_steps("mysql57", "5.7") + \
mysql_integration_tests_steps("mysql80", "8.0") + \
redis_integration_tests_steps() + \
memcached_integration_tests_steps() + \
-4
View File
@@ -66,10 +66,6 @@ def integration_benchmarks(prefix):
"GRAFANA_TEST_DB": "postgres",
"POSTGRES_HOST": "postgres",
}) + \
integration_benchmarks_step("mysql-5.7", {
"GRAFANA_TEST_DB": "mysql",
"MYSQL_HOST": "mysql57",
}) + \
integration_benchmarks_step("mysql-8.0", {
"GRAFANA_TEST_DB": "mysql",
"MYSQL_HOST": "mysql80",
@@ -71,7 +71,6 @@ def integration_tests(trigger, prefix, ver_mode = "pr"):
# test_steps = [a, b] + [c, d] + [e, f]...
test_steps = postgres_integration_tests_steps() + \
mysql_integration_tests_steps("mysql57", "5.7") + \
mysql_integration_tests_steps("mysql80", "8.0") + \
redis_integration_tests_steps() + \
memcached_integration_tests_steps() + \
-13
View File
@@ -10,7 +10,6 @@ load(
def integration_test_services_volumes():
return [
{"name": "postgres", "temp": {"medium": "memory"}},
{"name": "mysql57", "temp": {"medium": "memory"}},
{"name": "mysql80", "temp": {"medium": "memory"}},
]
@@ -29,18 +28,6 @@ def integration_test_services():
{"name": "postgres", "path": "/var/lib/postgresql/data/pgdata"},
],
},
{
"name": "mysql57",
"image": images["mysql5"],
"environment": {
"MYSQL_ROOT_PASSWORD": "rootpass",
"MYSQL_DATABASE": "grafana_tests",
"MYSQL_USER": "grafana",
"MYSQL_PASSWORD": "password",
},
"volumes": [{"name": "mysql57", "path": "/var/lib/mysql"}],
"commands": ["docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"],
},
{
"name": "mysql80",
"image": images["mysql8"],
-1
View File
@@ -23,7 +23,6 @@ images = {
"python": "python:3.8",
"postgres_alpine": "postgres:12.3-alpine",
"mimir": "grafana/mimir-alpine:r316-55f47f8",
"mysql5": "mysql:5.7.39",
"mysql8": "mysql:8.0.32",
"redis_alpine": "redis:6.2.11-alpine",
"memcached_alpine": "memcached:1.6.9-alpine",
+200 -200
View File
@@ -2572,10 +2572,10 @@ __metadata:
languageName: node
linkType: hard
"@eslint/js@npm:9.16.0":
version: 9.16.0
resolution: "@eslint/js@npm:9.16.0"
checksum: 10/122da09b6a2c6a92d68be26146af1da17d9d4e03cf9435a874d341f18519122eef9ed229e52a1b781dc5987239754954e80a0b11bbe8f65df96e9ef8f3b438ad
"@eslint/js@npm:9.17.0":
version: 9.17.0
resolution: "@eslint/js@npm:9.17.0"
checksum: 10/1a89e62f5c50e75d44565b7f3b91701455a999132c991e10bac59c118fbb54bdd54be22b9bda1ac730f78a2e64604403d65ce5dd7726d80b2632982cfc3d84ac
languageName: node
linkType: hard
@@ -2649,9 +2649,9 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/react@npm:0.27.0":
version: 0.27.0
resolution: "@floating-ui/react@npm:0.27.0"
"@floating-ui/react@npm:0.27.2":
version: 0.27.2
resolution: "@floating-ui/react@npm:0.27.2"
dependencies:
"@floating-ui/react-dom": "npm:^2.1.2"
"@floating-ui/utils": "npm:^0.2.8"
@@ -2659,7 +2659,7 @@ __metadata:
peerDependencies:
react: ">=17.0.0"
react-dom: ">=17.0.0"
checksum: 10/b04e8b356c6ffebbd7033cfb0ba7c08e83b3c0314b28e860ff1502595ca6eb245ef8b935b8879af044edca38a27335fefff15eddaabe3a7d39bcfd255b7fcd86
checksum: 10/c9ff0d7216cc7b51f066e8927852ba014bf51c79e48a1837f99760750df37fc3ed010dcdc89e57e673e2740e3bd2b91ccdcc6ccd13ec927950110a18b6b9cefa
languageName: node
linkType: hard
@@ -2829,9 +2829,9 @@ __metadata:
prismjs: "npm:1.29.0"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-select: "npm:5.8.3"
react-select: "npm:5.9.0"
react-select-event: "npm:5.5.1"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
ts-node: "npm:10.9.2"
tslib: "npm:2.7.0"
@@ -2901,7 +2901,7 @@ __metadata:
prismjs: "npm:1.29.0"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
style-loader: "npm:4.0.0"
ts-node: "npm:10.9.2"
@@ -2940,8 +2940,8 @@ __metadata:
micro-memoize: "npm:^4.1.2"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-select: "npm:5.8.3"
react-use: "npm:17.5.1"
react-select: "npm:5.9.0"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
ts-node: "npm:10.9.2"
tslib: "npm:2.7.0"
@@ -2981,7 +2981,7 @@ __metadata:
logfmt: "npm:^1.3.2"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-select: "npm:5.8.3"
react-select: "npm:5.9.0"
react-window: "npm:1.8.10"
rxjs: "npm:7.8.1"
stream-browserify: "npm:3.0.0"
@@ -3078,7 +3078,7 @@ __metadata:
monaco-editor: "npm:0.34.1"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
ts-node: "npm:10.9.2"
tslib: "npm:2.7.0"
@@ -3123,10 +3123,10 @@ __metadata:
prismjs: "npm:1.29.0"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-select: "npm:5.8.3"
react-select: "npm:5.9.0"
react-select-event: "npm:5.5.1"
react-test-renderer: "npm:18.2.0"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
ts-node: "npm:10.9.2"
tslib: "npm:2.7.0"
@@ -3180,9 +3180,9 @@ __metadata:
prismjs: "npm:1.29.0"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-select: "npm:5.8.3"
react-select: "npm:5.9.0"
react-select-event: "npm:5.5.1"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
semver: "npm:7.6.3"
stream-browserify: "npm:3.0.0"
@@ -3220,8 +3220,8 @@ __metadata:
lodash: "npm:4.17.21"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-select: "npm:5.8.3"
react-use: "npm:17.5.1"
react-select: "npm:5.9.0"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
ts-node: "npm:10.9.2"
tslib: "npm:2.7.0"
@@ -3295,7 +3295,7 @@ __metadata:
papaparse: "npm:5.4.1"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4"
rollup-plugin-dts: "npm:^6.1.1"
@@ -3363,7 +3363,7 @@ __metadata:
dependencies:
"@typescript-eslint/types": "npm:^8.9.0"
"@typescript-eslint/utils": "npm:^8.9.0"
eslint: "npm:9.16.0"
eslint: "npm:9.17.0"
tslib: "npm:2.7.0"
languageName: unknown
linkType: soft
@@ -3492,7 +3492,7 @@ __metadata:
jest-canvas-mock: "npm:2.5.2"
lodash: "npm:4.17.21"
react: "npm:18.2.0"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
react-virtualized-auto-sizer: "npm:1.0.24"
rollup: "npm:^4.22.4"
rollup-plugin-dts: "npm:^6.1.1"
@@ -3570,8 +3570,8 @@ __metadata:
"@types/systemjs": "npm:6.15.1"
jest: "npm:^29.6.4"
react: "npm:18.2.0"
react-select: "npm:5.8.3"
react-use: "npm:17.5.1"
react-select: "npm:5.9.0"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
ts-jest: "npm:29.2.5"
ts-node: "npm:10.9.2"
@@ -3592,7 +3592,7 @@ __metadata:
"@types/eslint": "npm:9.6.1"
"@types/webpack-bundle-analyzer": "npm:^4.7.0"
copy-webpack-plugin: "npm:12.0.2"
eslint: "npm:9.16.0"
eslint: "npm:9.17.0"
eslint-webpack-plugin: "npm:4.2.0"
fork-ts-checker-webpack-plugin: "npm:9.0.2"
glob: "npm:11.0.0"
@@ -3625,7 +3625,7 @@ __metadata:
dependencies:
"@emotion/css": "npm:11.13.5"
"@emotion/eslint-plugin": "npm:11.12.0"
"@floating-ui/react": "npm:0.27.0"
"@floating-ui/react": "npm:0.27.2"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/experimental": "npm:2.1.5"
@@ -3672,11 +3672,11 @@ __metadata:
date-fns: "npm:3.6.0"
debounce-promise: "npm:3.1.2"
esbuild: "npm:0.24.0"
eslint: "npm:9.16.0"
eslint: "npm:9.17.0"
eslint-config-prettier: "npm:9.1.0"
eslint-plugin-import: "npm:^2.31.0"
eslint-plugin-jest: "npm:28.9.0"
eslint-plugin-jsdoc: "npm:50.5.0"
eslint-plugin-jsdoc: "npm:50.6.1"
eslint-plugin-jsx-a11y: "npm:6.10.2"
eslint-plugin-lodash: "npm:8.0.0"
eslint-plugin-react: "npm:7.37.2"
@@ -3696,15 +3696,15 @@ __metadata:
moment-timezone: "npm:0.5.46"
monaco-promql: "npm:1.7.4"
pluralize: "npm:8.0.0"
prettier: "npm:3.3.3"
prettier: "npm:3.4.2"
prismjs: "npm:1.29.0"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-highlight-words: "npm:0.20.0"
react-select: "npm:5.8.3"
react-select: "npm:5.9.0"
react-select-event: "npm:5.5.1"
react-test-renderer: "npm:18.2.0"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
react-window: "npm:1.8.10"
rollup: "npm:^4.22.4"
rollup-plugin-dts: "npm:^6.1.1"
@@ -3756,7 +3756,7 @@ __metadata:
lodash: "npm:4.17.21"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4"
rollup-plugin-dts: "npm:^6.1.1"
@@ -3791,7 +3791,7 @@ __metadata:
"@types/react": "npm:18.3.3"
"@types/react-dom": "npm:18.2.25"
esbuild: "npm:0.24.0"
prettier: "npm:3.3.3"
prettier: "npm:3.4.2"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
rimraf: "npm:6.0.1"
@@ -3897,8 +3897,8 @@ __metadata:
lodash: "npm:4.17.21"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
react-select: "npm:5.8.3"
react-use: "npm:17.5.1"
react-select: "npm:5.9.0"
react-use: "npm:17.6.0"
react-virtualized-auto-sizer: "npm:1.0.24"
rxjs: "npm:7.8.1"
sql-formatter-plus: "npm:^1.3.6"
@@ -3928,7 +3928,7 @@ __metadata:
"@emotion/react": "npm:11.14.0"
"@emotion/serialize": "npm:1.3.3"
"@faker-js/faker": "npm:^9.0.0"
"@floating-ui/react": "npm:0.27.0"
"@floating-ui/react": "npm:0.27.2"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/faro-web-sdk": "npm:^1.3.6"
@@ -4015,11 +4015,11 @@ __metadata:
ol: "npm:7.4.0"
prismjs: "npm:1.29.0"
process: "npm:^0.11.10"
rc-cascader: "npm:3.30.0"
rc-cascader: "npm:3.31.0"
rc-drawer: "npm:7.2.0"
rc-slider: "npm:11.1.7"
rc-time-picker: "npm:^3.7.3"
rc-tooltip: "npm:6.2.1"
rc-tooltip: "npm:6.3.1"
react: "npm:18.2.0"
react-calendar: "npm:^5.1.0"
react-colorful: "npm:5.6.1"
@@ -4032,12 +4032,12 @@ __metadata:
react-inlinesvg: "npm:4.1.5"
react-loading-skeleton: "npm:3.5.0"
react-router-dom-v5-compat: "npm:^6.26.1"
react-select: "npm:5.8.3"
react-select: "npm:5.9.0"
react-select-event: "npm:^5.1.0"
react-table: "npm:7.8.0"
react-test-renderer: "npm:18.2.0"
react-transition-group: "npm:4.4.5"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
react-window: "npm:1.8.10"
rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4"
@@ -6610,8 +6610,8 @@ __metadata:
linkType: hard
"@rollup/plugin-typescript@npm:^12.1.0":
version: 12.1.1
resolution: "@rollup/plugin-typescript@npm:12.1.1"
version: 12.1.2
resolution: "@rollup/plugin-typescript@npm:12.1.2"
dependencies:
"@rollup/pluginutils": "npm:^5.1.0"
resolve: "npm:^1.22.1"
@@ -6624,7 +6624,7 @@ __metadata:
optional: true
tslib:
optional: true
checksum: 10/838d5e67d1b383154fab7ae1b0c58e91844c70380210b12c1d5f2ed5d2264d4fbd21ff991a13a4a72078dce897b5c482c70554a21671269219aa9d2525f14dcd
checksum: 10/1fd201b9430125686bcdb3eecfa4f8d9d5005a9d454f2d733c0fdc0f58e28f772209ecbfe4f5d1a42a2a7ac25746a5f37d15897d695326eaea0fe3a30857375d
languageName: node
linkType: hard
@@ -6783,22 +6783,22 @@ __metadata:
languageName: node
linkType: hard
"@rsdoctor/client@npm:0.4.11":
version: 0.4.11
resolution: "@rsdoctor/client@npm:0.4.11"
checksum: 10/fc59005c40dfb11582b16e74de768ef5fe73a0e06db472908b9568db870380554db3b87867e3bf33ce0251e2f351c1249d0181c7e575eeef3c64c64592f9b179
"@rsdoctor/client@npm:0.4.12":
version: 0.4.12
resolution: "@rsdoctor/client@npm:0.4.12"
checksum: 10/04ae4f8de34fdf5cec5b9db2072e646f3327a7ba7ca4a96942961fa26129e715db92c28ca68bf0f6c5149ccbfb9b73aa2d2984242bf8d35bc54f827e2ccae84e
languageName: node
linkType: hard
"@rsdoctor/core@npm:0.4.11":
version: 0.4.11
resolution: "@rsdoctor/core@npm:0.4.11"
"@rsdoctor/core@npm:0.4.12":
version: 0.4.12
resolution: "@rsdoctor/core@npm:0.4.12"
dependencies:
"@rsdoctor/graph": "npm:0.4.11"
"@rsdoctor/sdk": "npm:0.4.11"
"@rsdoctor/types": "npm:0.4.11"
"@rsdoctor/utils": "npm:0.4.11"
axios: "npm:^1.7.8"
"@rsdoctor/graph": "npm:0.4.12"
"@rsdoctor/sdk": "npm:0.4.12"
"@rsdoctor/types": "npm:0.4.12"
"@rsdoctor/utils": "npm:0.4.12"
axios: "npm:^1.7.9"
enhanced-resolve: "npm:5.12.0"
filesize: "npm:^10.1.6"
fs-extra: "npm:^11.1.1"
@@ -6807,31 +6807,31 @@ __metadata:
semver: "npm:^7.6.3"
source-map: "npm:^0.7.4"
webpack-bundle-analyzer: "npm:^4.10.2"
checksum: 10/b32242191ab4a202a997ffe290b3b62361942e052fe39803f2f458eb17b127516084c0b80705e4645edcf36a839f4e60f76a79c19f0068824bd32e9013b10788
checksum: 10/d77252288cec3ad9cd6389fb772125fcbdca669c0186a540a9270f2c3ac01b0620d188595e952a3683b0af4e4eef2c8816b0af71a2d87667eef7f2e47484a4b3
languageName: node
linkType: hard
"@rsdoctor/graph@npm:0.4.11":
version: 0.4.11
resolution: "@rsdoctor/graph@npm:0.4.11"
"@rsdoctor/graph@npm:0.4.12":
version: 0.4.12
resolution: "@rsdoctor/graph@npm:0.4.12"
dependencies:
"@rsdoctor/types": "npm:0.4.11"
"@rsdoctor/utils": "npm:0.4.11"
"@rsdoctor/types": "npm:0.4.12"
"@rsdoctor/utils": "npm:0.4.12"
lodash: "npm:^4.17.21"
socket.io: "npm:4.7.2"
socket.io: "npm:4.8.1"
source-map: "npm:^0.7.4"
checksum: 10/ba828662d7b4889aeb806c14f88a9321e9d10a7a6e568675a65afcf3d6013958526ee023a430946315d4899dce853e2e014876c2a63fe1f9e3dd24de6564f7b4
checksum: 10/53cb9dd4c8507cf22ee6088e4fc1b5d26fb2090dee1570fd1964659d854bab33ed7ca73862a84a256280a85aa076c5e18953830af2127fb583d5eaa5274a934d
languageName: node
linkType: hard
"@rsdoctor/sdk@npm:0.4.11":
version: 0.4.11
resolution: "@rsdoctor/sdk@npm:0.4.11"
"@rsdoctor/sdk@npm:0.4.12":
version: 0.4.12
resolution: "@rsdoctor/sdk@npm:0.4.12"
dependencies:
"@rsdoctor/client": "npm:0.4.11"
"@rsdoctor/graph": "npm:0.4.11"
"@rsdoctor/types": "npm:0.4.11"
"@rsdoctor/utils": "npm:0.4.11"
"@rsdoctor/client": "npm:0.4.12"
"@rsdoctor/graph": "npm:0.4.12"
"@rsdoctor/types": "npm:0.4.12"
"@rsdoctor/utils": "npm:0.4.12"
"@types/fs-extra": "npm:^11.0.4"
body-parser: "npm:1.20.3"
cors: "npm:2.8.5"
@@ -6841,16 +6841,16 @@ __metadata:
lodash: "npm:^4.17.21"
open: "npm:^8.4.2"
serve-static: "npm:1.16.2"
socket.io: "npm:4.7.2"
socket.io: "npm:4.8.1"
source-map: "npm:^0.7.4"
tapable: "npm:2.2.1"
checksum: 10/b66424c5fc61cbfd52d95c8dc5393715f73653a2d9b7acc04c825ed100b3185c9afe02dfc9450587454430e40a2cc21aa20225ae1b7927e43445d3440d599b47
checksum: 10/a0f20c0410e44fd9f7428fee39c45abf493144e73aa910d9c8f34760461d36d1144856c9ea3078afee2282817acb8bffb82bad9ec8443279dde5bf14875df495
languageName: node
linkType: hard
"@rsdoctor/types@npm:0.4.11":
version: 0.4.11
resolution: "@rsdoctor/types@npm:0.4.11"
"@rsdoctor/types@npm:0.4.12":
version: 0.4.12
resolution: "@rsdoctor/types@npm:0.4.12"
dependencies:
"@types/connect": "npm:3.4.38"
"@types/estree": "npm:1.0.5"
@@ -6862,16 +6862,16 @@ __metadata:
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/d9b27f4aeb51c2b39eaeef9e1fdf3080aa13b1a98a54767f464f3d8ef08a91309263372909ce62980c49886868a33bf712d8dc8f9e13b511a3b84eb840b498d3
checksum: 10/41cbfd7eb36999e79eed49ebc49e5665c5c8c68070f2981558cdf110fdcc60d97b2e653ead5c12cf9ebcef8b7de98b42bc20cf15258a42a3f2ae5a7b75aa8379
languageName: node
linkType: hard
"@rsdoctor/utils@npm:0.4.11":
version: 0.4.11
resolution: "@rsdoctor/utils@npm:0.4.11"
"@rsdoctor/utils@npm:0.4.12":
version: 0.4.12
resolution: "@rsdoctor/utils@npm:0.4.12"
dependencies:
"@babel/code-frame": "npm:7.25.7"
"@rsdoctor/types": "npm:0.4.11"
"@rsdoctor/types": "npm:0.4.12"
"@types/estree": "npm:1.0.5"
acorn: "npm:^8.10.0"
acorn-import-assertions: "npm:1.9.0"
@@ -6888,24 +6888,24 @@ __metadata:
lodash: "npm:^4.17.21"
rslog: "npm:^1.2.3"
strip-ansi: "npm:^6.0.1"
checksum: 10/dbeb86402f162a136ea6f95a8ef53f6f46ce39262d6a796cb0d642fbede15a81a7ed73fc9a48aa88bf39ca01bd277e42990b7cd2480159fd2a472205b1e5d05d
checksum: 10/f0640c4cabacbf0c65cb2c2bc36791c9f07c9e4ea7917636f5480c193043a7f16cf276734a91bcd9309ec3389987e586e3a557e99266d758affdb76a2efb6a5a
languageName: node
linkType: hard
"@rsdoctor/webpack-plugin@npm:^0.4.6":
version: 0.4.11
resolution: "@rsdoctor/webpack-plugin@npm:0.4.11"
version: 0.4.12
resolution: "@rsdoctor/webpack-plugin@npm:0.4.12"
dependencies:
"@rsdoctor/core": "npm:0.4.11"
"@rsdoctor/graph": "npm:0.4.11"
"@rsdoctor/sdk": "npm:0.4.11"
"@rsdoctor/types": "npm:0.4.11"
"@rsdoctor/utils": "npm:0.4.11"
"@rsdoctor/core": "npm:0.4.12"
"@rsdoctor/graph": "npm:0.4.12"
"@rsdoctor/sdk": "npm:0.4.12"
"@rsdoctor/types": "npm:0.4.12"
"@rsdoctor/utils": "npm:0.4.12"
fs-extra: "npm:^11.1.1"
lodash: "npm:^4.17.21"
peerDependencies:
webpack: 5.x
checksum: 10/c5c64d6731142fc0a5b0395434c7ac0dc13f743671a83d2a33bb88914f95438750cef5a47c923fc2dfb66f1258b8bd26c101f90c84895fd5f1cc9b2c789a888b
checksum: 10/e2bfc961f39bfa27e60ada29cccf2734db59062d78007cedaec3c794f8c39864c6f0e487879f565fcad100ec970d3e320c5927315fc2ed00d55a00924891b6f3
languageName: node
linkType: hard
@@ -8396,21 +8396,21 @@ __metadata:
linkType: hard
"@tanstack/react-virtual@npm:^3.5.1, @tanstack/react-virtual@npm:^3.9.0":
version: 3.11.1
resolution: "@tanstack/react-virtual@npm:3.11.1"
version: 3.11.2
resolution: "@tanstack/react-virtual@npm:3.11.2"
dependencies:
"@tanstack/virtual-core": "npm:3.10.9"
"@tanstack/virtual-core": "npm:3.11.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/ccc525e048ebf48470bcc3b89a3ea652d7d92abf372f269ae0e3bd7c21a6724971e56b65da3457b481498298e6cb788fbaf04b0a52ce0e7aff2df4f254b3c5fe
checksum: 10/a1136da0ec4c2ecbd4f996d8b84f228f0b8d851b15806e01049a160ad1d9b2eef0e0a491035fe017c6f84a0e125334f69ea23b32c180df23614ea4a8eeb7490c
languageName: node
linkType: hard
"@tanstack/virtual-core@npm:3.10.9":
version: 3.10.9
resolution: "@tanstack/virtual-core@npm:3.10.9"
checksum: 10/15140fc41c728ed08486eba4c9caadbdb3c594f02d3b55fddca63813bc32e8cde64faf6ca6385f9815aeeedbc441dd8c9590aca4319c16a91f39b1937ef4eac7
"@tanstack/virtual-core@npm:3.11.2":
version: 3.11.2
resolution: "@tanstack/virtual-core@npm:3.11.2"
checksum: 10/8433044a5c801052ba2e4cdda098cdc8e32adfd3a76ba31af7064bbdda60062fe221a3558096987baa66cd94f528855e887c282cb0f9eb99d3751457c2a62872
languageName: node
linkType: hard
@@ -9999,6 +9999,13 @@ __metadata:
languageName: node
linkType: hard
"@types/use-sync-external-store@npm:^0.0.6":
version: 0.0.6
resolution: "@types/use-sync-external-store@npm:0.0.6"
checksum: 10/a95ce330668501ad9b1c5b7f2b14872ad201e552a0e567787b8f1588b22c7040c7c3d80f142cbb9f92d13c4ea41c46af57a20f2af4edf27f224d352abcfe4049
languageName: node
linkType: hard
"@types/uuid@npm:9.0.8, @types/uuid@npm:^9.0.1":
version: 9.0.8
resolution: "@types/uuid@npm:9.0.8"
@@ -11460,7 +11467,7 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.4.0, axios@npm:^1.7.4, axios@npm:^1.7.8":
"axios@npm:^1.4.0, axios@npm:^1.7.4, axios@npm:^1.7.9":
version: 1.7.9
resolution: "axios@npm:1.7.9"
dependencies:
@@ -13163,20 +13170,13 @@ __metadata:
languageName: node
linkType: hard
"cookie@npm:^0.7.2":
"cookie@npm:^0.7.2, cookie@npm:~0.7.2":
version: 0.7.2
resolution: "cookie@npm:0.7.2"
checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f
languageName: node
linkType: hard
"cookie@npm:~0.4.1":
version: 0.4.2
resolution: "cookie@npm:0.4.2"
checksum: 10/2e1de9fdedca54881eab3c0477aeb067f281f3155d9cfee9d28dfb252210d09e85e9d175c0a60689661feb9e35e588515352f2456bc1f8e8db4267e05fd70137
languageName: node
linkType: hard
"copy-to-clipboard@npm:^3.3.1":
version: 3.3.1
resolution: "copy-to-clipboard@npm:3.3.1"
@@ -13386,7 +13386,7 @@ __metadata:
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5":
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
dependencies:
@@ -15050,21 +15050,21 @@ __metadata:
languageName: node
linkType: hard
"engine.io@npm:~6.5.2":
version: 6.5.5
resolution: "engine.io@npm:6.5.5"
"engine.io@npm:~6.6.0":
version: 6.6.2
resolution: "engine.io@npm:6.6.2"
dependencies:
"@types/cookie": "npm:^0.4.1"
"@types/cors": "npm:^2.8.12"
"@types/node": "npm:>=10.0.0"
accepts: "npm:~1.3.4"
base64id: "npm:2.0.0"
cookie: "npm:~0.4.1"
cookie: "npm:~0.7.2"
cors: "npm:~2.8.5"
debug: "npm:~4.3.1"
engine.io-parser: "npm:~5.2.1"
ws: "npm:~8.17.1"
checksum: 10/df8562e5249cf122efad77b909fe804b36ac5769676f963c997d4d18c91e014c68bb40661ff92f641b978baa0297be4000c2f3c3d1ce237cd1771952ccc5f38a
checksum: 10/381c0a715362bebf32c95f0e4247899b53ff9f8c0074b03f88748cf17635a02948ea3eca27905df37130bc963747c0d286acc78c757e6fc085fe8b8e17d76e01
languageName: node
linkType: hard
@@ -15750,8 +15750,8 @@ __metadata:
linkType: hard
"eslint-plugin-jest-dom@npm:^5.4.0":
version: 5.4.0
resolution: "eslint-plugin-jest-dom@npm:5.4.0"
version: 5.5.0
resolution: "eslint-plugin-jest-dom@npm:5.5.0"
dependencies:
"@babel/runtime": "npm:^7.16.3"
requireindex: "npm:^1.2.0"
@@ -15761,7 +15761,7 @@ __metadata:
peerDependenciesMeta:
"@testing-library/dom":
optional: true
checksum: 10/b8b0b0249d066658a75723892bc6f52d6bcf03ff0a69fc5020548c49f740613a8f3acce647f8f04b292606d2bd0ab3372a695aa3d90b4efb19e71870bbddf637
checksum: 10/73aaaa6117abbe3b197bc6b1e45839aaa9c2b4c86e7efbc4ff29f03318ec7f019a8e32652c06c61f06fdb22fb296c068a802a268e7b88aa6b71d3477d949b2c6
languageName: node
linkType: hard
@@ -15783,9 +15783,9 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-jsdoc@npm:50.5.0":
version: 50.5.0
resolution: "eslint-plugin-jsdoc@npm:50.5.0"
"eslint-plugin-jsdoc@npm:50.6.1":
version: 50.6.1
resolution: "eslint-plugin-jsdoc@npm:50.6.1"
dependencies:
"@es-joy/jsdoccomment": "npm:~0.49.0"
are-docs-informative: "npm:^0.0.2"
@@ -15800,7 +15800,7 @@ __metadata:
synckit: "npm:^0.9.1"
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
checksum: 10/4abe3ac34ad0d3bf34238968b6961c4ea9ec8eb058046f554cf60a702a91ac294eda8e475cadeaa1951e1a5317dd055543407787b04256fbd0728454af7e2ad0
checksum: 10/53fceff38a5317bb7c42c15a622100515aec89aea0d2bbf07e7d2d07eacdaa10ce625232a1bc7c1497f7bbe044675123d30cd3e123fa52fe5c7a9c336a59709c
languageName: node
linkType: hard
@@ -15973,16 +15973,16 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:9.16.0":
version: 9.16.0
resolution: "eslint@npm:9.16.0"
"eslint@npm:9.17.0":
version: 9.17.0
resolution: "eslint@npm:9.17.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.2.0"
"@eslint-community/regexpp": "npm:^4.12.1"
"@eslint/config-array": "npm:^0.19.0"
"@eslint/core": "npm:^0.9.0"
"@eslint/eslintrc": "npm:^3.2.0"
"@eslint/js": "npm:9.16.0"
"@eslint/js": "npm:9.17.0"
"@eslint/plugin-kit": "npm:^0.2.3"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
@@ -15991,7 +15991,7 @@ __metadata:
"@types/json-schema": "npm:^7.0.15"
ajv: "npm:^6.12.4"
chalk: "npm:^4.0.0"
cross-spawn: "npm:^7.0.5"
cross-spawn: "npm:^7.0.6"
debug: "npm:^4.3.2"
escape-string-regexp: "npm:^4.0.0"
eslint-scope: "npm:^8.2.0"
@@ -16018,7 +16018,7 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
checksum: 10/b47a34392a55bc08594ee2fed2777d7aff96f38b584791ccded44d88b72795acfe15b18643fd4046f680734a1f16244d554f34a485f660fa723e25d609fb12ba
checksum: 10/a48ee67dd4e737974bbb49ca5d12d0ce35bcd874507807599e3655bb398320ab27c9deed1aad508a963967815e626c21208f52158c2fc0796d0cc8186528efeb
languageName: node
linkType: hard
@@ -17551,7 +17551,7 @@ __metadata:
"@emotion/eslint-plugin": "npm:11.12.0"
"@emotion/react": "npm:11.14.0"
"@fingerprintjs/fingerprintjs": "npm:^3.4.2"
"@floating-ui/react": "npm:0.27.0"
"@floating-ui/react": "npm:0.27.2"
"@formatjs/intl-durationformat": "npm:^0.7.0"
"@glideapps/glide-data-grid": "npm:^6.0.0"
"@grafana/aws-sdk": "npm:0.5.0"
@@ -17719,12 +17719,12 @@ __metadata:
esbuild: "npm:0.24.0"
esbuild-loader: "npm:4.2.2"
esbuild-plugin-browserslist: "npm:^0.15.0"
eslint: "npm:9.16.0"
eslint: "npm:9.17.0"
eslint-config-prettier: "npm:9.1.0"
eslint-plugin-import: "npm:^2.31.0"
eslint-plugin-jest: "npm:28.9.0"
eslint-plugin-jest-dom: "npm:^5.4.0"
eslint-plugin-jsdoc: "npm:50.5.0"
eslint-plugin-jsdoc: "npm:50.6.1"
eslint-plugin-jsx-a11y: "npm:6.10.2"
eslint-plugin-lodash: "npm:8.0.0"
eslint-plugin-no-barrel-files: "npm:^1.1.1"
@@ -17785,7 +17785,7 @@ __metadata:
moment-timezone: "npm:0.5.46"
monaco-editor: "npm:0.34.1"
moveable: "npm:0.53.0"
msw: "npm:2.6.8"
msw: "npm:2.6.9"
mutationobserver-shim: "npm:0.3.7"
nanoid: "npm:^5.0.4"
ngtemplate-loader: "npm:2.1.0"
@@ -17799,12 +17799,12 @@ __metadata:
postcss-loader: "npm:8.1.1"
postcss-reporter: "npm:7.1.0"
postcss-scss: "npm:4.0.9"
prettier: "npm:3.3.3"
prettier: "npm:3.4.2"
prismjs: "npm:1.29.0"
pseudoizer: "npm:^0.1.0"
rc-slider: "npm:11.1.7"
rc-time-picker: "npm:3.7.3"
rc-tree: "npm:5.10.1"
rc-tree: "npm:5.11.0"
re-resizable: "npm:6.10.3"
react: "npm:18.2.0"
react-diff-viewer-continued: "npm:^3.4.0"
@@ -17818,20 +17818,20 @@ __metadata:
react-inlinesvg: "npm:4.1.5"
react-loading-skeleton: "npm:3.5.0"
react-moveable: "npm:0.56.0"
react-redux: "npm:9.1.2"
react-redux: "npm:9.2.0"
react-refresh: "npm:0.14.0"
react-resizable: "npm:3.0.5"
react-responsive-carousel: "npm:^3.2.23"
react-router: "npm:5.3.3"
react-router-dom: "npm:5.3.3"
react-router-dom-v5-compat: "npm:^6.26.1"
react-select: "npm:5.8.3"
react-select: "npm:5.9.0"
react-select-event: "npm:5.5.1"
react-split-pane: "npm:0.1.92"
react-table: "npm:7.8.0"
react-test-renderer: "npm:18.2.0"
react-transition-group: "npm:4.4.5"
react-use: "npm:17.5.1"
react-use: "npm:17.6.0"
react-virtual: "npm:2.10.4"
react-virtualized-auto-sizer: "npm:1.0.24"
react-window: "npm:1.8.10"
@@ -17888,7 +17888,7 @@ __metadata:
yaml: "npm:^2.0.0"
yargs: "npm:^17.5.1"
dependenciesMeta:
prettier@3.3.3:
prettier@3.4.2:
unplugged: true
languageName: unknown
linkType: soft
@@ -20713,8 +20713,8 @@ __metadata:
linkType: hard
"knip@npm:^5.10.0":
version: 5.37.1
resolution: "knip@npm:5.37.1"
version: 5.40.0
resolution: "knip@npm:5.40.0"
dependencies:
"@nodelib/fs.walk": "npm:1.2.8"
"@snyk/github-codeowners": "npm:1.1.0"
@@ -20727,7 +20727,7 @@ __metadata:
picocolors: "npm:^1.1.0"
picomatch: "npm:^4.0.1"
pretty-ms: "npm:^9.0.0"
smol-toml: "npm:^1.3.0"
smol-toml: "npm:^1.3.1"
strip-json-comments: "npm:5.0.1"
summary: "npm:2.1.0"
zod: "npm:^3.22.4"
@@ -20738,7 +20738,7 @@ __metadata:
bin:
knip: bin/knip.js
knip-bun: bin/knip-bun.js
checksum: 10/e73962f7daac5eb3d275654bbf719e91001aca8d03da9d0dc303cbe0cd47000114cf99d341b377b866f815e7d6821e3fdb1fca24e0a178e8204c1adaa2f2f41c
checksum: 10/238d76428a1f0cd3a184ebd9aaea2e07b3b8434cdee6c6e9230d6ae31f18b93188e2b60fbd64769d2448ce5b0114e662ee536843207514b1e95060b8740218d8
languageName: node
linkType: hard
@@ -22261,9 +22261,9 @@ __metadata:
languageName: node
linkType: hard
"msw@npm:2.6.8":
version: 2.6.8
resolution: "msw@npm:2.6.8"
"msw@npm:2.6.9":
version: 2.6.9
resolution: "msw@npm:2.6.9"
dependencies:
"@bundled-es-modules/cookie": "npm:^2.0.1"
"@bundled-es-modules/statuses": "npm:^1.0.1"
@@ -22290,7 +22290,7 @@ __metadata:
optional: true
bin:
msw: cli/index.js
checksum: 10/35d551c0fffe8b12cf174ae70ea8560bcc2e67f8887f20b43c7a3903ad4dc4272dc1c92f9ae68d30ad56435b391bcc5379495b1f9f0badcd4f2fdb614fbf7244
checksum: 10/20a74a5e49b780567aa3430c0de9f27830208f931d6109087a24566fcb3e68f058ff51b022891bfe6fe0f320afad22b4039593722b928aa9c4ab5b05c2746c4a
languageName: node
linkType: hard
@@ -24449,12 +24449,12 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:3.3.3, prettier@npm:^3.1.1":
version: 3.3.3
resolution: "prettier@npm:3.3.3"
"prettier@npm:3.4.2, prettier@npm:^3.1.1":
version: 3.4.2
resolution: "prettier@npm:3.4.2"
bin:
prettier: bin/prettier.cjs
checksum: 10/5beac1f30b5b40162532b8e2f7c3a4eb650910a2695e9c8512a62ffdc09dae93190c29db9107fa7f26d1b6c71aad3628ecb9b5de1ecb0911191099be109434d7
checksum: 10/a3e806fb0b635818964d472d35d27e21a4e17150c679047f5501e1f23bd4aa806adf660f0c0d35214a210d5d440da6896c2e86156da55f221a57938278dc326e
languageName: node
linkType: hard
@@ -24972,19 +24972,19 @@ __metadata:
languageName: node
linkType: hard
"rc-cascader@npm:3.30.0":
version: 3.30.0
resolution: "rc-cascader@npm:3.30.0"
"rc-cascader@npm:3.31.0":
version: 3.31.0
resolution: "rc-cascader@npm:3.31.0"
dependencies:
"@babel/runtime": "npm:^7.25.7"
classnames: "npm:^2.3.1"
rc-select: "npm:~14.16.2"
rc-tree: "npm:~5.10.1"
rc-tree: "npm:~5.11.0"
rc-util: "npm:^5.43.0"
peerDependencies:
react: ">=16.9.0"
react-dom: ">=16.9.0"
checksum: 10/c4ed8e122ad2dcc37fb1b190f4514fc040bf60b74bf90735c7485054978687387c039a977b151b28164fd41301f073c5491acd8557cd8841c9cd71c19b8166ee
checksum: 10/e0ffd71141630cd2f813a44ae9df57495c0118a80785a05e8faef374ea2a49a1ab6f5f21584f57911c22611c1431be2d76bd3d0c9b304d95931ac6fb2f5e0ad5
languageName: node
linkType: hard
@@ -25094,9 +25094,9 @@ __metadata:
languageName: node
linkType: hard
"rc-tooltip@npm:6.2.1":
version: 6.2.1
resolution: "rc-tooltip@npm:6.2.1"
"rc-tooltip@npm:6.3.1":
version: 6.3.1
resolution: "rc-tooltip@npm:6.3.1"
dependencies:
"@babel/runtime": "npm:^7.11.2"
"@rc-component/trigger": "npm:^2.0.0"
@@ -25104,13 +25104,13 @@ __metadata:
peerDependencies:
react: ">=16.9.0"
react-dom: ">=16.9.0"
checksum: 10/a82064d6d521ba4c03d074505402f6c38f9f50439037235969546b694e38977bab3420b46080bde295aca2436e6725d64b0a330a5ccdd51932deabbb1bf7585b
checksum: 10/e871f4430ba3711edc002cb55cd189471ae353bc2262ce648d14148d2648efa0849af87e85a750d03b23532a56c506724a6756bef1d42dcd48d061ccdf0024d8
languageName: node
linkType: hard
"rc-tree@npm:5.10.1, rc-tree@npm:~5.10.1":
version: 5.10.1
resolution: "rc-tree@npm:5.10.1"
"rc-tree@npm:5.11.0, rc-tree@npm:~5.11.0":
version: 5.11.0
resolution: "rc-tree@npm:5.11.0"
dependencies:
"@babel/runtime": "npm:^7.10.1"
classnames: "npm:2.x"
@@ -25120,7 +25120,7 @@ __metadata:
peerDependencies:
react: "*"
react-dom: "*"
checksum: 10/3fd1ad26ecce4435212aba83a87af8256805a8c2a3be3729995043848d7af267bcfc89dd09393fc5962f7a802b83184ad2736d407e18f007ce1a4c9ebd7a5629
checksum: 10/d1a8d544354c4345a352811e5db57a10b1e7c2bf237bc394eb94256bdfc071fc0aefd6b201fe526d127e315b1ed2ab4ca7a363a05ab2d12a12fafcf7fdcbdf05
languageName: node
linkType: hard
@@ -25458,11 +25458,11 @@ __metadata:
linkType: hard
"react-hook-form@npm:^7.49.2":
version: 7.53.2
resolution: "react-hook-form@npm:7.53.2"
version: 7.54.1
resolution: "react-hook-form@npm:7.54.1"
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
checksum: 10/172589ac5871bd41717e8c6bc4c825d149db88eb02e356a8fef4115615a56b21bf66ad16954575b1d6302ed265b8780f1975a2730d5aa10a53e412b56d9f72ca
checksum: 10/64060da8960a50584002bd5549f59dcb4d9c9b223b21ced1096a539ac520a3d9b65a8eb08c62d7f00b751f421ac133e3f82f23e01292dd2775d93f85791fd2f3
languageName: node
linkType: hard
@@ -25633,22 +25633,22 @@ __metadata:
languageName: node
linkType: hard
"react-redux@npm:9.1.2, react-redux@npm:^9.1.2":
version: 9.1.2
resolution: "react-redux@npm:9.1.2"
"react-redux@npm:9.2.0, react-redux@npm:^9.1.2":
version: 9.2.0
resolution: "react-redux@npm:9.2.0"
dependencies:
"@types/use-sync-external-store": "npm:^0.0.3"
use-sync-external-store: "npm:^1.0.0"
"@types/use-sync-external-store": "npm:^0.0.6"
use-sync-external-store: "npm:^1.4.0"
peerDependencies:
"@types/react": ^18.2.25
react: ^18.0
"@types/react": ^18.2.25 || ^19
react: ^18.0 || ^19
redux: ^5.0.0
peerDependenciesMeta:
"@types/react":
optional: true
redux:
optional: true
checksum: 10/319b3286f538da7e609ca90fc6762ffae007c5cf75e525a25237ac2feaee63d9cf76fe766817de1fc8f27e7bde825ca409c463037d26dd8e57c435d383f80c50
checksum: 10/b3d2f89f469169475ab0a9f8914d54a336ac9bc6a31af6e8dcfe9901e6fe2cfd8c1a3f6ce7a2f7f3e0928a93fbab833b668804155715598b7f2ad89927d3ff50
languageName: node
linkType: hard
@@ -25799,9 +25799,9 @@ __metadata:
languageName: node
linkType: hard
"react-select@npm:5.8.3":
version: 5.8.3
resolution: "react-select@npm:5.8.3"
"react-select@npm:5.9.0":
version: 5.9.0
resolution: "react-select@npm:5.9.0"
dependencies:
"@babel/runtime": "npm:^7.12.0"
"@emotion/cache": "npm:^11.4.0"
@@ -25811,11 +25811,11 @@ __metadata:
memoize-one: "npm:^6.0.0"
prop-types: "npm:^15.6.0"
react-transition-group: "npm:^4.3.0"
use-isomorphic-layout-effect: "npm:^1.1.2"
use-isomorphic-layout-effect: "npm:^1.2.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10/16a4543b1d9ffbf54dd2f749cc5ff765254ad96186fbdac164c1e20e3c40a7c64b726e020050775b3d54d10b9ac6e60ff9eb7fd443010982496dad814958e5fa
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/2206b6687d6584ff1426056a779d8014ef4d8c4d1d5253dea4f03b01569fdedab8ae6b683ef475ead7da5a824934008b682838d734841d9734e2a8a63b9959fd
languageName: node
linkType: hard
@@ -25951,9 +25951,9 @@ __metadata:
languageName: node
linkType: hard
"react-use@npm:17.5.1, react-use@npm:^17.4.0, react-use@npm:^17.4.2, react-use@npm:^17.5.0":
version: 17.5.1
resolution: "react-use@npm:17.5.1"
"react-use@npm:17.6.0, react-use@npm:^17.4.0, react-use@npm:^17.4.2, react-use@npm:^17.5.0":
version: 17.6.0
resolution: "react-use@npm:17.6.0"
dependencies:
"@types/js-cookie": "npm:^2.2.6"
"@xobotyi/scrollbar-width": "npm:^1.9.5"
@@ -25972,7 +25972,7 @@ __metadata:
peerDependencies:
react: "*"
react-dom: "*"
checksum: 10/2da403a9949dbd964b9b8e20dcd354db66b7f7d5ca1f42572fbcdb06bd49ee828c295be4912cb87abc163d1b54820bb8c5fa85314a16c4579d9e30bf9cbd5759
checksum: 10/a817b74e82b481a39d3539bfe8d3b535c08d59d44a75ea91f65e56a7ccaedb0de185159e50b44ea4a635dda0c1c7159f07530e81a1d64b57130e0a715a107795
languageName: node
linkType: hard
@@ -27700,7 +27700,7 @@ __metadata:
languageName: node
linkType: hard
"smol-toml@npm:^1.3.0":
"smol-toml@npm:^1.3.1":
version: 1.3.1
resolution: "smol-toml@npm:1.3.1"
checksum: 10/b999828ea46cf44ae90b6293884d6a139dfb4545ac6f86cbd1002568a943a43d8895ad82413855d095eec0c0bc21d23413c0a25a26c7fad6395c2ce42c2fdbd0
@@ -27761,18 +27761,18 @@ __metadata:
languageName: node
linkType: hard
"socket.io@npm:4.7.2":
version: 4.7.2
resolution: "socket.io@npm:4.7.2"
"socket.io@npm:4.8.1":
version: 4.8.1
resolution: "socket.io@npm:4.8.1"
dependencies:
accepts: "npm:~1.3.4"
base64id: "npm:~2.0.0"
cors: "npm:~2.8.5"
debug: "npm:~4.3.2"
engine.io: "npm:~6.5.2"
engine.io: "npm:~6.6.0"
socket.io-adapter: "npm:~2.5.2"
socket.io-parser: "npm:~4.2.4"
checksum: 10/03f2d196975f531fb068e31fb001ff4662e8acd1a6a4ddd8bb0359411aea3309d9764c0d2759dabd8fc96cf9840b2c4cdc70a473fa0e8f2b762ab619550de8e1
checksum: 10/b9b362b7f63fc7ebb58482b8a3ade6c971da7783b7611dfeebaa8b02be23cb948137ec218491ccda8be57e434e97d65b64edf1e9811e5245b23a888d41636f4a
languageName: node
linkType: hard
@@ -30060,15 +30060,15 @@ __metadata:
languageName: node
linkType: hard
"use-isomorphic-layout-effect@npm:^1.1.2":
version: 1.1.2
resolution: "use-isomorphic-layout-effect@npm:1.1.2"
"use-isomorphic-layout-effect@npm:^1.2.0":
version: 1.2.0
resolution: "use-isomorphic-layout-effect@npm:1.2.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/fd3787ed19f6cfbf70e2c5822d01bebbf96b00968195840d5ad61082b8e6ca7a8e2e46270c4096537d18a38ea57f4e4e9668cce5eec36fa4697ddba2ef1203fd
checksum: 10/84fc1074b4e3ee2886fde944baef4ec210453fc78861429fe50ae97be8209e492f18c059c6b2ff1a21df231d72d1638707dabca889bd9d7bee36f21c196a0d19
languageName: node
linkType: hard
@@ -30081,12 +30081,12 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.0.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.4.0":
version: 1.4.0
resolution: "use-sync-external-store@npm:1.4.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10/a676216affc203876bd47981103f201f28c2731361bb186367e12d287a7566763213a8816910c6eb88265eccd4c230426eb783d64c373c4a180905be8820ed8e
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/08bf581a8a2effaefc355e9d18ed025d436230f4cc973db2f593166df357cf63e47b9097b6e5089b594758bde322e1737754ad64905e030d70f8ff7ee671fd01
languageName: node
linkType: hard