Merge remote-tracking branch 'origin' into ivana/prom-backend-both

This commit is contained in:
Ivana
2021-09-27 15:32:04 +02:00
300 changed files with 5405 additions and 2677 deletions
+190 -214
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -40,14 +40,14 @@ go.sum @grafana/backend-platform
/pkg/tsdb/zipkin @grafana/observability-squad
/pkg/tsdb/tempo @grafana/observability-squad
# Unified Alerting
/pkg/services/ngalert @grafana/alerting-squad
/pkg/services/sqlstore/migrations/ualert @grafana/alerting-squad
# Database migrations
/pkg/services/sqlstore/migrations @grafana/backend-platform @grafana/hosted-grafana-team
*_mig.go @grafana/backend-platform @grafana/hosted-grafana-team
# Unified Alerting
/pkg/services/ngalert @grafana/alerting-squad
/pkg/services/sqlstore/migrations/ualert @grafana/alerting-squad
# Backend code docs
/contribute/style-guides/backend.md @grafana/backend-platform
/contribute/architecture/backend @grafana/backend-platform
+16
View File
@@ -8,3 +8,19 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
ignore:
# ignore minor/patch updates for now
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
# things that will probably need some thinking
- dependency-name: "d3"
- dependency-name: "husky"
- dependency-name: "slate"
- dependency-name: "slate-plain-serializer"
reviewers:
- "grafana/frontend-ops"
versioning-strategy: "increase-if-necessary"
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
# This is a basic workflow to help you get started with Actions
- uses: actions-ecosystem/action-regex-match@v2
- uses: actions-ecosystem/action-regex-match@v2.0.1
id: regex-match
with:
text: ${{ github.event.inputs.version }}
@@ -53,7 +53,7 @@ jobs:
repository: "grafana/grafana-github-actions"
path: ./actions
ref: main
- uses: actions/setup-node@v2.1.5
- uses: actions/setup-node@v2.4.0
with:
node-version: '14'
- name: Install Actions
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: generate-packages-docs
uses: actions/setup-node@v2.1.5
uses: actions/setup-node@v2.4.0
id: generate-docs
with:
node-version: '14'
+32 -11
View File
@@ -2,6 +2,7 @@ var config = {
defaults: {
concurrency: 1,
runners: ['axe'],
useIncognitoBrowserContext: false,
chromeLaunchConfig: {
args: ['--no-sandbox'],
},
@@ -10,79 +11,99 @@ var config = {
urls: [
{
url: '${HOST}/login',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/login',
wait: 500,
actions: [
"wait for element input[name='user'] to be added",
"set field input[name='user'] to admin",
"set field input[name='password'] to admin",
"click element button[aria-label='Login button']",
"wait for element [aria-label='Skip change password button'] to be visible",
],
threshold: 2,
threshold: 1,
rootElement: '.main-view',
},
{
url: '${HOST}/?orgId=1',
wait: 500,
threshold: 7,
},
{
url: '${HOST}/d/O6f11TZWk/panel-tests-bar-gauge',
hideElements: '.sidemenu',
wait: 500,
rootElement: '.main-view',
threshold: 2,
},
{
url: '${HOST}/d/O6f11TZWk/panel-tests-bar-gauge?orgId=1&editview=settings',
rootElement: '.dashboard-settings',
wait: 500,
rootElement: '.main-view',
threshold: 10,
},
{
url: '${HOST}/?orgId=1&search=open',
wait: 500,
rootElement: '.main-view',
threshold: 15,
threshold: 14,
},
{
url: '${HOST}/alerting/list',
wait: 500,
rootElement: '.main-view',
threshold: 7,
threshold: 6,
},
{
url: '${HOST}/datasources',
wait: 500,
rootElement: '.main-view',
threshold: 36,
threshold: 35,
},
{
url: '${HOST}/org/users',
wait: 500,
rootElement: '.main-view',
threshold: 4,
threshold: 3,
},
{
url: '${HOST}/org/teams',
wait: 500,
rootElement: '.main-view',
threshold: 1,
},
{
url: '${HOST}/plugins',
wait: 500,
rootElement: '.main-view',
threshold: 41,
threshold: 40,
},
{
url: '${HOST}/org',
wait: 500,
rootElement: '.main-view',
threshold: 2,
},
{
url: '${HOST}/org/apikeys',
wait: 500,
rootElement: '.main-view',
threshold: 5,
threshold: 3,
},
{
url: '${HOST}/dashboards',
wait: 500,
rootElement: '.main-view',
threshold: 11,
threshold: 7,
},
],
};
function myPa11yCiConfiguration(urls, defaults) {
const HOST_SERVER = process.env.HOST || 'localhost';
const PORT_SERVER = process.env.PORT || '3000';
const PORT_SERVER = process.env.PORT || '3001';
for (var idx = 0; idx < urls.length; idx++) {
urls[idx] = { ...urls[idx], url: urls[idx].url.replace('${HOST}', `${HOST_SERVER}:${PORT_SERVER}`) };
}
+24 -4
View File
@@ -2,6 +2,7 @@ var config = {
defaults: {
concurrency: 1,
runners: ['axe'],
useIncognitoBrowserContext: false,
chromeLaunchConfig: {
args: ['--no-sandbox'],
},
@@ -10,58 +11,78 @@ var config = {
urls: [
{
url: '${HOST}/login',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/login', //skip password and login
actions: [
"wait for element input[name='user'] to be added",
"set field input[name='user'] to admin",
"set field input[name='password'] to admin",
"click element button[aria-label='Login button']",
"wait for element [aria-label='Skip change password button'] to be visible",
],
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/?orgId=1',
wait: 500,
},
{
url: '${HOST}/d/O6f11TZWk/panel-tests-bar-gauge',
hideElements: '.sidemenu',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/d/O6f11TZWk/panel-tests-bar-gauge?orgId=1&editview=settings',
rootElement: '.dashboard-settings',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/?orgId=1&search=open',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/alerting/list',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/datasources',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/org/users',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/org/teams',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/plugins',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/org',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/org/apikeys',
wait: 500,
rootElement: '.main-view',
},
{
url: '${HOST}/dashboards',
wait: 500,
rootElement: '.main-view',
},
],
@@ -69,11 +90,10 @@ var config = {
function myPa11yCiConfiguration(urls, defaults) {
const HOST_SERVER = process.env.HOST || 'localhost';
const PORT_SERVER = process.env.PORT || '3000';
const PORT_SERVER = process.env.PORT || '3001';
for (var idx = 0; idx < urls.length; idx++) {
urls[idx] = { ...urls[idx], url: urls[idx].url.replace('${HOST}', `${HOST_SERVER}:${PORT_SERVER}`) };
}
return {
defaults: defaults,
urls: urls,
+13 -7
View File
@@ -1,4 +1,3 @@
<!-- 8.2.0-beta1 START -->
# 8.2.0-beta1 (2021-09-16)
@@ -58,20 +57,17 @@
### Breaking changes
The `monaco-editor` dependency in `grafana-ui` has been updated to a newer version (`0.27.0`), which is not completely backward compatible with the old version (`0.21.2`). The backward incompatible changes are fairly small, but they do exist, so if your code accesses the raw monaco-objects through the `grafana-ui` package, please check the [monaco-editor changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md) and apply any necessary changes. Issue [#39027](https://github.com/grafana/grafana/issues/39027)
The mandatory `css` prop in `grafana/ui` components has been removed.
The mandatory `css` prop in `grafana/ui` components has been removed.
Previous versions of `grafana/ui` components were typed incorrectly due to a dependency mismatch between emotion 10 and 11 causing a `css` prop to be added to components that extended react types.
Issue [#38078](https://github.com/grafana/grafana/issues/38078)
Issue [#38078](https://github.com/grafana/grafana/issues/38078)
Panel queries and/or annotation queries that used more than one statistic will be converted into one query/annotation per statistic. In case an alerting rule was based on a query row that had more than one statistic, it would now be based only on the first statistic for that query row. New alerting rules will not be created for migrated queries. Please note that in most cases it would not make sense to have an alerting rule that is based on multiple statistics anyway. Issue [#36925](https://github.com/grafana/grafana/issues/36925)
Panel and annotation queries using more than one statistic now converts into one query/annotation per statistic. Alerting rules based on a query row with multiple statistics now considers only the first statistic from that query row. New alerting rules are not created for migrated queries. Usually, it does not make sense to have an alerting rule based on multiple statistics. Issue [#36925](https://github.com/grafana/grafana/issues/36925)
### Deprecations
`getHighlighterExpressions` in datasource APIs ( used to highlight logs while editing queries) has been deprecated and will be removed in a future release.
# Deprecation notice
@@ -84,6 +80,16 @@ Panel queries and/or annotation queries that used more than one statistic will b
<!-- 8.2.0-beta1 END -->
<!-- 8.1.5 START -->
# 8.1.5 (2021-09-21)
### Bug fixes
- **BarChart:** Fixes panel error that happens on second refresh. [#39304](https://github.com/grafana/grafana/pull/39304), [@DanCech](https://github.com/DanCech)
<!-- 8.1.5 END -->
<!-- 8.1.4 START -->
# 8.1.4 (2021-09-16)
+1 -1
View File
@@ -86,7 +86,7 @@ golangci-lint: scripts/go/bin/golangci-lint
--config ./scripts/go/configs/.golangci.toml \
$(GO_FILES)
lint-go: golangci-lint # Run all code checks for backend.
lint-go: golangci-lint ## Run all code checks for backend. You can use GO_FILES to specify exact files to check
# with disabled SC1071 we are ignored some TCL,Expect `/usr/bin/env expect` scripts
shellcheck: $(SH_FILES) ## Run checks for shell scripts.
+4 -4
View File
@@ -30,8 +30,8 @@ The Grafana documentation is available at [grafana.com/docs](https://grafana.com
If you're interested in contributing to the Grafana project:
- Start by reading the [Contributing guide](/CONTRIBUTING.md).
- Learn how to set up your local environment, in our [Developer guide](/contribute/developer-guide.md).
- Start by reading the [Contributing guide](https://github.com/grafana/grafana/blob/HEAD/CONTRIBUTING.md).
- Learn how to set up your local environment, in our [Developer guide](https://github.com/grafana/grafana/blob/HEAD/contribute/developer-guide.md).
- Explore our [beginner-friendly issues](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22).
- Look through our [style guide and Storybook](https://developers.grafana.com/ui/latest/index.html).
@@ -40,8 +40,8 @@ If you're interested in contributing to the Grafana project:
- Follow [@grafana on Twitter](https://twitter.com/grafana/).
- Read and subscribe to the [Grafana blog](https://grafana.com/blog/).
- If you have a specific question, check out our [discussion forums](https://community.grafana.com/).
- For general discussions, join us on the [official Slack](http://slack.raintank.io/) team.
- For general discussions, join us on the [official Slack](https://slack.grafana.com) team.
## License
Grafana is distributed under [AGPL-3.0-only](LICENSE). For Apache-2.0 exceptions, see [LICENSING.md](LICENSING.md).
Grafana is distributed under [AGPL-3.0-only](LICENSE). For Apache-2.0 exceptions, see [LICENSING.md](https://github.com/grafana/grafana/blob/HEAD/LICENSING.md).
+1 -1
View File
@@ -62,7 +62,7 @@ We use [pa11y-ci](https://github.com/pa11y/pa11y-ci) to collect accessibility er
If the contribution introduces new a11y errors, our continuous integration will fail, preventing you to merge on the main branch. In those cases there are two alternatives for moving forward:
- Check the error log on the pipeline step `test-a11y-frontend-pr`, identify what was the error, and fix it.
- Locally run the command `yarn test:accessibility-report` that generates an HTML accessibility report, then go to the URL that contains your change, identify the error, and fix it. Keep in mind, a local Grafana instance needs to be running on `http://localhost:3000`.
- Locally run the command `yarn test:accessibility-report` that generates an HTML accessibility report, then go to the URL that contains your change, identify the error, and fix it. Keep in mind, a local e2e Grafana instance is going to be running on `http://localhost:3001`.
You can also prevent introducing a11y errors by installing an a11y plugin in your browser, for example, axe DevTools, Accessibility Insights for Web among others.
@@ -132,7 +132,7 @@
}
]
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@@ -263,7 +263,7 @@
}
]
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@@ -351,7 +351,7 @@
"showHeader": true,
"sortBy": []
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@@ -387,7 +387,7 @@
},
{
"collapsed": false,
"datasource": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 1,
"w": 24,
@@ -460,7 +460,7 @@
"value": [
{
"title": "Details",
"url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}"
"url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}"
}
]
}
@@ -479,7 +479,7 @@
"options": {
"showHeader": true
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"alias": "S1",
@@ -571,7 +571,7 @@
}
]
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@@ -604,6 +604,55 @@
}
],
"type": "table"
},
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 26
},
"id": 12,
"options": {
"footer": {
"show": true,
"fields": "",
"reducer": [
"sum"
]
},
"showHeader": true
},
"pluginVersion": "",
"title": "Footer",
"type": "table"
}
],
"schemaVersion": 27,
@@ -635,5 +684,5 @@
"timezone": "",
"title": "Panel Tests - React Table",
"uid": "U_bZIMRMk",
"version": 1
"version": 6
}
+10 -16
View File
@@ -8,19 +8,15 @@ weight = 150
# Configuration
Grafana has a number of configuration options that you can specify in a `.ini` configuration file or specified using environment variables.
Grafana has default and custom configuration files. You can customize your Grafana instance by modifying the custom configuration file or by using environment variables. To see the list of settings for a Grafana instance, refer to [View server settings]({{< relref "view-server/view-server-settings.md" >}}).
> **Note:** You must restart Grafana for any configuration changes to take effect.
> **Note:** After you add custom options, [uncomment](#remove-comments-in-the-ini-files) the relevant sections of the configuration file. Restart Grafana for your changes to take effect.
To see all settings currently applied to the Grafana server, refer to [View server settings]({{< relref "view-server/view-server-settings.md" >}}).
## Configuration file location
## Config file locations
The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change the location in this file.
_Do not_ change `defaults.ini`! Grafana defaults are stored in this file. Depending on your OS, make all configuration changes in either `custom.ini` or `grafana.ini`.
- Default configuration from `$WORKING_DIR/conf/defaults.ini`
- Custom configuration from `$WORKING_DIR/conf/custom.ini`
- The custom configuration file path can be overridden using the `--config` parameter
Depending on your OS, your custom configuration file is either the `$WORKING_DIR/conf/defaults.ini` file or the `/usr/local/etc/grafana/grafana.ini` file. The custom configuration file path can be overridden using the `--config` parameter.
### Linux
@@ -32,25 +28,23 @@ Refer to [Configure a Grafana Docker image]({{< relref "configure-docker.md" >}}
### Windows
`sample.ini` is in the same directory as `defaults.ini` and contains all the settings commented out. Copy `sample.ini` and name it `custom.ini`.
On Windows, the `sample.ini` file is located in the same directory as `defaults.ini` file. It contains all the settings commented out. Copy `sample.ini` and name it `custom.ini`.
### macOS
By default, the configuration file is located at `/usr/local/etc/grafana/grafana.ini`. For a Grafana instance installed using Homebrew, edit the `grafana.ini` file directly. Otherwise, add a configuration file named `custom.ini` to the `conf` folder to override any of the settings defined in `conf/defaults.ini`.
By default, the configuration file is located at `/usr/local/etc/grafana/grafana.ini`. For a Grafana instance installed using Homebrew, edit the `grafana.ini` file directly. Otherwise, add a configuration file named `custom.ini` to the `conf` folder to override the settings defined in `conf/defaults.ini`.
## Comments in .ini Files
## Remove comments in the .ini files
Semicolons (the `;` char) are the standard way to comment out lines in a `.ini` file. If you want to change a setting, you must delete the semicolon (`;`) in front of the setting before it will work.
Grafana uses semicolons (the `;` char) to comment out lines in a `.ini` file. You must uncomment each line in the `custom.ini` or the `grafana.ini` file that you are modify by removing `;` from the beginning of that line. Otherwise your changes will be ignored.
**Example**
For example:
```
# The HTTP port to use
;http_port = 3000
```
A common problem is forgetting to uncomment a line in the `custom.ini` (or `grafana.ini`) file which causes the configuration option to be ignored.
## Configure with environment variables
All options in the configuration file can be overridden using environment variables using the syntax:
@@ -51,26 +51,26 @@ These examples show how often and when reminders are sent for a triggered alert.
## List of supported notifiers
| Name | Type | Supports images | Supports alert rule tags |
| --------------------------------------------- | ------------------------- | ------------------ | ----------------------- |
| [DingDing](#dingdingdingtalk) | `dingding` | yes, external only | no |
| [Discord](#discord) | `discord` | yes | no |
| [Email](#email) | `email` | yes | no |
| [Google Hangouts Chat](#google-hangouts-chat) | `googlechat` | yes, external only | no |
| Hipchat | `hipchat` | yes, external only | no |
| [Kafka](#kafka) | `kafka` | yes, external only | no |
| Line | `line` | yes, external only | no |
| Microsoft Teams | `teams` | yes, external only | no |
| [Opsgenie](#opsgenie) | `opsgenie` | yes, external only | yes |
| [Pagerduty](#pagerduty) | `pagerduty` | yes, external only | yes |
| Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only | yes |
| [Pushover](#pushover) | `pushover` | yes | no |
| Sensu | `sensu` | yes, external only | no |
| [Sensu Go](#sensu-go) | `sensugo` | yes, external only | no |
| [Slack](#slack) | `slack` | yes | no |
| Telegram | `telegram` | yes | no |
| Threema | `threema` | yes, external only | no |
| VictorOps | `victorops` | yes, external only | yes |
| [Webhook](#webhook) | `webhook` | yes, external only | yes |
| --------------------------------------------- | ------------------------- | ------------------ | ------------------------ |
| [DingDing](#dingdingdingtalk) | `dingding` | yes, external only | no |
| [Discord](#discord) | `discord` | yes | no |
| [Email](#email) | `email` | yes | no |
| [Google Hangouts Chat](#google-hangouts-chat) | `googlechat` | yes, external only | no |
| Hipchat | `hipchat` | yes, external only | no |
| [Kafka](#kafka) | `kafka` | yes, external only | no |
| Line | `line` | yes, external only | no |
| Microsoft Teams | `teams` | yes, external only | no |
| [Opsgenie](#opsgenie) | `opsgenie` | yes, external only | yes |
| [Pagerduty](#pagerduty) | `pagerduty` | yes, external only | yes |
| Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only | yes |
| [Pushover](#pushover) | `pushover` | yes | no |
| Sensu | `sensu` | yes, external only | no |
| [Sensu Go](#sensu-go) | `sensugo` | yes, external only | no |
| [Slack](#slack) | `slack` | yes | no |
| Telegram | `telegram` | yes | no |
| Threema | `threema` | yes, external only | no |
| VictorOps | `victorops` | yes, external only | yes |
| [Webhook](#webhook) | `webhook` | yes, external only | yes |
### Email
@@ -55,6 +55,7 @@ Here is a minimal policy example:
"cloudwatch:ListMetrics",
"cloudwatch:GetMetricStatistics",
"cloudwatch:GetMetricData"
"cloudwatch:GetInsightRuleReport"
],
"Resource": "*"
},
@@ -48,8 +48,6 @@ Logs of usage insights contain the following fields, where the fields followed b
## Configuration
The following procedure assumes that you have installed Loki. Refer to [Install Loki](/docs/loki/latest/installation/).
To export your logs, enable the usage insights feature and [configure]({{< relref "../../administration/configuration.md" >}}) an export location in the configuration file:
```ini
@@ -58,7 +56,16 @@ To export your logs, enable the usage insights feature and [configure]({{< relre
enabled = true
# Storage type
storage = loki
```
The options for storage type are `loki` and `logger` (added in Grafana Enterprise 8.2).
If the storage type is set to `loki` you'll need to also configure Grafana
to export to a Loki ingestion server. To do this, you'll need Loki installed.
Refer to [Install Loki](/docs/loki/latest/installation/) for instructions
on how to install Loki.
```ini
[usage_insights.export.storage.loki]
# Set the communication protocol to use with Loki (can be grpc or http)
type = grpc
@@ -68,9 +75,12 @@ url = localhost:9095
tls = true
```
Using `logger` will print usage insights to your [Grafana server log]({{< relref "../../administration/configuration.md#log" >}}).
There is no option for configuring the `logger` storage type.
## Visualize Loki usage insights in Grafana
Now that your logs are exported into Loki, you can build Grafana dashboards to understand your Grafana instance usage.
If you export logs into Loki, you can build Grafana dashboards to understand your Grafana instance usage.
1. Add Loki as a data source. Refer to [Grafana fundamentals tutorial](/tutorials/grafana-fundamentals/#6).
1. Import one of the following dashboards:
+1 -1
View File
@@ -22,7 +22,7 @@ The uid can have a maximum length of 40 characters.
`POST /api/dashboards/db`
Creates a new dashboard or updates an existing dashboard.
Creates a new dashboard or updates an existing dashboard. When updating existing dashboards, if you do not define the `folderId` and `folderUid` properties, then the dashboard(s) are moved to the `General` folder.
**Example Request for new dashboard**:
+1
View File
@@ -9,6 +9,7 @@ Here you can find detailed release notes that list everything that is included i
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.2.0-beta1]({{< relref "release-notes-8-2-0-beta1" >}})
- [Release notes for 8.1.5]({{< relref "release-notes-8-1-5" >}})
- [Release notes for 8.1.4]({{< relref "release-notes-8-1-4" >}})
- [Release notes for 8.1.3]({{< relref "release-notes-8-1-3" >}})
- [Release notes for 8.1.2]({{< relref "release-notes-8-1-2" >}})
@@ -22,4 +22,3 @@ list = false
- **InfluxDB:** Fix regex to identify `/` as separator. [#39185](https://github.com/grafana/grafana/pull/39185), [@dsotirakis](https://github.com/dsotirakis)
- **LibraryPanels:** Fix update issues related to library panels in rows. [#38963](https://github.com/grafana/grafana/pull/38963), [@hugohaggmark](https://github.com/hugohaggmark)
- **Variables:** Fix variables not updating inside a Panel when the preceding Row uses "Repeat For". [#38935](https://github.com/grafana/grafana/pull/38935), [@axelavargas](https://github.com/axelavargas)
@@ -0,0 +1,13 @@
+++
title = "Release notes for Grafana 8.1.5"
[_build]
list = false
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.1.5
### Bug fixes
- **BarChart:** Fixes panel error that happens on second refresh. [#39304](https://github.com/grafana/grafana/pull/39304), [@DanCech](https://github.com/DanCech)
@@ -63,20 +63,17 @@ list = false
### Breaking changes
The `monaco-editor` dependency in `grafana-ui` has been updated to a newer version (`0.27.0`), which is not completely backward compatible with the old version (`0.21.2`). The backward incompatible changes are fairly small, but they do exist, so if your code accesses the raw monaco-objects through the `grafana-ui` package, please check the [monaco-editor changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md) and apply any necessary changes. Issue [#39027](https://github.com/grafana/grafana/issues/39027)
The mandatory `css` prop in `grafana/ui` components has been removed.
The mandatory `css` prop in `grafana/ui` components has been removed.
Previous versions of `grafana/ui` components were typed incorrectly due to a dependency mismatch between emotion 10 and 11 causing a `css` prop to be added to components that extended react types.
Issue [#38078](https://github.com/grafana/grafana/issues/38078)
Issue [#38078](https://github.com/grafana/grafana/issues/38078)
Panel queries and/or annotation queries that used more than one statistic will be converted into one query/annotation per statistic. In case an alerting rule was based on a query row that had more than one statistic, it would now be based only on the first statistic for that query row. New alerting rules will not be created for migrated queries. Please note that in most cases it would not make sense to have an alerting rule that is based on multiple statistics anyway. Issue [#36925](https://github.com/grafana/grafana/issues/36925)
Panel queries and/or annotation queries that used more than one statistic will be converted into one query/annotation per statistic. In case an alerting rule was based on a query row that had more than one statistic, it would now be based only on the first statistic for that query row. New alerting rules will not be created for migrated queries. Please note that in most cases it would not make sense to have an alerting rule that is based on multiple statistics anyway. Issue [#36925](https://github.com/grafana/grafana/issues/36925)
### Deprecations
`getHighlighterExpressions` in datasource APIs ( used to highlight logs while editing queries) has been deprecated and will be removed in a future release.
# Deprecation notice
+2 -4
View File
@@ -12,7 +12,7 @@ list = false
> **Note:** This topic will be updated frequently between now and the final release. Additionally, not all features listed here may be present in all the beta releases.
Grafana 8.2 continues to build on the foundation laid out in Grafana 8.0 & 8.1. We’ve continued to extend the GeoMap panel and bring new features to the Time Series panel. Grafana 8.2 also marks the start of our work to bring Grafana closer to all users with a focus on increasing Grafana’s accessibility, part of its continuing mission to democratize metrics *for everyone*.
Grafana 8.2 continues to build on the foundation laid out in Grafana 8.0 & 8.1. We’ve continued to extend the GeoMap panel and bring new features to the Time Series panel. Grafana 8.2 also marks the start of our work to bring Grafana closer to all users with a focus on increasing Grafana’s accessibility, part of its continuing mission to democratize metrics _for everyone_.
We’ve summarized what’s new in the release here, but you might also be interested in the announcement blog post as well. If you’d like all the details you can checkout the complete [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md).
@@ -34,7 +34,7 @@ A number of new visualizations and features are included in Grafana 8.2 includin
### XY Chart [beta]
The XY Chart panel, aka. scatter panel, is available in beta form. This chart is all about first-class support for data where x does not equate to time.
The XY Chart panel, aka. scatter panel, is available in beta form. This chart is all about first-class support for data where x does not equate to time.
{{< figure src="/static/img/docs/scatter-panel/scatter-placeholder.png" max-width="1200px" caption="Scatter Panel" >}}
@@ -62,8 +62,6 @@ Usage Insights Logs contain valuable information about user dashboard visits, qu
Reports are a powerful way to deliver insights directly to people’s email inboxes. Now you can create a report directly from any dashboard, using the Share button. This is especially useful combined with fine-grained access control, which you can use to grant Editors or Viewers the ability to create reports in Grafana. Learn more about Reporting in the [docs](​​https://grafana.com/docs/grafana/latest/enterprise/reporting/).
## Image Renderer performance improvements and measurement
You can use Grafana’s image renderer to generate JPEG and PDF images of panels and dashboards. These images are used for alert notifications, PDF exports, and reports sent by Grafana. We’ve added additional metrics to the image renderer to help you diagnose its performance, and included guidance in our documentation to help you configure it for the best mix of performance and resource usage. In our tests, we were able to reduce image load time from a 95th percentile of 10 seconds to less than 3 seconds under normal load.
+2 -2
View File
@@ -17,7 +17,7 @@ SUITE=${SUITE:-$DEFAULT_SUITE}
if [ "$1" == "debug" ]; then
echo -e "Debug mode"
SLOWMO=1
PARAMS="--headed --no-exit"
PARAMS="--no-exit"
fi
if [ "$1" == "dev" ]; then
@@ -28,5 +28,5 @@ fi
cd packages/grafana-e2e
yarn $CMD --env BASE_URL=$URL,SLOWMO=$SLOWMO \
--config defaultCommandTimeout=30000,integrationFolder=../../e2e/$SUITE/specs,screenshotsFolder=../../e2e/$SUITE/screenshots,videosFolder=../../e2e/$SUITE/videos,fileServerFolder=./cypress,viewportWidth=1920,viewportHeight=1080,trashAssetsBeforeRuns=false \
--config defaultCommandTimeout=30000,integrationFolder=../../e2e/$SUITE/specs,screenshotsFolder=../../e2e/$SUITE/screenshots,videosFolder=../../e2e/$SUITE/videos,fileServerFolder=./cypress,viewportWidth=1920,viewportHeight=1080,trashAssetsBeforeRuns=false,videoUploadOnPasses=false \
$PARAMS
+25 -25
View File
@@ -19,7 +19,7 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/Masterminds/semver v1.5.0
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
github.com/aws/aws-sdk-go v1.40.11
github.com/aws/aws-sdk-go v1.40.37
github.com/beevik/etree v1.1.0
github.com/benbjohnson/clock v1.1.0
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
@@ -27,14 +27,15 @@ require (
github.com/cortexproject/cortex v1.8.2-0.20210428155238-d382e1d80eaf
github.com/crewjam/saml v0.4.6-0.20201227203850-bca570abb2ce
github.com/davecgh/go-spew v1.1.1
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204
github.com/denisenkom/go-mssqldb v0.10.0
github.com/dop251/goja v0.0.0-20210804101310-32956a348b49
github.com/fatih/color v1.10.0
github.com/gchaincl/sqlhooks v1.3.0
github.com/getsentry/sentry-go v0.10.0
github.com/go-kit/kit v0.11.0
github.com/go-macaron/binding v0.0.0-20190806013118-0b4f37bab25b
github.com/go-openapi/strfmt v0.20.1
github.com/go-openapi/strfmt v0.20.2
github.com/go-redis/redis/v8 v8.11.3
github.com/go-sourcemap/sourcemap v2.1.3+incompatible
github.com/go-sql-driver/mysql v1.6.0
github.com/go-stack/stack v1.8.0
@@ -49,14 +50,14 @@ require (
github.com/gorilla/websocket v1.4.2
github.com/gosimple/slug v1.9.0
github.com/grafana/grafana-aws-sdk v0.7.0
github.com/grafana/grafana-plugin-sdk-go v0.113.0
github.com/grafana/grafana-plugin-sdk-go v0.114.0
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-hclog v0.16.1
github.com/hashicorp/go-plugin v1.4.2
github.com/hashicorp/go-version v1.3.0
github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec
github.com/influxdata/influxdb-client-go/v2 v2.2.3
github.com/influxdata/influxdb-client-go/v2 v2.3.1-0.20210518120617-5d1fff431040
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097
github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.11
@@ -68,20 +69,20 @@ require (
github.com/magefile/mage v1.11.0
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-sqlite3 v1.14.7
github.com/matttproud/golang_protobuf_extensions v1.0.1
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
github.com/opentracing/opentracing-go v1.2.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/alertmanager v0.23.0-rc.0.0.20210906104939-8da517524a87
github.com/prometheus/alertmanager v0.23.1-0.20210914172521-e35efbddb66a
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.30.0
github.com/prometheus/prometheus v1.8.2-0.20210621150501-ff58416a0b02
github.com/prometheus/prometheus v1.8.2-0.20210915140241-bd217c58a735
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.1.0
github.com/russellhaering/goxmldsig v1.1.1
github.com/smartystreets/goconvey v1.6.4
github.com/spyzhov/ajson v0.4.2
github.com/stretchr/testify v1.7.0
@@ -98,13 +99,13 @@ require (
go.opentelemetry.io/collector/model v0.31.0
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
golang.org/x/tools v0.1.5
gonum.org/v1/gonum v0.9.1
google.golang.org/api v0.54.0
google.golang.org/api v0.56.0
google.golang.org/grpc v1.40.0
google.golang.org/protobuf v1.27.1
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
@@ -112,16 +113,16 @@ require (
gopkg.in/ldap.v3 v3.1.0
gopkg.in/macaron.v1 v1.4.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/redis.v5 v5.2.9
gopkg.in/square/go-jose.v2 v2.5.1
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
xorm.io/builder v0.3.6
xorm.io/core v0.7.3
xorm.io/xorm v0.8.2
)
require (
cloud.google.com/go v0.90.0 // indirect
cloud.google.com/go v0.93.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 // indirect
github.com/FZambia/eagle v0.0.1 // indirect
github.com/FZambia/sentinel v1.1.0 // indirect
@@ -136,11 +137,13 @@ require (
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/centrifugal/protocol v0.7.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cockroachdb/apd/v2 v2.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/deepmap/oapi-codegen v1.3.13 // indirect
github.com/deepmap/oapi-codegen v1.6.0 // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
@@ -148,7 +151,7 @@ require (
github.com/emicklei/proto v1.6.15 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/go-kit/log v0.1.0 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-openapi/analysis v0.20.1 // indirect
github.com/go-openapi/errors v0.20.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -165,8 +168,8 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/flatbuffers v1.12.0 // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/google/flatbuffers v2.0.0+incompatible // indirect
github.com/googleapis/gax-go/v2 v2.1.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
@@ -183,7 +186,6 @@ require (
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/cpuid/v2 v2.0.5 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@@ -191,7 +193,7 @@ require (
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.42 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/mitchellh/go-testing-interface v1.14.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mna/redisc v1.2.1 // indirect
@@ -230,14 +232,12 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/goleak v1.1.10 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67 // indirect
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
xorm.io/builder v0.3.6 // indirect
)
replace github.com/apache/thrift => github.com/apache/thrift v0.14.1
+377 -50
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
{
"stable": "8.1.4",
"stable": "8.1.5",
"testing": "8.2.0-beta1"
}
+1 -1
View File
@@ -2,5 +2,5 @@
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "8.2.0-pre"
"version": "8.3.0-pre"
}
+6 -8
View File
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
@@ -191,7 +191,7 @@
"mutationobserver-shim": "0.3.3",
"ngtemplate-loader": "2.1.0",
"nodemon": "2.0.2",
"pa11y-ci": "2.4.2",
"pa11y-ci": "pa11y/pa11y-ci#5c842cf1b9fe2867b70ff5354851d985be8d71c4",
"postcss": "8.3.6",
"postcss-loader": "6.1.1",
"postcss-reporter": "7.0.2",
@@ -213,8 +213,8 @@
"testing-library-selector": "^0.1.3",
"ts-jest": "26.4.4",
"ts-node": "9.0.0",
"tslib": "2.2.0",
"typescript": "4.3.4",
"tslib": "2.3.1",
"typescript": "4.4.3",
"wait-on": "6.0.0",
"webpack": "5.51.1",
"webpack-bundle-analyzer": "4.4.2",
@@ -222,8 +222,7 @@
"webpack-cli": "4.8.0",
"webpack-dev-server": "4.0.0",
"webpack-merge": "5.8.0",
"worker-loader": "^3.0.8",
"zone.js": "0.7.8"
"worker-loader": "^3.0.8"
},
"dependencies": {
"@emotion/css": "11.1.3",
@@ -289,7 +288,7 @@
"ol": "6.7.0",
"papaparse": "5.3.0",
"pluralize": "^8.0.0",
"prismjs": "1.24.0",
"prismjs": "1.25.0",
"prop-types": "15.7.2",
"rc-cascader": "1.5.0",
"re-resizable": "^6.2.0",
@@ -328,7 +327,6 @@
"whatwg-fetch": "3.1.0"
},
"resolutions": {
"caniuse-db": "1.0.30000772",
"underscore": "1.12.1"
},
"workspaces": {
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -23,7 +23,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "5.0.2",
"@grafana/schema": "8.2.0-pre",
"@grafana/schema": "8.3.0-pre",
"@types/d3-interpolate": "^1.3.1",
"date-fns": "2.21.3",
"eventemitter3": "4.0.7",
@@ -55,7 +55,7 @@
"rollup-plugin-visualizer": "4.2.0",
"sinon": "8.1.1",
"tinycolor2": "1.4.1",
"typescript": "4.3.4"
"typescript": "4.4.3"
},
"volta": {
"extends": "../../package.json"
@@ -182,6 +182,27 @@ describe('PanelPlugin', () => {
expect(panel.fieldConfigDefaults.defaults.custom).toEqual(expectedDefaults);
});
test('throw error with array fieldConfigs', () => {
const panel = new PanelPlugin(() => {
return <div>Panel</div>;
});
panel.useFieldConfig({
useCustomConfig: (builder) => {
builder.addCustomEditor({
id: 'somethingUnique',
path: 'numericOption[0]',
name: 'Option editor',
description: 'Option editor description',
defaultValue: 10,
} as any);
},
});
expect(() => panel.fieldConfigRegistry).toThrowErrorMatchingInlineSnapshot(
`"[undefined] Field config paths do not support arrays: custom.somethingUnique"`
);
});
test('default values for nested paths', () => {
const panel = new PanelPlugin(() => {
return <div>Panel</div>;
@@ -75,6 +75,13 @@ export function createFieldConfigRegistry<TFieldConfigOptions>(
}
}
// assert that field configs do not use array path syntax
for (const item of registry.list()) {
if (item.path.indexOf('[') > 0) {
throw new Error(`[${pluginName}] Field config paths do not support arrays: ${item.id}`);
}
}
return registry;
}
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
@@ -43,7 +43,7 @@
"@grafana/tsconfig": "^1.0.0-rc1",
"commander": "5.0.0",
"execa": "4.0.0",
"typescript": "4.3.4",
"typescript": "4.4.3",
"yaml": "^1.8.3"
},
"volta": {
@@ -22,6 +22,9 @@ const webpackOptions = {
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
},
+7 -7
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -25,7 +25,7 @@
"clean": "rimraf ./dist ./compiled",
"docsExtract": "mkdir -p ../../reports/docs && api-extractor run 2>&1 | tee ../../reports/docs/$(basename $(pwd)).log",
"open": "cypress open",
"start": "cypress run --headless --browser chrome",
"start": "cypress run --browser=chrome",
"test": "pushd test && node ../dist/bin/grafana-e2e.js run",
"typecheck": "tsc --noEmit"
},
@@ -44,18 +44,18 @@
},
"types": "src/index.ts",
"dependencies": {
"@cypress/webpack-preprocessor": "5.9.0",
"@grafana/e2e-selectors": "8.2.0-pre",
"@cypress/webpack-preprocessor": "5.9.1",
"@grafana/e2e-selectors": "8.3.0-pre",
"@grafana/tsconfig": "^1.0.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"blink-diff": "1.0.13",
"commander": "5.0.0",
"cypress": "7.5.0",
"cypress-file-upload": "5.0.7",
"cypress": "8.4.1",
"cypress-file-upload": "5.0.8",
"execa": "4.0.0",
"resolve-as-bin": "2.1.0",
"ts-loader": "6.2.1",
"typescript": "4.3.4",
"typescript": "4.4.3",
"yaml": "^1.8.3"
},
"volta": {
+5 -5
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -22,9 +22,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "8.2.0-pre",
"@grafana/e2e-selectors": "8.2.0-pre",
"@grafana/ui": "8.2.0-pre",
"@grafana/data": "8.3.0-pre",
"@grafana/e2e-selectors": "8.3.0-pre",
"@grafana/ui": "8.3.0-pre",
"history": "4.10.1",
"systemjs": "0.20.19",
"systemjs-plugin-css": "0.1.37"
@@ -44,7 +44,7 @@
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-typescript2": "0.29.0",
"rollup-plugin-visualizer": "4.2.0",
"typescript": "4.3.4"
"typescript": "4.4.3"
},
"types": "src/index.ts",
"volta": {
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"description": "Grafana Schema Library",
"keywords": [
"typescript"
@@ -33,7 +33,7 @@
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-typescript2": "0.29.0",
"rollup-plugin-visualizer": "4.2.0",
"typescript": "4.3.4"
"typescript": "4.4.3"
},
"volta": {
"extends": "../../package.json"
+2 -3
View File
@@ -1,13 +1,12 @@
package schema
AxisPlacement: "auto" | "top" | "right" | "bottom" | "left" | "hidden" @cuetsy(kind="enum")
PointVisibility: "auto" | "never" | "always" @cuetsy(kind="enum")
VisibilityMode: "auto" | "never" | "always" @cuetsy(kind="enum")
DrawStyle: "line" | "bars" | "points" @cuetsy(kind="enum")
LineInterpolation: "linear" | "smooth" | "stepBefore" | "stepAfter" @cuetsy(kind="enum")
ScaleDistribution: "linear" | "log" @cuetsy(kind="enum")
GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(kind="enum")
StackingMode: "none" | "normal" | "percent" @cuetsy(kind="enum")
BarValueVisibility: "auto" | "never" | "always" @cuetsy(kind="enum")
BarAlignment: -1 | 0 | 1 @cuetsy(kind="enum",memberNames="Before|Center|After")
ScaleOrientation: 0 | 1 @cuetsy(kind="enum",memberNames="Horizontal|Vertical")
ScaleDirection: 1 | 1 | -1 | -1 @cuetsy(kind="enum",memberNames="Up|Right|Down|Left")
@@ -33,7 +32,7 @@ FillConfig: {
fillBelowTo?: string
} @cuetsy(kind="interface")
PointsConfig: {
showPoints?: PointVisibility
showPoints?: VisibilityMode
pointSize?: number
pointColor?: string
pointSymbol?: string
@@ -11,9 +11,9 @@ export enum AxisPlacement {
Right = 'right',
Top = 'top',
}
export enum PointVisibility {
Always = 'always',
export enum VisibilityMode {
Auto = 'auto',
Always = 'always',
Never = 'never',
}
export enum GraphDrawStyle {
@@ -45,12 +45,11 @@ export interface LineStyle {
dash?: number[];
fill?: 'solid' | 'dash' | 'dot' | 'square';
}
export interface PointsConfig {
pointColor?: string;
pointSize?: number;
pointSymbol?: string;
showPoints?: PointVisibility;
showPoints?: VisibilityMode;
}
export interface ScaleDistributionConfig {
log?: number;
@@ -71,15 +70,6 @@ export enum BarAlignment {
After = 1,
}
/**
* @alpha
*/
export enum BarValueVisibility {
Auto = 'auto',
Never = 'never',
Always = 'always',
}
/**
* @alpha
*/
+5 -5
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -28,10 +28,10 @@
"dependencies": {
"@babel/core": "7.13.14",
"@babel/preset-env": "7.13.12",
"@grafana/data": "8.2.0-pre",
"@grafana/data": "8.3.0-pre",
"@grafana/eslint-config": "2.5.0",
"@grafana/tsconfig": "^1.0.0-rc1",
"@grafana/ui": "8.2.0-pre",
"@grafana/ui": "8.3.0-pre",
"@types/command-exists": "^1.2.0",
"@types/expect-puppeteer": "3.3.1",
"@types/fs-extra": "^8.1.0",
@@ -103,8 +103,8 @@
"ts-jest": "26.4.4",
"ts-loader": "6.2.1",
"ts-node": "9.0.0",
"tslib": "2.2.0",
"typescript": "4.3.4",
"tslib": "2.3.1",
"typescript": "4.4.3",
"url-loader": "^2.0.1",
"webpack": "4.41.5"
},
@@ -1,6 +1,5 @@
import execa = require('execa');
import { promises as fs } from 'fs';
// @ts-ignore
import * as path from 'path';
import chalk from 'chalk';
import { useSpinner } from '../utils/useSpinner';
@@ -2,8 +2,6 @@ import { Task, TaskRunner } from './task';
import { pluginBuildRunner } from './plugin.build';
import { getPluginJson } from '../../config/utils/pluginValidation';
import { getPluginId } from '../../config/utils/getPluginId';
// @ts-ignore
import execa = require('execa');
import path = require('path');
import fs from 'fs-extra';
@@ -2,8 +2,6 @@ import { Task, TaskRunner } from './task';
import { bundlePlugin as bundleFn, PluginBundleOptions } from './plugin/bundle';
import { useSpinner } from '../utils/useSpinner';
import { lintPlugin } from './plugin.build';
// @ts-ignore
import execa = require('execa');
import path = require('path');
@@ -15,7 +13,7 @@ const yarnlink = () =>
try {
// Make sure we are not using package.json defined toolkit
await execa('yarn', ['remove', '@grafana/toolkit']);
} catch (e) {
} catch (e: any) {
console.log('\n', e.message, '\n');
}
await execa('yarn', ['link', '@grafana/toolkit']);
@@ -5,8 +5,6 @@ import { getPluginId } from '../../config/utils/getPluginId';
import { getCiFolder } from '../../plugins/env';
import { useSpinner } from '../utils/useSpinner';
import path = require('path');
// @ts-ignore
import execa = require('execa');
interface Command extends Array<any> {}
@@ -120,7 +118,7 @@ const prepareRelease = ({ dryrun, verbose }: any) =>
console.log('skipping empty line');
}
}
} catch (ex) {
} catch (ex: any) {
const err: string = ex.message;
if (opts['okOnError'] && Array.isArray(opts['okOnError'])) {
let trueError = true;
@@ -18,7 +18,7 @@ const bundleManagedPluginsRunner: TaskRunner<BundeManagedOptions> = async () =>
console.log(`[${scope}]: ${plugin} building...`);
await execa('yarn', ['build'], { cwd: `${MANAGED_PLUGINS_PATH}/${scope}/${plugin}` });
console.log(`[${scope}]: ${plugin} bundled`);
} catch (e) {
} catch (e: any) {
console.log(e.stdout);
}
}
@@ -3,8 +3,6 @@ import { getPluginJson } from '../../config/utils/pluginValidation';
import { getCiFolder } from '../../plugins/env';
import path = require('path');
import fs = require('fs');
// @ts-ignore
// import execa = require('execa');
import GithubClient from './githubClient';
import { AxiosResponse } from 'axios';
@@ -79,7 +77,7 @@ class GitHubRelease {
if (latestRelease.data.tag_name === `v${pluginInfo.version}`) {
await this.git.client.delete(`releases/${latestRelease.data.id}`);
}
} catch (reason) {
} catch (reason: any) {
if (reason.response.status !== 404) {
// 404 just means no release found. Not an error. Anything else though, re throw the error
throw reason;
@@ -102,7 +100,7 @@ class GitHubRelease {
`https://uploads.github.com/repos/${this.username}/${this.repository}/releases/${newReleaseResponse.data.id}/assets`
);
await Promise.all(publishPromises);
} catch (reason) {
} catch (reason: any) {
console.error(reason.data?.message ?? reason.response.data ?? reason);
// Rethrow the error so that we can trigger a non-zero exit code to circle-ci
throw reason;
@@ -6,7 +6,7 @@ export const useSpinner = async (label: string, fn: () => Promise<any>, killProc
try {
await fn();
spinner.succeed();
} catch (err) {
} catch (err: any) {
spinner.fail(err.message || err);
if (err.stdout) {
@@ -274,7 +274,7 @@ export const loadWebpackConfig: WebpackConfigurationGetter = async (options) =>
);
}
return (configGetter as CustomWebpackConfigurationGetter)(baseConfig, options);
} catch (err) {
} catch (err: any) {
if (err.code === 'ENOENT') {
return baseConfig;
}
@@ -75,7 +75,7 @@ export async function signManifest(manifest: ManifestInfo): Promise<string> {
}
return info.data;
} catch (err) {
} catch (err: any) {
if ((err.response && err.response.data) || err.response.data.message) {
throw new Error('Error signing manifest: ' + err.response.data.message);
}
+2 -1
View File
@@ -4,7 +4,8 @@
"module": "commonjs",
"outDir": "dist/src",
"rootDirs": ["."],
"typeRoots": ["node_modules/@types"]
"typeRoots": ["node_modules/@types"],
"useUnknownInCatchVariables": false
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
+7 -7
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -33,9 +33,9 @@
"@emotion/css": "11.1.3",
"@emotion/react": "11.1.5",
"@grafana/aws-sdk": "0.0.3",
"@grafana/data": "8.2.0-pre",
"@grafana/e2e-selectors": "8.2.0-pre",
"@grafana/schema": "8.2.0-pre",
"@grafana/data": "8.3.0-pre",
"@grafana/e2e-selectors": "8.3.0-pre",
"@grafana/schema": "8.3.0-pre",
"@grafana/slate-react": "0.22.10-grafana",
"@grafana/tsconfig": "^1.0.0-rc1",
"@monaco-editor/react": "4.2.2",
@@ -83,10 +83,10 @@
"@storybook/addon-essentials": "6.3.7",
"@storybook/addon-knobs": "6.3.0",
"@storybook/addon-storysource": "6.3.7",
"@storybook/react": "6.3.7",
"@storybook/theming": "6.3.7",
"@storybook/builder-webpack5": "6.3.7",
"@storybook/manager-webpack5": "6.3.7",
"@storybook/react": "6.3.7",
"@storybook/theming": "6.3.7",
"@testing-library/jest-dom": "5.11.9",
"@types/classnames": "2.2.7",
"@types/common-tags": "^1.8.0",
@@ -124,7 +124,7 @@
"rollup-plugin-visualizer": "4.2.0",
"storybook-dark-mode": "1.0.8",
"ts-loader": "8.0.11",
"typescript": "4.3.4",
"typescript": "4.4.3",
"webpack-filter-warnings-plugin": "1.2.1"
},
"types": "src/index.ts",
@@ -4,6 +4,7 @@ import {
DisplayValue,
VizOrientation,
ThresholdsMode,
FALLBACK_COLOR,
Field,
FieldType,
getDisplayProcessor,
@@ -12,6 +13,7 @@ import {
import {
BarGauge,
Props,
getCellColor,
getValueColor,
getBasicAndGradientStyles,
getBarGradient,
@@ -74,6 +76,69 @@ function getValue(value: number, title?: string): DisplayValue {
}
describe('BarGauge', () => {
describe('getCellColor', () => {
it('returns a fallback if the positionValue is null', () => {
const props = getProps();
expect(getCellColor(null, props.value, props.display)).toEqual({
background: FALLBACK_COLOR,
border: FALLBACK_COLOR,
});
});
it('does not show as lit if the value is null (somehow)', () => {
const props = getProps();
expect(getCellColor(1, (null as unknown) as DisplayValue, props.display)).toEqual(
expect.objectContaining({
isLit: false,
})
);
});
it('does not show as lit if the numeric value is NaN', () => {
const props = getProps();
expect(
getCellColor(
1,
{
numeric: NaN,
text: '0',
},
props.display
)
).toEqual(
expect.objectContaining({
isLit: false,
})
);
});
it('does not show as lit if the positionValue is greater than the numeric value', () => {
const props = getProps();
expect(getCellColor(75, props.value, props.display)).toEqual(
expect.objectContaining({
isLit: false,
})
);
});
it('shows as lit otherwise', () => {
const props = getProps();
expect(getCellColor(1, props.value, props.display)).toEqual(
expect.objectContaining({
isLit: true,
})
);
});
it('returns a fallback if there is no display processor', () => {
const props = getProps();
expect(getCellColor(null, props.value, undefined)).toEqual({
background: FALLBACK_COLOR,
border: FALLBACK_COLOR,
});
});
});
describe('Get value color', () => {
it('should get the threshold color if value is same as a threshold', () => {
const props = getProps();
@@ -122,43 +122,8 @@ export class BarGauge extends PureComponent<Props> {
);
}
getCellColor(positionValue: TimeSeriesValue): CellColors {
const { value, display } = this.props;
if (positionValue === null) {
return {
background: FALLBACK_COLOR,
border: FALLBACK_COLOR,
};
}
const color = display ? display(positionValue).color : null;
if (color) {
// if we are past real value the cell is not "on"
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
return {
background: tinycolor(color).setAlpha(0.18).toRgbString(),
border: 'transparent',
isLit: false,
};
} else {
return {
background: tinycolor(color).setAlpha(0.95).toRgbString(),
backgroundShade: tinycolor(color).setAlpha(0.55).toRgbString(),
border: tinycolor(color).setAlpha(0.9).toRgbString(),
isLit: true,
};
}
}
return {
background: FALLBACK_COLOR,
border: FALLBACK_COLOR,
};
}
renderRetroBars(): ReactNode {
const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props;
const { display, field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props;
const {
valueHeight,
valueWidth,
@@ -200,7 +165,7 @@ export class BarGauge extends PureComponent<Props> {
for (let i = 0; i < cellCount; i++) {
const currentValue = minValue + (valueRange / cellCount) * i;
const cellColor = this.getCellColor(currentValue);
const cellColor = getCellColor(currentValue, value, display);
const cellStyles: CSSProperties = {
borderRadius: '2px',
};
@@ -425,6 +390,44 @@ export function calculateBarAndValueDimensions(props: Props): BarAndValueDimensi
};
}
export function getCellColor(
positionValue: TimeSeriesValue,
value: Props['value'],
display: Props['display']
): CellColors {
if (positionValue === null) {
return {
background: FALLBACK_COLOR,
border: FALLBACK_COLOR,
};
}
const color = display ? display(positionValue).color : null;
if (color) {
// if we are past real value the cell is not "on"
if (value === null || isNaN(value.numeric) || (positionValue !== null && positionValue > value.numeric)) {
return {
background: tinycolor(color).setAlpha(0.18).toRgbString(),
border: 'transparent',
isLit: false,
};
} else {
return {
background: tinycolor(color).setAlpha(0.95).toRgbString(),
backgroundShade: tinycolor(color).setAlpha(0.55).toRgbString(),
border: tinycolor(color).setAlpha(0.9).toRgbString(),
isLit: true,
};
}
}
return {
background: FALLBACK_COLOR,
border: FALLBACK_COLOR,
};
}
export function getValuePercent(value: number, minValue: number, maxValue: number): number {
return Math.min((value - minValue) / (maxValue - minValue), 1);
}
@@ -1,4 +1,6 @@
export class SelectionReference {
import { VirtualElement } from '@popperjs/core/lib/types';
export class SelectionReference implements VirtualElement {
getBoundingClientRect() {
const selection = window.getSelection();
const node = selection && selection.anchorNode;
@@ -15,7 +17,7 @@ export class SelectionReference {
right: 0,
width: 0,
height: 0,
};
} as DOMRect;
}
get clientWidth() {
@@ -18,7 +18,7 @@ import {
GraphFieldConfig,
GraphGradientMode,
LineInterpolation,
PointVisibility,
VisibilityMode,
StackingMode,
} from '@grafana/schema';
@@ -48,7 +48,7 @@ function mockDataFrame() {
spanNulls: false,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
showPoints: VisibilityMode.Always,
stacking: {
group: 'A',
mode: StackingMode.Normal,
@@ -72,7 +72,7 @@ function mockDataFrame() {
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
showPoints: VisibilityMode.Always,
stacking: {
group: 'A',
mode: StackingMode.Normal,
@@ -96,7 +96,7 @@ function mockDataFrame() {
spanNulls: false,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
showPoints: VisibilityMode.Always,
stacking: {
group: 'B',
mode: StackingMode.Normal,
@@ -119,7 +119,7 @@ function mockDataFrame() {
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
showPoints: VisibilityMode.Always,
stacking: {
group: 'B',
mode: StackingMode.Normal,
@@ -142,7 +142,7 @@ function mockDataFrame() {
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: PointVisibility.Always,
showPoints: VisibilityMode.Always,
stacking: {
group: 'B',
mode: StackingMode.None,
@@ -125,6 +125,7 @@ const getStyles = (theme: GrafanaTheme2) => {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin: 0;
max-width: 240px;
border-radius: 2px;
@@ -13,7 +13,7 @@ import {
AxisPlacement,
GraphDrawStyle,
GraphFieldConfig,
PointVisibility,
VisibilityMode,
ScaleDirection,
ScaleOrientation,
} from '@grafana/schema';
@@ -39,7 +39,7 @@ interface State {
const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line,
showPoints: PointVisibility.Auto,
showPoints: VisibilityMode.Auto,
axisPlacement: AxisPlacement.Hidden,
};
@@ -163,7 +163,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
const colorMode = getFieldColorModeForField(field);
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
const pointsMode =
customConfig.drawStyle === GraphDrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints;
builder.addSeries({
pxAlign: false,
@@ -20,7 +20,7 @@ import {
GraphDrawStyle,
GraphFieldConfig,
GraphTresholdsStyleMode,
PointVisibility,
VisibilityMode,
ScaleDirection,
ScaleOrientation,
} from '@grafana/schema';
@@ -31,7 +31,7 @@ const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line,
showPoints: PointVisibility.Auto,
showPoints: VisibilityMode.Auto,
axisPlacement: AxisPlacement.Auto,
};
@@ -154,7 +154,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
}
const showPoints =
customConfig.drawStyle === GraphDrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints;
let pointsFilter: uPlot.Series.Points.Filter = () => null;
@@ -6,7 +6,7 @@ import {
GraphGradientMode,
GraphTresholdsStyleMode,
LineInterpolation,
PointVisibility,
VisibilityMode,
StackingMode,
} from '@grafana/schema';
@@ -34,10 +34,10 @@ export const graphFieldOptions = {
] as Array<SelectableValue<BarAlignment>>,
showPoints: [
{ label: 'Auto', value: PointVisibility.Auto, description: 'Show points when the density is low' },
{ label: 'Always', value: PointVisibility.Always },
{ label: 'Never', value: PointVisibility.Never },
] as Array<SelectableValue<PointVisibility>>,
{ label: 'Auto', value: VisibilityMode.Auto, description: 'Show points when the density is low' },
{ label: 'Always', value: VisibilityMode.Always },
{ label: 'Never', value: VisibilityMode.Never },
] as Array<SelectableValue<VisibilityMode>>,
axisPlacement: [
{ label: 'Auto', value: AxisPlacement.Auto, description: 'First field on the left, everything else on the right' },
@@ -5,7 +5,7 @@ import {
GraphGradientMode,
AxisPlacement,
GraphDrawStyle,
PointVisibility,
VisibilityMode,
ScaleOrientation,
ScaleDirection,
GraphTresholdsStyleMode,
@@ -479,7 +479,7 @@ describe('UPlotConfigBuilder', () => {
scaleKey: 'scale-x',
fillOpacity: 50,
gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto,
showPoints: VisibilityMode.Auto,
pointSize: 5,
lineColor: '#0000ff',
lineWidth: 1,
@@ -544,7 +544,7 @@ describe('UPlotConfigBuilder', () => {
scaleKey: 'scale-x',
fillOpacity: 50,
gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto,
showPoints: VisibilityMode.Auto,
lineColor: '#0000ff',
lineWidth: 1,
spanNulls: false,
@@ -555,7 +555,7 @@ describe('UPlotConfigBuilder', () => {
scaleKey: 'scale-x',
fillOpacity: 50,
gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto,
showPoints: VisibilityMode.Auto,
pointSize: 5,
lineColor: '#00ff00',
lineWidth: 1,
@@ -568,7 +568,7 @@ describe('UPlotConfigBuilder', () => {
scaleKey: 'scale-x',
fillOpacity: 50,
gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto,
showPoints: VisibilityMode.Auto,
pointSize: 5,
lineColor: '#ff0000',
lineWidth: 1,
@@ -17,7 +17,7 @@ import {
LineConfig,
LineInterpolation,
PointsConfig,
PointVisibility,
VisibilityMode,
} from '@grafana/schema';
import { PlotConfigBuilder } from '../types';
import { getHueGradientFn, getOpacityGradientFn, getScaleGradientFn } from './gradientFills';
@@ -118,13 +118,13 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
if (drawStyle === GraphDrawStyle.Points) {
pointsConfig.points!.show = true;
} else {
if (showPoints === PointVisibility.Auto) {
if (showPoints === VisibilityMode.Auto) {
if (drawStyle === GraphDrawStyle.Bars) {
pointsConfig.points!.show = false;
}
} else if (showPoints === PointVisibility.Never) {
} else if (showPoints === VisibilityMode.Never) {
pointsConfig.points!.show = false;
} else if (showPoints === PointVisibility.Always) {
} else if (showPoints === VisibilityMode.Always) {
pointsConfig.points!.show = true;
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ export {
GraphDrawStyle as DrawStyle,
// All exports
AxisPlacement,
PointVisibility,
VisibilityMode as PointVisibility,
LineInterpolation,
ScaleDistribution,
GraphGradientMode,
@@ -16,7 +16,7 @@ export {
ScaleDistributionConfig,
HideSeriesConfig,
BarAlignment,
BarValueVisibility,
VisibilityMode as BarValueVisibility,
ScaleOrientation,
ScaleDirection,
LineConfig,
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@jaegertracing/jaeger-ui-components",
"version": "8.2.0-pre",
"version": "8.3.0-pre",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -11,13 +11,13 @@
"devDependencies": {
"@wojtekmaj/enzyme-adapter-react-17": "0.6.2",
"enzyme": "3.11.0",
"typescript": "4.3.4"
"typescript": "4.4.3"
},
"dependencies": {
"@emotion/css": "11.1.3",
"@emotion/react": "11.1.5",
"@grafana/data": "8.2.0-pre",
"@grafana/ui": "8.2.0-pre",
"@grafana/data": "8.3.0-pre",
"@grafana/ui": "8.3.0-pre",
"@types/classnames": "^2.2.7",
"@types/deep-freeze": "^0.1.1",
"@types/hoist-non-react-statics": "^3.3.1",
+8 -3
View File
@@ -1,10 +1,15 @@
package api
import "github.com/grafana/grafana/pkg/models"
import (
"context"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
)
// updateDashboardACL updates a dashboard's ACL items.
//
// Stubbable by tests.
var updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error {
return hs.SQLStore.UpdateDashboardACL(dashID, items)
var updateDashboardACL = func(ctx context.Context, s dashboards.Store, dashID int64, items []*models.DashboardAcl) error {
return s.UpdateDashboardACLCtx(ctx, dashID, items)
}
+1 -1
View File
@@ -462,7 +462,7 @@ func PauseAlert(c *models.ReqContext, dto dtos.PauseAlertCommand) response.Respo
return response.Error(500, "Get Alert failed", err)
}
guardian := guardian.New(query.Result.DashboardId, c.OrgId, c.SignedInUser)
guardian := guardian.New(c.Req.Context(), query.Result.DashboardId, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
if err != nil {
return response.Error(500, "Error while checking permissions for Alert", err)
+1 -1
View File
@@ -265,7 +265,7 @@ func canSaveByDashboardID(c *models.ReqContext, dashboardID int64) (bool, error)
}
if dashboardID != 0 {
guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
guard := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err
}
+4 -3
View File
@@ -53,9 +53,9 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/profile/switch-org/:id", reqSignedInNoAnonymous, hs.ChangeActiveOrgAndRedirectToHome)
r.Get("/org/", reqOrgAdmin, hs.Index)
r.Get("/org/new", reqGrafanaAdmin, hs.Index)
r.Get("/datasources/", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesRead)), hs.Index)
r.Get("/datasources/new", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesCreate)), hs.Index)
r.Get("/datasources/edit/*", authorize(reqOrgAdmin, ac.EvalPermission(ActionDatasourcesRead)), hs.Index)
r.Get("/datasources/", authorize(reqOrgAdmin, dataSourcesConfigurationAccessEvaluator), hs.Index)
r.Get("/datasources/new", authorize(reqOrgAdmin, dataSourcesNewAccessEvaluator), hs.Index)
r.Get("/datasources/edit/*", authorize(reqOrgAdmin, dataSourcesEditAccessEvaluator), hs.Index)
r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), hs.Index)
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index)
@@ -438,6 +438,7 @@ func (hs *HTTPServer) registerRoutes() {
// POST Live data to be processed according to channel rules.
liveRoute.Post("/push/:streamId/:path", hs.LivePushGateway.HandlePath)
liveRoute.Get("/channel-rules", routing.Wrap(hs.Live.HandleChannelRulesListHTTP), reqOrgAdmin)
liveRoute.Get("/pipeline-entities", routing.Wrap(hs.Live.HandlePipelineEntitiesListHTTP), reqOrgAdmin)
liveRoute.Get("/remote-write-backends", routing.Wrap(hs.Live.HandleRemoteWriteBackendsListHTTP), reqOrgAdmin)
}
})
+10 -10
View File
@@ -91,7 +91,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
}
}
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(c.Req.Context(), dash.Id, c.OrgId, c.SignedInUser)
if canView, err := guardian.CanView(); err != nil || !canView {
return dashboardGuardianResponse(err)
}
@@ -175,7 +175,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
dash.Data.Set("version", dash.Version)
// load library panels JSON for this dashboard
err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(c, dash)
err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(c.Req.Context(), dash)
if err != nil {
return response.Error(500, "Error while loading library panels", err)
}
@@ -238,13 +238,13 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
return rsp
}
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(c.Req.Context(), dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
// disconnect all library elements for this dashboard
err := hs.LibraryElementService.DisconnectElementsFromDashboard(c, dash.Id)
err := hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.Id)
if err != nil {
hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
}
@@ -370,7 +370,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
}
// connect library panels for this dashboard after the dashboard is stored and has an ID
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dashboard)
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c.Req.Context(), c.SignedInUser, dashboard)
if err != nil {
return response.Error(500, "Error while connecting library panels", err)
}
@@ -508,7 +508,7 @@ func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *models.ReqContext
func GetDashboardVersions(c *models.ReqContext) response.Response {
dashID := c.ParamsInt64(":dashboardId")
guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
guardian := guardian.New(c.Req.Context(), dashID, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -547,7 +547,7 @@ func GetDashboardVersions(c *models.ReqContext) response.Response {
func GetDashboardVersion(c *models.ReqContext) response.Response {
dashID := c.ParamsInt64(":dashboardId")
guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
guardian := guardian.New(c.Req.Context(), dashID, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -584,13 +584,13 @@ func GetDashboardVersion(c *models.ReqContext) response.Response {
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
func CalculateDashboardDiff(c *models.ReqContext, apiOptions dtos.CalculateDiffOptions) response.Response {
guardianBase := guardian.New(apiOptions.Base.DashboardId, c.OrgId, c.SignedInUser)
guardianBase := guardian.New(c.Req.Context(), apiOptions.Base.DashboardId, c.OrgId, c.SignedInUser)
if canSave, err := guardianBase.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
if apiOptions.Base.DashboardId != apiOptions.New.DashboardId {
guardianNew := guardian.New(apiOptions.New.DashboardId, c.OrgId, c.SignedInUser)
guardianNew := guardian.New(c.Req.Context(), apiOptions.New.DashboardId, c.OrgId, c.SignedInUser)
if canSave, err := guardianNew.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -633,7 +633,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext, apiCmd dtos.
return rsp
}
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(c.Req.Context(), dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
+3 -3
View File
@@ -18,7 +18,7 @@ func (hs *HTTPServer) GetDashboardPermissionList(c *models.ReqContext) response.
return rsp
}
g := guardian.New(dashID, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), dashID, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
@@ -62,7 +62,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext, apiCmd dt
return rsp
}
g := guardian.New(dashID, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), dashID, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}
@@ -99,7 +99,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext, apiCmd dt
return response.Error(403, "Cannot remove own admin permission for a folder", nil)
}
if err := updateDashboardACL(hs, dashID, items); err != nil {
if err := updateDashboardACL(c.Req.Context(), hs.SQLStore, dashID, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) ||
errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) {
return response.Error(409, err.Error(), err)
+6 -4
View File
@@ -6,15 +6,17 @@ import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDashboardPermissionAPIEndpoint(t *testing.T) {
@@ -342,7 +344,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
updateDashboardACL = origUpdateDashboardACL
})
var gotItems []*models.DashboardAcl
updateDashboardACL = func(hs *HTTPServer, folderID int64, items []*models.DashboardAcl) error {
updateDashboardACL = func(_ context.Context, _ dashboards.Store, folderID int64, items []*models.DashboardAcl) error {
gotItems = items
return nil
}
@@ -368,7 +370,7 @@ func callUpdateDashboardPermissions(t *testing.T, sc *scenarioContext) {
t.Cleanup(func() {
updateDashboardACL = origUpdateDashboardACL
})
updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error {
updateDashboardACL = func(_ context.Context, _ dashboards.Store, dashID int64, items []*models.DashboardAcl) error {
return nil
}
+1 -1
View File
@@ -254,7 +254,7 @@ func DeleteDashboardSnapshot(c *models.ReqContext) response.Response {
dashboardID := query.Result.Dashboard.Get("id").MustInt64()
guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
guardian := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
canEdit, err := guardian.CanEdit()
if err != nil {
return response.Error(500, "Error while checking permissions for snapshot", err)
+39 -10
View File
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
dboards "github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -86,9 +87,37 @@ type testState struct {
dashQueries []*models.GetDashboardQuery
}
type usageStatsMock struct {
t *testing.T
metricsFuncs []usagestats.MetricsFunc
}
func (usm *usageStatsMock) RegisterMetricsFunc(fn usagestats.MetricsFunc) {
usm.metricsFuncs = append(usm.metricsFuncs, fn)
}
func (usm *usageStatsMock) GetUsageReport(_ context.Context) (usagestats.Report, error) {
all := make(map[string]interface{})
for _, fn := range usm.metricsFuncs {
fnMetrics, err := fn()
require.NoError(usm.t, err)
for name, value := range fnMetrics {
all[name] = value
}
}
return usagestats.Report{Metrics: all}, nil
}
func (usm *usageStatsMock) ShouldBeReported(_ string) bool {
return true
}
func (usm *usageStatsMock) RegisterSendReportCallback(_ usagestats.SendReportCallbackFunc) {}
func newTestLive(t *testing.T) *live.GrafanaLive {
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, nil, sqlstore.InitTestDB(t))
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, nil, sqlstore.InitTestDB(t), &usageStatsMock{t: t})
require.NoError(t, err)
return gLive
}
@@ -1233,7 +1262,7 @@ func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboard
type mockLibraryPanelService struct {
}
func (m *mockLibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
func (m *mockLibraryPanelService) LoadLibraryPanelsForDashboard(c context.Context, dash *models.Dashboard) error {
return nil
}
@@ -1241,42 +1270,42 @@ func (m *mockLibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Da
return nil
}
func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard) error {
return nil
}
func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard, folderID int64) error {
func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser *models.SignedInUser, dash *models.Dashboard, folderID int64) error {
return nil
}
type mockLibraryElementService struct {
}
func (l *mockLibraryElementService) CreateElement(c *models.ReqContext, cmd libraryelements.CreateLibraryElementCommand) (libraryelements.LibraryElementDTO, error) {
func (l *mockLibraryElementService) CreateElement(c context.Context, signedInUser *models.SignedInUser, cmd libraryelements.CreateLibraryElementCommand) (libraryelements.LibraryElementDTO, error) {
return libraryelements.LibraryElementDTO{}, nil
}
// GetElement gets an element from a UID.
func (l *mockLibraryElementService) GetElement(c *models.ReqContext, UID string) (libraryelements.LibraryElementDTO, error) {
func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser *models.SignedInUser, UID string) (libraryelements.LibraryElementDTO, error) {
return libraryelements.LibraryElementDTO{}, nil
}
// GetElementsForDashboard gets all connected elements for a specific dashboard.
func (l *mockLibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]libraryelements.LibraryElementDTO, error) {
func (l *mockLibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]libraryelements.LibraryElementDTO, error) {
return map[string]libraryelements.LibraryElementDTO{}, nil
}
// ConnectElementsToDashboard connects elements to a specific dashboard.
func (l *mockLibraryElementService) ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error {
func (l *mockLibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser *models.SignedInUser, elementUIDs []string, dashboardID int64) error {
return nil
}
// DisconnectElementsFromDashboard disconnects elements from a specific dashboard.
func (l *mockLibraryElementService) DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error {
func (l *mockLibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error {
return nil
}
// DeleteLibraryElementsInFolder deletes all elements for a specific folder.
func (l *mockLibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error {
func (l *mockLibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser *models.SignedInUser, folderUID string) error {
return nil
}
+5 -5
View File
@@ -44,7 +44,7 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response {
return apierrors.ToFolderErrorResponse(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
return response.JSON(200, toFolderDto(c.Req.Context(), g, folder))
}
@@ -55,7 +55,7 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
return apierrors.ToFolderErrorResponse(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
return response.JSON(200, toFolderDto(c.Req.Context(), g, folder))
}
@@ -73,7 +73,7 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext, cmd models.CreateFolder
}
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
return response.JSON(200, toFolderDto(c.Req.Context(), g, folder))
}
@@ -84,13 +84,13 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext, cmd models.UpdateFolder
return apierrors.ToFolderErrorResponse(err)
}
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), cmd.Result.Id, c.OrgId, c.SignedInUser)
return response.JSON(200, toFolderDto(c.Req.Context(), g, cmd.Result))
}
func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c, macaron.Params(c.Req)[":uid"])
err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, macaron.Params(c.Req)[":uid"])
if err != nil {
if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) {
return response.Error(403, "Folder could not be deleted because it contains library elements in use", err)
+5 -4
View File
@@ -4,6 +4,8 @@ import (
"errors"
"time"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/apierrors"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
@@ -11,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
macaron "gopkg.in/macaron.v1"
)
func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Response {
@@ -22,7 +23,7 @@ func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Res
return apierrors.ToFolderErrorResponse(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
return apierrors.ToFolderErrorResponse(models.ErrFolderAccessDenied)
@@ -69,7 +70,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos.
return apierrors.ToFolderErrorResponse(err)
}
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
canAdmin, err := g.CanAdmin()
if err != nil {
return apierrors.ToFolderErrorResponse(err)
@@ -112,7 +113,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos.
return response.Error(403, "Cannot remove own admin permission for a folder", nil)
}
if err := updateDashboardACL(hs, folder.Id, items); err != nil {
if err := updateDashboardACL(c.Req.Context(), hs.SQLStore, folder.Id, items); err != nil {
if errors.Is(err, models.ErrDashboardAclInfoMissing) {
err = models.ErrFolderAclInfoMissing
}
+7 -4
View File
@@ -1,20 +1,23 @@
package api
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
dashboardifaces "github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFolderPermissionAPIEndpoint(t *testing.T) {
@@ -360,7 +363,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
updateDashboardACL = origUpdateDashboardACL
})
var gotItems []*models.DashboardAcl
updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error {
updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, _ int64, items []*models.DashboardAcl) error {
gotItems = items
return nil
}
@@ -385,7 +388,7 @@ func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) {
t.Cleanup(func() {
updateDashboardACL = origUpdateDashboardACL
})
updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error {
updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, dashID int64, items []*models.DashboardAcl) error {
return nil
}
+1 -5
View File
@@ -22,7 +22,6 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
@@ -68,7 +67,6 @@ type HTTPServer struct {
httpSrv *http.Server
middlewares []macaron.Handler
UsageStatsService usagestats.UsageStats
PluginContextProvider *plugincontext.Provider
RouteRegister routing.RouteRegister
Bus bus.Bus
@@ -118,8 +116,7 @@ type ServerOptions struct {
func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routing.RouteRegister, bus bus.Bus,
renderService rendering.Service, licensing models.Licensing, hooksService *hooks.HooksService,
cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore,
dataService *tsdb.Service, alertEngine *alerting.AlertEngine,
usageStatsService *usagestats.UsageStatsService, pluginRequestValidator models.PluginRequestValidator,
dataService *tsdb.Service, alertEngine *alerting.AlertEngine, pluginRequestValidator models.PluginRequestValidator,
pluginManager plugins.Manager, backendPM backendplugin.Manager, settingsProvider setting.Provider,
dataSourceCache datasources.CacheService, userTokenService models.UserTokenService,
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service,
@@ -148,7 +145,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
SQLStore: sqlStore,
DataService: dataService,
AlertEngine: alertEngine,
UsageStatsService: usageStatsService,
PluginRequestValidator: pluginRequestValidator,
PluginManager: pluginManager,
BackendPluginManager: backendPM,
-10
View File
@@ -18,16 +18,6 @@ const (
darkName = "dark"
)
// dataSourcesConfigurationAccessEvaluator is used to protect the "Configure > Data sources" tab access
var dataSourcesConfigurationAccessEvaluator = ac.EvalAll(
ac.EvalPermission(ActionDatasourcesRead, ScopeDatasourcesAll),
ac.EvalAny(
ac.EvalPermission(ActionDatasourcesCreate),
ac.EvalPermission(ActionDatasourcesDelete),
ac.EvalPermission(ActionDatasourcesWrite),
),
)
func (hs *HTTPServer) getProfileNode(c *models.ReqContext) *dtos.NavLink {
// Only set login if it's different from the name
var login string
+2 -2
View File
@@ -224,12 +224,12 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa
return hs.dashboardSaveErrorToApiResponse(err)
}
err = hs.LibraryPanelService.ImportLibraryPanelsForDashboard(c, dash, apiCmd.FolderId)
err = hs.LibraryPanelService.ImportLibraryPanelsForDashboard(c.Req.Context(), c.SignedInUser, dash, apiCmd.FolderId)
if err != nil {
return response.Error(500, "Error while importing library panels", err)
}
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dash)
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c.Req.Context(), c.SignedInUser, dash)
if err != nil {
return response.Error(500, "Error while connecting library panels", err)
}
+4 -4
View File
@@ -14,7 +14,7 @@ func GetOrgQuotas(c *models.ReqContext) response.Response {
}
query := models.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
if err := bus.Dispatch(&query); err != nil {
if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get org quotas", err)
}
@@ -32,7 +32,7 @@ func UpdateOrgQuota(c *models.ReqContext, cmd models.UpdateOrgQuotaCmd) response
return response.Error(404, "Invalid quota target", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
if err := bus.DispatchCtx(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to update org quotas", err)
}
return response.Success("Organization quota updated")
@@ -44,7 +44,7 @@ func GetUserQuotas(c *models.ReqContext) response.Response {
}
query := models.GetUserQuotasQuery{UserId: c.ParamsInt64(":id")}
if err := bus.Dispatch(&query); err != nil {
if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get org quotas", err)
}
@@ -62,7 +62,7 @@ func UpdateUserQuota(c *models.ReqContext, cmd models.UpdateUserQuotaCmd) respon
return response.Error(404, "Invalid quota target", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
if err := bus.DispatchCtx(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to update org quotas", err)
}
return response.Success("Organization quota updated")
+26
View File
@@ -90,3 +90,29 @@ func (hs *HTTPServer) declareFixedRoles() error {
return hs.AccessControl.DeclareFixedRoles(registrations...)
}
// Evaluators
// here is the list of complex evaluators we use in this package
// dataSourcesConfigurationAccessEvaluator is used to protect the "Configure > Data sources" tab access
var dataSourcesConfigurationAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionDatasourcesRead, ScopeDatasourcesAll),
accesscontrol.EvalAny(
accesscontrol.EvalPermission(ActionDatasourcesCreate),
accesscontrol.EvalPermission(ActionDatasourcesDelete),
accesscontrol.EvalPermission(ActionDatasourcesWrite),
),
)
// dataSourcesNewAccessEvaluator is used to protect the "Configure > Data sources > New" page access
var dataSourcesNewAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionDatasourcesRead, ScopeDatasourcesAll),
accesscontrol.EvalPermission(ActionDatasourcesCreate),
accesscontrol.EvalPermission(ActionDatasourcesWrite),
)
// dataSourcesEditAccessEvaluator is used to protect the "Configure > Data sources > Edit" page access
var dataSourcesEditAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionDatasourcesRead, ScopeDatasourcesAll),
accesscontrol.EvalPermission(ActionDatasourcesWrite),
)
-87
View File
@@ -1,87 +0,0 @@
package gtime
import (
"fmt"
"regexp"
"strconv"
"time"
)
var dateUnitPattern = regexp.MustCompile(`^(\d+)([dwMy])$`)
// ParseInterval parses an interval with support for all units that Grafana uses.
// An interval is relative to the current wall time.
func ParseInterval(inp string) (time.Duration, error) {
dur, period, err := parse(inp)
if err != nil {
return 0, err
}
if period == "" {
return dur, nil
}
num := int(dur)
// Use UTC to ensure that the interval is deterministic, and daylight saving
// doesn't cause surprises
now := time.Now().UTC()
switch period {
case "d":
return now.AddDate(0, 0, num).Sub(now), nil
case "w":
return now.AddDate(0, 0, num*7).Sub(now), nil
case "M":
return now.AddDate(0, num, 0).Sub(now), nil
case "y":
return now.AddDate(num, 0, 0).Sub(now), nil
}
return 0, fmt.Errorf("invalid interval %q", inp)
}
// ParseDuration parses a duration with support for all units that Grafana uses.
// Durations are independent of wall time.
func ParseDuration(inp string) (time.Duration, error) {
dur, period, err := parse(inp)
if err != nil {
return 0, err
}
if period == "" {
return dur, nil
}
// The average number of days in a year, using the Julian calendar
const daysInAYear = 365.25
const day = 24 * time.Hour
const week = 7 * day
const year = time.Duration(float64(day) * daysInAYear)
const month = time.Duration(float64(year) / 12)
switch period {
case "d":
return dur * day, nil
case "w":
return dur * week, nil
case "M":
return dur * month, nil
case "y":
return dur * year, nil
}
return 0, fmt.Errorf("invalid duration %q", inp)
}
func parse(inp string) (time.Duration, string, error) {
result := dateUnitPattern.FindSubmatch([]byte(inp))
if len(result) != 3 {
dur, err := time.ParseDuration(inp)
return dur, "", err
}
num, err := strconv.Atoi(string(result[1]))
if err != nil {
return 0, "", err
}
return time.Duration(num), string(result[2]), nil
}
-102
View File
@@ -1,102 +0,0 @@
package gtime
import (
"fmt"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestParseInterval(t *testing.T) {
daysInMonth, daysInYear := calculateDays()
tcs := []struct {
inp string
duration time.Duration
err *regexp.Regexp
}{
{inp: "1d", duration: 24 * time.Hour},
{inp: "1w", duration: 168 * time.Hour},
{inp: "2w", duration: 2 * 168 * time.Hour},
{inp: "1M", duration: time.Duration(daysInMonth * 24 * int(time.Hour))},
{inp: "1y", duration: time.Duration(daysInYear * 24 * int(time.Hour))},
{inp: "5y", duration: time.Duration(calculateDays5y() * 24 * int(time.Hour))},
{inp: "invalid-duration", err: regexp.MustCompile(`^time: invalid duration "?invalid-duration"?$`)},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) {
res, err := ParseInterval(tc.inp)
if tc.err == nil {
require.NoError(t, err, "input %q", tc.inp)
require.Equal(t, tc.duration, res, "input %q", tc.inp)
} else {
require.Error(t, err, "input %q", tc.inp)
require.Regexp(t, tc.err, err.Error())
}
})
}
}
func TestParseDuration(t *testing.T) {
tcs := []struct {
inp string
duration time.Duration
err *regexp.Regexp
}{
{inp: "1s", duration: time.Second},
{inp: "1m", duration: time.Minute},
{inp: "1h", duration: time.Hour},
{inp: "1d", duration: 24 * time.Hour},
{inp: "1w", duration: 7 * 24 * time.Hour},
{inp: "2w", duration: 2 * 7 * 24 * time.Hour},
{inp: "1M", duration: time.Duration(730.5 * float64(time.Hour))},
{inp: "1y", duration: 365.25 * 24 * time.Hour},
{inp: "5y", duration: 5 * 365.25 * 24 * time.Hour},
{inp: "invalid-duration", err: regexp.MustCompile(`^time: invalid duration "?invalid-duration"?$`)},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) {
res, err := ParseDuration(tc.inp)
if tc.err == nil {
require.NoError(t, err, "input %q", tc.inp)
require.Equal(t, tc.duration, res, "input %q", tc.inp)
} else {
require.Error(t, err, "input %q", tc.inp)
require.Regexp(t, tc.err, err.Error())
}
})
}
}
func calculateDays() (int, int) {
now := time.Now().UTC()
currentYear, currentMonth, _ := now.Date()
firstDayOfMonth := time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, time.UTC)
daysInMonth := firstDayOfMonth.AddDate(0, 1, -1).Day()
t1 := time.Date(currentYear, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(currentYear+1, 1, 1, 0, 0, 0, 0, time.UTC)
daysInYear := int(t2.Sub(t1).Hours() / 24)
return daysInMonth, daysInYear
}
func calculateDays5y() int {
now := time.Now().UTC()
currentYear, _, _ := now.Date()
var daysInYear int
for i := 0; i < 5; i++ {
t1 := time.Date(currentYear+i, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(currentYear+i+1, 1, 1, 0, 0, 0, 0, time.UTC)
daysInYear = daysInYear + int(t2.Sub(t1).Hours()/24)
}
return daysInYear
}
+6 -2
View File
@@ -1,6 +1,10 @@
package dashboards
import "github.com/grafana/grafana/pkg/models"
import (
"context"
"github.com/grafana/grafana/pkg/models"
)
// Store is a dashboard store.
type Store interface {
@@ -12,7 +16,7 @@ type Store interface {
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)
UpdateDashboardACL(uid int64, items []*models.DashboardAcl) error
UpdateDashboardACLCtx(ctx context.Context, uid int64, items []*models.DashboardAcl) error
// SaveAlerts saves dashboard alerts.
SaveAlerts(dashID int64, alerts []*models.Alert) error
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/expr/mathexp"
)
+5 -4
View File
@@ -1,15 +1,16 @@
package remotecache
import (
"context"
"crypto/tls"
"fmt"
"strconv"
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
redis "gopkg.in/redis.v5"
)
const redisCacheType = "redis"
@@ -91,13 +92,13 @@ func (s *redisStorage) Set(key string, val interface{}, expires time.Duration) e
if err != nil {
return err
}
status := s.c.Set(key, string(value), expires)
status := s.c.Set(context.TODO(), key, string(value), expires)
return status.Err()
}
// Get gets value by given key in session.
func (s *redisStorage) Get(key string) (interface{}, error) {
v := s.c.Get(key)
v := s.c.Get(context.TODO(), key)
item := &cachedItem{}
err := decodeGob([]byte(v.Val()), item)
@@ -113,6 +114,6 @@ func (s *redisStorage) Get(key string) (interface{}, error) {
// Delete delete a key from session.
func (s *redisStorage) Delete(key string) error {
cmd := s.c.Del(key)
cmd := s.c.Del(context.TODO(), key)
return cmd.Err()
}
+1 -1
View File
@@ -5,8 +5,8 @@ import (
"fmt"
"testing"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
redis "gopkg.in/redis.v5"
)
func Test_parseRedisConnStr(t *testing.T) {
+15 -157
View File
@@ -2,168 +2,26 @@ package usagestats
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type UsageStats interface {
GetUsageReport(context.Context) (UsageReport, error)
RegisterMetricsFunc(MetricsFunc)
ShouldBeReported(string) bool
type Report struct {
Version string `json:"version"`
Metrics map[string]interface{} `json:"metrics"`
Os string `json:"os"`
Arch string `json:"arch"`
Edition string `json:"edition"`
HasValidLicense bool `json:"hasValidLicense"`
Packaging string `json:"packaging"`
UsageStatsId string `json:"usageStatsId"`
}
type MetricsFunc func() (map[string]interface{}, error)
type UsageStatsService struct {
Cfg *setting.Cfg
Bus bus.Bus
SQLStore *sqlstore.SQLStore
AlertingUsageStats alerting.UsageStatsQuerier
PluginManager plugins.Manager
SocialService social.Service
grafanaLive *live.GrafanaLive
kvStore *kvstore.NamespacedKVStore
type SendReportCallbackFunc func()
log log.Logger
oauthProviders map[string]bool
externalMetrics []MetricsFunc
concurrentUserStatsCache memoConcurrentUserStats
liveStats liveUsageStats
startTime time.Time
}
type liveUsageStats struct {
numClientsMax int
numClientsMin int
numClientsSum int
numUsersMax int
numUsersMin int
numUsersSum int
sampleCount int
}
func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore,
alertingStats alerting.UsageStatsQuerier, pluginManager plugins.Manager,
socialService social.Service, grafanaLive *live.GrafanaLive,
kvStore kvstore.KVStore) *UsageStatsService {
s := &UsageStatsService{
Cfg: cfg,
Bus: bus,
SQLStore: sqlStore,
AlertingUsageStats: alertingStats,
oauthProviders: socialService.GetOAuthProviders(),
PluginManager: pluginManager,
grafanaLive: grafanaLive,
kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
startTime: time.Now(),
}
return s
}
func (uss *UsageStatsService) Run(ctx context.Context) error {
uss.updateTotalStats()
// try to load last sent time from kv store
lastSent := time.Now()
if val, ok, err := uss.kvStore.Get(ctx, "last_sent"); err != nil {
uss.log.Error("Failed to get last sent time", "error", err)
} else if ok {
if parsed, err := time.Parse(time.RFC3339, val); err != nil {
uss.log.Error("Failed to parse last sent time", "error", err)
} else {
lastSent = parsed
}
}
// calculate initial send delay
sendInterval := time.Hour * 24
nextSendInterval := time.Until(lastSent.Add(sendInterval))
if nextSendInterval < time.Minute {
nextSendInterval = time.Minute
}
sendReportTicker := time.NewTicker(nextSendInterval)
updateStatsTicker := time.NewTicker(time.Minute * 30)
defer sendReportTicker.Stop()
defer updateStatsTicker.Stop()
for {
select {
case <-sendReportTicker.C:
if err := uss.sendUsageStats(ctx); err != nil {
uss.log.Warn("Failed to send usage stats", "error", err)
}
lastSent = time.Now()
if err := uss.kvStore.Set(ctx, "last_sent", lastSent.Format(time.RFC3339)); err != nil {
uss.log.Warn("Failed to update last sent time", "error", err)
}
if nextSendInterval != sendInterval {
nextSendInterval = sendInterval
sendReportTicker.Reset(nextSendInterval)
}
// always reset live stats every report tick
uss.resetLiveStats()
case <-updateStatsTicker.C:
uss.updateTotalStats()
uss.sampleLiveStats()
case <-ctx.Done():
return ctx.Err()
}
}
}
type memoConcurrentUserStats struct {
stats *concurrentUsersStats
memoized time.Time
}
const concurrentUserStatsCacheLifetime = time.Hour
func (uss *UsageStatsService) GetConcurrentUsersStats(ctx context.Context) (*concurrentUsersStats, error) {
memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime)
if !uss.concurrentUserStatsCache.memoized.Before(memoizationPeriod) {
return uss.concurrentUserStatsCache.stats, nil
}
uss.concurrentUserStatsCache.stats = &concurrentUsersStats{}
err := uss.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
// Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive.
rawSQL := `
SELECT
COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3,
COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6,
COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9,
COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12,
COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15,
COUNT(1) AS bucket_le_inf
FROM (select count(1) as tokens from user_auth_token group by user_id) uat;`
_, err := sess.SQL(rawSQL).Get(uss.concurrentUserStatsCache.stats)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err)
}
uss.concurrentUserStatsCache.memoized = time.Now()
return uss.concurrentUserStatsCache.stats, nil
type Service interface {
GetUsageReport(context.Context) (Report, error)
RegisterMetricsFunc(MetricsFunc)
RegisterSendReportCallback(SendReportCallbackFunc)
ShouldBeReported(string) bool
}
+149
View File
@@ -0,0 +1,149 @@
package service
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type UsageStats struct {
Cfg *setting.Cfg
Bus bus.Bus
SQLStore *sqlstore.SQLStore
PluginManager plugins.Manager
SocialService social.Service
kvStore *kvstore.NamespacedKVStore
log log.Logger
oauthProviders map[string]bool
externalMetrics []usagestats.MetricsFunc
concurrentUserStatsCache memoConcurrentUserStats
startTime time.Time
sendReportCallbacks []usagestats.SendReportCallbackFunc
}
func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, pluginManager plugins.Manager,
socialService social.Service, kvStore kvstore.KVStore) *UsageStats {
s := &UsageStats{
Cfg: cfg,
Bus: bus,
SQLStore: sqlStore,
oauthProviders: socialService.GetOAuthProviders(),
PluginManager: pluginManager,
kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
startTime: time.Now(),
}
return s
}
func (uss *UsageStats) Run(ctx context.Context) error {
uss.updateTotalStats()
// try to load last sent time from kv store
lastSent := time.Now()
if val, ok, err := uss.kvStore.Get(ctx, "last_sent"); err != nil {
uss.log.Error("Failed to get last sent time", "error", err)
} else if ok {
if parsed, err := time.Parse(time.RFC3339, val); err != nil {
uss.log.Error("Failed to parse last sent time", "error", err)
} else {
lastSent = parsed
}
}
// calculate initial send delay
sendInterval := time.Hour * 24
nextSendInterval := time.Until(lastSent.Add(sendInterval))
if nextSendInterval < time.Minute {
nextSendInterval = time.Minute
}
sendReportTicker := time.NewTicker(nextSendInterval)
updateStatsTicker := time.NewTicker(time.Minute * 30)
defer sendReportTicker.Stop()
defer updateStatsTicker.Stop()
for {
select {
case <-sendReportTicker.C:
if err := uss.sendUsageStats(ctx); err != nil {
uss.log.Warn("Failed to send usage stats", "error", err)
}
lastSent = time.Now()
if err := uss.kvStore.Set(ctx, "last_sent", lastSent.Format(time.RFC3339)); err != nil {
uss.log.Warn("Failed to update last sent time", "error", err)
}
if nextSendInterval != sendInterval {
nextSendInterval = sendInterval
sendReportTicker.Reset(nextSendInterval)
}
for _, callback := range uss.sendReportCallbacks {
callback()
}
case <-updateStatsTicker.C:
uss.updateTotalStats()
case <-ctx.Done():
return ctx.Err()
}
}
}
type memoConcurrentUserStats struct {
stats *concurrentUsersStats
memoized time.Time
}
const concurrentUserStatsCacheLifetime = time.Hour
func (uss *UsageStats) GetConcurrentUsersStats(ctx context.Context) (*concurrentUsersStats, error) {
memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime)
if !uss.concurrentUserStatsCache.memoized.Before(memoizationPeriod) {
return uss.concurrentUserStatsCache.stats, nil
}
uss.concurrentUserStatsCache.stats = &concurrentUsersStats{}
err := uss.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
// Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive.
rawSQL := `
SELECT
COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3,
COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6,
COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9,
COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12,
COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15,
COUNT(1) AS bucket_le_inf
FROM (select count(1) as tokens from user_auth_token group by user_id) uat;`
_, err := sess.SQL(rawSQL).Get(uss.concurrentUserStatsCache.stats)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err)
}
uss.concurrentUserStatsCache.memoized = time.Now()
return uss.concurrentUserStatsCache.stats, nil
}
func (uss *UsageStats) RegisterSendReportCallback(c usagestats.SendReportCallbackFunc) {
uss.sendReportCallbacks = append(uss.sendReportCallbacks, c)
}
@@ -1,4 +1,4 @@
package usagestats
package service
type concurrentUsersStats struct {
BucketLE3 int32 `xorm:"bucket_le_3"`
@@ -1,4 +1,4 @@
package usagestats
package service
import (
"bytes"
@@ -12,23 +12,13 @@ import (
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
)
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
type UsageReport struct {
Version string `json:"version"`
Metrics map[string]interface{} `json:"metrics"`
Os string `json:"os"`
Arch string `json:"arch"`
Edition string `json:"edition"`
HasValidLicense bool `json:"hasValidLicense"`
Packaging string `json:"packaging"`
UsageStatsId string `json:"usageStatsId"`
}
func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, error) {
func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, error) {
version := strings.ReplaceAll(uss.Cfg.BuildVersion, ".", "_")
metrics := map[string]interface{}{}
@@ -37,7 +27,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
if uss.Cfg.IsEnterprise {
edition = "enterprise"
}
report := UsageReport{
report := usagestats.Report{
Version: version,
Metrics: metrics,
Os: runtime.GOOS,
@@ -93,20 +83,6 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
metrics["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit
metrics["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin
liveUsersAvg := 0
liveClientsAvg := 0
if uss.liveStats.sampleCount > 0 {
liveUsersAvg = uss.liveStats.numUsersSum / uss.liveStats.sampleCount
liveClientsAvg = uss.liveStats.numClientsSum / uss.liveStats.sampleCount
}
metrics["stats.live_samples.count"] = uss.liveStats.sampleCount
metrics["stats.live_users_max.count"] = uss.liveStats.numUsersMax
metrics["stats.live_users_min.count"] = uss.liveStats.numUsersMin
metrics["stats.live_users_avg.count"] = liveUsersAvg
metrics["stats.live_clients_max.count"] = uss.liveStats.numClientsMax
metrics["stats.live_clients_min.count"] = uss.liveStats.numClientsMin
metrics["stats.live_clients_avg.count"] = liveClientsAvg
ossEditionCount := 1
enterpriseEditionCount := 0
if uss.Cfg.IsEnterprise {
@@ -174,28 +150,6 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
metrics["stats.packaging."+uss.Cfg.Packaging+".count"] = 1
metrics["stats.distributor."+uss.Cfg.ReportingDistributor+".count"] = 1
// Alerting stats
alertingUsageStats, err := uss.AlertingUsageStats.QueryUsageStats()
if err != nil {
uss.log.Error("Failed to get alerting usage stats", "error", err)
return report, err
}
var addAlertingUsageStats = func(dsType string, usageCount int) {
metrics[fmt.Sprintf("stats.alerting.ds.%s.count", dsType)] = usageCount
}
alertingOtherCount := 0
for dsType, usageCount := range alertingUsageStats.DatasourceUsage {
if uss.ShouldBeReported(dsType) {
addAlertingUsageStats(dsType, usageCount)
} else {
alertingOtherCount += usageCount
}
}
addAlertingUsageStats("other", alertingOtherCount)
// fetch datasource access stats
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := uss.Bus.Dispatch(&dsAccessStats); err != nil {
@@ -276,7 +230,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
return report, nil
}
func (uss *UsageStatsService) registerExternalMetrics(metrics map[string]interface{}) {
func (uss *UsageStats) registerExternalMetrics(metrics map[string]interface{}) {
for _, fn := range uss.externalMetrics {
fnMetrics, err := fn()
if err != nil {
@@ -290,11 +244,11 @@ func (uss *UsageStatsService) registerExternalMetrics(metrics map[string]interfa
}
}
func (uss *UsageStatsService) RegisterMetricsFunc(fn MetricsFunc) {
func (uss *UsageStats) RegisterMetricsFunc(fn usagestats.MetricsFunc) {
uss.externalMetrics = append(uss.externalMetrics, fn)
}
func (uss *UsageStatsService) sendUsageStats(ctx context.Context) error {
func (uss *UsageStats) sendUsageStats(ctx context.Context) error {
if !uss.Cfg.ReportingEnabled {
return nil
}
@@ -319,7 +273,7 @@ func (uss *UsageStatsService) sendUsageStats(ctx context.Context) error {
// sendUsageStats sends usage statistics.
//
// Stubbable by tests.
var sendUsageStats = func(uss *UsageStatsService, data *bytes.Buffer) {
var sendUsageStats = func(uss *UsageStats, data *bytes.Buffer) {
go func() {
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Post(usageStatsURL, "application/json", data)
@@ -333,35 +287,7 @@ var sendUsageStats = func(uss *UsageStatsService, data *bytes.Buffer) {
}()
}
func (uss *UsageStatsService) sampleLiveStats() {
current := uss.grafanaLive.UsageStats()
uss.liveStats.sampleCount++
uss.liveStats.numClientsSum += current.NumClients
uss.liveStats.numUsersSum += current.NumUsers
if current.NumClients > uss.liveStats.numClientsMax {
uss.liveStats.numClientsMax = current.NumClients
}
if current.NumClients < uss.liveStats.numClientsMin {
uss.liveStats.numClientsMin = current.NumClients
}
if current.NumUsers > uss.liveStats.numUsersMax {
uss.liveStats.numUsersMax = current.NumUsers
}
if current.NumUsers < uss.liveStats.numUsersMin {
uss.liveStats.numUsersMin = current.NumUsers
}
}
func (uss *UsageStatsService) resetLiveStats() {
uss.liveStats = liveUsageStats{}
}
func (uss *UsageStatsService) updateTotalStats() {
func (uss *UsageStats) updateTotalStats() {
if !uss.Cfg.MetricsEndpointEnabled || uss.Cfg.MetricsEndpointDisableTotalStats {
return
}
@@ -401,7 +327,7 @@ func (uss *UsageStatsService) updateTotalStats() {
}
}
func (uss *UsageStatsService) ShouldBeReported(dsType string) bool {
func (uss *UsageStats) ShouldBeReported(dsType string) bool {
ds := uss.PluginManager.GetDataSource(dsType)
if ds == nil {
return false
@@ -410,7 +336,7 @@ func (uss *UsageStatsService) ShouldBeReported(dsType string) bool {
return ds.Signature.IsValid() || ds.Signature.IsInternal()
}
func (uss *UsageStatsService) GetUsageStatsId(ctx context.Context) string {
func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string {
anonId, ok, err := uss.kvStore.Get(ctx, "anonymous_id")
if err != nil {
uss.log.Error("Failed to get usage stats id", "error", err)
@@ -1,4 +1,4 @@
package usagestats
package service
import (
"context"
@@ -19,7 +19,7 @@ import (
func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
uss := &UsageStatsService{
uss := &UsageStats{
Bus: bus.New(),
SQLStore: sqlStore,
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
@@ -1,4 +1,4 @@
package usagestats
package service
import (
"bytes"
@@ -11,16 +11,14 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
@@ -29,10 +27,10 @@ import (
// This is to ensure that the interface contract is held by the implementation
func Test_InterfaceContractValidity(t *testing.T) {
newUsageStats := func() UsageStats {
return &UsageStatsService{}
newUsageStats := func() usagestats.Service {
return &UsageStats{}
}
v, ok := newUsageStats().(*UsageStatsService)
v, ok := newUsageStats().(*UsageStats)
assert.NotNil(t, v)
assert.True(t, ok)
@@ -201,7 +199,6 @@ func TestMetrics(t *testing.T) {
})
createConcurrentTokens(t, uss.SQLStore)
uss.AlertingUsageStats = &alertingUsageMock{}
uss.oauthProviders = map[string]bool{
"github": true,
@@ -221,7 +218,7 @@ func TestMetrics(t *testing.T) {
sendUsageStats = origSendUsageStats
})
statsSent := false
sendUsageStats = func(uss *UsageStatsService, b *bytes.Buffer) {
sendUsageStats = func(uss *UsageStats, b *bytes.Buffer) {
statsSent = true
}
@@ -371,11 +368,6 @@ func TestMetrics(t *testing.T) {
assert.Equal(t, 6+7, metrics.Get("stats.ds_access.other.direct.count").MustInt())
assert.Equal(t, 4+8, metrics.Get("stats.ds_access.other.proxy.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.alerting.ds.prometheus.count").MustInt())
assert.Equal(t, 2, metrics.Get("stats.alerting.ds.graphite.count").MustInt())
assert.Equal(t, 5, metrics.Get("stats.alerting.ds.mysql.count").MustInt())
assert.Equal(t, 90, metrics.Get("stats.alerting.ds.other.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.alert_notifiers.slack.count").MustInt())
assert.Equal(t, 2, metrics.Get("stats.alert_notifiers.webhook.count").MustInt())
@@ -560,19 +552,6 @@ func TestMetrics(t *testing.T) {
})
}
type alertingUsageMock struct{}
func (aum *alertingUsageMock) QueryUsageStats() (*alerting.UsageStats, error) {
return &alerting.UsageStats{
DatasourceUsage: map[string]int{
"prometheus": 1,
"graphite": 2,
"mysql": 5,
"unknown-datasource": 90,
},
}, nil
}
type fakePluginManager struct {
manager.PluginManager
@@ -592,7 +571,7 @@ func (pm *fakePluginManager) PanelCount() int {
return len(pm.panels)
}
func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStatsService) {
func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStats) {
t.Helper()
uss.PluginManager = &fakePluginManager{
@@ -635,28 +614,19 @@ type httpResp struct {
err error
}
func createService(t *testing.T, cfg setting.Cfg) *UsageStatsService {
func createService(t *testing.T, cfg setting.Cfg) *UsageStats {
t.Helper()
sqlStore := sqlstore.InitTestDB(t)
return &UsageStatsService{
Bus: bus.New(),
Cfg: &cfg,
SQLStore: sqlStore,
AlertingUsageStats: &alertingUsageMock{},
externalMetrics: make([]MetricsFunc, 0),
PluginManager: &fakePluginManager{},
grafanaLive: newTestLive(t),
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
startTime: time.Now().Add(-1 * time.Minute),
return &UsageStats{
Bus: bus.New(),
Cfg: &cfg,
SQLStore: sqlStore,
externalMetrics: make([]usagestats.MetricsFunc, 0),
PluginManager: &fakePluginManager{},
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
startTime: time.Now().Add(-1 * time.Minute),
}
}
func newTestLive(t *testing.T) *live.GrafanaLive {
cfg := &setting.Cfg{AppURL: "http://localhost:3000/"}
gLive, err := live.ProvideService(nil, cfg, routing.NewRouteRegister(), nil, nil, nil, nil, sqlstore.InitTestDB(t))
require.NoError(t, err)
return gLive
}
+1 -1
View File
@@ -15,9 +15,9 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
+4 -4
View File
@@ -16,7 +16,7 @@ import (
func TestMiddlewareQuota(t *testing.T) {
t.Run("With user not logged in", func(t *testing.T) {
middlewareScenario(t, "and global quota not reached", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
@@ -33,7 +33,7 @@ func TestMiddlewareQuota(t *testing.T) {
}, configure)
middlewareScenario(t, "and global quota reached", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
@@ -53,7 +53,7 @@ func TestMiddlewareQuota(t *testing.T) {
})
middlewareScenario(t, "and global session quota not reached", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
@@ -101,7 +101,7 @@ func TestMiddlewareQuota(t *testing.T) {
}, nil
}
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
bus.AddHandlerCtx("globalQuota", func(_ context.Context, query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
@@ -5,7 +5,7 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
"github.com/grafana/grafana/pkg/models"
backendmanager "github.com/grafana/grafana/pkg/plugins/backendplugin/manager"
"github.com/grafana/grafana/pkg/plugins/manager"
@@ -42,7 +42,7 @@ func ProvideBackgroundServiceRegistry(
rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService,
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, pm *manager.PluginManager,
backendPM *backendmanager.Manager, metrics *metrics.InternalMetricsService,
usageStats *usagestats.UsageStatsService, tracing *tracing.TracingService, remoteCache *remotecache.RemoteCache,
usageStats *uss.UsageStats, tracing *tracing.TracingService, remoteCache *remotecache.RemoteCache,
// Need to make sure these are initialized, is there a better place to put them?
_ *azuremonitor.Service, _ *cloudwatch.CloudWatchService, _ *elasticsearch.Service, _ *graphite.Service,
_ *influxdb.Service, _ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service,
+3 -2
View File
@@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@@ -87,8 +88,8 @@ var wireBasicSet = wire.NewSet(
hooks.ProvideService,
kvstore.ProvideService,
localcache.ProvideService,
usagestats.ProvideService,
wire.Bind(new(usagestats.UsageStats), new(*usagestats.UsageStatsService)),
uss.ProvideService,
wire.Bind(new(usagestats.Service), new(*uss.UsageStats)),
manager.ProvideService,
wire.Bind(new(plugins.Manager), new(*manager.PluginManager)),
backendmanager.ProvideService,
+65 -6
View File
@@ -1,6 +1,7 @@
package accesscontrol
import (
"encoding/json"
"time"
)
@@ -11,9 +12,12 @@ type RoleRegistration struct {
Grants []string
}
// Role is the model for Role in RBAC.
type Role struct {
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
OrgID int64 `json:"-" xorm:"org_id"`
Version int64 `json:"version"`
UID string `json:"uid"`
UID string `xorm:"uid" json:"uid"`
Name string `json:"name"`
Description string `json:"description"`
@@ -23,25 +27,80 @@ type Role struct {
type RoleDTO struct {
Version int64 `json:"version"`
UID string `json:"uid"`
UID string `xorm:"uid" json:"uid"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []Permission `json:"permissions,omitempty"`
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
OrgID int64 `json:"-" xorm:"org_id"`
Updated time.Time `json:"updated"`
Created time.Time `json:"created"`
}
func (r RoleDTO) Role() Role {
return Role{
ID: r.ID,
OrgID: r.OrgID,
UID: r.UID,
Name: r.Name,
Description: r.Description,
Updated: r.Updated,
Created: r.Created,
}
}
func (r RoleDTO) Global() bool {
return r.OrgID == GlobalOrgID
}
func (r Role) Global() bool {
return r.OrgID == GlobalOrgID
}
func (r RoleDTO) MarshalJSON() ([]byte, error) {
type Alias RoleDTO
return json.Marshal(&struct {
Alias
Global bool `json:"global" xorm:"-"`
}{
Alias: (Alias)(r),
Global: r.Global(),
})
}
func (r Role) MarshalJSON() ([]byte, error) {
type Alias Role
return json.Marshal(&struct {
Alias
Global bool `json:"global" xorm:"-"`
}{
Alias: (Alias)(r),
Global: r.Global(),
})
}
// Permission is the model for access control permissions.
type Permission struct {
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
RoleID int64 `json:"-" xorm:"role_id"`
Action string `json:"action"`
Scope string `json:"scope"`
Updated time.Time `json:"updated"`
Created time.Time `json:"created"`
}
func (p RoleDTO) Role() Role {
return Role{
Name: p.Name,
Description: p.Description,
func (p Permission) OSSPermission() Permission {
return Permission{
Action: p.Action,
Scope: p.Scope,
}
}
const (
GlobalOrgID = 0
// Permission actions
// Users actions
@@ -12,7 +12,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
)
func ProvideService(cfg *setting.Cfg, usageStats usagestats.UsageStats) *OSSAccessControlService {
func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessControlService {
s := &OSSAccessControlService{
Cfg: cfg,
UsageStats: usageStats,
@@ -25,7 +25,7 @@ func ProvideService(cfg *setting.Cfg, usageStats usagestats.UsageStats) *OSSAcce
// OSSAccessControlService is the service implementing role based access control.
type OSSAccessControlService struct {
Cfg *setting.Cfg
UsageStats usagestats.UsageStats
UsageStats usagestats.Service
Log log.Logger
registrations accesscontrol.RegistrationList
}
@@ -54,7 +54,7 @@ func (usm *usageStatsMock) RegisterMetricsFunc(fn usagestats.MetricsFunc) {
usm.metricsFuncs = append(usm.metricsFuncs, fn)
}
func (usm *usageStatsMock) GetUsageReport(_ context.Context) (usagestats.UsageReport, error) {
func (usm *usageStatsMock) GetUsageReport(_ context.Context) (usagestats.Report, error) {
all := make(map[string]interface{})
for _, fn := range usm.metricsFuncs {
fnMetrics, err := fn()
@@ -64,13 +64,15 @@ func (usm *usageStatsMock) GetUsageReport(_ context.Context) (usagestats.UsageRe
all[name] = value
}
}
return usagestats.UsageReport{Metrics: all}, nil
return usagestats.Report{Metrics: all}, nil
}
func (usm *usageStatsMock) ShouldBeReported(_ string) bool {
return true
}
func (usm *usageStatsMock) RegisterSendReportCallback(_ usagestats.SendReportCallbackFunc) {}
type evaluatingPermissionsTestCase struct {
desc string
user userTestCase
+42 -13
View File
@@ -9,6 +9,7 @@ import (
"github.com/benbjohnson/clock"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/rendering"
@@ -29,13 +30,14 @@ type AlertEngine struct {
DataService plugins.DataRequestHandler
Cfg *setting.Cfg
execQueue chan *Job
ticker *Ticker
scheduler scheduler
evalHandler evalHandler
ruleReader ruleReader
log log.Logger
resultHandler resultHandler
execQueue chan *Job
ticker *Ticker
scheduler scheduler
evalHandler evalHandler
ruleReader ruleReader
log log.Logger
resultHandler resultHandler
usageStatsService usagestats.Service
}
// IsDisabled returns true if the alerting service is disable for this instance.
@@ -45,13 +47,14 @@ func (e *AlertEngine) IsDisabled() bool {
// ProvideAlertEngine returns a new AlertEngine.
func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidator models.PluginRequestValidator,
dataService plugins.DataRequestHandler, cfg *setting.Cfg) *AlertEngine {
dataService plugins.DataRequestHandler, usageStatsService usagestats.Service, cfg *setting.Cfg) *AlertEngine {
e := &AlertEngine{
Cfg: cfg,
RenderService: renderer,
Bus: bus,
RequestValidator: requestValidator,
DataService: dataService,
Cfg: cfg,
RenderService: renderer,
Bus: bus,
RequestValidator: requestValidator,
DataService: dataService,
usageStatsService: usageStatsService,
}
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New(), 1)
e.execQueue = make(chan *Job, 1000)
@@ -61,6 +64,8 @@ func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidato
e.log = log.New("alerting.engine")
e.resultHandler = newResultHandler(e.RenderService)
e.registerUsageMetrics()
return e
}
@@ -237,3 +242,27 @@ func (e *AlertEngine) processJob(attemptID int, attemptChan chan int, cancelChan
close(attemptChan)
}()
}
func (e *AlertEngine) registerUsageMetrics() {
e.usageStatsService.RegisterMetricsFunc(func() (map[string]interface{}, error) {
alertingUsageStats, err := e.QueryUsageStats()
if err != nil {
return nil, err
}
alertingOtherCount := 0
metrics := map[string]interface{}{}
for dsType, usageCount := range alertingUsageStats.DatasourceUsage {
if e.usageStatsService.ShouldBeReported(dsType) {
metrics[fmt.Sprintf("stats.alerting.ds.%s.count", dsType)] = usageCount
} else {
alertingOtherCount += usageCount
}
}
metrics["stats.alerting.ds.other.count"] = alertingOtherCount
return metrics, nil
})
}
@@ -18,7 +18,8 @@ import (
func TestEngineTimeouts(t *testing.T) {
Convey("Alerting engine timeout tests", t, func() {
engine := ProvideAlertEngine(nil, nil, nil, nil, setting.NewCfg())
usMock := &usageStatsMock{t: t}
engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, setting.NewCfg())
setting.AlertingNotificationTimeout = 30 * time.Second
setting.AlertingMaxAttempts = 3
engine.resultHandler = &FakeResultHandler{}

Some files were not shown because too many files have changed in this diff Show More