Merge remote-tracking branch 'origin' into ivana/prom-backend-both
This commit is contained in:
+190
-214
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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**:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "8.1.4",
|
||||
"stable": "8.1.5",
|
||||
"testing": "8.2.0-beta1"
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "8.2.0-pre"
|
||||
"version": "8.3.0-pre"
|
||||
}
|
||||
|
||||
+6
-8
@@ -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": {
|
||||
|
||||
@@ -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,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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
+2
-2
@@ -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"),
|
||||
+17
-47
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user