Merge branch 'v9.0.x' of github.com:grafana/grafana into v9.0.x

This commit is contained in:
eleijonmarck
2022-08-01 13:51:30 +02:00
62 changed files with 713 additions and 178 deletions
@@ -14,4 +14,7 @@ on:
jobs:
workflow-call:
uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.2
uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.6
with:
frontend-path-regexp: public\/app\/plugins\/datasource\/(grafana-azure-monitor-datasource|cloud-monitoring|cloudwatch)
backend-path-regexp: pkg\/tsdb\/(azuremonitor|cloudmonitoring|cloudwatch)
+23
View File
@@ -1,3 +1,26 @@
<!-- 9.0.5 START -->
# 9.0.5 (2022-07-26)
### Features and enhancements
- **Access control:** Show dashboard settings to users who can edit dashboard. [#52535](https://github.com/grafana/grafana/pull/52535), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Allow the webhook notifier to support a custom Authorization header. [#52515](https://github.com/grafana/grafana/pull/52515), [@gotjosh](https://github.com/gotjosh)
- **Chore:** Upgrade to Go version 1.17.12. [#52523](https://github.com/grafana/grafana/pull/52523), [@sakjur](https://github.com/sakjur)
- **Plugins:** Add signature wildcard globbing for dedicated private plugin type. [#52163](https://github.com/grafana/grafana/pull/52163), [@wbrowne](https://github.com/wbrowne)
- **Prometheus:** Don't show errors from unsuccessful API checks like rules or exemplar checks. [#52193](https://github.com/grafana/grafana/pull/52193), [@darrenjaneczek](https://github.com/darrenjaneczek)
### Bug fixes
- **Access control:** Allow organisation admins to add existing users to org (#51668). [#52553](https://github.com/grafana/grafana/pull/52553), [@vtorosyan](https://github.com/vtorosyan)
- **Alerting:** Fix alert panel instance-based rules filtering. [#52583](https://github.com/grafana/grafana/pull/52583), [@konrad147](https://github.com/konrad147)
- **Apps:** Fixes navigation between different app plugin pages. [#52571](https://github.com/grafana/grafana/pull/52571), [@torkelo](https://github.com/torkelo)
- **Cloudwatch:** Upgrade grafana-aws-sdk to fix auth issue with secret keys. [#52420](https://github.com/grafana/grafana/pull/52420), [@sarahzinger](https://github.com/sarahzinger)
- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52661](https://github.com/grafana/grafana/pull/52661), [@academo](https://github.com/academo)
- **Loki:** Fix `show context` not working in some occasions. [#52458](https://github.com/grafana/grafana/pull/52458), [@svennergr](https://github.com/svennergr)
- **RBAC:** Fix permissions on dashboards and folders created by anonymous users. [#52615](https://github.com/grafana/grafana/pull/52615), [@gamab](https://github.com/gamab)
<!-- 9.0.5 END -->
<!-- 9.0.4 START -->
# 9.0.4 (2022-07-20)
-1
View File
@@ -27,7 +27,6 @@ RUN apk add --no-cache gcc g++ make
WORKDIR /grafana
COPY go.mod go.sum embed.go Makefile build.go package.json ./
COPY cue cue
COPY packages/grafana-schema packages/grafana-schema
COPY public/app/plugins public/app/plugins
COPY public/api-spec.json public/api-spec.json
-1
View File
@@ -29,7 +29,6 @@ COPY go.mod go.sum embed.go ./
COPY Makefile build.go package.json ./
COPY .bingo .bingo
COPY pkg pkg/
COPY cue cue/
COPY cue.mod cue.mod/
COPY packages/grafana-schema packages/grafana-schema/
COPY public/app/plugins public/app/plugins/
+2 -2
View File
@@ -842,8 +842,8 @@ max_attempts = 3
min_interval = 10s
[unified_alerting.screenshots]
# Enable screenshots in notifications. This option requires a remote HTTP image rendering service. Please
# see [rendering] for further configuration options.
# Enable screenshots in notifications. This option requires the Grafana Image Renderer plugin.
# For more information on configuration options, refer to [rendering].
capture = false
# The maximum number of screenshots that can be taken at the same time. This option is different from
@@ -77,7 +77,7 @@ The following list contains role-based access control actions.
| `licensing:read` | n/a | Read licensing information. |
| `licensing:write` | n/a | Update the license token. |
| `org.users:write` | `users:*` <br> `users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of a user. |
| `org.users:add` | `users:*` | Add a user to an organization. |
| `org.users:add` | `users:*` | Add a user to an organization or invite a new user to an organization. |
| `org.users:read` | `users:*` <br> `users:id:*` | Get user profiles within an organization. |
| `org.users:remove` | `users:*` <br> `users:id:*` | Remove a user from an organization. |
| `org:create` | n/a | Create an organization. |
@@ -60,7 +60,7 @@ The following tables list permissions associated with basic and fixed roles.
| `fixed:licensing:reader` | `licensing:read`<br>`licensing.reports:read` | Read licensing information and licensing reports. |
| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and <br>`licensing:write`<br>`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. |
| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. |
| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and <br>`org.users:add`<br>`org.users:remove`<br>`org.users:write` | Within a single organization, add a user, invite a user, read information about a user and their role, remove a user from that organization, or change the role of a user. |
| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and <br>`org.users:add`<br>`org.users:remove`<br>`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. |
| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs:create`<br>`orgs:delete`<br>`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. |
| `fixed:organization:reader` | `orgs:read`<br>`orgs.quotas:read` | Read an organization and its quotas. |
| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs.preferences:read`<br>`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. |
@@ -15,7 +15,7 @@ weight: 400
# Create a Grafana managed alerting rule
Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}}
@@ -17,7 +17,7 @@ weight: 400
# Create a Grafana Mimir or Loki managed alerting rule
Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
## Before you begin
@@ -23,7 +23,7 @@ These are the data sources that are compatible with and supported by Grafana Ale
- [Graphite]({{< relref "../../datasources/graphite/" >}})
- [InfluxDB]({{< relref "../../datasources/influxdb/" >}})
- [Loki]({{< relref "../../datasources/loki/" >}})
- ]Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}})
- [Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}})
- [MySQL]({{< relref "../../datasources/mysql/" >}})
- [Open TSDB]({{< relref "../../datasources/opentsdb/" >}})
- [PostgreSQL]({{< relref "../../datasources/postgres/" >}})
@@ -34,8 +34,8 @@ To use images in notifications, Grafana must be set up to use [image rendering](
If Grafana has been set up to use [image rendering]({{< relref "../setup-grafana/image-rendering/" >}}) images in notifications can be turned on via the `capture` option in `[unified_alerting.screenshots]`:
# Enable screenshots in notifications. This option requires a remote HTTP image rendering service. Please
# see [rendering] for further configuration options.
# Enable screenshots in notifications. This option requires the Grafana Image Renderer plugin.
# For more information on configuration options, refer to [rendering].
capture = true
It is recommended that `max_concurrent_screenshots` is set to a value that is less than or equal to `concurrent_render_request_limit`. The default value for both `max_concurrent_screenshots` and `concurrent_render_request_limit` is `5`:
@@ -30,6 +30,7 @@ Read more about the available dimensions in the [CloudWatch Metrics and Dimensio
| `EC2 Instance Attributes` | Returns a list of attributes matching the specified `region`, `attribute_name`, and `filters`. |
| `Resource ARNs` | Returns a list of ARNs matching the specified `region`, `resource_type` and `tags`. |
| `Statistics` | Returns a list of all the standard statistics. |
| `LogGroups` | Returns a list of all log groups matching the specified `region`. |
For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
+1 -1
View File
@@ -471,7 +471,7 @@ Content-Type: application/json
`POST /api/admin/pause-all-alerts`
> **Note:** This API is relevant for the [legacy dashboard alerts]({{< relref "../../old-alerting/" >}}) only. For default alerting, use [silences]({{< relref "../../alerting/silences/" >}}) to stop alerts from being delivered.
> **Note:** This API is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/v8.5/alerting/old-alerting/) only. For default alerting, use [silences]({{< relref "../../alerting/silences/" >}}) to stop alerts from being delivered.
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+1 -1
View File
@@ -15,7 +15,7 @@ title: 'Alerting HTTP API '
# Alerting API
> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< relref "../../old-alerting/" >}}) only.
> **Note:** This topic is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/v8.5/alerting/old-alerting/) only.
You can find Grafana alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts.
@@ -185,7 +185,7 @@ Content-Type: application/json
## Create notification channel
You can find the full list of [supported notifiers]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/notifications/" >}}) on the alert notifiers page.
You can find the full list of [supported notifiers](https://grafana.com/docs/grafana/v8.5/alerting/old-alerting/notifications/) on the alert notifiers page.
`POST /api/alert-notifications`
@@ -27,9 +27,15 @@ Grafana can be installed on many different operating systems. For a list of the
To sign in to Grafana for the first time:
1. Open your web browser and go to http://localhost:3000/. The default HTTP port that Grafana listens to is `3000` unless you have configured a different port.
1. On the signin page, enter `admin` for username and password.
1. Click **Sign in**. If successful, you will see a prompt to change the password.
1. Open your web browser and go to http://localhost:3000/.
The default HTTP port that Grafana listens to is `3000` unless you have configured a different port.
1. On the sign-in page, enter `admin` for the username and password.
1. Click **Sign in**.
If successful, you will see a prompt to change the password.
1. Click **OK** on the prompt and change your password.
> **Note:** We strongly recommend that you change the default administrator password.
@@ -41,7 +47,10 @@ To create your first dashboard:
1. Click the **New dashboard** item under the **Dashboards** icon in the side menu.
1. On the dashboard, click **Add an empty panel**.
1. In the New dashboard/Edit panel view, go to the **Query** tab.
1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query/" >}}) by selecting `-- Grafana --` from the data source selector. This generates the Random Walk dashboard.
1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query/" >}}) by selecting `-- Grafana --` from the data source selector.
This generates the Random Walk dashboard.
1. Click the **Save** icon in the top right corner of your screen to save the dashboard.
1. Add a descriptive name, and then click **Save**.
@@ -19,7 +19,7 @@ You can place any panel in any location you want and controls its size. The chan
1. Hover your cursor over the panel, and click-and-drag the panel to its new location.
1. To resize a penal, click the zoom in (+) and zoom out (-) icons.
1. To resize a panel, click the zoom in (+) and zoom out (-) icons.
![](/static/img/docs/animated_gifs/drag_drop.gif)
+1
View File
@@ -10,6 +10,7 @@ weight: 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 9.0.5]({{< relref "release-notes-9-0-5" >}})
- [Release notes for 9.0.4]({{< relref "release-notes-9-0-4" >}})
- [Release notes for 9.0.3]({{< relref "release-notes-9-0-3" >}})
- [Release notes for 9.0.2]({{< relref "release-notes-9-0-2" >}})
@@ -0,0 +1,26 @@
+++
title = "Release notes for Grafana 9.0.5"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 9.0.5
### Features and enhancements
- **Access control:** Show dashboard settings to users who can edit dashboard. [#52535](https://github.com/grafana/grafana/pull/52535), [@grafanabot](https://github.com/grafanabot)
- **Alerting:** Allow the webhook notifier to support a custom Authorization header. [#52515](https://github.com/grafana/grafana/pull/52515), [@gotjosh](https://github.com/gotjosh)
- **Chore:** Upgrade to Go version 1.17.12. [#52523](https://github.com/grafana/grafana/pull/52523), [@sakjur](https://github.com/sakjur)
- **Plugins:** Add signature wildcard globbing for dedicated private plugin type. [#52163](https://github.com/grafana/grafana/pull/52163), [@wbrowne](https://github.com/wbrowne)
- **Prometheus:** Don't show errors from unsuccessful API checks like rules or exemplar checks. [#52193](https://github.com/grafana/grafana/pull/52193), [@darrenjaneczek](https://github.com/darrenjaneczek)
### Bug fixes
- **Access control:** Allow organisation admins to add existing users to org (#51668). [#52553](https://github.com/grafana/grafana/pull/52553), [@vtorosyan](https://github.com/vtorosyan)
- **Alerting:** Fix alert panel instance-based rules filtering. [#52583](https://github.com/grafana/grafana/pull/52583), [@konrad147](https://github.com/konrad147)
- **Apps:** Fixes navigation between different app plugin pages. [#52571](https://github.com/grafana/grafana/pull/52571), [@torkelo](https://github.com/torkelo)
- **Cloudwatch:** Upgrade grafana-aws-sdk to fix auth issue with secret keys. [#52420](https://github.com/grafana/grafana/pull/52420), [@sarahzinger](https://github.com/sarahzinger)
- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52661](https://github.com/grafana/grafana/pull/52661), [@academo](https://github.com/academo)
- **Loki:** Fix `show context` not working in some occasions. [#52458](https://github.com/grafana/grafana/pull/52458), [@svennergr](https://github.com/svennergr)
- **RBAC:** Fix permissions on dashboards and folders created by anonymous users. [#52615](https://github.com/grafana/grafana/pull/52615), [@gamab](https://github.com/gamab)
@@ -675,7 +675,8 @@ Default is `false`.
Set to `true` to automatically add new users to the main organization
(id 1). When set to `false`, new users automatically cause a new
organization to be created for that new user. Default is `true`.
organization to be created for that new user. The organization will be
created even if the `allow_org_create` setting is set to `false`. Default is `true`.
### auto_assign_org_id
@@ -6,4 +6,4 @@ title: Stack series link
## Axis
For full instructions, refer to [Change axis display]({{< relref "../time-series/change-axis-display/" >}}).
For full instructions, refer to [Change axis display]({{< relref "../../visualizations/time-series/change-axis-display/" >}}).
@@ -6,4 +6,4 @@ title: Stack series link
### Stack series
For full instructions, refer to [Graph stacked time series]({{< relref "../time-series/graph-time-series-stacking/" >}}).
For full instructions, refer to [Graph stacked time series]({{< relref "../../visualizations/time-series/graph-time-series-stacking/" >}}).
+1 -1
View File
@@ -74,7 +74,7 @@ You can now dynamically apply value filters to any table column. This option can
{{< figure src="/static/img/docs/v72/table_column_filters.png" max-width="800px" caption="Table column filters" >}}
[Filter table columns]({{< relref "../visualizations/table/filter-table-columns/" >}}) has been added as a result of this feature.
[Filter table columns]({{< relref "../visualizations/table/#filter-table-columns" >}}) has been added as a result of this feature.
### New field override selection options
@@ -114,8 +114,6 @@ You can now provide detailed information to alert notification recipients by inj
{{< figure src="/static/img/docs/alerting/alert-notification-template-7-4.png" max-width="700px" caption="Variable support in alert notifications" >}}
For more information, refer to the [alert notification docs]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/add-notification-template/" >}}).
### Content security policy support
We have added support for [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), a layer of security that helps detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks.
+1 -1
View File
@@ -4,5 +4,5 @@
"packages": [
"packages/*"
],
"version": "9.0.4"
"version": "9.0.5"
}
+2 -2
View File
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "9.0.4",
"version": "9.0.5",
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
@@ -330,7 +330,7 @@
"logfmt": "^1.3.2",
"lru-cache": "7.10.1",
"memoize-one": "6.0.0",
"moment": "2.29.3",
"moment": "2.29.4",
"moment-timezone": "0.5.34",
"monaco-editor": "^0.31.1",
"monaco-promql": "1.7.4",
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "9.0.4",
"version": "9.0.5",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -22,14 +22,14 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.0",
"@grafana/schema": "9.0.4",
"@grafana/schema": "9.0.5",
"@types/d3-interpolate": "^1.4.0",
"d3-interpolate": "1.4.0",
"date-fns": "2.28.0",
"eventemitter3": "4.0.7",
"lodash": "4.17.21",
"marked": "4.0.16",
"moment": "2.29.3",
"moment": "2.29.4",
"moment-timezone": "0.5.34",
"ol": "6.14.1",
"papaparse": "5.3.2",
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "9.0.4",
"version": "9.0.5",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "9.0.4",
"version": "9.0.5",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -48,7 +48,7 @@
"@babel/core": "7.17.8",
"@babel/preset-env": "7.17.10",
"@cypress/webpack-preprocessor": "5.11.1",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/e2e-selectors": "9.0.5",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "8.2.5",
+4 -4
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "9.0.4",
"version": "9.0.5",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -22,9 +22,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "9.0.4",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/ui": "9.0.4",
"@grafana/data": "9.0.5",
"@grafana/e2e-selectors": "9.0.5",
"@grafana/ui": "9.0.5",
"@sentry/browser": "6.19.7",
"history": "4.10.1",
"lodash": "4.17.21",
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "9.0.4",
"version": "9.0.5",
"description": "Grafana Schema Library",
"keywords": [
"typescript"
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "9.0.4",
"version": "9.0.5",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -38,10 +38,10 @@
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@grafana/data": "9.0.4",
"@grafana/data": "9.0.5",
"@grafana/eslint-config": "^4.0.0",
"@grafana/tsconfig": "^1.2.0-rc1",
"@grafana/ui": "9.0.4",
"@grafana/ui": "9.0.5",
"@jest/core": "27.5.1",
"@types/command-exists": "^1.2.0",
"@types/eslint": "8.4.1",
+5 -5
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "9.0.4",
"version": "9.0.5",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -33,9 +33,9 @@
"@emotion/css": "11.9.0",
"@emotion/react": "11.9.0",
"@grafana/aws-sdk": "0.0.36",
"@grafana/data": "9.0.4",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/schema": "9.0.4",
"@grafana/data": "9.0.5",
"@grafana/e2e-selectors": "9.0.5",
"@grafana/schema": "9.0.5",
"@grafana/slate-react": "0.22.10-grafana",
"@monaco-editor/react": "4.3.1",
"@popperjs/core": "2.11.5",
@@ -58,7 +58,7 @@
"jquery": "3.6.0",
"lodash": "4.17.21",
"memoize-one": "6.0.0",
"moment": "2.29.3",
"moment": "2.29.4",
"monaco-editor": "^0.31.1",
"ol": "6.14.1",
"prismjs": "1.28.0",
@@ -67,7 +67,7 @@ export const CardContainer = ({
};
export const getCardContainerStyles = stylesFactory(
(theme: GrafanaTheme2, disabled = false, disableHover = false, isSelected = false) => {
(theme: GrafanaTheme2, disabled = false, disableHover = false, isSelected) => {
const isSelectable = isSelected !== undefined;
return {
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@jaegertracing/jaeger-ui-components",
"version": "9.0.4",
"version": "9.0.5",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -28,10 +28,10 @@
},
"dependencies": {
"@emotion/css": "11.9.0",
"@grafana/data": "9.0.4",
"@grafana/e2e-selectors": "9.0.4",
"@grafana/runtime": "9.0.4",
"@grafana/ui": "9.0.4",
"@grafana/data": "9.0.5",
"@grafana/e2e-selectors": "9.0.5",
"@grafana/runtime": "9.0.5",
"@grafana/ui": "9.0.5",
"chance": "^1.0.10",
"classnames": "^2.2.5",
"combokeys": "^3.0.0",
@@ -43,7 +43,7 @@
"lodash": "4.17.21",
"lru-memoize": "^1.1.0",
"memoize-one": "6.0.0",
"moment": "2.29.3",
"moment": "2.29.4",
"moment-timezone": "0.5.34",
"prop-types": "15.8.1",
"react": "17.0.2",
-7
View File
@@ -443,13 +443,6 @@ var orgsCreateAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ActionOrgsCreate),
)
// usersInviteEvaluator is used to protect the "Configuration > Users > Invite" page access
// accessible to org admins and server admins by default
var usersInviteEvaluator = ac.EvalAny(
ac.EvalPermission(ac.ActionUsersCreate),
ac.EvalPermission(ac.ActionOrgUsersAdd),
)
// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access
// grants access to a user when they can either create teams or can read and update a team
var teamsAccessEvaluator = ac.EvalAny(
+4 -4
View File
@@ -59,7 +59,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/datasources/edit/*", authorize(reqOrgAdmin, datasources.EditPageAccess), hs.Index)
r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index)
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, usersInviteEvaluator), hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), hs.Index)
r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index)
r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
@@ -238,9 +238,9 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUserForCurrentOrg))
// invites
orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.GetPendingOrgInvites))
orgRoute.Post("/invites", authorize(reqOrgAdmin, usersInviteEvaluator), quota("user"), routing.Wrap(hs.AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.RevokeInvite))
orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.GetPendingOrgInvites))
orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), quota("user"), routing.Wrap(hs.AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.RevokeInvite))
// prefs
orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead)), routing.Wrap(hs.GetOrgPreferences))
+2 -1
View File
@@ -235,7 +235,8 @@ func (hs *HTTPServer) DeleteDataSourceByName(c *models.ReqContext) response.Resp
func validateURL(cmdType string, url string) response.Response {
if _, err := datasource.ValidateURL(cmdType, url); err != nil {
return response.Error(400, fmt.Sprintf("Validation error, invalid URL: %q", url), err)
datasourcesLogger.Error("Failed to validate URL", "url", url)
return response.Error(http.StatusBadRequest, "Validation error, invalid URL", err)
}
return nil
+4 -9
View File
@@ -40,6 +40,9 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response {
if !inviteDto.Role.IsValid() {
return response.Error(400, "Invalid role specified", nil)
}
if !c.OrgRole.Includes(inviteDto.Role) && !c.IsGrafanaAdmin {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
// first try get existing user
userQuery := models.GetUserByLoginQuery{LoginOrEmail: inviteDto.LoginOrEmail}
@@ -60,15 +63,6 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response {
return hs.inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
}
// Evaluate permissions for inviting a new user to Grafana
hasAccess, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ac.ActionUsersCreate))
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to evaluate permissions", err)
}
if !hasAccess {
return response.Error(http.StatusForbidden, "Permission denied: not permitted to create a new user", err)
}
if setting.DisableLoginForm {
return response.Error(400, "Cannot invite when login is disabled.", nil)
}
@@ -79,6 +73,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response {
cmd.Name = inviteDto.Name
cmd.Status = models.TmpUserInvitePending
cmd.InvitedByUserId = c.UserId
var err error
cmd.Code, err = util.GetRandomString(30)
if err != nil {
return response.Error(500, "Could not generate random string", err)
+4 -12
View File
@@ -23,7 +23,7 @@ func TestOrgInvitesAPIEndpointAccess(t *testing.T) {
tests := []accessControlTestCase2{
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can invite and existing user to his org",
desc: "org viewer with the correct permissions can invite an existing user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
@@ -31,7 +31,7 @@ func TestOrgInvitesAPIEndpointAccess(t *testing.T) {
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with missing permissions cannot invite and existing user to his org",
desc: "org viewer with missing permissions cannot invite an existing user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{},
@@ -39,26 +39,18 @@ func TestOrgInvitesAPIEndpointAccess(t *testing.T) {
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the wrong scope cannot invite and existing user to his org",
desc: "org viewer with the wrong scope cannot invite an existing user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: "users:id:100"}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with user add permission cannot invite a new user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can invite a new user to his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionUsersCreate}},
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
+15 -9
View File
@@ -21,7 +21,7 @@ func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *models.ReqContext) response.Resp
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgId = c.OrgId
return hs.addOrgUserHelper(c.Req.Context(), cmd)
return hs.addOrgUserHelper(c, cmd)
}
// POST /api/orgs/:orgId/users
@@ -36,16 +36,19 @@ func (hs *HTTPServer) AddOrgUser(c *models.ReqContext) response.Response {
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
return hs.addOrgUserHelper(c.Req.Context(), cmd)
return hs.addOrgUserHelper(c, cmd)
}
func (hs *HTTPServer) addOrgUserHelper(ctx context.Context, cmd models.AddOrgUserCommand) response.Response {
func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd models.AddOrgUserCommand) response.Response {
if !cmd.Role.IsValid() {
return response.Error(400, "Invalid role specified", nil)
}
if !c.OrgRole.Includes(cmd.Role) && !c.IsGrafanaAdmin {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
userQuery := models.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
err := hs.SQLStore.GetUserByLogin(ctx, &userQuery)
err := hs.SQLStore.GetUserByLogin(c.Req.Context(), &userQuery)
if err != nil {
return response.Error(404, "User not found", nil)
}
@@ -54,7 +57,7 @@ func (hs *HTTPServer) addOrgUserHelper(ctx context.Context, cmd models.AddOrgUse
cmd.UserId = userToAdd.Id
if err := hs.SQLStore.AddOrgUser(ctx, &cmd); err != nil {
if err := hs.SQLStore.AddOrgUser(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrOrgUserAlreadyAdded) {
return response.JSON(409, util.DynMap{
"message": "User is already member of this organization",
@@ -217,7 +220,7 @@ func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *models.ReqContext) response.
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
return hs.updateOrgUserHelper(c.Req.Context(), cmd)
return hs.updateOrgUserHelper(c, cmd)
}
// PATCH /api/orgs/:orgId/users/:userId
@@ -235,14 +238,17 @@ func (hs *HTTPServer) UpdateOrgUser(c *models.ReqContext) response.Response {
if err != nil {
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
return hs.updateOrgUserHelper(c.Req.Context(), cmd)
return hs.updateOrgUserHelper(c, cmd)
}
func (hs *HTTPServer) updateOrgUserHelper(ctx context.Context, cmd models.UpdateOrgUserCommand) response.Response {
func (hs *HTTPServer) updateOrgUserHelper(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response {
if !cmd.Role.IsValid() {
return response.Error(400, "Invalid role specified", nil)
}
if err := hs.SQLStore.UpdateOrgUser(ctx, &cmd); err != nil {
if !c.OrgRole.Includes(cmd.Role) && !c.IsGrafanaAdmin {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
if err := hs.SQLStore.UpdateOrgUser(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, models.ErrLastOrgAdmin) {
return response.Error(400, "Cannot change role so that there is no organization admin left", nil)
}
+106
View File
@@ -570,6 +570,112 @@ func TestPostOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
}
}
func TestOrgUsersAPIEndpointWithSetPerms_AccessControl(t *testing.T) {
type accessControlTestCase2 struct {
expectedCode int
desc string
url string
method string
permissions []*accesscontrol.Permission
input string
}
tests := []accessControlTestCase2{
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can add a user as a viewer to his org",
url: "/api/org/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot add a user as an editor to his org",
url: "/api/org/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can add a user as a viewer to his org",
url: "/api/orgs/1/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot add a user as an editor to his org",
url: "/api/orgs/1/users",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/org/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/org/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/orgs/1/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot update a user's role to a viewer in his org",
url: fmt.Sprintf("/api/orgs/1/users/%d", testEditorOrg1.UserId),
method: http.MethodPatch,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: accesscontrol.ScopeUsersAll}},
input: `{"role": "` + string(models.ROLE_EDITOR) + `"}`,
},
{
expectedCode: http.StatusOK,
desc: "org viewer with the correct permissions can invite a user as a viewer in his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
input: `{"loginOrEmail": "newUserEmail@test.com", "sendEmail": false, "role": "` + string(models.ROLE_VIEWER) + `"}`,
},
{
expectedCode: http.StatusForbidden,
desc: "org viewer with the correct permissions cannot invite a user as an editor in his org",
url: "/api/org/invites",
method: http.MethodPost,
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionUsersCreate}},
input: `{"loginOrEmail": "newUserEmail@test.com", "sendEmail": false, "role": "` + string(models.ROLE_EDITOR) + `"}`,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
sc := setupHTTPServer(t, true, true)
setInitCtxSignedInViewer(sc.initCtx)
setupOrgUsersDBForAccessControlTests(t, sc.db)
setAccessControlPermissions(sc.acmock, test.permissions, sc.initCtx.OrgId)
input := strings.NewReader(test.input)
response := callAPI(sc.server, test.method, test.url, input, t)
assert.Equal(t, test.expectedCode, response.Code)
})
}
}
func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
url := "/api/orgs/%v/users/%v"
type testCase struct {
+53 -10
View File
@@ -16,6 +16,7 @@ import (
"strings"
"github.com/gobwas/glob"
// TODO: replace deprecated `golang.org/x/crypto` package https://github.com/grafana/grafana/issues/46050
// nolint:staticcheck
"golang.org/x/crypto/openpgp"
@@ -88,15 +89,8 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err)
}
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText))
if err != nil {
return nil, fmt.Errorf("%v: %w", "failed to parse public key", err)
}
if _, err := openpgp.CheckDetachedSignature(keyring,
bytes.NewBuffer(block.Bytes),
block.ArmoredSignature.Body); err != nil {
return nil, fmt.Errorf("%v: %w", "failed to check signature", err)
if err = validateManifest(manifest, block); err != nil {
return nil, err
}
return &manifest, nil
@@ -132,7 +126,7 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro
manifest, err := readPluginManifest(byteValue)
if err != nil {
mlog.Debug("Plugin signature invalid", "id", plugin.ID)
mlog.Debug("Plugin signature invalid", "id", plugin.ID, "err", err)
return plugins.Signature{
Status: plugins.SignatureInvalid,
}, nil
@@ -314,3 +308,52 @@ func urlMatch(specs []string, target string, signatureType plugins.SignatureType
}
return false, nil
}
type invalidFieldErr struct {
field string
}
func (r invalidFieldErr) Error() string {
return fmt.Sprintf("valid manifest field %s is required", r.field)
}
func validateManifest(m pluginManifest, block *clearsign.Block) error {
if len(m.Plugin) == 0 {
return invalidFieldErr{field: "plugin"}
}
if len(m.Version) == 0 {
return invalidFieldErr{field: "version"}
}
if len(m.KeyID) == 0 {
return invalidFieldErr{field: "keyId"}
}
if m.Time == 0 {
return invalidFieldErr{field: "time"}
}
if len(m.Files) == 0 {
return invalidFieldErr{field: "files"}
}
if m.isV2() {
if len(m.SignedByOrg) == 0 {
return invalidFieldErr{field: "signedByOrg"}
}
if len(m.SignedByOrgName) == 0 {
return invalidFieldErr{field: "signedByOrgName"}
}
if !m.SignatureType.IsValid() {
return fmt.Errorf("%s is not a valid signature type", m.SignatureType)
}
}
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText))
if err != nil {
return fmt.Errorf("%v: %w", "failed to parse public key", err)
}
if _, err = openpgp.CheckDetachedSignature(keyring,
bytes.NewBuffer(block.Bytes),
block.ArmoredSignature.Body); err != nil {
return fmt.Errorf("%v: %w", "failed to check signature", err)
}
return nil
}
@@ -393,3 +393,87 @@ func Test_urlMatch_private(t *testing.T) {
})
}
}
func Test_validateManifest(t *testing.T) {
tcs := []struct {
name string
manifest *pluginManifest
expectedErr string
}{
{
name: "Empty plugin field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Plugin = "" }),
expectedErr: "valid manifest field plugin is required",
},
{
name: "Empty keyId field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.KeyID = "" }),
expectedErr: "valid manifest field keyId is required",
},
{
name: "Empty signedByOrg field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignedByOrg = "" }),
expectedErr: "valid manifest field signedByOrg is required",
},
{
name: "Empty signedByOrgName field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignedByOrgName = "" }),
expectedErr: "valid manifest field SignedByOrgName is required",
},
{
name: "Empty signatureType field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignatureType = "" }),
expectedErr: "valid manifest field signatureType is required",
},
{
name: "Invalid signatureType field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignatureType = "invalidSignatureType" }),
expectedErr: "valid manifest field signatureType is required",
},
{
name: "Empty files field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Files = map[string]string{} }),
expectedErr: "valid manifest field files is required",
},
{
name: "Empty time field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Time = 0 }),
expectedErr: "valid manifest field time is required",
},
{
name: "Empty version field",
manifest: createV2Manifest(t, func(m *pluginManifest) { m.Version = "" }),
expectedErr: "valid manifest field version is required",
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
err := validateManifest(*tc.manifest, nil)
require.Errorf(t, err, tc.expectedErr)
})
}
}
func createV2Manifest(t *testing.T, cbs ...func(*pluginManifest)) *pluginManifest {
t.Helper()
m := &pluginManifest{
Plugin: "grafana-test-app",
Version: "2.5.3",
KeyID: "7e4d0c6a708866e7",
Time: 1586817677115,
Files: map[string]string{
"plugin.json": "55556b845e91935cc48fae3aa67baf0f22694c3f",
},
ManifestVersion: "2.0.0",
SignatureType: plugins.GrafanaSignature,
SignedByOrg: "grafana",
SignedByOrgName: "grafana",
}
for _, cb := range cbs {
cb(m)
}
return m
}
+10
View File
@@ -177,10 +177,20 @@ type SignatureType string
const (
GrafanaSignature SignatureType = "grafana"
CommercialSignature SignatureType = "commercial"
CommunitySignature SignatureType = "community"
PrivateSignature SignatureType = "private"
PrivateGlobSignature SignatureType = "private-glob"
)
func (s SignatureType) IsValid() bool {
switch s {
case GrafanaSignature, CommercialSignature, CommunitySignature, PrivateSignature, PrivateGlobSignature:
return true
}
return false
}
type PluginFiles map[string]struct{}
type Signature struct {
+129
View File
@@ -13,6 +13,7 @@ import (
"github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/bus"
@@ -53,6 +54,17 @@ type SQLStore struct {
skipEnsureDefaultOrgAndUser bool
migrations registry.DatabaseMigrator
tracer tracing.Tracer
metrics struct {
maxOpenConnections prometheus.Gauge
openConnections prometheus.Gauge
inUse prometheus.Gauge
idle prometheus.Gauge
waitCount prometheus.Counter
waitDuration prometheus.Counter
maxIdleClosed prometheus.Counter
maxIdleTimeClosed prometheus.Counter
maxLifetimeClosed prometheus.Counter
}
}
func ProvideService(cfg *setting.Cfg, cacheService *localcache.CacheService, migrations registry.DatabaseMigrator, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) {
@@ -73,6 +85,11 @@ func ProvideService(cfg *setting.Cfg, cacheService *localcache.CacheService, mig
return nil, err
}
s.tracer = tracer
s.initMetrics()
prometheus.MustRegister(s)
return s, nil
}
@@ -425,6 +442,118 @@ func (ss *SQLStore) readConfig() error {
return nil
}
// initMetrics initializes the database connection metrics
func (ss *SQLStore) initMetrics() {
namespace := "grafana"
subsystem := "database"
ss.metrics.maxOpenConnections = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_max_open",
Help: "Maximum number of open connections to the database",
})
ss.metrics.openConnections = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_open",
Help: "The number of established connections both in use and idle",
})
ss.metrics.inUse = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_in_use",
Help: "The number of connections currently in use",
})
ss.metrics.idle = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_idle",
Help: "The number of idle connections",
})
ss.metrics.waitCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_wait_count_total",
Help: "The total number of connections waited for",
})
ss.metrics.waitDuration = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_wait_duration_seconds",
Help: "The total time blocked waiting for a new connection",
})
ss.metrics.maxIdleClosed = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_max_idle_closed_total",
Help: "The total number of connections closed due to SetMaxIdleConns",
})
ss.metrics.maxIdleTimeClosed = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_max_idle_closed_seconds",
Help: "The total number of connections closed due to SetConnMaxIdleTime",
})
ss.metrics.maxLifetimeClosed = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "conn_max_lifetime_closed_total",
Help: "The total number of connections closed due to SetConnMaxLifetime",
})
}
// collectDBStats instruments connections stats from the database.
func (ss *SQLStore) collectDBstats() {
dbstats := ss.engine.DB().Stats()
ss.metrics.maxOpenConnections.Set(float64(dbstats.MaxOpenConnections))
ss.metrics.openConnections.Set(float64(dbstats.MaxOpenConnections))
ss.metrics.inUse.Set(float64(dbstats.InUse))
ss.metrics.idle.Set(float64(dbstats.Idle))
ss.metrics.waitCount.Add(float64(dbstats.WaitCount))
ss.metrics.waitDuration.Add(float64(dbstats.WaitDuration / time.Second))
ss.metrics.maxIdleClosed.Add(float64(dbstats.MaxIdleClosed))
ss.metrics.maxIdleTimeClosed.Add(float64(dbstats.MaxIdleTimeClosed))
ss.metrics.maxLifetimeClosed.Add(float64(dbstats.MaxLifetimeClosed))
}
// Collect implements Prometheus.Collector.
func (ss *SQLStore) Collect(ch chan<- prometheus.Metric) {
ss.collectDBstats()
ss.metrics.maxOpenConnections.Collect(ch)
ss.metrics.openConnections.Collect(ch)
ss.metrics.inUse.Collect(ch)
ss.metrics.idle.Collect(ch)
ss.metrics.waitCount.Collect(ch)
ss.metrics.waitDuration.Collect(ch)
ss.metrics.maxIdleClosed.Collect(ch)
ss.metrics.maxIdleTimeClosed.Collect(ch)
ss.metrics.maxLifetimeClosed.Collect(ch)
}
// Describe implements Prometheus.Collector.
func (ss *SQLStore) Describe(ch chan<- *prometheus.Desc) {
ss.metrics.maxOpenConnections.Describe(ch)
ss.metrics.openConnections.Describe(ch)
ss.metrics.inUse.Describe(ch)
ss.metrics.idle.Describe(ch)
ss.metrics.waitCount.Describe(ch)
ss.metrics.waitDuration.Describe(ch)
ss.metrics.maxIdleClosed.Describe(ch)
ss.metrics.maxIdleTimeClosed.Describe(ch)
ss.metrics.maxLifetimeClosed.Describe(ch)
}
// ITestDB is an interface of arguments for testing db
type ITestDB interface {
Helper()
+9 -6
View File
@@ -73,6 +73,11 @@ const (
alertMaxAttempts = 8
alertPollPeriod = 1000 * time.Millisecond
logsQueryMode = "Logs"
// QueryTypes
annotationQuery = "annotationQuery"
logAction = "logAction"
timeSeriesQuery = "timeSeriesQuery"
)
var plog = log.New("tsdb.cloudwatch")
@@ -356,15 +361,13 @@ func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDa
return e.executeLogAlertQuery(ctx, req)
}
queryType := model.QueryType
var result *backend.QueryDataResponse
switch queryType {
case "annotationQuery":
switch model.QueryType {
case annotationQuery:
result, err = e.executeAnnotationQuery(req.PluginContext, model, q)
case "logAction":
case logAction:
result, err = e.executeLogActions(ctx, req)
case "timeSeriesQuery":
case timeSeriesQuery:
fallthrough
default:
result, err = e.executeTimeSeriesQuery(ctx, req)
+3 -3
View File
@@ -37,7 +37,7 @@ type QueryJson struct {
Statistic *string `json:"statistic,omitempty"`
Statistics []*string `json:"statistics,omitempty"`
TimezoneUTCOffset string `json:"timezoneUTCOffset,omitempty"`
QueryType string `json:"queryType,omitempty"`
QueryType string `json:"type,omitempty"`
Hide *bool `json:"hide,omitempty"`
Alias *string `json:"alias,omitempty"`
}
@@ -58,7 +58,7 @@ func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime
}
queryType := model.QueryType
if queryType != "timeSeriesQuery" && queryType != "" {
if queryType != timeSeriesQuery && queryType != "" {
continue
}
@@ -166,7 +166,7 @@ func parseRequestQuery(model QueryJson, refId string, startTime time.Time, endTi
Label: "",
MatchExact: true,
Statistic: "",
ReturnData: false,
ReturnData: true,
UsedExpression: "",
RefId: refId,
Id: model.Id,
@@ -11,6 +11,16 @@ import (
"github.com/stretchr/testify/require"
)
func TestQueryJSON(t *testing.T) {
jsonString := []byte(`{
"type": "timeSeriesQuery"
}`)
var res QueryJson
err := json.Unmarshal(jsonString, &res)
require.NoError(t, err)
assert.Equal(t, "timeSeriesQuery", res.QueryType)
}
func TestRequestParser(t *testing.T) {
t.Run("Query migration ", func(t *testing.T) {
t.Run("legacy statistics field is migrated", func(t *testing.T) {
@@ -306,6 +316,34 @@ func TestRequestParser(t *testing.T) {
})
})
t.Run("hide and returnData", func(t *testing.T) {
t.Run("default", func(t *testing.T) {
query := getBaseJsonQuery()
query.QueryType = "timeSeriesQuery"
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
require.NoError(t, err)
require.True(t, res.ReturnData)
})
t.Run("hide is true", func(t *testing.T) {
query := getBaseJsonQuery()
query.QueryType = "timeSeriesQuery"
true := true
query.Hide = &true
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
require.NoError(t, err)
require.False(t, res.ReturnData)
})
t.Run("hide is false", func(t *testing.T) {
query := getBaseJsonQuery()
query.QueryType = "timeSeriesQuery"
false := false
query.Hide = &false
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
require.NoError(t, err)
require.True(t, res.ReturnData)
})
})
t.Run("ID is the string `query` appended with refId if refId is a valid MetricData ID", func(t *testing.T) {
query := getBaseJsonQuery()
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
@@ -1,6 +1,6 @@
{
"name": "@grafana-plugins/input-datasource",
"version": "9.0.4",
"version": "9.0.5",
"description": "Input Datasource",
"private": true,
"repository": {
@@ -15,15 +15,15 @@
},
"author": "Grafana Labs",
"devDependencies": {
"@grafana/toolkit": "9.0.4",
"@grafana/toolkit": "9.0.5",
"@types/jest": "26.0.15",
"@types/lodash": "4.14.149",
"@types/react": "17.0.30",
"lodash": "4.17.21"
},
"dependencies": {
"@grafana/data": "9.0.4",
"@grafana/ui": "9.0.4",
"@grafana/data": "9.0.5",
"@grafana/ui": "9.0.5",
"jquery": "3.5.1",
"react": "17.0.1",
"react-dom": "17.0.1",
+1 -3
View File
@@ -37,9 +37,7 @@ export class UsersActionBar extends PureComponent<Props> {
{ label: 'Users', value: 'users' },
{ label: `Pending Invites (${pendingInvitesCount})`, value: 'invites' },
];
const canAddToOrg: boolean =
contextSrv.hasAccess(AccessControlAction.UsersCreate, canInvite) ||
contextSrv.hasAccess(AccessControlAction.OrgUsersAdd, canInvite);
const canAddToOrg: boolean = contextSrv.hasAccess(AccessControlAction.OrgUsersAdd, canInvite);
return (
<div className="page-action-bar" data-testid="users-action-bar">
@@ -81,7 +81,7 @@ describe('VariableEditor', () => {
render(<VariableQueryEditor {...props} />);
await waitFor(() => {
const querySelect = screen.queryByRole('combobox', { name: 'Query Type' });
const querySelect = screen.queryByRole('combobox', { name: 'Query type' });
expect(querySelect).toBeInTheDocument();
expect(screen.queryByText('Regions')).toBeInTheDocument();
// Should not render any fields besides Query Type
@@ -103,7 +103,7 @@ describe('VariableEditor', () => {
render(<VariableQueryEditor {...props} />);
await waitFor(() => {
const querySelect = screen.queryByRole('combobox', { name: 'Query Type' });
const querySelect = screen.queryByRole('combobox', { name: 'Query type' });
expect(querySelect).toBeInTheDocument();
expect(screen.queryByText('Metrics')).toBeInTheDocument();
const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
@@ -216,7 +216,7 @@ describe('VariableEditor', () => {
};
render(<VariableQueryEditor {...props} />);
const querySelect = screen.queryByLabelText('Query Type');
const querySelect = screen.queryByLabelText('Query type');
expect(querySelect).toBeInTheDocument();
expect(screen.queryByText('Dimension Values')).toBeInTheDocument();
const regionSelect = screen.getByRole('combobox', { name: 'Region' });
@@ -240,4 +240,21 @@ describe('VariableEditor', () => {
});
});
});
describe('LogGroups queryType is selected', () => {
it('should only render region and prefix', async () => {
const props = defaultProps;
props.query = {
...defaultQuery,
queryType: VariableQueryType.LogGroups,
};
render(<VariableQueryEditor {...props} />);
await waitFor(() => {
screen.getByLabelText('Log group prefix');
screen.getByLabelText('Region');
});
expect(screen.queryByLabelText('Namespace')).not.toBeInTheDocument();
});
});
});
@@ -25,6 +25,7 @@ const queryTypes: Array<{ value: string; label: string }> = [
{ value: VariableQueryType.EC2InstanceAttributes, label: 'EC2 Instance Attributes' },
{ value: VariableQueryType.ResourceArns, label: 'Resource ARNs' },
{ value: VariableQueryType.Statistics, label: 'Statistics' },
{ value: VariableQueryType.LogGroups, label: 'Log Groups' },
];
export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
@@ -88,6 +89,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
VariableQueryType.EBSVolumeIDs,
VariableQueryType.EC2InstanceAttributes,
VariableQueryType.ResourceArns,
VariableQueryType.LogGroups,
].includes(parsedQuery.queryType);
const hasNamespaceField = [
VariableQueryType.Metrics,
@@ -100,7 +102,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
value={parsedQuery.queryType}
options={queryTypes}
onChange={(value: VariableQueryType) => onQueryChange({ ...parsedQuery, queryType: value })}
label="Query Type"
label="Query type"
inputId={`variable-query-type-${query.refId}`}
/>
{hasRegionField && (
@@ -135,7 +137,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
value={dimensionKey || null}
options={dimensionKeys}
onChange={(value: string) => onQueryChange({ ...parsedQuery, dimensionKey: value })}
label="Dimension Key"
label="Dimension key"
inputId={`variable-query-dimension-key-${query.refId}`}
/>
<InlineField label="Dimensions" labelWidth={20} tooltip="Dimensions to filter the returned values on">
@@ -163,9 +165,8 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
<>
<VariableTextField
value={parsedQuery.attributeName}
placeholder="attribute name"
onBlur={(value: string) => onQueryChange({ ...parsedQuery, attributeName: value })}
label="Attribute Name"
label="Attribute name"
interactive={true}
tooltip={
<>
@@ -210,9 +211,8 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
<>
<VariableTextField
value={parsedQuery.resourceType}
placeholder="resource type"
onBlur={(value: string) => onQueryChange({ ...parsedQuery, resourceType: value })}
label="Resource Type"
label="Resource type"
/>
<InlineField label="Tags" labelWidth={20} tooltip="Tags to filter the returned values on.">
<MultiFilter
@@ -225,6 +225,13 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
</InlineField>
</>
)}
{parsedQuery.queryType === VariableQueryType.LogGroups && (
<VariableTextField
value={query.logGroupPrefix ?? ''}
onBlur={(value: string) => onQueryChange({ ...parsedQuery, logGroupPrefix: value })}
label="Log group prefix"
/>
)}
</>
);
};
@@ -6,9 +6,9 @@ const LABEL_WIDTH = 20;
interface VariableTextFieldProps {
onBlur: (value: string) => void;
placeholder: string;
value: string;
label: string;
placeholder?: string;
tooltip?: PopoverContent;
interactive?: boolean;
}
@@ -377,6 +377,7 @@ export enum VariableQueryType {
EC2InstanceAttributes = 'ec2InstanceAttributes',
ResourceArns = 'resourceARNs',
Statistics = 'statistics',
LogGroups = 'logGroups',
}
export interface OldVariableQuery extends DataQuery {
@@ -405,6 +406,7 @@ export interface VariableQuery extends DataQuery {
attributeName: string;
resourceType: string;
tags?: MultiFilters;
logGroupPrefix?: string;
}
export interface LegacyAnnotationQuery extends MetricStat, DataQuery {
@@ -19,6 +19,7 @@ ds.datasource.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a'
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]);
ds.datasource.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]);
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]);
ds.datasource.describeLogGroups = jest.fn().mockResolvedValue(['a', 'b']);
const getDimensionValues = jest.fn().mockResolvedValue([{ label: 'e', value: 'e' }]);
const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]);
const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]);
@@ -167,4 +168,14 @@ describe('variables', () => {
{ text: 'SampleCount', value: 'SampleCount', expandable: true },
]);
});
describe('log groups', () => {
it('should call describe log groups', async () => {
const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.LogGroups });
expect(result).toEqual([
{ text: 'a', value: 'a', expandable: true },
{ text: 'b', value: 'b', expandable: true },
]);
});
});
});
@@ -45,6 +45,8 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
return this.handleResourceARNsQuery(query);
case VariableQueryType.Statistics:
return this.handleStatisticsQuery();
case VariableQueryType.LogGroups:
return this.handleLogGroupsQuery(query);
}
} catch (error) {
console.error(`Could not run CloudWatchMetricFindQuery ${query}`, error);
@@ -52,6 +54,15 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
}
async handleLogGroupsQuery({ region, logGroupPrefix }: VariableQuery) {
const logGroups = await this.datasource.describeLogGroups({ region, logGroupNamePrefix: logGroupPrefix });
return logGroups.map((s) => ({
text: s,
value: s,
expandable: true,
}));
}
async handleRegionsQuery() {
const regions = await this.datasource.getRegions();
return regions.map((s: { label: string; value: string }) => ({
@@ -25,7 +25,7 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe
}>({});
const isMultiSelect = () => {
return item.op === operators[0].label;
return operators.find((op) => op.label === item.op)?.isMultiValue;
};
const getSelectOptionsFromString = (item?: string): string[] => {
@@ -124,8 +124,8 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe
}
const operators = [
{ label: '=~', value: '=~' },
{ label: '=', value: '=' },
{ label: '!=', value: '!=' },
{ label: '!~', value: '!~' },
{ label: '=~', value: '=~', isMultiValue: true },
{ label: '=', value: '=', isMultiValue: false },
{ label: '!=', value: '!=', isMultiValue: false },
{ label: '!~', value: '!~', isMultiValue: true },
];
@@ -31,6 +31,21 @@ describe('LabelFilters', () => {
expect(getAddButton()).toBeInTheDocument();
});
it('renders multiple values for regex selectors', async () => {
setup([
{ label: 'bar', op: '!~', value: 'baz|bat|bau' },
{ label: 'foo', op: '!~', value: 'fop|for|fos' },
]);
expect(screen.getByText(/bar/)).toBeInTheDocument();
expect(screen.getByText(/baz/)).toBeInTheDocument();
expect(screen.getByText(/bat/)).toBeInTheDocument();
expect(screen.getByText(/bau/)).toBeInTheDocument();
expect(screen.getByText(/foo/)).toBeInTheDocument();
expect(screen.getByText(/for/)).toBeInTheDocument();
expect(screen.getByText(/fos/)).toBeInTheDocument();
expect(getAddButton()).toBeInTheDocument();
});
it('adds new label', async () => {
const { onChange } = setup([{ label: 'foo', op: '=', value: 'bar' }]);
await userEvent.click(getAddButton());
@@ -19,6 +19,7 @@ import {
measureText,
PlotLegend,
Portal,
TooltipDisplayMode,
UPlotConfigBuilder,
UPLOT_AXIS_FONT_SIZE,
usePanelContext,
@@ -176,6 +177,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
rowIndex={datapointIdx}
columnIndex={seriesIdx}
sortOrder={options.tooltip.sort}
mode={options.tooltip.mode}
/>
</>
);
@@ -206,7 +208,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
};
// Color by value
let getColor: ((seriesIdx: number, valueIdx: number) => string) | undefined = undefined;
let getColor: ((seriesIdx: number, valueIdx: number, value: number) => string) | undefined = undefined;
let fillOpacity = 1;
@@ -215,7 +217,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
const disp = colorByField.display!;
fillOpacity = (colorByField.config.custom.fillOpacity ?? 100) / 100;
// gradientMode? ignore?
getColor = (seriesIdx: number, valueIdx: number) => disp(colorByField.values.get(valueIdx)).color!;
getColor = (seriesIdx: number, valueIdx: number, value: number) => disp(value).color!;
}
const prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
@@ -285,6 +287,10 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
});
}
if (options.tooltip.mode === TooltipDisplayMode.None) {
return null;
}
return (
<Portal>
{hover && coords && (
@@ -10,7 +10,7 @@ import {
GrafanaTheme2,
LinkModel,
} from '@grafana/data';
import { SortOrder } from '@grafana/schema';
import { SortOrder, TooltipDisplayMode } from '@grafana/schema';
import { LinkButton, useStyles2, VerticalGroup } from '@grafana/ui';
export interface Props {
@@ -18,9 +18,10 @@ export interface Props {
rowIndex?: number | null; // the hover row
columnIndex?: number | null; // the hover column
sortOrder?: SortOrder;
mode?: TooltipDisplayMode | null;
}
export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder }: Props) => {
export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode }: Props) => {
const styles = useStyles2(getStyles);
if (!data || rowIndex == null) {
@@ -60,12 +61,19 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder }: Props)
return (
<table className={styles.infoWrap}>
<tbody>
{displayValues.map((v, i) => (
<tr key={`${i}/${rowIndex}`} className={i === columnIndex ? styles.highlight : ''}>
<th>{v[0]}:</th>
<td>{v[2]}</td>
{(mode === TooltipDisplayMode.Multi || mode === null) &&
displayValues.map((v, i) => (
<tr key={`${i}/${rowIndex}`} className={i === columnIndex ? styles.highlight : ''}>
<th>{v[0]}:</th>
<td>{v[2]}</td>
</tr>
))}
{mode === TooltipDisplayMode.Single && columnIndex && (
<tr key={`${columnIndex}/${rowIndex}`}>
<th>{displayValues[columnIndex][0]}:</th>
<td>{displayValues[columnIndex][2]}</td>
</tr>
))}
)}
{links.length > 0 && (
<tr>
<td colSpan={2}>
+31 -31
View File
@@ -3791,9 +3791,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource"
dependencies:
"@grafana/data": 9.0.4
"@grafana/toolkit": 9.0.4
"@grafana/ui": 9.0.4
"@grafana/data": 9.0.5
"@grafana/toolkit": 9.0.5
"@grafana/ui": 9.0.5
"@types/jest": 26.0.15
"@types/lodash": 4.14.149
"@types/react": 17.0.30
@@ -3831,12 +3831,12 @@ __metadata:
languageName: node
linkType: hard
"@grafana/data@9.0.4, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data":
"@grafana/data@9.0.5, @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.0
"@grafana/schema": 9.0.4
"@grafana/schema": 9.0.5
"@grafana/tsconfig": ^1.2.0-rc1
"@rollup/plugin-commonjs": 22.0.0
"@rollup/plugin-json": 4.1.0
@@ -3867,7 +3867,7 @@ __metadata:
history: 4.10.1
lodash: 4.17.21
marked: 4.0.16
moment: 2.29.3
moment: 2.29.4
moment-timezone: 0.5.34
ol: 6.14.1
papaparse: 5.3.2
@@ -3889,7 +3889,7 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/e2e-selectors@9.0.4, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors":
"@grafana/e2e-selectors@9.0.5, @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:
@@ -3913,7 +3913,7 @@ __metadata:
"@babel/core": 7.17.8
"@babel/preset-env": 7.17.10
"@cypress/webpack-preprocessor": 5.11.1
"@grafana/e2e-selectors": 9.0.4
"@grafana/e2e-selectors": 9.0.5
"@grafana/tsconfig": ^1.2.0-rc1
"@mochajs/json-file-reporter": ^1.2.0
"@rollup/plugin-commonjs": 22.0.0
@@ -3998,14 +3998,14 @@ __metadata:
languageName: node
linkType: hard
"@grafana/runtime@9.0.4, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime":
"@grafana/runtime@9.0.5, @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": 9.0.4
"@grafana/e2e-selectors": 9.0.4
"@grafana/data": 9.0.5
"@grafana/e2e-selectors": 9.0.5
"@grafana/tsconfig": ^1.2.0-rc1
"@grafana/ui": 9.0.4
"@grafana/ui": 9.0.5
"@rollup/plugin-commonjs": 22.0.0
"@rollup/plugin-node-resolve": 13.3.0
"@sentry/browser": 6.19.7
@@ -4034,7 +4034,7 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/schema@9.0.4, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
"@grafana/schema@9.0.5, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
version: 0.0.0-use.local
resolution: "@grafana/schema@workspace:packages/grafana-schema"
dependencies:
@@ -4081,7 +4081,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/toolkit@9.0.4, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit":
"@grafana/toolkit@9.0.5, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit":
version: 0.0.0-use.local
resolution: "@grafana/toolkit@workspace:packages/grafana-toolkit"
dependencies:
@@ -4097,10 +4097,10 @@ __metadata:
"@babel/preset-env": ^7.16.11
"@babel/preset-react": ^7.16.7
"@babel/preset-typescript": ^7.16.7
"@grafana/data": 9.0.4
"@grafana/data": 9.0.5
"@grafana/eslint-config": ^4.0.0
"@grafana/tsconfig": ^1.2.0-rc1
"@grafana/ui": 9.0.4
"@grafana/ui": 9.0.5
"@jest/core": 27.5.1
"@types/command-exists": ^1.2.0
"@types/eslint": 8.4.1
@@ -4184,7 +4184,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/ui@9.0.4, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
"@grafana/ui@9.0.5, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
version: 0.0.0-use.local
resolution: "@grafana/ui@workspace:packages/grafana-ui"
dependencies:
@@ -4192,9 +4192,9 @@ __metadata:
"@emotion/css": 11.9.0
"@emotion/react": 11.9.0
"@grafana/aws-sdk": 0.0.36
"@grafana/data": 9.0.4
"@grafana/e2e-selectors": 9.0.4
"@grafana/schema": 9.0.4
"@grafana/data": 9.0.5
"@grafana/e2e-selectors": 9.0.5
"@grafana/schema": 9.0.5
"@grafana/slate-react": 0.22.10-grafana
"@grafana/tsconfig": ^1.2.0-rc1
"@mdx-js/react": 1.6.22
@@ -4281,7 +4281,7 @@ __metadata:
lodash: 4.17.21
memoize-one: 6.0.0
mock-raf: 1.0.1
moment: 2.29.3
moment: 2.29.4
monaco-editor: ^0.31.1
ol: 6.14.1
postcss: 8.4.14
@@ -4431,11 +4431,11 @@ __metadata:
resolution: "@jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components"
dependencies:
"@emotion/css": 11.9.0
"@grafana/data": 9.0.4
"@grafana/e2e-selectors": 9.0.4
"@grafana/runtime": 9.0.4
"@grafana/data": 9.0.5
"@grafana/e2e-selectors": 9.0.5
"@grafana/runtime": 9.0.5
"@grafana/tsconfig": ^1.2.0-rc1
"@grafana/ui": 9.0.4
"@grafana/ui": 9.0.5
"@testing-library/react": 12.1.4
"@testing-library/user-event": 14.2.0
"@types/classnames": ^2.2.7
@@ -4460,7 +4460,7 @@ __metadata:
lodash: 4.17.21
lru-memoize: ^1.1.0
memoize-one: 6.0.0
moment: 2.29.3
moment: 2.29.4
moment-timezone: 0.5.34
prop-types: 15.8.1
react: 17.0.2
@@ -19629,7 +19629,7 @@ __metadata:
lru-cache: 7.10.1
memoize-one: 6.0.0
mini-css-extract-plugin: 2.6.0
moment: 2.29.3
moment: 2.29.4
moment-timezone: 0.5.34
monaco-editor: ^0.31.1
monaco-promql: 1.7.4
@@ -24569,10 +24569,10 @@ __metadata:
languageName: node
linkType: hard
"moment@npm:2.29.3, moment@npm:2.x, moment@npm:>= 2.9.0, moment@npm:^2.19.4, moment@npm:^2.20.1":
version: 2.29.3
resolution: "moment@npm:2.29.3"
checksum: 2e780e36d9a1823c08a1b6313cbb08bd01ecbb2a9062095820a34f42c878991ccba53abaa6abb103fd5c01e763724f295162a8c50b7e95b4f1c992ef0772d3f0
"moment@npm:2.29.4, moment@npm:2.x, moment@npm:>= 2.9.0, moment@npm:^2.19.4, moment@npm:^2.20.1":
version: 2.29.4
resolution: "moment@npm:2.29.4"
checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e
languageName: node
linkType: hard