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

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"]

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
...

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)

View File

@@ -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

View File

@@ -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

View File

@@ -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" %}}

View File

@@ -4,5 +4,5 @@
"packages": [
"packages/*"
],
"version": "10.2.1"
"version": "10.2.2"
}

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",

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",

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",

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",

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",

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",

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"
},

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",

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"

View File

@@ -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 {
/**

View File

@@ -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;

View File

@@ -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 {
/**

View File

@@ -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;

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 {
/**

View File

@@ -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 {
/**

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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');

View File

@@ -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

View File

@@ -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 {
/**

View File

@@ -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;

View File

@@ -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',

View File

@@ -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 {
/**

View File

@@ -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 {
/**

View File

@@ -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');

View File

@@ -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.

View File

@@ -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',

View File

@@ -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;

View File

@@ -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 {
/**

View File

@@ -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 {
/**

View File

@@ -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 {
/**

View File

@@ -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>;

View File

@@ -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',

View File

@@ -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;

View File

@@ -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

View File

@@ -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',

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",

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",

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

View File

@@ -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{}

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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',
}),
};
};

View File

@@ -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}
/>
);
};

View File

@@ -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}
/>
);
};

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),
}),
});

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 }) => (
<>

View File

@@ -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 (

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 });
};

View File

@@ -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,
})}
/>
);
}

View File

@@ -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) {

View File

@@ -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 }]);
});
});
});

View File

@@ -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}

View File

@@ -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

View File

@@ -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,

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[]> {

View File

@@ -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.');
}
);

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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 = {

View File

@@ -31,6 +31,7 @@ const setup = (propOverrides?: object) => {
page: 0,
hasFetched: false,
perPage: 10,
rolesLoading: false,
};
Object.assign(props, propOverrides);

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,
};
}

View File

@@ -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 }));
};
}

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;

View File

@@ -38,6 +38,7 @@ const setup = (propOverrides?: object) => {
changePage: mockToolkitActionCreator(pageChanged),
changeSort: mockToolkitActionCreator(sortChanged),
isLoading: false,
rolesLoading: false,
};
Object.assign(props, propOverrides);

View File

@@ -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}

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);
};

View File

@@ -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;

View File

@@ -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",

View File

@@ -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);
});
});
});
});

View File

@@ -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;
}

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[];
}

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 {

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;

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/**",
],
),

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