Compare commits

..

15 Commits

Author SHA1 Message Date
Ivan Ortega Alba 161e3cac50 [v10.2.x] FeatureToggle: Disable dashgpt by default and mark it as preview (#78349)
FeatureToggle: Disable `dashgpt` by default and mark it as preview (#78348)

(cherry picked from commit ddfe4e1bdd)
2023-11-20 10:27:24 +01:00
Ivan Ortega Alba f0b77df43d [v10.2.x] SaveDashboardPrompt: Reduce time to open drawer when many changes applied (#78308)
* SaveDashboard: Reduce time to open drawer when many changes applied (#78283)

(cherry picked from commit f32f8a160e)
2023-11-20 09:58:43 +01:00
grafana-delivery-bot[bot] f8239ab814 [v10.2.x] Update angular-plugins.md (#78343)
Update angular-plugins.md (#78341)

removes:
- humio - deprecated and removed from catalog
- shoreline - updated to react

(cherry picked from commit e1862f82c9)

Co-authored-by: David Harris <david.harris@grafana.com>
2023-11-17 15:49:19 +00:00
grafana-delivery-bot[bot] 9ad0edceb8 [v10.2.x] Correcting availability of hashicorp vault integration (#78337)
Correcting availability of hashicorp vault integration (#78321)

Correcting availability

(cherry picked from commit 98cc57b00b)

Co-authored-by: Timur Olzhabayev <timur.olzhabayev@grafana.com>
2023-11-17 09:33:05 -06:00
grafana-delivery-bot[bot] d996ce6ff8 [v10.2.x] Update angular-plugins.md (#78338)
Update angular-plugins.md (#76829)

Remove oracle plugins as these have migrated

(cherry picked from commit 4073e50da9)

Co-authored-by: David Harris <david.harris@grafana.com>
2023-11-17 15:29:28 +00:00
grafana-delivery-bot[bot] 574904291c [v10.2.x] Folders: Fix fetching empty folder (#78306)
Folders: Fix fetching empty folder (#78280)

(cherry picked from commit 5e50d9b178)

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
2023-11-17 11:59:06 +02:00
Horst Gutmann bd8c0118ef [v10.2.x] CI: Test backend on feature-toggles documentation changes (#78302)
CI: Test backend on feature-toggles documentation changes (#78177)

Run backend tests if the feature-toggles documentation changes

(cherry picked from commit d78b3fea2f)
2023-11-17 10:39:52 +02:00
Jo 12d569f8fd [v10.2.x] RolePicker: Optimise rendering inside lists of items (#78260)
* Role picker: Fix flickering at service accounts page (#77049)

* Role picker: Fix flickering at service accounts page

* Set role picker fixed width

* Fix betterer

* Fix styles

(cherry picked from commit aa7a6da985)

* RolePicker: Optimise rendering inside lists of items (#77297)

* Role picker: Load users roles in batch

* Use orgId in request

* Add roles to OrgUser type

* Improve loading logic

* Improve loading indicator

* Fix org page

* Update service accounts page

* Use bulk roles query for teams

* Use POST requests for search

* Use post request for teams

* Update betterer results

* Review suggestions

* AdminEditOrgPage: move API calls to separate file

(cherry picked from commit cf7a2ea733)

---------

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
2023-11-16 13:04:15 +01:00
grafana-delivery-bot[bot] 2bd48632f6 [v10.2.x] Alerting: Use correct URL for modify export (#78232)
Alerting: Use correct URL for modify export (#77714)

(cherry picked from commit 901a6bfa69)

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
2023-11-15 22:11:04 +02:00
grafana-delivery-bot[bot] 1a41311415 [v10.2.x] Alerting: Fix export with modifications URL when mounted on subpath (#78217)
Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
Fix export with modifications URL when mounted on subpath (#77622)
2023-11-15 21:01:40 +01:00
grafana-delivery-bot[bot] 685bbda728 [v10.2.x] Explore: Fix queries (cached & non) count in usage insights (#78216)
Explore: Fix queries (cached & non) count in usage insights (#78097)

* Fix: Fix queries (cached & non) count in usage insights

* also keep deprecated error property

* Fix & refactor tests

(cherry picked from commit 42a3f36c18)

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
2023-11-15 16:45:26 +00:00
grafana-delivery-bot[bot] a36ead12c3 Release: Bump version to 10.2.2 (#78135)
"Release: Updated versions in package to 10.2.2"

Co-authored-by: grafana-delivery-bot[bot] <132647405+grafana-delivery-bot[bot]@users.noreply.github.com>
2023-11-14 19:10:13 +02:00
grafana-delivery-bot[bot] 02b14cbd52 [v10.2.x] Changelog: Updated changelog for 10.2.1 (#78128)
Changelog: Updated changelog for 10.2.1 (#78125)

* Changelog: Updated changelog for 10.2.1

* Lint

---------

Co-authored-by: grafanabot <bot@grafana.com>
Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
(cherry picked from commit c5af7ca15f)

Co-authored-by: grafana-delivery-bot[bot] <132647405+grafana-delivery-bot[bot]@users.noreply.github.com>
2023-11-14 16:51:32 +00:00
grafana-delivery-bot[bot] b2724eab43 [v10.2.x] InfluxDB: Fix multi variable interpolation (#78119)
InfluxDB: Fix multi variable interpolation (#78068)

fix special variable escape

(cherry picked from commit 656808a41b)

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
2023-11-14 16:18:47 +01:00
Levente Balogh 11918a9518 [v10.2.x] Plugins: Keep working when there is no internet access (#78092)
Plugins: Keep working when there is no internet access (#77978)

* fix: make connections and plugins-catalog work when GCOM is not available

* fix: remove unused import

(cherry picked from commit ea12eecac5)
2023-11-14 10:18:41 +02:00
91 changed files with 813 additions and 523 deletions
+1 -23
View File
@@ -1455,8 +1455,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
[0, 0, 0, "Styles should be written using objects.", "6"]
],
"public/app/core/components/RolePicker/RolePickerMenu.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
@@ -1464,27 +1463,6 @@ exports[`better eslint`] = {
"public/app/core/components/RolePicker/ValueContainer.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/core/components/RolePicker/styles.ts:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"],
[0, 0, 0, "Styles should be written using objects.", "8"],
[0, 0, 0, "Styles should be written using objects.", "9"],
[0, 0, 0, "Styles should be written using objects.", "10"],
[0, 0, 0, "Styles should be written using objects.", "11"],
[0, 0, 0, "Styles should be written using objects.", "12"],
[0, 0, 0, "Styles should be written using objects.", "13"],
[0, 0, 0, "Styles should be written using objects.", "14"],
[0, 0, 0, "Styles should be written using objects.", "15"],
[0, 0, 0, "Styles should be written using objects.", "16"],
[0, 0, 0, "Styles should be written using objects.", "17"],
[0, 0, 0, "Styles should be written using objects.", "18"]
],
"public/app/core/components/Select/OldFolderPicker.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
+2 -1
View File
@@ -365,6 +365,7 @@ trigger:
- go.sum
- go.mod
- public/app/plugins/**/plugin.json
- docs/sources/setup-grafana/configure-grafana/feature-toggles/**
- devenv/**
type: docker
volumes:
@@ -4646,6 +4647,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 91bdf33c65b3a12b92d5990a6843c4e7d6ab35fd77b4ccf64da7cc8594917c45
hmac: 4c7b649577b838d8f6590b14b52fe6b7d19859f69f70f51ea22acbe4f6c8ea5d
...
+36
View File
@@ -1,3 +1,39 @@
<!-- 10.2.1 START -->
# 10.2.1 (2023-11-13)
### Features and enhancements
- **Stat:** Add panel option to control wide layout. [#78012](https://github.com/grafana/grafana/issues/78012), [@nmarrs](https://github.com/nmarrs)
### Bug fixes
- **Dashboards:** Fix dashboard listing when user can't list any folders. [#77988](https://github.com/grafana/grafana/issues/77988), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **Search:** Modify query for better performance. [#77713](https://github.com/grafana/grafana/issues/77713), [@papagian](https://github.com/papagian)
- **RBAC:** Allow scoping access to root level dashboards. [#77608](https://github.com/grafana/grafana/issues/77608), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **CloudWatch Logs:** Add labels to alert and expression queries. [#77594](https://github.com/grafana/grafana/issues/77594), [@iwysiu](https://github.com/iwysiu)
- **Bug Fix:** Respect data source version when provisioning. [#77542](https://github.com/grafana/grafana/issues/77542), [@andresmgot](https://github.com/andresmgot)
- **Explore:** Fix support for angular based datasource editors. [#77505](https://github.com/grafana/grafana/issues/77505), [@Elfo404](https://github.com/Elfo404)
- **Plugins:** Fix status_source always being "plugin" in plugin request logs. [#77436](https://github.com/grafana/grafana/issues/77436), [@xnyo](https://github.com/xnyo)
- **InfluxDB:** Fix aliasing with $measurement or $m on backend mode. [#77383](https://github.com/grafana/grafana/issues/77383), [@itsmylife](https://github.com/itsmylife)
- **InfluxDB:** Fix parsing multiple tags on backend mode. [#77382](https://github.com/grafana/grafana/issues/77382), [@itsmylife](https://github.com/itsmylife)
- **Explore:** Fix panes vertical scrollbar not being draggable. [#77344](https://github.com/grafana/grafana/issues/77344), [@Elfo404](https://github.com/Elfo404)
- **Explore:** Avoid reinitializing graph on every query run. [#77290](https://github.com/grafana/grafana/issues/77290), [@Elfo404](https://github.com/Elfo404)
- **Bug fix:** Correctly set permissions on provisioned dashboards. [#77230](https://github.com/grafana/grafana/issues/77230), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **InfluxDB:** Fix adhoc filter calls by properly checking optional parameter in metricFindQuery. [#77145](https://github.com/grafana/grafana/issues/77145), [@itsmylife](https://github.com/itsmylife)
- **InfluxDB:** Fix table parsing with backend mode. [#76990](https://github.com/grafana/grafana/issues/76990), [@itsmylife](https://github.com/itsmylife)
- **Alerting:** Alert rule constraint violations return as 400s in provisioning API. [#76978](https://github.com/grafana/grafana/issues/76978), [@alexweav](https://github.com/alexweav)
- **PresenceIndicators:** Do not retry failed views/recent API calls. (Enterprise)
- **Analytics:** Use panel renderer rather than legacy flot graph. (Enterprise)
### Breaking changes
For the existing backend mode users who have table visualization might see some inconsistencies on their panels. We have updated the table column naming. This will potentially affect field transformations and/or field overrides. To resolve this either:
- Update transformation
- Update field override Issue [#76990](https://github.com/grafana/grafana/issues/76990)
<!-- 10.2.1 END -->
<!-- 10.2.0 START -->
# 10.2.0 (2023-10-24)
@@ -300,10 +300,6 @@ Latest Version: 1.1.2 | Signature: Community | Last Updated: 2021
Lack of recent activity in the [project repository](https://github.com/hawkular/hawkular-grafana-datasource) in the past 5 years suggests project _may_ not be actively maintained.
{{% /admonition %}}
### [Humio](https://grafana.com/grafana/plugins/humio-datasource/)
Latest Version: 3.3.1 | Signature: Commercial | Last Updated: 2022
### [IBM APM](https://grafana.com/grafana/plugins/ibm-apm-datasource/)
Latest Version: 0.9.1 | Signature: Community | Last Updated: 2021
@@ -382,14 +378,6 @@ Latest Version: 1.0.1 | Signature: Community | Last Updated: 2021
> **Migration available - plugin superseded:** this plugin was [discontinued in favour of the InfluxDB data source](https://github.com/ntop/ntopng-grafana-datasource) - a Core plugin included in Grafana, additional guidance is available [here](https://www.ntop.org/guides/ntopng/basic_concepts/timeseries.html#influxdb-driver).
### [Oracle Cloud Infrastructure Logs](https://grafana.com/grafana/plugins/oci-logs-datasource/)
Latest Version: 3.0.0 | Signature: Commercial | Last Updated: 2023
### [Oracle Cloud Infrastructure Metrics](https://grafana.com/grafana/plugins/oci-metrics-datasource/)
Latest Version: 4.0.1 | Signature: Commercial | Last Updated: 2023
### [Warp 10](https://grafana.com/grafana/plugins/ovh-warp10-datasource/)
Latest Version: 2.2.1 | Signature: Community | Last Updated: 2021
@@ -444,10 +432,6 @@ Latest Version: 1.2.3 | Signature: Community | Last Updated: 2021
Lack of recent activity in the [project repository](https://github.com/netxms/grafana) in the past 2 years suggests project _may_ not be actively maintained.
{{% /admonition %}}
### [Shoreline Data Source](https://grafana.com/grafana/plugins/shorelinesoftware-shoreline-datasource/)
Latest Version: 1.1.0 | Signature: Commercial | Last Updated: 6 months ago
### [Sidewinder](https://grafana.com/grafana/plugins/sidewinder-datasource/)
Latest Version: 0.2.1 | Signature: Community | Last Updated: 2021
@@ -50,7 +50,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `toggleLabelsInLogsUI` | Enable toggleable filters in log details view | Yes |
| `azureMonitorDataplane` | Adds dataplane compliant frame metadata in the Azure Monitor datasource | Yes |
| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | Yes |
| `dashgpt` | Enable AI powered features in dashboards | Yes |
| `newBrowseDashboards` | New browse/manage dashboards UI | Yes |
| `alertingInsights` | Show the new alerting insights landing page | Yes |
| `cloudWatchWildCardDimensionValues` | Fetches dimension values from CloudWatch to correctly label wildcard dimensions | Yes |
@@ -79,6 +78,7 @@ Some features are enabled by default. You can disable these feature by setting t
| `sqlDatasourceDatabaseSelection` | Enables previous SQL data source dataset dropdown behavior |
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the `useCachingService` feature toggle is enabled and the datasource has caching and async query support enabled |
| `splitScopes` | Support faster dashboard and folder search by splitting permission scopes into parts |
| `dashgpt` | Enable AI powered features in dashboards |
| `reportingRetries` | Enables rendering retries for the reporting feature |
## Experimental feature toggles
@@ -16,7 +16,7 @@ weight: 500
If you manage your secrets with [Hashicorp Vault](https://www.hashicorp.com/products/vault), you can use them for [Configuration]({{< relref "../../../configure-grafana" >}}) and [Provisioning]({{< relref "../../../../administration/provisioning" >}}).
{{% admonition type="note" %}}
Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud).
Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}).
{{% /admonition %}}
{{% admonition type="note" %}}
+1 -1
View File
@@ -4,5 +4,5 @@
"packages": [
"packages/*"
],
"version": "10.2.1"
"version": "10.2.2"
}
+1 -1
View File
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "10.2.1",
"version": "10.2.2",
"repository": "github:grafana/grafana",
"scripts": {
"build": "yarn i18n:compile && NODE_ENV=production webpack --progress --config scripts/webpack/webpack.prod.js",
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "10.2.1",
"version": "10.2.2",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -36,7 +36,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@grafana/schema": "10.2.1",
"@grafana/schema": "10.2.2",
"@types/d3-interpolate": "^3.0.0",
"@types/string-hash": "1.1.1",
"d3-interpolate": "3.0.1",
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "10.2.1",
"version": "10.2.2",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "10.2.1",
"version": "10.2.2",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -63,8 +63,8 @@
"@babel/core": "7.23.0",
"@babel/preset-env": "7.23.2",
"@cypress/webpack-preprocessor": "5.17.1",
"@grafana/e2e-selectors": "10.2.1",
"@grafana/schema": "10.2.1",
"@grafana/e2e-selectors": "10.2.2",
"@grafana/schema": "10.2.2",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "9.1.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@grafana/eslint-plugin",
"description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.",
"version": "10.2.1",
"version": "10.2.2",
"main": "./index.cjs",
"author": "Grafana Labs",
"license": "Apache-2.0",
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/flamegraph",
"version": "10.2.1",
"version": "10.2.2",
"description": "Grafana flamegraph visualization component",
"keywords": [
"grafana",
@@ -44,8 +44,8 @@
],
"dependencies": {
"@emotion/css": "11.11.2",
"@grafana/data": "10.2.1",
"@grafana/ui": "10.2.1",
"@grafana/data": "10.2.2",
"@grafana/ui": "10.2.2",
"@leeoniya/ufuzzy": "1.0.8",
"d3": "^7.8.5",
"lodash": "4.17.21",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@grafana/plugin-configs",
"description": "Shared dependencies and files for core plugins",
"private": true,
"version": "10.2.1",
"version": "10.2.2",
"dependencies": {
"tslib": "2.6.0"
},
+4 -4
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "10.2.1",
"version": "10.2.2",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -37,10 +37,10 @@
"postpack": "mv package.json.bak package.json"
},
"dependencies": {
"@grafana/data": "10.2.1",
"@grafana/e2e-selectors": "10.2.1",
"@grafana/data": "10.2.2",
"@grafana/e2e-selectors": "10.2.2",
"@grafana/faro-web-sdk": "1.2.1",
"@grafana/ui": "10.2.1",
"@grafana/ui": "10.2.2",
"history": "4.10.1",
"lodash": "4.17.21",
"rxjs": "7.8.1",
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "10.2.1",
"version": "10.2.2",
"description": "Grafana Schema Library",
"keywords": [
"typescript"
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
/**
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
limit: number;
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithTextFormatting {
/**
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends common.SingleStatBaseOptions {
displayMode: common.BarGaugeDisplayMode;
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export enum VizDisplayMode {
Candles = 'candles',
@@ -11,7 +11,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export enum HorizontalConstraint {
Center = 'center',
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface MetricStat {
/**
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
/**
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
selectedSeries: number;
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export type UpdateConfig = {
render: boolean,
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends common.SingleStatBaseOptions {
minVizHeight: number;
@@ -11,7 +11,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
basemap: ui.MapLayerOptions;
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export type PyroscopeQueryType = ('metrics' | 'profile' | 'both');
@@ -11,7 +11,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
/**
* Controls the color mode of the heatmap
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
/**
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
dedupStrategy: common.LogsDedupStrategy;
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export enum QueryEditorMode {
Builder = 'builder',
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
/**
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface ArcOption {
/**
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export type ParcaQueryType = ('metrics' | 'profile' | 'both');
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
/**
* Select the pie chart display style.
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export enum QueryEditorMode {
Builder = 'builder',
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends common.SingleStatBaseOptions {
colorMode: common.BigValueColorMode;
@@ -11,7 +11,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
/**
@@ -11,7 +11,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
/**
@@ -11,7 +11,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options {
/**
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface TempoQuery extends common.DataQuery {
filters: Array<TraceqlFilter>;
@@ -9,7 +9,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export enum TextMode {
Code = 'code',
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export interface Options extends common.OptionsWithTimezones {
legend: common.VizLegendOptions;
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
/**
* Identical to timeseries... except it does not have timezone settings
@@ -11,7 +11,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "10.2.1";
export const pluginVersion = "10.2.2";
export enum SeriesMapping {
Auto = 'auto',
+4 -4
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "10.2.1",
"version": "10.2.2",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -49,10 +49,10 @@
"dependencies": {
"@emotion/css": "11.11.2",
"@emotion/react": "11.11.1",
"@grafana/data": "10.2.1",
"@grafana/e2e-selectors": "10.2.1",
"@grafana/data": "10.2.2",
"@grafana/e2e-selectors": "10.2.2",
"@grafana/faro-web-sdk": "1.2.1",
"@grafana/schema": "10.2.1",
"@grafana/schema": "10.2.2",
"@leeoniya/ufuzzy": "1.0.8",
"@monaco-editor/react": "4.6.0",
"@popperjs/core": "2.11.8",
+1 -2
View File
@@ -707,10 +707,9 @@ var (
{
Name: "dashgpt",
Description: "Enable AI powered features in dashboards",
Stage: FeatureStageGeneralAvailability,
Stage: FeatureStagePublicPreview,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
Expression: "true", // on by default
},
{
Name: "reportingRetries",
+1 -1
View File
@@ -100,7 +100,7 @@ configurableSchedulerTick,experimental,@grafana/alerting-squad,false,false,true,
influxdbSqlSupport,experimental,@grafana/observability-metrics,false,false,false,false
alertingNoDataErrorExecution,privatePreview,@grafana/alerting-squad,false,false,true,false
angularDeprecationUI,experimental,@grafana/plugins-platform-backend,false,false,false,true
dashgpt,GA,@grafana/dashboards-squad,false,false,false,true
dashgpt,preview,@grafana/dashboards-squad,false,false,false,true
reportingRetries,preview,@grafana/sharing-squad,false,false,true,false
newBrowseDashboards,GA,@grafana/grafana-frontend-platform,false,false,false,true
sseGroupByDatasource,experimental,@grafana/observability-metrics,false,false,false,false
1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
100 influxdbSqlSupport experimental @grafana/observability-metrics false false false false
101 alertingNoDataErrorExecution privatePreview @grafana/alerting-squad false false true false
102 angularDeprecationUI experimental @grafana/plugins-platform-backend false false false true
103 dashgpt GA preview @grafana/dashboards-squad false false false true
104 reportingRetries preview @grafana/sharing-squad false false true false
105 newBrowseDashboards GA @grafana/grafana-frontend-platform false false false true
106 sseGroupByDatasource experimental @grafana/observability-metrics false false false false
@@ -88,6 +88,10 @@ func (d *DashboardFolderStoreImpl) GetFolderByUID(ctx context.Context, orgID int
func (d *DashboardFolderStoreImpl) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) {
m := make(map[string]*folder.Folder, len(uids))
if len(uids) == 0 {
return m, nil
}
var folders []*folder.Folder
if err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
b := strings.Builder{}
@@ -1,6 +1,6 @@
{
"name": "@grafana-plugins/input-datasource",
"version": "10.2.1",
"version": "10.2.2",
"description": "Input Datasource",
"private": true,
"repository": {
@@ -28,8 +28,8 @@
"webpack": "5.76.0"
},
"dependencies": {
"@grafana/data": "10.2.1",
"@grafana/ui": "10.2.1",
"@grafana/data": "10.2.2",
"@grafana/ui": "10.2.2",
"react": "18.2.0",
"tslib": "2.5.0"
}
@@ -1,6 +1,6 @@
import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react';
import { ClickOutsideWrapper, HorizontalGroup, Spinner } from '@grafana/ui';
import { ClickOutsideWrapper, useTheme2 } from '@grafana/ui';
import { Role, OrgRole } from 'app/types';
import { RolePickerInput } from './RolePickerInput';
@@ -24,6 +24,7 @@ export interface Props {
*/
apply?: boolean;
maxWidth?: string | number;
width?: string | number;
}
export const RolePicker = ({
@@ -40,6 +41,7 @@ export const RolePicker = ({
canUpdateRoles = true,
apply = false,
maxWidth = ROLE_PICKER_WIDTH,
width,
}: Props): JSX.Element | null => {
const [isOpen, setOpen] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
@@ -47,6 +49,8 @@ export const RolePicker = ({
const [query, setQuery] = useState('');
const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 });
const ref = useRef<HTMLDivElement>(null);
const theme = useTheme2();
const widthPx = typeof width === 'number' ? theme.spacing(width) : width;
useEffect(() => {
setSelectedBuiltInRole(basicRole);
@@ -146,21 +150,13 @@ export const RolePicker = ({
return options;
};
if (isLoading) {
return (
<HorizontalGroup justify="center">
<span>Loading...</span>
<Spinner size={16} />
</HorizontalGroup>
);
}
return (
<div
data-testid="role-picker"
style={{
position: 'relative',
maxWidth,
maxWidth: widthPx || maxWidth,
width: widthPx,
}}
ref={ref}
>
@@ -175,6 +171,8 @@ export const RolePicker = ({
isFocused={isOpen}
disabled={disabled}
showBasicRole={showBasicRole}
width={widthPx}
isLoading={isLoading}
/>
{isOpen && (
<RolePickerMenu
@@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
import React, { FormEvent, HTMLProps, useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, getInputStyles, sharedInputStyle, styleMixins, Tooltip, Icon } from '@grafana/ui';
import { useStyles2, getInputStyles, sharedInputStyle, styleMixins, Tooltip, Icon, Spinner } from '@grafana/ui';
import { Role } from '../../../types';
@@ -18,6 +18,8 @@ interface InputProps extends HTMLProps<HTMLInputElement> {
showBasicRole?: boolean;
isFocused?: boolean;
disabled?: boolean;
width?: string;
isLoading?: boolean;
onQueryChange: (query?: string) => void;
onOpen: (event: FormEvent<HTMLElement>) => void;
onClose: () => void;
@@ -30,12 +32,14 @@ export const RolePickerInput = ({
isFocused,
query,
showBasicRole,
width,
isLoading,
onOpen,
onClose,
onQueryChange,
...rest
}: InputProps): JSX.Element => {
const styles = useStyles2(getRolePickerInputStyles, false, !!isFocused, !!disabled, false);
const styles = useStyles2(getRolePickerInputStyles, false, !!isFocused, !!disabled, false, width);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
@@ -61,6 +65,11 @@ export const RolePickerInput = ({
numberOfRoles={appliedRoles.length}
showBuiltInRole={showBasicRoleOnLabel}
/>
{isLoading && (
<div className={styles.spinner}>
<Spinner size={16} inline />
</div>
)}
</div>
) : (
<div className={styles.wrapper}>
@@ -125,7 +134,8 @@ const getRolePickerInputStyles = (
invalid: boolean,
focused: boolean,
disabled: boolean,
withPrefix: boolean
withPrefix: boolean,
width?: string
) => {
const styles = getInputStyles({ theme, invalid });
@@ -138,21 +148,22 @@ const getRolePickerInputStyles = (
${styleMixins.focusCss(theme.v1)}
`,
disabled && styles.inputDisabled,
css`
min-width: ${ROLE_PICKER_WIDTH}px;
min-height: 32px;
height: auto;
flex-direction: row;
padding-right: 24px;
max-width: 100%;
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
position: relative;
box-sizing: border-box;
cursor: default;
`,
css({
minWidth: width || ROLE_PICKER_WIDTH + 'px',
width: width,
minHeight: '32px',
height: 'auto',
flexDirection: 'row',
paddingRight: theme.spacing(1),
maxWidth: '100%',
alignItems: 'center',
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'flex-start',
position: 'relative',
boxSizing: 'border-box',
cursor: 'default',
}),
withPrefix &&
css`
padding-left: 0;
@@ -180,6 +191,11 @@ const getRolePickerInputStyles = (
margin-bottom: ${theme.spacing(0.5)};
}
`,
spinner: css({
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-end',
}),
};
};
@@ -12,6 +12,7 @@ export interface Props {
orgId?: number;
roleOptions: Role[];
disabled?: boolean;
roles?: Role[];
onApplyRoles?: (newRoles: Role[]) => void;
pendingRoles?: Role[];
/**
@@ -27,19 +28,27 @@ export interface Props {
*/
apply?: boolean;
maxWidth?: string | number;
width?: string | number;
isLoading?: boolean;
}
export const TeamRolePicker = ({
teamId,
roleOptions,
disabled,
roles,
onApplyRoles,
pendingRoles,
apply = false,
maxWidth,
width,
isLoading,
}: Props) => {
const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => {
const [{ loading, value: appliedRoles = roles || [] }, getTeamRoles] = useAsyncFn(async () => {
try {
if (roles) {
return roles;
}
if (apply && Boolean(pendingRoles?.length)) {
return pendingRoles;
}
@@ -51,11 +60,11 @@ export const TeamRolePicker = ({
console.error('Error loading options', e);
}
return [];
}, [teamId, pendingRoles]);
}, [teamId, pendingRoles, roles]);
useEffect(() => {
getTeamRoles();
}, [teamId, getTeamRoles, pendingRoles]);
}, [getTeamRoles]);
const onRolesChange = async (roles: Role[]) => {
if (!apply) {
@@ -76,11 +85,12 @@ export const TeamRolePicker = ({
onRolesChange={onRolesChange}
roleOptions={roleOptions}
appliedRoles={appliedRoles}
isLoading={loading}
isLoading={loading || isLoading}
disabled={disabled}
basicRoleDisabled={true}
canUpdateRoles={canUpdateRoles}
maxWidth={maxWidth}
width={width}
/>
);
};
@@ -9,6 +9,7 @@ import { fetchUserRoles, updateUserRoles } from './api';
export interface Props {
basicRole: OrgRole;
roles?: Role[];
userId: number;
orgId?: number;
onBasicRoleChange: (newRole: OrgRole) => void;
@@ -31,10 +32,13 @@ export interface Props {
onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void;
pendingRoles?: Role[];
maxWidth?: string | number;
width?: string | number;
isLoading?: boolean;
}
export const UserRolePicker = ({
basicRole,
roles,
userId,
orgId,
onBasicRoleChange,
@@ -46,9 +50,14 @@ export const UserRolePicker = ({
onApplyRoles,
pendingRoles,
maxWidth,
width,
isLoading,
}: Props) => {
const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => {
const [{ loading, value: appliedRoles = roles || [] }, getUserRoles] = useAsyncFn(async () => {
try {
if (roles) {
return roles;
}
if (apply && Boolean(pendingRoles?.length)) {
return pendingRoles;
}
@@ -61,14 +70,14 @@ export const UserRolePicker = ({
console.error('Error loading options');
}
return [];
}, [orgId, userId, pendingRoles]);
}, [orgId, userId, pendingRoles, roles]);
useEffect(() => {
// only load roles when there is an Org selected
if (orgId) {
getUserRoles();
}
}, [orgId, getUserRoles, pendingRoles]);
}, [getUserRoles, orgId]);
const onRolesChange = async (roles: Role[]) => {
if (!apply) {
@@ -90,7 +99,7 @@ export const UserRolePicker = ({
onRolesChange={onRolesChange}
onBasicRoleChange={onBasicRoleChange}
roleOptions={roleOptions}
isLoading={loading}
isLoading={loading || isLoading}
disabled={disabled}
basicRoleDisabled={basicRoleDisabled}
basicRoleDisabledMessage={basicRoleDisabledMessage}
@@ -98,6 +107,7 @@ export const UserRolePicker = ({
apply={apply}
canUpdateRoles={canUpdateRoles}
maxWidth={maxWidth}
width={width}
/>
);
};
+114 -114
View File
@@ -4,119 +4,119 @@ import { GrafanaTheme2 } from '@grafana/data';
import { ROLE_PICKER_SUBMENU_MIN_WIDTH } from './constants';
export const getStyles = (theme: GrafanaTheme2) => {
return {
hideScrollBar: css`
.scrollbar-view {
/* Hide scrollbar for Chrome, Safari, and Opera */
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for Firefox */
scrollbar-width: none;
}
`,
menuWrapper: css`
display: flex;
max-height: 650px;
position: absolute;
z-index: ${theme.zIndex.dropdown};
overflow: hidden;
min-width: auto;
`,
menu: css`
min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
export const getStyles = (theme: GrafanaTheme2) => ({
hideScrollBar: css({
'.scrollbar-view': {
/* Hide scrollbar for Chrome, Safari, and Opera */
'&::-webkit-scrollbar': {
display: 'none',
},
/* Hide scrollbar for Firefox */
scrollbarWidth: 'none',
},
}),
menuWrapper: css({
display: 'flex',
maxHeight: '650px',
position: 'absolute',
zIndex: theme.zIndex.dropdown,
overflow: 'hidden',
minWidth: 'auto',
}),
menu: css({
minWidth: `${ROLE_PICKER_SUBMENU_MIN_WIDTH}px`,
'& > div': {
paddingTop: theme.spacing(1),
},
}),
menuLeft: css({
right: 0,
flexDirection: 'row-reverse',
}),
subMenu: css({
height: '100%',
minWidth: `${ROLE_PICKER_SUBMENU_MIN_WIDTH}px`,
display: 'flex',
flexDirection: 'column',
borderLeft: `1px solid ${theme.components.input.borderColor}`,
& > div {
padding-top: ${theme.spacing(1)};
}
`,
menuLeft: css`
right: 0;
flex-direction: row-reverse;
`,
subMenu: css`
height: 100%;
min-width: ${ROLE_PICKER_SUBMENU_MIN_WIDTH}px;
display: flex;
flex-direction: column;
border-left: 1px solid ${theme.components.input.borderColor};
'& > div': {
paddingTop: theme.spacing(1),
},
}),
subMenuLeft: css({
borderRight: `1px solid ${theme.components.input.borderColor}`,
borderLeft: 'unset',
}),
groupHeader: css({
padding: theme.spacing(0, 4.5),
display: 'flex',
alignItems: 'center',
color: theme.colors.text.primary,
fontWeight: theme.typography.fontWeightBold,
}),
container: css({
padding: theme.spacing(1),
border: `1px ${theme.colors.border.weak} solid`,
borderRadius: theme.shape.radius.default,
backgroundColor: theme.colors.background.primary,
zIndex: theme.zIndex.modal,
}),
menuSection: css({
marginBottom: theme.spacing(2),
}),
menuOptionCheckbox: css({
display: 'flex',
margin: theme.spacing(0, 1, 0, 0.25),
}),
menuButtonRow: css({
backgroundColor: theme.colors.background.primary,
padding: theme.spacing(1),
}),
menuOptionBody: css({
fontWeight: theme.typography.fontWeightRegular,
padding: theme.spacing(0, 1.5, 0, 0),
}),
menuOptionDisabled: css({
color: theme.colors.text.disabled,
cursor: 'not-allowed',
}),
menuOptionExpand: css({
position: 'absolute',
right: theme.spacing(1.25),
color: theme.colors.text.disabled,
& > div {
padding-top: ${theme.spacing(1)};
}
`,
subMenuLeft: css`
border-right: 1px solid ${theme.components.input.borderColor};
border-left: unset;
`,
groupHeader: css`
padding: ${theme.spacing(0, 4.5)};
display: flex;
align-items: center;
color: ${theme.colors.text.primary};
font-weight: ${theme.typography.fontWeightBold};
`,
container: css`
padding: ${theme.spacing(1)};
border: 1px ${theme.colors.border.weak} solid;
border-radius: ${theme.shape.radius.default};
background-color: ${theme.colors.background.primary};
z-index: ${theme.zIndex.modal};
`,
menuSection: css`
margin-bottom: ${theme.spacing(2)};
`,
menuOptionCheckbox: css`
display: flex;
margin: ${theme.spacing(0, 1, 0, 0.25)};
`,
menuButtonRow: css`
background-color: ${theme.colors.background.primary};
padding: ${theme.spacing(1)};
`,
menuOptionBody: css`
font-weight: ${theme.typography.fontWeightRegular};
padding: ${theme.spacing(0, 1.5, 0, 0)};
`,
menuOptionDisabled: css`
color: ${theme.colors.text.disabled};
cursor: not-allowed;
`,
menuOptionExpand: css`
position: absolute;
right: ${theme.spacing(1.25)};
color: ${theme.colors.text.disabled};
&:after {
content: '>';
}
`,
menuOptionInfoSign: css`
color: ${theme.colors.text.disabled};
`,
basicRoleSelector: css`
margin: ${theme.spacing(1, 1.25, 1, 1.5)};
`,
subMenuPortal: css`
height: 100%;
> div {
height: 100%;
}
`,
subMenuButtonRow: css`
background-color: ${theme.colors.background.primary};
padding: ${theme.spacing(1)};
`,
checkboxPartiallyChecked: css`
input {
&:checked + span {
&:after {
border-width: 0 3px 0px 0;
transform: rotate(90deg);
}
}
}
`,
};
};
'&:after': {
content: '">"',
},
}),
menuOptionInfoSign: css({
color: theme.colors.text.disabled,
}),
basicRoleSelector: css({
margin: theme.spacing(1, 1.25, 1, 1.5),
}),
subMenuPortal: css({
height: '100%',
'> div': {
height: '100%',
},
}),
subMenuButtonRow: css({
backgroundColor: theme.colors.background.primary,
padding: theme.spacing(1),
}),
checkboxPartiallyChecked: css({
input: {
'&:checked + span': {
'&:after': {
borderWidth: '0 3px 0px 0',
transform: 'rotate(90deg)',
},
},
},
}),
loadingSpinner: css({
marginLeft: theme.spacing(1),
}),
});
+10 -27
View File
@@ -1,42 +1,20 @@
import React, { useState, useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { NavModelItem, UrlQueryValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { NavModelItem } from '@grafana/data';
import { Form, Field, Input, Button, Legend, Alert } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { OrgUser, AccessControlAction, OrgRole } from 'app/types';
import { OrgUsersTable } from './Users/OrgUsersTable';
const perPage = 30;
import { getOrg, getOrgUsers, getUsersRoles, removeOrgUser, updateOrgName, updateOrgUserRole } from './api';
interface OrgNameDTO {
orgName: string;
}
const getOrg = async (orgId: UrlQueryValue) => {
return await getBackendSrv().get(`/api/orgs/${orgId}`);
};
const getOrgUsers = async (orgId: UrlQueryValue, page: number) => {
if (contextSrv.hasPermission(AccessControlAction.OrgUsersRead)) {
return getBackendSrv().get(`/api/orgs/${orgId}/users/search`, accessControlQueryParam({ perpage: perPage, page }));
}
return { orgUsers: [] };
};
const updateOrgUserRole = (orgUser: OrgUser, orgId: UrlQueryValue) => {
return getBackendSrv().patch(`/api/orgs/${orgId}/users/${orgUser.userId}`, orgUser);
};
const removeOrgUser = (orgUser: OrgUser, orgId: UrlQueryValue) => {
return getBackendSrv().delete(`/api/orgs/${orgId}/users/${orgUser.userId}`);
};
interface Props extends GrafanaRouteComponentProps<{ id: string }> {}
const AdminEditOrgPage = ({ match }: Props) => {
@@ -51,6 +29,11 @@ const AdminEditOrgPage = ({ match }: Props) => {
const [orgState, fetchOrg] = useAsyncFn(() => getOrg(orgId), []);
const [, fetchOrgUsers] = useAsyncFn(async (page) => {
const result = await getOrgUsers(orgId, page);
if (contextSrv.licensedAccessControlEnabled()) {
await getUsersRoles(orgId, result.orgUsers);
}
const totalPages = result?.perPage !== 0 ? Math.ceil(result.totalCount / result.perPage) : 0;
setTotalPages(totalPages);
setUsers(result.orgUsers);
@@ -62,8 +45,8 @@ const AdminEditOrgPage = ({ match }: Props) => {
fetchOrgUsers(page);
}, [fetchOrg, fetchOrgUsers, page]);
const updateOrgName = async (name: string) => {
return await getBackendSrv().put(`/api/orgs/${orgId}`, { ...orgState.value, name });
const onUpdateOrgName = async (name: string) => {
await updateOrgName(name, orgId);
};
const renderMissingPermissionMessage = () => (
@@ -101,7 +84,7 @@ const AdminEditOrgPage = ({ match }: Props) => {
{orgState.value && (
<Form
defaultValues={{ orgName: orgState.value.name }}
onSubmit={(values: OrgNameDTO) => updateOrgName(values.orgName)}
onSubmit={(values: OrgNameDTO) => onUpdateOrgName(values.orgName)}
>
{({ register, errors }) => (
<>
@@ -57,6 +57,7 @@ export interface Props {
changePage: (page: number) => void;
page: number;
totalPages: number;
rolesLoading?: boolean;
}
export const OrgUsersTable = ({
@@ -68,6 +69,7 @@ export const OrgUsersTable = ({
changePage,
page,
totalPages,
rolesLoading,
}: Props) => {
const [userToRemove, setUserToRemove] = useState<OrgUser | null>(null);
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
@@ -128,12 +130,15 @@ export const OrgUsersTable = ({
return contextSrv.licensedAccessControlEnabled() ? (
<UserRolePicker
userId={original.userId}
roles={original.roles || []}
isLoading={rolesLoading}
orgId={orgId}
roleOptions={roleOptions}
basicRole={value}
onBasicRoleChange={(newRole) => onRoleChange(newRole, original)}
basicRoleDisabled={basicRoleDisabled}
basicRoleDisabledMessage={disabledRoleMessage}
width={40}
/>
) : (
<OrgRolePicker
@@ -182,7 +187,7 @@ export const OrgUsersTable = ({
},
},
],
[orgId, roleOptions, onRoleChange]
[rolesLoading, orgId, roleOptions, onRoleChange]
);
return (
+38
View File
@@ -0,0 +1,38 @@
import { UrlQueryValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/core';
import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { OrgUser, AccessControlAction } from 'app/types';
const perPage = 30;
export const getOrg = async (orgId: UrlQueryValue) => {
return await getBackendSrv().get(`/api/orgs/${orgId}`);
};
export const getOrgUsers = async (orgId: UrlQueryValue, page: number) => {
if (contextSrv.hasPermission(AccessControlAction.OrgUsersRead)) {
return getBackendSrv().get(`/api/orgs/${orgId}/users/search`, accessControlQueryParam({ perpage: perPage, page }));
}
return { orgUsers: [] };
};
export const getUsersRoles = async (orgId: number, users: OrgUser[]) => {
const userIds = users.map((u) => u.userId);
const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId });
users.forEach((u) => {
u.roles = roles ? roles[u.userId] || [] : [];
});
};
export const updateOrgUserRole = (orgUser: OrgUser, orgId: UrlQueryValue) => {
return getBackendSrv().patch(`/api/orgs/${orgId}/users/${orgUser.userId}`, orgUser);
};
export const removeOrgUser = (orgUser: OrgUser, orgId: UrlQueryValue) => {
return getBackendSrv().delete(`/api/orgs/${orgId}/users/${orgUser.userId}`);
};
export const updateOrgName = (name: string, orgId: number) => {
return getBackendSrv().put(`/api/orgs/${orgId}`, { name });
};
@@ -5,7 +5,6 @@ import { useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { locationService } from '@grafana/runtime';
import {
Button,
ClipboardButton,
@@ -148,13 +147,9 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
<Menu.Item
label="Modify export"
icon="edit"
onClick={() =>
locationService.push(
createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, {
returnTo: location.pathname + location.search,
})
)
}
url={createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, {
returnTo: location.pathname + location.search,
})}
/>
);
}
@@ -4,7 +4,7 @@ import React, { Fragment, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2, textUtil, urlUtil } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import {
Button,
ClipboardButton,
@@ -238,17 +238,11 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
}
if (isGrafanaRulerRule(rulerRule)) {
moreActionsButtons.push(
<Menu.Item
label="Modify export"
icon="edit"
onClick={() =>
locationService.push(
createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`)
)
}
/>
const modifyUrl = createUrl(
`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`
);
moreActionsButtons.push(<Menu.Item label="Modify export" icon="edit" url={modifyUrl} />);
}
if (hasCreateRulePermission && !isFederated) {
@@ -13,6 +13,7 @@ import { EventTrackingSrc } from './tracking';
import { Role } from './utils';
const mockedUseOpenAiStreamState = {
messages: [],
setMessages: jest.fn(),
reply: 'I am a robot',
streamStatus: StreamStatus.IDLE,
@@ -43,6 +44,7 @@ describe('GenAIButton', () => {
describe('when LLM plugin is not configured', () => {
beforeAll(() => {
jest.mocked(useOpenAIStream).mockReturnValue({
messages: [],
error: undefined,
streamStatus: StreamStatus.IDLE,
reply: 'Some completed genereated text',
@@ -64,7 +66,10 @@ describe('GenAIButton', () => {
describe('when LLM plugin is properly configured, so it is enabled', () => {
const setMessagesMock = jest.fn();
beforeEach(() => {
setMessagesMock.mockClear();
jest.mocked(useOpenAIStream).mockReturnValue({
messages: [],
error: undefined,
streamStatus: StreamStatus.IDLE,
reply: 'Some completed genereated text',
@@ -100,6 +105,20 @@ describe('GenAIButton', () => {
expect(setMessagesMock).toHaveBeenCalledWith([{ content: 'Generate X', role: 'system' as Role }]);
});
it('should call the messages when they are provided as callback', async () => {
const onGenerate = jest.fn();
const messages = jest.fn().mockReturnValue([{ content: 'Generate X', role: 'system' as Role }]);
const onClick = jest.fn();
setup({ onGenerate, messages, temperature: 3, onClick, eventTrackingSrc });
const generateButton = await screen.findByRole('button');
await fireEvent.click(generateButton);
expect(messages).toHaveBeenCalledTimes(1);
expect(setMessagesMock).toHaveBeenCalledTimes(1);
expect(setMessagesMock).toHaveBeenCalledWith([{ content: 'Generate X', role: 'system' as Role }]);
});
it('should call the onClick callback', async () => {
const onGenerate = jest.fn();
const onClick = jest.fn();
@@ -116,6 +135,7 @@ describe('GenAIButton', () => {
describe('when it is generating data', () => {
beforeEach(() => {
jest.mocked(useOpenAIStream).mockReturnValue({
messages: [],
error: undefined,
streamStatus: StreamStatus.GENERATING,
reply: 'Some incomplete generated text',
@@ -160,7 +180,10 @@ describe('GenAIButton', () => {
describe('when there is an error generating data', () => {
const setMessagesMock = jest.fn();
beforeEach(() => {
setMessagesMock.mockClear();
jest.mocked(useOpenAIStream).mockReturnValue({
messages: [],
error: new Error('Something went wrong'),
streamStatus: StreamStatus.IDLE,
reply: '',
@@ -224,5 +247,19 @@ describe('GenAIButton', () => {
await waitFor(() => expect(onClick).toHaveBeenCalledTimes(1));
});
it('should call the messages when they are provided as callback', async () => {
const onGenerate = jest.fn();
const messages = jest.fn().mockReturnValue([{ content: 'Generate X', role: 'system' as Role }]);
const onClick = jest.fn();
setup({ onGenerate, messages, temperature: 3, onClick, eventTrackingSrc });
const generateButton = await screen.findByRole('button');
await fireEvent.click(generateButton);
expect(messages).toHaveBeenCalledTimes(1);
expect(setMessagesMock).toHaveBeenCalledTimes(1);
expect(setMessagesMock).toHaveBeenCalledWith([{ content: 'Generate X', role: 'system' as Role }]);
});
});
});
@@ -18,7 +18,7 @@ export interface GenAIButtonProps {
// Button click handler
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
// Messages to send to the LLM plugin
messages: Message[];
messages: Message[] | (() => Message[]);
// Callback function that the LLM plugin streams responses to
onGenerate: (response: string) => void;
// Temperature for the LLM plugin. Default is 1.
@@ -43,8 +43,14 @@ export const GenAIButton = ({
}: GenAIButtonProps) => {
const styles = useStyles2(getStyles);
const { setMessages, reply, value, error, streamStatus } = useOpenAIStream(OPEN_AI_MODEL, temperature);
const {
messages: streamMessages,
setMessages,
reply,
value,
error,
streamStatus,
} = useOpenAIStream(OPEN_AI_MODEL, temperature);
const [history, setHistory] = useState<string[]>([]);
const [showHistory, setShowHistory] = useState(true);
@@ -56,7 +62,7 @@ export const GenAIButton = ({
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!hasHistory) {
onClickProp?.(e);
setMessages(messages);
setMessages(typeof messages === 'function' ? messages() : messages);
} else {
if (setShowHistory) {
setShowHistory(true);
@@ -154,7 +160,7 @@ export const GenAIButton = ({
content={
<GenAIHistory
history={history}
messages={messages}
messages={streamMessages}
onApplySuggestion={onApplySuggestion}
updateHistory={pushHistoryEntry}
eventTrackingSrc={eventTrackingSrc}
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useCallback } from 'react';
import { DashboardModel } from '../../state';
@@ -27,7 +27,7 @@ const CHANGES_GENERATION_STANDARD_PROMPT = [
].join('.\n');
export const GenAIDashboardChangesButton = ({ dashboard, onGenerate, disabled }: GenAIDashboardChangesButtonProps) => {
const messages = useMemo(() => getMessages(dashboard), [dashboard]);
const messages = useCallback(() => getMessages(dashboard), [dashboard]);
return (
<GenAIButton
@@ -26,6 +26,7 @@ export function useOpenAIStream(
temperature = 1
): {
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
messages: Message[];
reply: string;
streamStatus: StreamStatus;
error: Error | undefined;
@@ -138,6 +139,7 @@ export function useOpenAIStream(
return {
setMessages,
messages,
reply,
streamStatus,
error,
+17 -6
View File
@@ -30,13 +30,24 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
}
export async function getRemotePlugins(): Promise<RemotePlugin[]> {
// We are also fetching deprecated plugins, because we would like to be able to label plugins in the list that are both installed and deprecated.
// (We won't show not installed deprecated plugins in the list)
const { items: remotePlugins }: { items: RemotePlugin[] } = await getBackendSrv().get(`${GCOM_API_ROOT}/plugins`, {
includeDeprecated: true,
});
try {
const { items: remotePlugins }: { items: RemotePlugin[] } = await getBackendSrv().get(`${GCOM_API_ROOT}/plugins`, {
// We are also fetching deprecated plugins, because we would like to be able to label plugins in the list that are both installed and deprecated.
// (We won't show not installed deprecated plugins in the list)
includeDeprecated: true,
});
return remotePlugins.filter(isRemotePluginVisibleByConfig);
return remotePlugins.filter(isRemotePluginVisibleByConfig);
} catch (error) {
if (isFetchError(error)) {
// It can happen that GCOM is not available, in that case we show a limited set of information to the user.
error.isHandled = true;
console.error('Failed to fetch plugins from catalog (default https://grafana.com/api/plugins)');
return [];
}
throw error;
}
}
export async function getPluginErrors(): Promise<PluginError[]> {
@@ -1,5 +1,5 @@
import { createAction, createAsyncThunk, Update } from '@reduxjs/toolkit';
import { from, forkJoin, timeout, lastValueFrom, catchError, throwError } from 'rxjs';
import { from, forkJoin, timeout, lastValueFrom, catchError, of } from 'rxjs';
import { PanelPlugin, PluginError } from '@grafana/data';
import { getBackendSrv, isFetchError } from '@grafana/runtime';
@@ -25,9 +25,18 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/pending` });
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/pending` });
const local$ = from(getLocalPlugins());
const remote$ = from(getRemotePlugins());
const TIMEOUT = 500;
const pluginErrors$ = from(getPluginErrors());
const local$ = from(getLocalPlugins());
// Unknown error while fetching remote plugins from GCOM.
// (In this case we still operate, but only with locally available plugins.)
const remote$ = from(getRemotePlugins()).pipe(
catchError((err) => {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/rejected` });
console.error(err);
return of([]);
})
);
forkJoin({
local: local$,
@@ -35,20 +44,14 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t
pluginErrors: pluginErrors$,
})
.pipe(
// Fetching the list of plugins from GCOM is slow / errors out
// Fetching the list of plugins from GCOM is slow / times out
// (We are waiting for TIMEOUT, and if there is still no response from GCOM we continue with locally
// installed plugins only by returning a new observable. We also still wait for the remote request to finish or
// time out, but we don't block the main execution flow.)
timeout({
each: 500,
each: TIMEOUT,
with: () => {
remote$
// The request to fetch remote plugins from GCOM failed
.pipe(
catchError((err) => {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/rejected` });
return throwError(
() => new Error('Failed to fetch plugins from catalog (default https://grafana.com/api/plugins)')
);
})
)
// Remote plugins loaded after a timeout, updating the store
.subscribe(async (remote: RemotePlugin[]) => {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/fulfilled` });
@@ -75,16 +78,23 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t
remote?: RemotePlugin[];
pluginErrors: PluginError[];
}) => {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/fulfilled` });
// Both local and remote plugins are loaded
if (local && remote) {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/fulfilled` });
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/fulfilled` });
thunkApi.dispatch(addPlugins(mergeLocalsAndRemotes(local, remote, pluginErrors)));
// Only remote plugins are loaded (remote timed out)
} else if (local) {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/fulfilled` });
thunkApi.dispatch(addPlugins(mergeLocalsAndRemotes(local, [], pluginErrors)));
}
},
(error) => {
console.error(error);
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/rejected` });
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/rejected` });
return thunkApi.rejectWithValue('Unknown error.');
}
);
@@ -2,11 +2,12 @@ import {
CoreApp,
DataFrame,
DataQueryError,
DataQueryRequest,
getDefaultTimeRange,
DataSourceApi,
dateTime,
LoadingState,
PanelData,
DataQueryRequest,
} from '@grafana/data';
import { MetaAnalyticsEventName, reportMetaAnalytics } from '@grafana/runtime';
@@ -84,155 +85,196 @@ const multipleDataframesWithSameRefId = [
},
];
function getTestData(requestApp: string, series: DataFrame[] = []): PanelData {
function getTestData(
overrides: Partial<DataQueryRequest> = {},
series?: DataFrame[],
errors?: DataQueryError[]
): PanelData {
const now = dateTime();
return {
request: {
app: requestApp,
panelId: 2,
app: CoreApp.Dashboard,
startTime: now.unix(),
endTime: now.add(1, 's').unix(),
} as DataQueryRequest,
series,
state: LoadingState.Done,
timeRange: {
from: dateTime(),
to: dateTime(),
raw: { from: '1h', to: 'now' },
interval: '1s',
intervalMs: 1000,
range: getDefaultTimeRange(),
requestId: '1',
scopedVars: {},
targets: [],
timezone: 'utc',
...overrides,
},
series: series || [],
state: LoadingState.Done,
timeRange: getDefaultTimeRange(),
errors,
};
}
function getTestDataForExplore(requestApp: string, series: DataFrame[] = []): PanelData {
const now = dateTime();
const error: DataQueryError = { message: 'test error' };
return {
request: {
app: requestApp,
startTime: now.unix(),
endTime: now.add(1, 's').unix(),
} as DataQueryRequest,
series,
state: LoadingState.Done,
timeRange: {
from: dateTime(),
to: dateTime(),
raw: { from: '1h', to: 'now' },
},
error: error,
};
}
describe('emitDataRequestEvent - from a dashboard panel', () => {
it('Should report meta analytics', () => {
const data = getTestData(CoreApp.Dashboard);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceUid: datasource.uid,
datasourceType: datasource.type,
source: 'dashboard',
describe('emitDataRequestEvent', () => {
describe('From a dashboard panel', () => {
it('Should report meta analytics', () => {
const data = getTestData({
panelId: 2,
dashboardUid: 'test', // from dashboard srv
dataSize: 0,
duration: 1,
totalQueries: 0,
cachedQueries: 0,
})
);
});
});
emitDataRequestEvent(datasource)(data);
it('Should report meta analytics with counts for cached and total queries', () => {
const data = getTestData(CoreApp.Dashboard, partiallyCachedSeries);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceUid: datasource.uid,
datasourceType: datasource.type,
source: 'dashboard',
panelId: 2,
dashboardUid: 'test',
dataSize: 2,
duration: 1,
totalQueries: 2,
cachedQueries: 1,
})
);
});
it('Should report meta analytics with counts for cached and total queries when same refId spread across multiple DataFrames', () => {
const data = getTestData(CoreApp.Dashboard, multipleDataframesWithSameRefId);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceUid: datasource.uid,
datasourceType: datasource.type,
source: 'dashboard',
panelId: 2,
dashboardUid: 'test', // from dashboard srv
dataSize: 2,
duration: 1,
totalQueries: 1,
cachedQueries: 1,
})
);
});
it('Should not report meta analytics twice if the request receives multiple responses', () => {
const data = getTestData(CoreApp.Dashboard);
const fn = emitDataRequestEvent(datasource);
fn(data);
fn(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
});
it('Should not report meta analytics in edit mode', () => {
mockGetUrlSearchParams.mockImplementationOnce(() => {
return { editPanel: 2 };
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceUid: datasource.uid,
datasourceType: datasource.type,
source: CoreApp.Dashboard,
panelId: 2,
dashboardUid: 'test', // from dashboard srv
dataSize: 0,
duration: 1,
totalQueries: 0,
cachedQueries: 0,
})
);
});
it('Should report meta analytics with counts for cached and total queries', () => {
const data = getTestData(
{
panelId: 2,
},
partiallyCachedSeries
);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceUid: datasource.uid,
datasourceType: datasource.type,
source: CoreApp.Dashboard,
panelId: 2,
dashboardUid: 'test',
dataSize: 2,
duration: 1,
totalQueries: 2,
cachedQueries: 1,
})
);
});
it('Should report meta analytics with counts for cached and total queries when same refId spread across multiple DataFrames', () => {
const data = getTestData(
{
panelId: 2,
},
multipleDataframesWithSameRefId
);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceUid: datasource.uid,
datasourceType: datasource.type,
source: CoreApp.Dashboard,
panelId: 2,
dashboardUid: 'test', // from dashboard srv
dataSize: 2,
duration: 1,
totalQueries: 1,
cachedQueries: 1,
})
);
});
it('Should not report meta analytics twice if the request receives multiple responses', () => {
const data = getTestData();
const fn = emitDataRequestEvent(datasource);
fn(data);
fn(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
});
it('Should not report meta analytics in edit mode', () => {
mockGetUrlSearchParams.mockImplementationOnce(() => {
return { editPanel: 2 };
});
const data = getTestData();
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).not.toBeCalled();
});
const data = getTestData(CoreApp.Dashboard);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).not.toBeCalled();
});
});
// Previously we filtered out Explore events due to too many errors being generated while a user is building a query
// This tests that we send an event for Explore queries but do not record errors
describe('emitDataRequestEvent - from Explore', () => {
it('Should report meta analytics', () => {
const data = getTestDataForExplore(CoreApp.Explore);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
source: 'explore',
datasourceName: 'test',
datasourceUid: 'test',
dataSize: 0,
duration: 1,
totalQueries: 0,
})
// Previously we filtered out Explore and Correlations events due to too many errors being generated while a user is building a query
// This tests that we send an event for both queries but do not record errors
describe('From Explore', () => {
const data = getTestData(
{
app: CoreApp.Explore,
},
undefined,
[{ message: 'test error' }]
);
it('Should report meta analytics', () => {
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
source: CoreApp.Explore,
datasourceName: 'test',
datasourceUid: 'test',
dataSize: 0,
duration: 1,
totalQueries: 0,
})
);
});
it('Should not report errors', () => {
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(expect.not.objectContaining({ error: 'test error' }));
});
});
describe('emitDataRequestEvent - from Explore', () => {
// Previously we filtered out Explore and Correlations events due to too many errors being generated while a user is building a query
// This tests that we send an event for both queries but do not record errors
describe('From Correlations', () => {
const data = getTestData(
{
app: CoreApp.Correlations,
},
undefined,
[{ message: 'some error' }]
);
it('Should report meta analytics', () => {
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
source: CoreApp.Correlations,
datasourceName: 'test',
datasourceUid: 'test',
dataSize: 0,
duration: 1,
totalQueries: 0,
})
);
});
it('Should not report errors', () => {
const data = getTestDataForExplore(CoreApp.Explore);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
@@ -1,4 +1,4 @@
import { PanelData, LoadingState, DataSourceApi, CoreApp, urlUtil } from '@grafana/data';
import { PanelData, LoadingState, DataSourceApi, urlUtil, CoreApp } from '@grafana/data';
import { reportMetaAnalytics, MetaAnalyticsEventName, DataRequestEventPayload } from '@grafana/runtime';
import { getDashboardSrv } from '../../dashboard/services/DashboardSrv';
@@ -31,10 +31,10 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
duration: data.request.endTime! - data.request.startTime,
};
if (data.request.app === CoreApp.Explore || data.request.app === CoreApp.Correlations) {
enrichWithInfo(eventData, data);
} else {
enrichWithDashboardInfo(eventData, data);
enrichWithInfo(eventData, data);
if (data.request.app !== CoreApp.Explore && data.request.app !== CoreApp.Correlations) {
enrichWithErrorData(eventData, data);
}
if (data.series && data.series.length > 0) {
@@ -50,11 +50,6 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
};
function enrichWithInfo(eventData: DataRequestEventPayload, data: PanelData) {
const totalQueries = Object.keys(data.series).length;
eventData.totalQueries = totalQueries;
}
function enrichWithDashboardInfo(eventData: DataRequestEventPayload, data: PanelData) {
const queryCacheStatus: { [key: string]: boolean } = {};
for (let i = 0; i < data.series.length; i++) {
const refId = data.series[i].refId;
@@ -62,12 +57,10 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
queryCacheStatus[refId] = data.series[i].meta?.isCachedResponse ?? false;
}
}
const totalQueries = Object.keys(queryCacheStatus).length;
const cachedQueries = Object.values(queryCacheStatus).filter((val) => val === true).length;
eventData.totalQueries = Object.keys(queryCacheStatus).length;
eventData.cachedQueries = Object.values(queryCacheStatus).filter((val) => val === true).length;
eventData.panelId = data.request!.panelId;
eventData.totalQueries = totalQueries;
eventData.cachedQueries = cachedQueries;
const dashboard = getDashboardSrv().getCurrent();
if (dashboard) {
@@ -76,9 +69,13 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
eventData.dashboardUid = dashboard.uid;
eventData.folderName = dashboard.meta.folderTitle;
}
if (data.error) {
eventData.error = data.error.message;
}
}
}
function enrichWithErrorData(eventData: DataRequestEventPayload, data: PanelData) {
if (data.errors?.length) {
eventData.error = data.errors.join(', ');
} else if (data.error) {
eventData.error = data.error.message;
}
}
@@ -220,7 +220,7 @@ export const ServiceAccountsListPageUnconnected = ({
<th>ID</th>
<th>Roles</th>
<th>Tokens</th>
<th style={{ width: '34px' }} />
<th style={{ width: '120px' }} />
</tr>
</thead>
<tbody>
@@ -77,10 +77,12 @@ const ServiceAccountListItem = memo(
userId={serviceAccount.id}
orgId={serviceAccount.orgId}
basicRole={serviceAccount.role}
roles={serviceAccount.roles || []}
onBasicRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)}
roleOptions={roleOptions}
basicRoleDisabled={!canUpdateRole}
disabled={serviceAccount.isDisabled}
width={40}
/>
)}
</td>
@@ -11,6 +11,8 @@ import {
acOptionsLoaded,
pageChanged,
queryChanged,
rolesFetchBegin,
rolesFetchEnd,
serviceAccountsFetchBegin,
serviceAccountsFetched,
serviceAccountsFetchEnd,
@@ -51,6 +53,18 @@ export function fetchServiceAccounts(
serviceAccountStateFilter
)}&accesscontrol=true`
);
if (contextSrv.licensedAccessControlEnabled()) {
dispatch(rolesFetchBegin());
const orgId = contextSrv.user.orgId;
const userIds = result?.serviceAccounts.map((u: ServiceAccountDTO) => u.id);
const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId });
result.serviceAccounts.forEach((u: ServiceAccountDTO) => {
u.roles = roles ? roles[u.id] || [] : [];
});
dispatch(rolesFetchEnd());
}
dispatch(serviceAccountsFetched(result));
}
} catch (error) {
@@ -32,12 +32,24 @@ export const serviceAccountProfileSlice = createSlice({
serviceAccountTokensLoaded: (state, action: PayloadAction<ApiKey[]>): ServiceAccountProfileState => {
return { ...state, tokens: action.payload, isLoading: false };
},
rolesFetchBegin: (state) => {
return { ...state, rolesLoading: true };
},
rolesFetchEnd: (state) => {
return { ...state, rolesLoading: false };
},
},
});
export const serviceAccountProfileReducer = serviceAccountProfileSlice.reducer;
export const { serviceAccountLoaded, serviceAccountTokensLoaded, serviceAccountFetchBegin, serviceAccountFetchEnd } =
serviceAccountProfileSlice.actions;
export const {
serviceAccountLoaded,
serviceAccountTokensLoaded,
serviceAccountFetchBegin,
serviceAccountFetchEnd,
rolesFetchBegin,
rolesFetchEnd,
} = serviceAccountProfileSlice.actions;
// serviceAccountsListPage
export const initialStateList: ServiceAccountsState = {
@@ -31,6 +31,7 @@ const setup = (propOverrides?: object) => {
page: 0,
hasFetched: false,
perPage: 10,
rolesLoading: false,
};
Object.assign(props, propOverrides);
+14 -2
View File
@@ -46,6 +46,7 @@ export const TeamList = ({
changeQuery,
totalPages,
page,
rolesLoading,
changePage,
changeSort,
}: Props) => {
@@ -100,7 +101,17 @@ export const TeamList = ({
AccessControlAction.ActionTeamsRolesList,
original
);
return canSeeTeamRoles && <TeamRolePicker teamId={original.id} roleOptions={roleOptions} />;
return (
canSeeTeamRoles && (
<TeamRolePicker
teamId={original.id}
roles={original.roles || []}
isLoading={rolesLoading}
roleOptions={roleOptions}
width={40}
/>
)
);
},
},
]
@@ -136,7 +147,7 @@ export const TeamList = ({
},
},
],
[displayRolePicker, roleOptions, deleteTeam]
[displayRolePicker, rolesLoading, roleOptions, deleteTeam]
);
return (
@@ -225,6 +236,7 @@ function mapStateToProps(state: StoreState) {
noTeams: state.teams.noTeams,
totalPages: state.teams.totalPages,
hasFetched: state.teams.hasFetched,
rolesLoading: state.teams.rolesLoading,
};
}
@@ -16,6 +16,8 @@ import {
teamMembersLoaded,
teamsLoaded,
sortChanged,
rolesFetchBegin,
rolesFetchEnd,
} from './reducers';
export function loadTeams(initial = false): ThunkResult<void> {
@@ -39,6 +41,16 @@ export function loadTeams(initial = false): ThunkResult<void> {
noTeams = response.teams.length === 0;
}
if (contextSrv.licensedAccessControlEnabled()) {
dispatch(rolesFetchBegin());
const teamIds = response?.teams.map((t: Team) => t.id);
const roles = await getBackendSrv().post(`/api/access-control/teams/roles/search`, { teamIds });
response.teams.forEach((t: Team) => {
t.roles = roles ? roles[t.id] || [] : [];
});
dispatch(rolesFetchEnd());
}
dispatch(teamsLoaded({ noTeams, ...response }));
};
}
+8 -1
View File
@@ -38,10 +38,17 @@ const teamsSlice = createSlice({
sortChanged: (state, action: PayloadAction<TeamsState['sort']>): TeamsState => {
return { ...state, sort: action.payload, page: 1 };
},
rolesFetchBegin: (state) => {
return { ...state, rolesLoading: true };
},
rolesFetchEnd: (state) => {
return { ...state, rolesLoading: false };
},
},
});
export const { teamsLoaded, queryChanged, pageChanged, sortChanged } = teamsSlice.actions;
export const { teamsLoaded, queryChanged, pageChanged, sortChanged, rolesFetchBegin, rolesFetchEnd } =
teamsSlice.actions;
export const teamsReducer = teamsSlice.reducer;
@@ -38,6 +38,7 @@ const setup = (propOverrides?: object) => {
changePage: mockToolkitActionCreator(pageChanged),
changeSort: mockToolkitActionCreator(sortChanged),
isLoading: false,
rolesLoading: false,
};
Object.assign(props, propOverrides);
@@ -26,6 +26,7 @@ function mapStateToProps(state: StoreState) {
invitees: selectInvitesMatchingQuery(state.invites, searchQuery),
externalUserMngInfo: state.users.externalUserMngInfo,
isLoading: state.users.isLoading,
rolesLoading: state.users.rolesLoading,
};
}
@@ -53,6 +54,7 @@ export const UsersListPageUnconnected = ({
invitees,
externalUserMngInfo,
isLoading,
rolesLoading,
loadUsers,
fetchInvitees,
changePage,
@@ -86,6 +88,7 @@ export const UsersListPageUnconnected = ({
<OrgUsersTable
users={users}
orgId={contextSrv.user.orgId}
rolesLoading={rolesLoading}
onRoleChange={onRoleChange}
onRemoveUser={onRemoveUser}
fetchData={changeSort}
+23 -4
View File
@@ -2,21 +2,43 @@ import { debounce } from 'lodash';
import { getBackendSrv } from '@grafana/runtime';
import { FetchDataArgs } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { OrgUser } from 'app/types';
import { ThunkResult } from '../../../types';
import { usersLoaded, pageChanged, usersFetchBegin, usersFetchEnd, searchQueryChanged, sortChanged } from './reducers';
import {
usersLoaded,
pageChanged,
usersFetchBegin,
usersFetchEnd,
searchQueryChanged,
sortChanged,
rolesFetchBegin,
rolesFetchEnd,
} from './reducers';
export function loadUsers(): ThunkResult<void> {
return async (dispatch, getState) => {
try {
dispatch(usersFetchBegin());
const { perPage, page, searchQuery, sort } = getState().users;
const users = await getBackendSrv().get(
`/api/org/users/search`,
accessControlQueryParam({ perpage: perPage, page, query: searchQuery, sort })
);
if (contextSrv.licensedAccessControlEnabled()) {
dispatch(rolesFetchBegin());
const orgId = contextSrv.user.orgId;
const userIds = users?.orgUsers.map((u: OrgUser) => u.userId);
const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId });
users.orgUsers.forEach((u: OrgUser) => {
u.roles = roles ? roles[u.userId] || [] : [];
});
dispatch(rolesFetchEnd());
}
dispatch(usersLoaded(users));
} catch (error) {
usersFetchEnd();
@@ -42,7 +64,6 @@ export function removeUser(userId: number): ThunkResult<void> {
export function changePage(page: number): ThunkResult<void> {
return async (dispatch) => {
dispatch(usersFetchBegin());
dispatch(pageChanged(page));
dispatch(loadUsers());
};
@@ -51,7 +72,6 @@ export function changePage(page: number): ThunkResult<void> {
export function changeSort({ sortBy }: FetchDataArgs<OrgUser>): ThunkResult<void> {
const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined;
return async (dispatch) => {
dispatch(usersFetchBegin());
dispatch(sortChanged(sort));
dispatch(loadUsers());
};
@@ -59,7 +79,6 @@ export function changeSort({ sortBy }: FetchDataArgs<OrgUser>): ThunkResult<void
export function changeSearchQuery(query: string): ThunkResult<void> {
return async (dispatch) => {
dispatch(usersFetchBegin());
dispatch(searchQueryChanged(query));
fetchUsersWithDebounce(dispatch);
};
@@ -13,6 +13,7 @@ export const initialState: UsersState = {
externalUserMngLinkName: config.externalUserMngLinkName,
externalUserMngLinkUrl: config.externalUserMngLinkUrl,
isLoading: false,
rolesLoading: false,
};
export interface UsersFetchResult {
@@ -22,6 +23,13 @@ export interface UsersFetchResult {
totalCount: number;
}
export interface UsersRolesFetchResult {
orgUsers: OrgUser[];
perPage: number;
page: number;
totalCount: number;
}
const usersSlice = createSlice({
name: 'users',
initialState,
@@ -60,6 +68,12 @@ const usersSlice = createSlice({
usersFetchEnd: (state) => {
return { ...state, isLoading: false };
},
rolesFetchBegin: (state) => {
return { ...state, rolesLoading: true };
},
rolesFetchEnd: (state) => {
return { ...state, rolesLoading: false };
},
},
});
@@ -71,6 +85,8 @@ export const {
usersFetchEnd,
pageChanged,
sortChanged,
rolesFetchBegin,
rolesFetchEnd,
} = usersSlice.actions;
export const usersReducer = usersSlice.reducer;
@@ -2,14 +2,14 @@
"name": "@grafana-plugins/grafana-testdata-datasource",
"description": "Generates test data in different forms",
"private": true,
"version": "10.2.1",
"version": "10.2.2",
"dependencies": {
"@emotion/css": "11.11.2",
"@grafana/data": "10.2.1",
"@grafana/data": "10.2.2",
"@grafana/experimental": "1.7.0",
"@grafana/runtime": "10.2.1",
"@grafana/schema": "10.2.1",
"@grafana/ui": "10.2.1",
"@grafana/runtime": "10.2.2",
"@grafana/schema": "10.2.2",
"@grafana/ui": "10.2.2",
"lodash": "4.17.21",
"react": "18.2.0",
"react-use": "17.4.0",
@@ -17,8 +17,8 @@
"tslib": "2.6.0"
},
"devDependencies": {
"@grafana/e2e-selectors": "10.2.1",
"@grafana/plugin-configs": "10.2.1",
"@grafana/e2e-selectors": "10.2.2",
"@grafana/plugin-configs": "10.2.2",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/jest": "29.5.4",
@@ -7,7 +7,7 @@ import config from 'app/core/config';
import { TemplateSrv } from '../../../features/templating/template_srv';
import { BROWSER_MODE_DISABLED_MESSAGE } from './constants';
import InfluxDatasource from './datasource';
import InfluxDatasource, { influxSpecialRegexEscape } from './datasource';
import {
getMockDSInstanceSettings,
getMockInfluxDS,
@@ -409,5 +409,21 @@ describe('InfluxDataSource Frontend Mode', () => {
expect(qData).toBe(qe);
});
});
describe('influxSpecialRegexEscape', () => {
it('should escape the dot properly', () => {
const value = 'value.with-dot';
const expectation = `value\.with-dot`;
const result = influxSpecialRegexEscape(value);
expect(result).toBe(expectation);
});
it('should escape the url properly', () => {
const value = 'https://aaaa-aa-aaa.bbb.ccc.ddd:8443/jolokia';
const expectation = `https:\/\/aaaa-aa-aaa\.bbb\.ccc\.ddd:8443\/jolokia`;
const result = influxSpecialRegexEscape(value);
expect(result).toBe(expectation);
});
});
});
});
@@ -785,5 +785,10 @@ export function influxRegularEscape(value: string | string[]) {
}
export function influxSpecialRegexEscape(value: string | string[]) {
return typeof value === 'string' ? value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&') : value;
if (typeof value !== 'string') {
return value;
}
value = value.replace(/\\/g, '\\\\\\\\');
value = value.replace(/[$^*{}\[\]\'+?.()|]/g, '$&');
return value;
}
+2
View File
@@ -36,6 +36,7 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata {
isDisabled: boolean;
teams: string[];
role: OrgRole;
roles?: Role[];
}
export interface ServiceAccountCreateApiResponse {
@@ -52,6 +53,7 @@ export interface ServiceAccountCreateApiResponse {
export interface ServiceAccountProfileState {
serviceAccount: ServiceAccountDTO;
isLoading: boolean;
rolesLoading?: boolean;
tokens: ApiKey[];
}
+6
View File
@@ -1,5 +1,6 @@
import { Team as TeamDTO } from '@grafana/schema/src/raw/team/x/team_types.gen';
import { Role } from './accessControl';
import { TeamPermissionLevel } from './acl';
// The team resource
@@ -37,6 +38,10 @@ export interface Team {
* TODO - it seems it's a team_member.permission, unlikely it should belong to the team kind
*/
permission: TeamPermissionLevel;
/**
* RBAC roles assigned to the team.
*/
roles?: Role[];
}
export interface TeamMember {
@@ -64,6 +69,7 @@ export interface TeamsState {
totalPages: number;
hasFetched: boolean;
sort?: string;
rolesLoading?: boolean;
}
export interface TeamState {
+5
View File
@@ -1,6 +1,8 @@
import { SelectableValue, WithAccessControlMetadata } from '@grafana/data';
import { Role } from 'app/types';
import { OrgRole } from '.';
export interface OrgUser extends WithAccessControlMetadata {
avatarUrl: string;
email: string;
@@ -10,6 +12,8 @@ export interface OrgUser extends WithAccessControlMetadata {
name: string;
orgId: number;
role: OrgRole;
// RBAC roles
roles?: Role[];
userId: number;
isDisabled: boolean;
authLabels?: string[];
@@ -76,6 +80,7 @@ export interface UsersState {
externalUserMngLinkName: string;
externalUserMngInfo: string;
isLoading: boolean;
rolesLoading?: boolean;
page: number;
perPage: number;
totalPages: number;
+1
View File
@@ -99,6 +99,7 @@ def pr_pipelines():
"go.sum",
"go.mod",
"public/app/plugins/**/plugin.json",
"docs/sources/setup-grafana/configure-grafana/feature-toggles/**",
"devenv/**",
],
),
+25 -25
View File
@@ -2839,13 +2839,13 @@ __metadata:
resolution: "@grafana-plugins/grafana-testdata-datasource@workspace:public/app/plugins/datasource/grafana-testdata-datasource"
dependencies:
"@emotion/css": 11.11.2
"@grafana/data": 10.2.1
"@grafana/e2e-selectors": 10.2.1
"@grafana/data": 10.2.2
"@grafana/e2e-selectors": 10.2.2
"@grafana/experimental": 1.7.0
"@grafana/plugin-configs": 10.2.1
"@grafana/runtime": 10.2.1
"@grafana/schema": 10.2.1
"@grafana/ui": 10.2.1
"@grafana/plugin-configs": 10.2.2
"@grafana/runtime": 10.2.2
"@grafana/schema": 10.2.2
"@grafana/ui": 10.2.2
"@testing-library/react": 14.0.0
"@testing-library/user-event": 14.4.3
"@types/jest": 29.5.4
@@ -2870,9 +2870,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource"
dependencies:
"@grafana/data": 10.2.1
"@grafana/data": 10.2.2
"@grafana/tsconfig": ^1.2.0-rc1
"@grafana/ui": 10.2.1
"@grafana/ui": 10.2.2
"@types/jest": 26.0.15
"@types/react": 18.0.28
copy-webpack-plugin: 11.0.0
@@ -2908,12 +2908,12 @@ __metadata:
languageName: node
linkType: hard
"@grafana/data@10.2.1, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data":
"@grafana/data@10.2.2, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data":
version: 0.0.0-use.local
resolution: "@grafana/data@workspace:packages/grafana-data"
dependencies:
"@braintree/sanitize-url": 6.0.2
"@grafana/schema": 10.2.1
"@grafana/schema": 10.2.2
"@grafana/tsconfig": ^1.2.0-rc1
"@rollup/plugin-commonjs": 25.0.2
"@rollup/plugin-json": 6.0.0
@@ -2973,7 +2973,7 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/e2e-selectors@10.2.1, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors":
"@grafana/e2e-selectors@10.2.2, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors":
version: 0.0.0-use.local
resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors"
dependencies:
@@ -3010,8 +3010,8 @@ __metadata:
"@babel/core": 7.23.0
"@babel/preset-env": 7.23.2
"@cypress/webpack-preprocessor": 5.17.1
"@grafana/e2e-selectors": 10.2.1
"@grafana/schema": 10.2.1
"@grafana/e2e-selectors": 10.2.2
"@grafana/schema": 10.2.2
"@grafana/tsconfig": ^1.2.0-rc1
"@mochajs/json-file-reporter": ^1.2.0
"@rollup/plugin-node-resolve": 15.2.3
@@ -3149,9 +3149,9 @@ __metadata:
"@babel/preset-env": 7.23.2
"@babel/preset-react": 7.22.5
"@emotion/css": 11.11.2
"@grafana/data": 10.2.1
"@grafana/data": 10.2.2
"@grafana/tsconfig": ^1.2.0-rc1
"@grafana/ui": 10.2.1
"@grafana/ui": 10.2.2
"@leeoniya/ufuzzy": 1.0.8
"@rollup/plugin-node-resolve": 15.2.3
"@testing-library/jest-dom": ^6.1.2
@@ -3225,7 +3225,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/plugin-configs@10.2.1, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs":
"@grafana/plugin-configs@10.2.2, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs":
version: 0.0.0-use.local
resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs"
dependencies:
@@ -3240,15 +3240,15 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/runtime@10.2.1, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime":
"@grafana/runtime@10.2.2, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime":
version: 0.0.0-use.local
resolution: "@grafana/runtime@workspace:packages/grafana-runtime"
dependencies:
"@grafana/data": 10.2.1
"@grafana/e2e-selectors": 10.2.1
"@grafana/data": 10.2.2
"@grafana/e2e-selectors": 10.2.2
"@grafana/faro-web-sdk": 1.2.1
"@grafana/tsconfig": ^1.2.0-rc1
"@grafana/ui": 10.2.1
"@grafana/ui": 10.2.2
"@rollup/plugin-commonjs": 25.0.2
"@rollup/plugin-node-resolve": 15.2.3
"@testing-library/dom": 9.3.3
@@ -3302,7 +3302,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/schema@10.2.1, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
"@grafana/schema@10.2.2, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
version: 0.0.0-use.local
resolution: "@grafana/schema@workspace:packages/grafana-schema"
dependencies:
@@ -3336,17 +3336,17 @@ __metadata:
languageName: node
linkType: hard
"@grafana/ui@10.2.1, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
"@grafana/ui@10.2.2, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
version: 0.0.0-use.local
resolution: "@grafana/ui@workspace:packages/grafana-ui"
dependencies:
"@babel/core": 7.23.0
"@emotion/css": 11.11.2
"@emotion/react": 11.11.1
"@grafana/data": 10.2.1
"@grafana/e2e-selectors": 10.2.1
"@grafana/data": 10.2.2
"@grafana/e2e-selectors": 10.2.2
"@grafana/faro-web-sdk": 1.2.1
"@grafana/schema": 10.2.1
"@grafana/schema": 10.2.2
"@grafana/tsconfig": ^1.2.0-rc1
"@leeoniya/ufuzzy": 1.0.8
"@mdx-js/react": 1.6.22