Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
161e3cac50 | ||
|
|
f0b77df43d | ||
|
|
f8239ab814 | ||
|
|
9ad0edceb8 | ||
|
|
d996ce6ff8 | ||
|
|
574904291c | ||
|
|
bd8c0118ef | ||
|
|
12d569f8fd | ||
|
|
2bd48632f6 | ||
|
|
1a41311415 | ||
|
|
685bbda728 | ||
|
|
a36ead12c3 | ||
|
|
02b14cbd52 | ||
|
|
b2724eab43 | ||
|
|
11918a9518 |
@@ -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"]
|
||||
|
||||
@@ -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
CHANGELOG.md
36
CHANGELOG.md
@@ -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" %}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "10.2.1"
|
||||
"version": "10.2.2"
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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
public/app/features/admin/api.ts
Normal file
38
public/app/features/admin/api.ts
Normal 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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/**",
|
||||
],
|
||||
),
|
||||
|
||||
50
yarn.lock
50
yarn.lock
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user