Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
commit
e4837d452e
95 files changed
+6686
-5513
No files matched your search
+1
-173
@@ -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
|
||||
|
||||
...
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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/).
|
||||
+92
@@ -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/).
|
||||
+25
-24
@@ -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.
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
|
||||
+17
-39
@@ -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
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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%',
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "folders/count",
|
||||
"name": "folders/counts",
|
||||
"singularName": "",
|
||||
"namespaced": true,
|
||||
"kind": "DescendantCounts",
|
||||
|
||||
@@ -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]
|
||||
|
||||
+17
-3
@@ -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 () => {
|
||||
|
||||
+2
-3
@@ -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('/', '\\/');
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+98
@@ -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();
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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() + \
|
||||
|
||||
@@ -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() + \
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user