Merge branch 'v9.0.x' into release-v9.0.0

This commit is contained in:
Dimitris Sotirakis
2022-06-15 10:57:21 +03:00
committed by GitHub
177 changed files with 5155 additions and 4803 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ name: publish_docs
on:
push:
branches:
- main
- v9.0.x
paths:
- 'docs/sources/**'
- 'packages/grafana-*/**'
@@ -41,7 +41,7 @@ jobs:
host: github.com
github_pat: '${{ secrets.GH_BOT_ACCESS_TOKEN }}'
source_folder: docs/sources
target_folder: content/docs/grafana/next
target_folder: content/docs/grafana/latest
allow_no_changes: 'true'
- shell: bash
run: |
+197
View File
@@ -1,3 +1,200 @@
<!-- 9.0.0 START -->
# 9.0.0 (2022-06-10)
### Features and enhancements
- **API:** Add GET /api/annotations/:annotationId endpoint. [#47739](https://github.com/grafana/grafana/pull/47739), [@scottbock](https://github.com/scottbock)
- **API:** Add endpoint for updating a data source by its UID. [#49396](https://github.com/grafana/grafana/pull/49396), [@papagian](https://github.com/papagian)
- **AccessControl:** Add enterprise only setting for rbac permission cache. [#49006](https://github.com/grafana/grafana/pull/49006), [@kalleep](https://github.com/kalleep)
- **AccessControl:** Document basic roles changes and provisioning V2. [#48910](https://github.com/grafana/grafana/pull/48910), [@gamab](https://github.com/gamab)
- **AccessControl:** Enable RBAC by default. [#48813](https://github.com/grafana/grafana/pull/48813), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **AddDataSourceConfig:** Remove deprecated checkHealth prop. [#50296](https://github.com/grafana/grafana/pull/50296), [@kaydelaney](https://github.com/kaydelaney)
- **Alerting:** Add Image URLs to Microsoft Teams notifier. [#49385](https://github.com/grafana/grafana/pull/49385), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add RBAC actions and role for provisioning API routes. [#50459](https://github.com/grafana/grafana/pull/50459), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Add Screenshot URLs to Pagerduty Notifier. [#49377](https://github.com/grafana/grafana/pull/49377), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add a "Reason" to Alert Instances to show underlying cause of state. [#49259](https://github.com/grafana/grafana/pull/49259), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add a general screenshot service and alerting-specific image service. [#49293](https://github.com/grafana/grafana/pull/49293), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add image url or file attachment to email notifications. [#49381](https://github.com/grafana/grafana/pull/49381), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add image_urls to OpsGenie notification details. [#49379](https://github.com/grafana/grafana/pull/49379), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add notification policy flow chart. [#49405](https://github.com/grafana/grafana/pull/49405), [@peterholmberg](https://github.com/peterholmberg)
- **Alerting:** Attach image URL to alerts in Webhook notifier format. [#49378](https://github.com/grafana/grafana/pull/49378), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Attach image URLs or upload files to Discord notifications. [#49439](https://github.com/grafana/grafana/pull/49439), [@alexweav](https://github.com/alexweav)
- **Alerting:** Attach image URLs to Google Chat notifications. [#49445](https://github.com/grafana/grafana/pull/49445), [@alexweav](https://github.com/alexweav)
- **Alerting:** Attach screenshot data to Unified Alerting notifications. [#49374](https://github.com/grafana/grafana/pull/49374), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Create folder for alerting when start from the scratch. [#48866](https://github.com/grafana/grafana/pull/48866), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Modify alertmanager endpoints for proxying using the datasource UID. [#47978](https://github.com/grafana/grafana/pull/47978), [@papagian](https://github.com/papagian)
- **Alerting:** Modify endpoint for testing a datasource rule using the UID. [#48070](https://github.com/grafana/grafana/pull/48070), [@papagian](https://github.com/papagian)
- **Alerting:** Modify prometheus endpoints for proxying using the datasource UID. [#48052](https://github.com/grafana/grafana/pull/48052), [@papagian](https://github.com/papagian)
- **Alerting:** State Manager takes screenshots. [#49338](https://github.com/grafana/grafana/pull/49338), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Use UID scope for folders authorization. [#48970](https://github.com/grafana/grafana/pull/48970), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** modify ruler endpoints for proxying using the datasource UID. [#48046](https://github.com/grafana/grafana/pull/48046), [@papagian](https://github.com/papagian)
- **Angular:** Adds back two angular directives that are still used by remaining angular bits and plugins. [#50380](https://github.com/grafana/grafana/pull/50380), [@torkelo](https://github.com/torkelo)
- **Azure Monitor:** Add Resource Picker to Metrics Queries. [#49029](https://github.com/grafana/grafana/pull/49029), [@sarahzinger](https://github.com/sarahzinger)
- **Azure Monitor:** Add search feature to resource picker. [#48234](https://github.com/grafana/grafana/pull/48234), [@sarahzinger](https://github.com/sarahzinger)
- **AzureMonitor:** Add support for selecting multiple options when using the equals and not equals dimension filters. [#48650](https://github.com/grafana/grafana/pull/48650), [@aangelisc](https://github.com/aangelisc)
- **AzureMonitor:** Remove deprecated code. [#48328](https://github.com/grafana/grafana/pull/48328), [@andresmgot](https://github.com/andresmgot)
- **Build:** Change names to PascalCase to match. [#48949](https://github.com/grafana/grafana/pull/48949), [@zuchka](https://github.com/zuchka)
- **Chore:** Remove deprecated DataSourceAPI methods. [#49313](https://github.com/grafana/grafana/pull/49313), [@ifrost](https://github.com/ifrost)
- **Chore:** Upgrade typescript to 4.6.4. [#49016](https://github.com/grafana/grafana/pull/49016), [@kaydelaney](https://github.com/kaydelaney)
- **Cloud Monitoring:** Use new annotation API. [#49026](https://github.com/grafana/grafana/pull/49026), [@kevinwcyu](https://github.com/kevinwcyu)
- **CloudMonitoring:** Allow to set a custom value or disable graph_period. [#48646](https://github.com/grafana/grafana/pull/48646), [@andresmgot](https://github.com/andresmgot)
- **CloudWatch:** Add generic filter component to variable editor. [#47907](https://github.com/grafana/grafana/pull/47907), [@iwysiu](https://github.com/iwysiu)
- **CloudWatch:** Added missing AWS/AppRunner metrics. [#49174](https://github.com/grafana/grafana/pull/49174), [@Aton-Kish](https://github.com/Aton-Kish)
- **CloudWatch:** Enable support for dynamic labels with migrated alias patterns. [#49173](https://github.com/grafana/grafana/pull/49173), [@sunker](https://github.com/sunker)
- **Cloudwatch:** Pass label in deep link. [#49160](https://github.com/grafana/grafana/pull/49160), [@sunker](https://github.com/sunker)
- **Cloudwatch:** Use new annotation API. [#48102](https://github.com/grafana/grafana/pull/48102), [@sunker](https://github.com/sunker)
- **Dashboard:** Validate dashboards against schema on save. [#48252](https://github.com/grafana/grafana/pull/48252), [@sdboyer](https://github.com/sdboyer)
- **DashboardPickerByID:** Add option to exclude dashboards. [#49211](https://github.com/grafana/grafana/pull/49211), [@Clarity-89](https://github.com/Clarity-89)
- **DashboardPickerById:** Add optionLabel prop. [#47556](https://github.com/grafana/grafana/pull/47556), [@Clarity-89](https://github.com/Clarity-89)
- **Dashboards:** Display values of 0 with the configured decimal places. [#48155](https://github.com/grafana/grafana/pull/48155), [@wx1322](https://github.com/wx1322)
- **Data:** Remove deprecated types and functions from valueMappings. [#50035](https://github.com/grafana/grafana/pull/50035), [@kaydelaney](https://github.com/kaydelaney)
- **Elasticsearch:** Remove browser access mode. [#49014](https://github.com/grafana/grafana/pull/49014), [@gabor](https://github.com/gabor)
- **Elasticsearch:** Remove support for versions after their end of the life (<7.10.0). [#48715](https://github.com/grafana/grafana/pull/48715), [@ivanahuckova](https://github.com/ivanahuckova)
- **Encryption:** Add support for multiple data keys per day. [#47765](https://github.com/grafana/grafana/pull/47765), [@joanlopez](https://github.com/joanlopez)
- **Encryption:** Enable envelope encryption by default. [#49301](https://github.com/grafana/grafana/pull/49301), [@joanlopez](https://github.com/joanlopez)
- **Explore:** Remove support for legacy, compact format URLs. [#49350](https://github.com/grafana/grafana/pull/49350), [@gelicia](https://github.com/gelicia)
- **Explore:** Skip Angular error handling when Angular support is disabled. [#49311](https://github.com/grafana/grafana/pull/49311), [@ifrost](https://github.com/ifrost)
- **Explore:** simplify support for multiple query editors. [#48701](https://github.com/grafana/grafana/pull/48701), [@Elfo404](https://github.com/Elfo404)
- **FeatureToggles:** Support changing feature toggles with URL parameters. [#50275](https://github.com/grafana/grafana/pull/50275), [@ryantxu](https://github.com/ryantxu)
- **FileUpload:** Make component accessible by keyboard navigation. [#47497](https://github.com/grafana/grafana/pull/47497), [@tolzhabayev](https://github.com/tolzhabayev)
- **Formatting:** Make SI number formats more robust. [#50117](https://github.com/grafana/grafana/pull/50117), [@kaydelaney](https://github.com/kaydelaney)
- **Graph:** Deprecate Graph (old) and make it no longer a visualization option for new panels. [#48034](https://github.com/grafana/grafana/pull/48034), [@torkelo](https://github.com/torkelo)
- **IconButton:** IconButtons are now correctly aligned in Safari. [#48759](https://github.com/grafana/grafana/pull/48759), [@ashharrison90](https://github.com/ashharrison90)
- **Logger:** Enable new logging format by default. [#47584](https://github.com/grafana/grafana/pull/47584), [@ying-jeanne](https://github.com/ying-jeanne)
- **Loki:** Add more query patterns. [#50248](https://github.com/grafana/grafana/pull/50248), [@ivanahuckova](https://github.com/ivanahuckova)
- **Loki:** Enable new visual query builder by default. [#48346](https://github.com/grafana/grafana/pull/48346), [@ivanahuckova](https://github.com/ivanahuckova)
- **Loki:** use the same dataframe-format for both live and normal queries. [#47153](https://github.com/grafana/grafana/pull/47153), [@gabor](https://github.com/gabor)
- **OAuth:** Make allowed email domain case insensitive. [#49252](https://github.com/grafana/grafana/pull/49252), [@Jguer](https://github.com/Jguer)
- **Panels:** Use the No value option when showing the no data message. [#47675](https://github.com/grafana/grafana/pull/47675), [@torkelo](https://github.com/torkelo)
- **Plugins:** Remove plugin list panel. [#46914](https://github.com/grafana/grafana/pull/46914), [@tolzhabayev](https://github.com/tolzhabayev)
- **Query History:** Enable new query history by default. [#49407](https://github.com/grafana/grafana/pull/49407), [@ifrost](https://github.com/ifrost)
- **QueryEditorRow:** Show query errors next to query in a consistent way across Grafana. [#47613](https://github.com/grafana/grafana/pull/47613), [@torkelo](https://github.com/torkelo)
- **SAML:** Implement Name Templates for assertion_attribute_name option. [#48022](https://github.com/grafana/grafana/pull/48022), [@mmandrus](https://github.com/mmandrus)
- **Service accounts:** Do not display service accounts assigned to team. [#48995](https://github.com/grafana/grafana/pull/48995), [@eleijonmarck](https://github.com/eleijonmarck)
- **Settings:** Use Grafana Azure SDK to pass Azure env vars for external plugins. [#48954](https://github.com/grafana/grafana/pull/48954), [@kostrse](https://github.com/kostrse)
- **Shortcuts:** Add shortcut to show shortcuts to the list of shortcuts. [#48395](https://github.com/grafana/grafana/pull/48395), [@ivanahuckova](https://github.com/ivanahuckova)
- **Traces Panel:** Add new Traces Panel visualization. [#47534](https://github.com/grafana/grafana/pull/47534), [@joey-grafana](https://github.com/joey-grafana)
- **Traces:** Filter by service/span name and operation in Tempo and Jaeger. [#48209](https://github.com/grafana/grafana/pull/48209), [@joey-grafana](https://github.com/joey-grafana)
- **Transformations:** Allow more complex regex expressions in `Rename by regex`. [#48179](https://github.com/grafana/grafana/pull/48179), [@ashharrison90](https://github.com/ashharrison90)
- **grafana/ui:** Add default type="button" to <Button>. [#48183](https://github.com/grafana/grafana/pull/48183), [@axelavargas](https://github.com/axelavargas)
### Bug fixes
- **Alerting:** Fix database unavailable removes rules from scheduler. [#49874](https://github.com/grafana/grafana/pull/49874), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **AzureMonitor:** Fix auto-selection of time-grain for metrics. [#49278](https://github.com/grafana/grafana/pull/49278), [@aangelisc](https://github.com/aangelisc)
- **DataSources:** Fixes issue with expressions not being queried. [#50446](https://github.com/grafana/grafana/pull/50446), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana)
- **GraphNG:** Fix thresholds by color not following data update. [#48571](https://github.com/grafana/grafana/pull/48571), [@zoltanbedi](https://github.com/zoltanbedi)
- **Jaeger:** Update operations dropdown. [#49329](https://github.com/grafana/grafana/pull/49329), [@joey-grafana](https://github.com/joey-grafana)
- **Login:** Fix mismatching label on auth_module in user list. [#49177](https://github.com/grafana/grafana/pull/49177), [@Jguer](https://github.com/Jguer)
- **Playlists:** Save button now correctly creates a new playlist. [#50381](https://github.com/grafana/grafana/pull/50381), [@ashharrison90](https://github.com/ashharrison90)
- **RBAC:** Fix migrations running in the wrong order causing inheritance problem in enterprise. [#50452](https://github.com/grafana/grafana/pull/50452), [@gamab](https://github.com/gamab)
- **RBAC:** Fix migrations running into the wrong order. (Enterprise)
- **ServiceAccounts:** Add identifiable token prefix to service account tokens. [#49011](https://github.com/grafana/grafana/pull/49011), [@Jguer](https://github.com/Jguer)
- **Traces:** Fix missing CopyButton on KeyValueTables and overlapping of panels. [#49271](https://github.com/grafana/grafana/pull/49271), [@svennergr](https://github.com/svennergr)
### Breaking changes
The `@grafana/ui` package helper function `selectOptionInTest` used in frontend tests has been removed as it caused testing libraries to be bundled in the production code of Grafana. If you were using this helper function in your tests please update your code accordingly:
```js
// before
import { selectOptionInTest } from '@grafana/ui';
// ...test usage
await selectOptionInTest(selectEl, 'Option 2');
// after
import { select } from 'react-select-event';
// ...test usage
await select(selectEl, 'Option 2', { container: document.body });
```
Issue [#50442](https://github.com/grafana/grafana/issues/50442)
Removed deprecated `checkHealth` prop from the `@grafana/e2e` `addDataSource` config. Previously this value defaulted to `false`, and has not been used in end-to-end tests since Grafana 8.0.3. Issue [#50296](https://github.com/grafana/grafana/issues/50296)
Removes the deprecated `LegacyBaseMap`, `LegacyValueMapping`, `LegacyValueMap`, and `LegacyRangeMap` types, and `getMappedValue` function from grafana-data. Migration is as follows:
| Old | New |
| ------------- | ------------- |
| `LegacyBaseMap` | `MappingType` |
| `LegacyValueMapping` | `ValueMapping` |
| `LegacyValueMap` | `ValueMap` |
| `LegacyRangeMap` | `RangeMap` |
| `getMappedValue` | `getValueMappingResult` | Issue [#50035](https://github.com/grafana/grafana/issues/50035)
This change fixes a bug in Grafana where intermittent failure of database, network between Grafana and the database, or error in querying the database would cause all alert rules to be unscheduled in Grafana. Following this change scheduled alert rules are not updated unless the query is successful.
The `get_alert_rules_duration_seconds` metric has been renamed to `schedule_query_alert_rules_duration_seconds`. Issue [#49874](https://github.com/grafana/grafana/issues/49874)
- Any secret (data sources credential, alert manager credential, etc, etc) created or modified with Grafana v9.0 won't be decryptable from any previous version (by default) because the way encrypted secrets are stored into the database has changed. Although secrets created or modified with previous versions will still be decryptable by Grafana v9.0.
- If required, although generally discouraged, the `disableEnvelopeEncryption` feature toggle can be enabled to keep envelope encryption disabled once updating to Grafana v9.0.
- In case of need to rollback to an earlier version of Grafana (i.e. Grafana v8.x) for any reason, after being created or modified any secret with Grafana v9.0, the `envelopeEncryption` feature toggle will need to be enabled to keep backwards compatibility (only from `v8.3.x` a bit unstable, from `8.5.x` stable).
- As a final attempt to deal with issues related with the aforementioned situations, the `grafana-cli admin secrets-migration rollback` command has been designed to move back all the Grafana secrets encrypted with envelope encryption to legacy encryption. So, after running that command it should be safe to disable envelope encryption and/or roll back to a previous version of Grafana.
- Alternatively or complementarily to all the points above, backing up the Grafana database before updating could be a good idea to prevent disasters (although the risk of getting some secrets corrupted only applies to those updates/created with after updating to Grafana v9.0). Issue [#49301](https://github.com/grafana/grafana/issues/49301)
- According to the dynamic labels documentation, you can use up to five dynamic values per label. There’s currently no such restriction in the alias pattern system, so if more than 5 patterns are being used the GetMetricData API will return an error.
- Dynamic labels only allow ${LABEL} to be used once per query. There’s no such restriction in the alias pattern system, so in case more than 1 is being used the GetMetricData API will return an error.
- When no alias is provided by the user, Grafana will no longer fallback with custom rules for naming the legend.
- In case a search expression is being used and no data is returned, Grafana will no longer expand dimension values, for instance when using a multi-valued template variable or star wildcard `*` in the dimension value field. Ref https://github.com/grafana/grafana/issues/20729
- Time series might be displayed in a different order. Using for example the dynamic label `${PROP('MetricName')}`, might have the consequence that the time series are returned in a different order compared to when the alias pattern `{{metric}}` is used
Issue [#49173](https://github.com/grafana/grafana/issues/49173)
In Elasticsearch, browser access mode was deprecated in grafana 7.4.0 and removed in 9.0.0. If you used this mode, please switch to server access mode on the datasource configuration page. Issue [#49014](https://github.com/grafana/grafana/issues/49014)
Environment variables passed from Grafana to external Azure plugins have been renamed:
- `AZURE_CLOUD` renamed to `GFAZPL_AZURE_CLOUD`
- `AZURE_MANAGED_IDENTITY_ENABLED` renamed to `GFAZPL_MANAGED_IDENTITY_ENABLED`
- `AZURE_MANAGED_IDENTITY_CLIENT_ID` renamed to `GFAZPL_MANAGED_IDENTITY_CLIENT_ID`
There are no known plugins which were relying on these variables. Moving forward plugins should read Azure settings only via Grafana Azure SDK which properly handles old and new environment variables. Issue [#48954](https://github.com/grafana/grafana/issues/48954)
Removes support for for ElasticSearch versions after their end-of-life, currently versions < 7.10.0. To continue to use ElasticSearch data source, upgrade ElasticSearch to version 7.10.0+.
Issue [#48715](https://github.com/grafana/grafana/issues/48715)
Application Insights and Insight Analytics queries in Azure Monitor were deprecated in Grafana 8.0 and finally removed in 9.0. Deprecated queries will no longer be executed. Please refer to the [documentation](https://grafana.com/docs/grafana/latest/datasources/azuremonitor/deprecated-application-insights/) for more information about this change.
Issue [#48328](https://github.com/grafana/grafana/issues/48328)
**grafana/ui: Button now specifies a default type="button"**
The `Button` component provided by @grafana/ui now specifies a default `type="button"` when no type is provided. In previous versions, if the attribute was not specified for buttons associated with a `<form>` the default value was `submit` per the [specification](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type)
You can preserve the old behavior by explicitly setting the type attribute: `<Button type="submit" />`
[Github Issue #41863](https://github.com/grafana/grafana/issues/41863).
Issue [#48183](https://github.com/grafana/grafana/issues/48183)
The `Rename by regex` transformation has been improved to allow global patterns of the form `/<stringToReplace>/g`. Depending on the regex match used, this may cause some transformations to behave slightly differently. You can guarantee the same behaviour as before by wrapping the `match` string in forward slashes (`/`), e.g. `(.*)` would become `/(.*)/` Issue [#48179](https://github.com/grafana/grafana/issues/48179)
`<Select />` menus will now portal to the document body by default. This is to give more consistent behaviour when positioning and overlaying. If you were setting `menuShouldPortal={true}` before you can safely remove that prop and behaviour will be the same. If you weren't explicitly setting that prop, there should be no visible changes in behaviour but your tests may need updating. Please see the original PR (https://github.com/grafana/grafana/pull/36398) for migration guides. If you were setting `menuShouldPortal={false}` this will continue to prevent the menu from portalling.
Issue [#48176](https://github.com/grafana/grafana/issues/48176)
Grafana alerting endpoint prefixed with `api/v1/rule/test` that tests a rule against a Corte/Loki data source now expects the data source UID as a path parameter instead of the data source numeric identifier. Issue [#48070](https://github.com/grafana/grafana/issues/48070)
Grafana alerting endpoints prefixed with `api/prometheus/` that proxy requests to a Cortex/Loki data source now expect the data source UID as a path parameter instead of the data source numeric identifier. Issue [#48052](https://github.com/grafana/grafana/issues/48052)
Grafana alerting endpoints prefixed with `api/ruler/` that proxy requests to a Cortex/Loki data source now expect the data source UID as a path parameter instead of the data source numeric identifier. Issue [#48046](https://github.com/grafana/grafana/issues/48046)
Grafana alerting endpoints prefixed with `api/alertmanager/` that proxy requests to an Alertmanager now expect the data source UID as a path parameter instead of the data source numeric identifier. Issue [#47978](https://github.com/grafana/grafana/issues/47978)
The format of log messages have been updated, `lvl` is now `level` and `eror`and `dbug` has been replaced with `error` and `debug`. The precision of timestamps has been increased. To smooth the transition, it is possible to opt-out of the new log format by enabling the feature toggle `oldlog`. This option will be removed in a future minor release. Issue [#47584](https://github.com/grafana/grafana/issues/47584)
In the Loki data source, the dataframe format used to represent Loki logs-data has been changed to a more efficient format. The query-result is represented by a single dataframe with a "labels" column, instead of the separate dataframes for every labels-value. When displaying such data in explore, or in a logs-panel in the dashboard will continue to work without changes, but if the data was loaded into a different dashboard-panel, or Transforms were used, adjustments may be necessary. For example, if you used the "labels to fields" transformation with the logs data, please switch to the "extract fields" transformation. Issue [#47153](https://github.com/grafana/grafana/issues/47153)
### Deprecations
`setExploreQueryField`, `setExploreMetricsQueryField` and `setExploreLogsQueryField` are now deprecated and will be removed in a future release. If you need to set a different query editor for Explore, conditionally render based on `props.app` in your regular query editor. Please refer to https://grafana.com/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/ for more information.
Issue [#48701](https://github.com/grafana/grafana/issues/48701)
### Plugin development fixes & changes
- **Chore:** Remove react-testing-lib from bundles. [#50442](https://github.com/grafana/grafana/pull/50442), [@jackw](https://github.com/jackw)
- **Select:** Portal menu by default. [#48176](https://github.com/grafana/grafana/pull/48176), [@ashharrison90](https://github.com/ashharrison90)
<!-- 9.0.0 END -->
<!-- 9.0.0-beta3 START -->
# 9.0.0-beta3 (2022-06-06)
+3 -3
View File
@@ -22,11 +22,11 @@ Folders are a way to organize and group dashboards - very useful if you have a l
## How To Create A Folder
- Create a folder by using the Create Folder link in the side menu (under the create menu (+ icon))
- Use the create Folder button on the Manage Dashboards page.
- Create a folder by using the **New folder** link in the side menu (under the **Dashboards** menu)
- Use the **New folder** button on the **Browse dashboards** page.
- When saving a dashboard, you can either choose a folder for the dashboard to be saved in or create a new folder
On the Create Folder page, fill in a unique name for the folder and then click Create.
On the New folder page, fill in a unique name for the folder and then click Create.
## Manage Dashboards
+1 -1
View File
@@ -34,7 +34,7 @@ the dashboard, and will also be added as a required input when the dashboard is
## Import dashboard
To import a dashboard click the + icon in the side menu, and then click **Import**.
To import a dashboard click **Import** under the **Dashboards** icon in the side menu.
{{< figure src="/static/img/docs/v70/import_step1.png" max-width="700px" >}}
+1 -1
View File
@@ -15,7 +15,7 @@ title: 'Alerting HTTP API '
# Alerting API
> **Note:** This topic is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/latest/alerting/old-alerting/) only.
> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/" >}}) only.
You can find Grafana alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts.
@@ -185,7 +185,7 @@ Content-Type: application/json
## Create notification channel
You can find the full list of [supported notifiers](https://grafana.com/docs/grafana/latest/alerting/old-alerting/notifications/) on the alert notifiers page.
You can find the full list of [supported notifiers]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/notifications/" >}}) on the alert notifiers page.
`POST /api/alert-notifications`
+17 -17
View File
@@ -42,7 +42,7 @@ HTTP/1.1 200
Content-Type: application/json
[
{
"id": 1,
"uid": "1",
"name": "my playlist",
"interval": "5m"
}
@@ -51,7 +51,7 @@ Content-Type: application/json
## Get one playlist
`GET /api/playlists/:id`
`GET /api/playlists/:uid`
**Example Request**:
@@ -67,14 +67,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
HTTP/1.1 200
Content-Type: application/json
{
"id" : 1,
"uid" : "1",
"name": "my playlist",
"interval": "5m",
"orgId": "my org",
"items": [
{
"id": 1,
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_id",
"value": "3",
"order": 1,
@@ -82,7 +82,7 @@ Content-Type: application/json
},
{
"id": 2,
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
@@ -94,7 +94,7 @@ Content-Type: application/json
## Get Playlist items
`GET /api/playlists/:id/items`
`GET /api/playlists/:uid/items`
**Example Request**:
@@ -112,7 +112,7 @@ Content-Type: application/json
[
{
"id": 1,
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_id",
"value": "3",
"order": 1,
@@ -120,7 +120,7 @@ Content-Type: application/json
},
{
"id": 2,
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
@@ -131,7 +131,7 @@ Content-Type: application/json
## Get Playlist dashboards
`GET /api/playlists/:id/dashboards`
`GET /api/playlists/:uid/dashboards`
**Example Request**:
@@ -198,7 +198,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
HTTP/1.1 200
Content-Type: application/json
{
"id": 1,
"uid": "1",
"name": "my playlist",
"interval": "5m"
}
@@ -206,7 +206,7 @@ Content-Type: application/json
## Update a playlist
`PUT /api/playlists/:id`
`PUT /api/playlists/:uid`
**Example Request**:
@@ -220,14 +220,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"interval": "5m",
"items": [
{
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dashboard"
},
{
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
@@ -243,14 +243,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
HTTP/1.1 200
Content-Type: application/json
{
"id" : 1,
"uid" : "1",
"name": "my playlist",
"interval": "5m",
"orgId": "my org",
"items": [
{
"id": 1,
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_id",
"value": "3",
"order": 1,
@@ -258,7 +258,7 @@ Content-Type: application/json
},
{
"id": 2,
"playlistId": 1,
"playlistUid": "1",
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
@@ -270,7 +270,7 @@ Content-Type: application/json
## Delete a playlist
`DELETE /api/playlists/:id`
`DELETE /api/playlists/:uid`
**Example Request**:
@@ -38,7 +38,7 @@ To sign in to Grafana for the first time:
To create your first dashboard:
1. Click the **+** icon on the side menu.
1. Click the **New dashboard** item under the **Dashboards** icon in the side menu.
1. On the dashboard, click **Add an empty panel**.
1. In the New dashboard/Edit panel view, go to the **Query** tab.
1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query/" >}}) by selecting `-- Grafana --` from the data source selector. This generates the Random Walk dashboard.
@@ -16,7 +16,7 @@ Add a Grafana library panel to a dashboard when you want to provide visualizatio
**To add a library panel to a dashboard**:
1. Hover over the **+** option on the left menu, then select **Create** from the drop-down options.
1. Hover over the **Dashboards** option on the left menu, then select **New dashboard** from the drop-down options.
The **Add** panel dialog opens.
{{< figure src="/static/img/docs/library-panels/add-library-panel-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
+3 -1
View File
@@ -10,9 +10,11 @@ weight: 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 9.0.0-beta3]({{< relref "release-notes-9-0-0-beta3/" >}})
- [Release notes for 9.0.0]({{< relref "release-notes-9-0-0" >}})
- [Release notes for 9.0.0-beta3]({{< relref "release-notes-9-0-0-beta3" >}})
- [Release notes for 9.0.0-beta2]({{< relref "release-notes-9-0-0-beta2/" >}})
- [Release notes for 9.0.0-beta1]({{< relref "release-notes-9-0-0-beta1/" >}})
- [Release notes for 8.5.5]({{< relref "release-notes-8-5-5" >}})
- [Release notes for 8.5.4]({{< relref "release-notes-8-5-4/" >}})
- [Release notes for 8.5.3]({{< relref "release-notes-8-5-3/" >}})
- [Release notes for 8.5.2]({{< relref "release-notes-8-5-2/" >}})
@@ -0,0 +1,200 @@
+++
title = "Release notes for Grafana 9.0.0"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 9.0.0
### Features and enhancements
- **API:** Add GET /api/annotations/:annotationId endpoint. [#47739](https://github.com/grafana/grafana/pull/47739), [@scottbock](https://github.com/scottbock)
- **API:** Add endpoint for updating a data source by its UID. [#49396](https://github.com/grafana/grafana/pull/49396), [@papagian](https://github.com/papagian)
- **AccessControl:** Add enterprise only setting for rbac permission cache. [#49006](https://github.com/grafana/grafana/pull/49006), [@kalleep](https://github.com/kalleep)
- **AccessControl:** Document basic roles changes and provisioning V2. [#48910](https://github.com/grafana/grafana/pull/48910), [@gamab](https://github.com/gamab)
- **AccessControl:** Enable RBAC by default. [#48813](https://github.com/grafana/grafana/pull/48813), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **AddDataSourceConfig:** Remove deprecated checkHealth prop. [#50296](https://github.com/grafana/grafana/pull/50296), [@kaydelaney](https://github.com/kaydelaney)
- **Alerting:** Add Image URLs to Microsoft Teams notifier. [#49385](https://github.com/grafana/grafana/pull/49385), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add RBAC actions and role for provisioning API routes. [#50459](https://github.com/grafana/grafana/pull/50459), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Add Screenshot URLs to Pagerduty Notifier. [#49377](https://github.com/grafana/grafana/pull/49377), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add a "Reason" to Alert Instances to show underlying cause of state. [#49259](https://github.com/grafana/grafana/pull/49259), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add a general screenshot service and alerting-specific image service. [#49293](https://github.com/grafana/grafana/pull/49293), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add image url or file attachment to email notifications. [#49381](https://github.com/grafana/grafana/pull/49381), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add image_urls to OpsGenie notification details. [#49379](https://github.com/grafana/grafana/pull/49379), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Add notification policy flow chart. [#49405](https://github.com/grafana/grafana/pull/49405), [@peterholmberg](https://github.com/peterholmberg)
- **Alerting:** Attach image URL to alerts in Webhook notifier format. [#49378](https://github.com/grafana/grafana/pull/49378), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Attach image URLs or upload files to Discord notifications. [#49439](https://github.com/grafana/grafana/pull/49439), [@alexweav](https://github.com/alexweav)
- **Alerting:** Attach image URLs to Google Chat notifications. [#49445](https://github.com/grafana/grafana/pull/49445), [@alexweav](https://github.com/alexweav)
- **Alerting:** Attach screenshot data to Unified Alerting notifications. [#49374](https://github.com/grafana/grafana/pull/49374), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Create folder for alerting when start from the scratch. [#48866](https://github.com/grafana/grafana/pull/48866), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Modify alertmanager endpoints for proxying using the datasource UID. [#47978](https://github.com/grafana/grafana/pull/47978), [@papagian](https://github.com/papagian)
- **Alerting:** Modify endpoint for testing a datasource rule using the UID. [#48070](https://github.com/grafana/grafana/pull/48070), [@papagian](https://github.com/papagian)
- **Alerting:** Modify prometheus endpoints for proxying using the datasource UID. [#48052](https://github.com/grafana/grafana/pull/48052), [@papagian](https://github.com/papagian)
- **Alerting:** State Manager takes screenshots. [#49338](https://github.com/grafana/grafana/pull/49338), [@joeblubaugh](https://github.com/joeblubaugh)
- **Alerting:** Use UID scope for folders authorization. [#48970](https://github.com/grafana/grafana/pull/48970), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** modify ruler endpoints for proxying using the datasource UID. [#48046](https://github.com/grafana/grafana/pull/48046), [@papagian](https://github.com/papagian)
- **Angular:** Adds back two angular directives that are still used by remaining angular bits and plugins. [#50380](https://github.com/grafana/grafana/pull/50380), [@torkelo](https://github.com/torkelo)
- **Azure Monitor:** Add Resource Picker to Metrics Queries. [#49029](https://github.com/grafana/grafana/pull/49029), [@sarahzinger](https://github.com/sarahzinger)
- **Azure Monitor:** Add search feature to resource picker. [#48234](https://github.com/grafana/grafana/pull/48234), [@sarahzinger](https://github.com/sarahzinger)
- **AzureMonitor:** Add support for selecting multiple options when using the equals and not equals dimension filters. [#48650](https://github.com/grafana/grafana/pull/48650), [@aangelisc](https://github.com/aangelisc)
- **AzureMonitor:** Remove deprecated code. [#48328](https://github.com/grafana/grafana/pull/48328), [@andresmgot](https://github.com/andresmgot)
- **Build:** Change names to PascalCase to match. [#48949](https://github.com/grafana/grafana/pull/48949), [@zuchka](https://github.com/zuchka)
- **Chore:** Remove deprecated DataSourceAPI methods. [#49313](https://github.com/grafana/grafana/pull/49313), [@ifrost](https://github.com/ifrost)
- **Chore:** Upgrade typescript to 4.6.4. [#49016](https://github.com/grafana/grafana/pull/49016), [@kaydelaney](https://github.com/kaydelaney)
- **Cloud Monitoring:** Use new annotation API. [#49026](https://github.com/grafana/grafana/pull/49026), [@kevinwcyu](https://github.com/kevinwcyu)
- **CloudMonitoring:** Allow to set a custom value or disable graph_period. [#48646](https://github.com/grafana/grafana/pull/48646), [@andresmgot](https://github.com/andresmgot)
- **CloudWatch:** Add generic filter component to variable editor. [#47907](https://github.com/grafana/grafana/pull/47907), [@iwysiu](https://github.com/iwysiu)
- **CloudWatch:** Added missing AWS/AppRunner metrics. [#49174](https://github.com/grafana/grafana/pull/49174), [@Aton-Kish](https://github.com/Aton-Kish)
- **CloudWatch:** Enable support for dynamic labels with migrated alias patterns. [#49173](https://github.com/grafana/grafana/pull/49173), [@sunker](https://github.com/sunker)
- **Cloudwatch:** Pass label in deep link. [#49160](https://github.com/grafana/grafana/pull/49160), [@sunker](https://github.com/sunker)
- **Cloudwatch:** Use new annotation API. [#48102](https://github.com/grafana/grafana/pull/48102), [@sunker](https://github.com/sunker)
- **Dashboard:** Validate dashboards against schema on save. [#48252](https://github.com/grafana/grafana/pull/48252), [@sdboyer](https://github.com/sdboyer)
- **DashboardPickerByID:** Add option to exclude dashboards. [#49211](https://github.com/grafana/grafana/pull/49211), [@Clarity-89](https://github.com/Clarity-89)
- **DashboardPickerById:** Add optionLabel prop. [#47556](https://github.com/grafana/grafana/pull/47556), [@Clarity-89](https://github.com/Clarity-89)
- **Dashboards:** Display values of 0 with the configured decimal places. [#48155](https://github.com/grafana/grafana/pull/48155), [@wx1322](https://github.com/wx1322)
- **Data:** Remove deprecated types and functions from valueMappings. [#50035](https://github.com/grafana/grafana/pull/50035), [@kaydelaney](https://github.com/kaydelaney)
- **Elasticsearch:** Remove browser access mode. [#49014](https://github.com/grafana/grafana/pull/49014), [@gabor](https://github.com/gabor)
- **Elasticsearch:** Remove support for versions after their end of the life (<7.10.0). [#48715](https://github.com/grafana/grafana/pull/48715), [@ivanahuckova](https://github.com/ivanahuckova)
- **Encryption:** Add support for multiple data keys per day. [#47765](https://github.com/grafana/grafana/pull/47765), [@joanlopez](https://github.com/joanlopez)
- **Encryption:** Enable envelope encryption by default. [#49301](https://github.com/grafana/grafana/pull/49301), [@joanlopez](https://github.com/joanlopez)
- **Explore:** Remove support for legacy, compact format URLs. [#49350](https://github.com/grafana/grafana/pull/49350), [@gelicia](https://github.com/gelicia)
- **Explore:** Skip Angular error handling when Angular support is disabled. [#49311](https://github.com/grafana/grafana/pull/49311), [@ifrost](https://github.com/ifrost)
- **Explore:** simplify support for multiple query editors. [#48701](https://github.com/grafana/grafana/pull/48701), [@Elfo404](https://github.com/Elfo404)
- **FeatureToggles:** Support changing feature toggles with URL parameters. [#50275](https://github.com/grafana/grafana/pull/50275), [@ryantxu](https://github.com/ryantxu)
- **FileUpload:** Make component accessible by keyboard navigation. [#47497](https://github.com/grafana/grafana/pull/47497), [@tolzhabayev](https://github.com/tolzhabayev)
- **Formatting:** Make SI number formats more robust. [#50117](https://github.com/grafana/grafana/pull/50117), [@kaydelaney](https://github.com/kaydelaney)
- **Graph:** Deprecate Graph (old) and make it no longer a visualization option for new panels. [#48034](https://github.com/grafana/grafana/pull/48034), [@torkelo](https://github.com/torkelo)
- **IconButton:** IconButtons are now correctly aligned in Safari. [#48759](https://github.com/grafana/grafana/pull/48759), [@ashharrison90](https://github.com/ashharrison90)
- **Logger:** Enable new logging format by default. [#47584](https://github.com/grafana/grafana/pull/47584), [@ying-jeanne](https://github.com/ying-jeanne)
- **Loki:** Add more query patterns. [#50248](https://github.com/grafana/grafana/pull/50248), [@ivanahuckova](https://github.com/ivanahuckova)
- **Loki:** Enable new visual query builder by default. [#48346](https://github.com/grafana/grafana/pull/48346), [@ivanahuckova](https://github.com/ivanahuckova)
- **Loki:** use the same dataframe-format for both live and normal queries. [#47153](https://github.com/grafana/grafana/pull/47153), [@gabor](https://github.com/gabor)
- **OAuth:** Make allowed email domain case insensitive. [#49252](https://github.com/grafana/grafana/pull/49252), [@Jguer](https://github.com/Jguer)
- **Panels:** Use the No value option when showing the no data message. [#47675](https://github.com/grafana/grafana/pull/47675), [@torkelo](https://github.com/torkelo)
- **Plugins:** Remove plugin list panel. [#46914](https://github.com/grafana/grafana/pull/46914), [@tolzhabayev](https://github.com/tolzhabayev)
- **Query History:** Enable new query history by default. [#49407](https://github.com/grafana/grafana/pull/49407), [@ifrost](https://github.com/ifrost)
- **QueryEditorRow:** Show query errors next to query in a consistent way across Grafana. [#47613](https://github.com/grafana/grafana/pull/47613), [@torkelo](https://github.com/torkelo)
- **SAML:** Implement Name Templates for assertion_attribute_name option. [#48022](https://github.com/grafana/grafana/pull/48022), [@mmandrus](https://github.com/mmandrus)
- **Service accounts:** Do not display service accounts assigned to team. [#48995](https://github.com/grafana/grafana/pull/48995), [@eleijonmarck](https://github.com/eleijonmarck)
- **Settings:** Use Grafana Azure SDK to pass Azure env vars for external plugins. [#48954](https://github.com/grafana/grafana/pull/48954), [@kostrse](https://github.com/kostrse)
- **Shortcuts:** Add shortcut to show shortcuts to the list of shortcuts. [#48395](https://github.com/grafana/grafana/pull/48395), [@ivanahuckova](https://github.com/ivanahuckova)
- **Traces Panel:** Add new Traces Panel visualization. [#47534](https://github.com/grafana/grafana/pull/47534), [@joey-grafana](https://github.com/joey-grafana)
- **Traces:** Filter by service/span name and operation in Tempo and Jaeger. [#48209](https://github.com/grafana/grafana/pull/48209), [@joey-grafana](https://github.com/joey-grafana)
- **Transformations:** Allow more complex regex expressions in `Rename by regex`. [#48179](https://github.com/grafana/grafana/pull/48179), [@ashharrison90](https://github.com/ashharrison90)
- **grafana/ui:** Add default type="button" to <Button>. [#48183](https://github.com/grafana/grafana/pull/48183), [@axelavargas](https://github.com/axelavargas)
### Bug fixes
- **Alerting:** Fix database unavailable removes rules from scheduler. [#49874](https://github.com/grafana/grafana/pull/49874), [@grobinson-grafana](https://github.com/grobinson-grafana)
- **AzureMonitor:** Fix auto-selection of time-grain for metrics. [#49278](https://github.com/grafana/grafana/pull/49278), [@aangelisc](https://github.com/aangelisc)
- **DataSources:** Fixes issue with expressions not being queried. [#50446](https://github.com/grafana/grafana/pull/50446), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana)
- **GraphNG:** Fix thresholds by color not following data update. [#48571](https://github.com/grafana/grafana/pull/48571), [@zoltanbedi](https://github.com/zoltanbedi)
- **Jaeger:** Update operations dropdown. [#49329](https://github.com/grafana/grafana/pull/49329), [@joey-grafana](https://github.com/joey-grafana)
- **Login:** Fix mismatching label on auth_module in user list. [#49177](https://github.com/grafana/grafana/pull/49177), [@Jguer](https://github.com/Jguer)
- **Playlists:** Save button now correctly creates a new playlist. [#50381](https://github.com/grafana/grafana/pull/50381), [@ashharrison90](https://github.com/ashharrison90)
- **RBAC:** Fix migrations running in the wrong order causing inheritance problem in enterprise. [#50452](https://github.com/grafana/grafana/pull/50452), [@gamab](https://github.com/gamab)
- **RBAC:** Fix migrations running into the wrong order. (Enterprise)
- **ServiceAccounts:** Add identifiable token prefix to service account tokens. [#49011](https://github.com/grafana/grafana/pull/49011), [@Jguer](https://github.com/Jguer)
- **Traces:** Fix missing CopyButton on KeyValueTables and overlapping of panels. [#49271](https://github.com/grafana/grafana/pull/49271), [@svennergr](https://github.com/svennergr)
### Breaking changes
The `@grafana/ui` package helper function `selectOptionInTest` used in frontend tests has been removed as it caused testing libraries to be bundled in the production code of Grafana. If you were using this helper function in your tests please update your code accordingly:
```js
// before
import { selectOptionInTest } from '@grafana/ui';
// ...test usage
await selectOptionInTest(selectEl, 'Option 2');
// after
import { select } from 'react-select-event';
// ...test usage
await select(selectEl, 'Option 2', { container: document.body });
```
Issue [#50442](https://github.com/grafana/grafana/issues/50442)
Removed deprecated `checkHealth` prop from the `@grafana/e2e` `addDataSource` config. Previously this value defaulted to `false`, and has not been used in end-to-end tests since Grafana 8.0.3. Issue [#50296](https://github.com/grafana/grafana/issues/50296)
Removes the deprecated `LegacyBaseMap`, `LegacyValueMapping`, `LegacyValueMap`, and `LegacyRangeMap` types, and `getMappedValue` function from grafana-data. Migration is as follows:
| Old | New |
| ------------- | ------------- |
| `LegacyBaseMap` | `MappingType` |
| `LegacyValueMapping` | `ValueMapping` |
| `LegacyValueMap` | `ValueMap` |
| `LegacyRangeMap` | `RangeMap` |
| `getMappedValue` | `getValueMappingResult` | Issue [#50035](https://github.com/grafana/grafana/issues/50035)
This change fixes a bug in Grafana where intermittent failure of database, network between Grafana and the database, or error in querying the database would cause all alert rules to be unscheduled in Grafana. Following this change scheduled alert rules are not updated unless the query is successful.
The `get_alert_rules_duration_seconds` metric has been renamed to `schedule_query_alert_rules_duration_seconds`. Issue [#49874](https://github.com/grafana/grafana/issues/49874)
- Any secret (data sources credential, alert manager credential, etc, etc) created or modified with Grafana v9.0 won't be decryptable from any previous version (by default) because the way encrypted secrets are stored into the database has changed. Although secrets created or modified with previous versions will still be decryptable by Grafana v9.0.
- If required, although generally discouraged, the `disableEnvelopeEncryption` feature toggle can be enabled to keep envelope encryption disabled once updating to Grafana v9.0.
- In case of need to rollback to an earlier version of Grafana (i.e. Grafana v8.x) for any reason, after being created or modified any secret with Grafana v9.0, the `envelopeEncryption` feature toggle will need to be enabled to keep backwards compatibility (only from `v8.3.x` a bit unstable, from `8.5.x` stable).
- As a final attempt to deal with issues related with the aforementioned situations, the `grafana-cli admin secrets-migration rollback` command has been designed to move back all the Grafana secrets encrypted with envelope encryption to legacy encryption. So, after running that command it should be safe to disable envelope encryption and/or roll back to a previous version of Grafana.
- Alternatively or complementarily to all the points above, backing up the Grafana database before updating could be a good idea to prevent disasters (although the risk of getting some secrets corrupted only applies to those updates/created with after updating to Grafana v9.0). Issue [#49301](https://github.com/grafana/grafana/issues/49301)
- According to the dynamic labels documentation, you can use up to five dynamic values per label. There’s currently no such restriction in the alias pattern system, so if more than 5 patterns are being used the GetMetricData API will return an error.
- Dynamic labels only allow ${LABEL} to be used once per query. There’s no such restriction in the alias pattern system, so in case more than 1 is being used the GetMetricData API will return an error.
- When no alias is provided by the user, Grafana will no longer fallback with custom rules for naming the legend.
- In case a search expression is being used and no data is returned, Grafana will no longer expand dimension values, for instance when using a multi-valued template variable or star wildcard `*` in the dimension value field. Ref https://github.com/grafana/grafana/issues/20729
- Time series might be displayed in a different order. Using for example the dynamic label `${PROP('MetricName')}`, might have the consequence that the time series are returned in a different order compared to when the alias pattern `{{metric}}` is used
Issue [#49173](https://github.com/grafana/grafana/issues/49173)
In Elasticsearch, browser access mode was deprecated in grafana 7.4.0 and removed in 9.0.0. If you used this mode, please switch to server access mode on the datasource configuration page. Issue [#49014](https://github.com/grafana/grafana/issues/49014)
Environment variables passed from Grafana to external Azure plugins have been renamed:
- `AZURE_CLOUD` renamed to `GFAZPL_AZURE_CLOUD`
- `AZURE_MANAGED_IDENTITY_ENABLED` renamed to `GFAZPL_MANAGED_IDENTITY_ENABLED`
- `AZURE_MANAGED_IDENTITY_CLIENT_ID` renamed to `GFAZPL_MANAGED_IDENTITY_CLIENT_ID`
There are no known plugins which were relying on these variables. Moving forward plugins should read Azure settings only via Grafana Azure SDK which properly handles old and new environment variables. Issue [#48954](https://github.com/grafana/grafana/issues/48954)
Removes support for for ElasticSearch versions after their end-of-life, currently versions < 7.10.0. To continue to use ElasticSearch data source, upgrade ElasticSearch to version 7.10.0+.
Issue [#48715](https://github.com/grafana/grafana/issues/48715)
Application Insights and Insight Analytics queries in Azure Monitor were deprecated in Grafana 8.0 and finally removed in 9.0. Deprecated queries will no longer be executed. Please refer to the [documentation](https://grafana.com/docs/grafana/latest/datasources/azuremonitor/deprecated-application-insights/) for more information about this change.
Issue [#48328](https://github.com/grafana/grafana/issues/48328)
**grafana/ui: Button now specifies a default type="button"**
The `Button` component provided by @grafana/ui now specifies a default `type="button"` when no type is provided. In previous versions, if the attribute was not specified for buttons associated with a `<form>` the default value was `submit` per the [specification](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type)
You can preserve the old behavior by explicitly setting the type attribute: `<Button type="submit" />`
[Github Issue #41863](https://github.com/grafana/grafana/issues/41863).
Issue [#48183](https://github.com/grafana/grafana/issues/48183)
The `Rename by regex` transformation has been improved to allow global patterns of the form `/<stringToReplace>/g`. Depending on the regex match used, this may cause some transformations to behave slightly differently. You can guarantee the same behaviour as before by wrapping the `match` string in forward slashes (`/`), e.g. `(.*)` would become `/(.*)/` Issue [#48179](https://github.com/grafana/grafana/issues/48179)
`<Select />` menus will now portal to the document body by default. This is to give more consistent behaviour when positioning and overlaying. If you were setting `menuShouldPortal={true}` before you can safely remove that prop and behaviour will be the same. If you weren't explicitly setting that prop, there should be no visible changes in behaviour but your tests may need updating. Please see the original PR (https://github.com/grafana/grafana/pull/36398) for migration guides. If you were setting `menuShouldPortal={false}` this will continue to prevent the menu from portalling.
Issue [#48176](https://github.com/grafana/grafana/issues/48176)
Grafana alerting endpoint prefixed with `api/v1/rule/test` that tests a rule against a Corte/Loki data source now expects the data source UID as a path parameter instead of the data source numeric identifier. Issue [#48070](https://github.com/grafana/grafana/issues/48070)
Grafana alerting endpoints prefixed with `api/prometheus/` that proxy requests to a Cortex/Loki data source now expect the data source UID as a path parameter instead of the data source numeric identifier. Issue [#48052](https://github.com/grafana/grafana/issues/48052)
Grafana alerting endpoints prefixed with `api/ruler/` that proxy requests to a Cortex/Loki data source now expect the data source UID as a path parameter instead of the data source numeric identifier. Issue [#48046](https://github.com/grafana/grafana/issues/48046)
Grafana alerting endpoints prefixed with `api/alertmanager/` that proxy requests to an Alertmanager now expect the data source UID as a path parameter instead of the data source numeric identifier. Issue [#47978](https://github.com/grafana/grafana/issues/47978)
The format of log messages have been updated, `lvl` is now `level` and `eror`and `dbug` has been replaced with `error` and `debug`. The precision of timestamps has been increased. To smooth the transition, it is possible to opt-out of the new log format by enabling the feature toggle `oldlog`. This option will be removed in a future minor release. Issue [#47584](https://github.com/grafana/grafana/issues/47584)
In the Loki data source, the dataframe format used to represent Loki logs-data has been changed to a more efficient format. The query-result is represented by a single dataframe with a "labels" column, instead of the separate dataframes for every labels-value. When displaying such data in explore, or in a logs-panel in the dashboard will continue to work without changes, but if the data was loaded into a different dashboard-panel, or Transforms were used, adjustments may be necessary. For example, if you used the "labels to fields" transformation with the logs data, please switch to the "extract fields" transformation. Issue [#47153](https://github.com/grafana/grafana/issues/47153)
### Deprecations
`setExploreQueryField`, `setExploreMetricsQueryField` and `setExploreLogsQueryField` are now deprecated and will be removed in a future release. If you need to set a different query editor for Explore, conditionally render based on `props.app` in your regular query editor. Please refer to https://grafana.com/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/ for more information.
Issue [#48701](https://github.com/grafana/grafana/issues/48701)
### Plugin development fixes & changes
- **Chore:** Remove react-testing-lib from bundles. [#50442](https://github.com/grafana/grafana/pull/50442), [@jackw](https://github.com/jackw)
- **Select:** Portal menu by default. [#48176](https://github.com/grafana/grafana/pull/48176), [@ashharrison90](https://github.com/ashharrison90)
@@ -31,11 +31,11 @@ Grafana will now persist all long term data in the database. How to configure th
Grafana alerting provides a new [highly-available model]({{< relref "../alerting/high-availability/" >}}). It also preserves the semantics of legacy dashboard alerting by executing all alerts on every server and by sending notifications only once per alert. Load distribution between servers is not supported at this time.
For instructions on setting up alerting high availability, see [enable alerting high availability](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/).
For instructions on setting up alerting high availability, see [Enable alerting high availability]({{< relref "../alerting/high-availability/enable-alerting-ha/" >}}).
**Legacy dashboard alerts**
Legacy Grafana alerting supports a limited form of high availability. In this model, [alert notifications](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
Legacy Grafana Alerting supports a limited form of high availability. In this model, [alert notifications]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/notifications/" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
## Grafana Live
@@ -20,7 +20,7 @@ Alerting rules can only query backend data sources with alerting enabled:
## Metrics from the alerting engine
The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../setup-grafana/set-up-grafana-monitoring/" >}}). See also, [View alert rules and their current state]({{< relref "../../alerting/old-alerting/view-alerts/" >}}).
The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../setup-grafana/set-up-grafana-monitoring/" >}}). See also, [View alert rules and their current state]({{< relref "../../alerting/fundamentals/state-and-health/" >}}).
| Metric Name | Type | Description |
| ------------------------------------------- | --------- | ---------------------------------------------------------------------------------------- |
+1 -1
View File
@@ -114,7 +114,7 @@ You can now provide detailed information to alert notification recipients by inj
{{< figure src="/static/img/docs/alerting/alert-notification-template-7-4.png" max-width="700px" caption="Variable support in alert notifications" >}}
For more information, refer to the [alert notification docs](https://grafana.com/docs/grafana/latest/alerting/old-alerting/add-notification-template/).
For more information, refer to the [alert notification docs]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/add-notification-template/" >}}).
### Content security policy support
+22 -2
View File
@@ -15,7 +15,7 @@ title: What's new in Grafana v9.0
weight: -33
---
# What’s new in Grafana v9.0 ((beta))
# What’s new in Grafana v9.0
As tradition goes, GrafanaCon - our yearly community event for Grafana open source users, is also where we launch the latest software release of Grafana. Keeping up with tradition, we are excited to be announcing Grafana v9.0 - a release that elevates Grafana’s ease of use, discovery of data through new and improved visualizations and a default unified alerting experience.
@@ -23,7 +23,7 @@ A big focus for Grafana is making observability and data visualization and analy
The release also includes a brand-new powerful and fast heatmap visualization, a more accessible navigation menu, improvements to dashboard search, advanced security and authentication features, and more.
We’ve summarized what’s new in the beta release here. If you’d like all the details you can check out the complete [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md).
We’ve summarized what’s new in the release here. If you’d like all the details you can check out the complete [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md).
## Prometheus query builder
@@ -235,6 +235,26 @@ You can find the complete list of breaking changes in the links below. Please ch
- https://grafana.com/docs/grafana/next/release-notes/release-notes-9-0-0-beta3/
- https://grafana.com/docs/grafana/next/release-notes/release-notes-9-0-0
### Envelope encryption enabled by default
Since v8.3 a new kind of encryption called "envelope encryption" was added, for those secrets stored in the Grafana
database (data source credentials, alerting notification channel credentials, oauth tokens, etc), behind a feature
toggle named `envelopeEncryption`.
In v9.0, `envelopeEncryption` feature toggle has been replaced in favor of `disableEnvelopeEncryption` and envelope encryption is
the encryption mechanism used by default.
Therefore, any secret created or updated in Grafana v9.0 won't be decryptable by any previous Grafana version unless the
feature toggle `envelopeEncryption` is enabled in the previous version (only available since v8.3).
This needs to be considered in high availability setups, progressive rollouts or in case of need to roll back to a previous Grafana version for any reason.
The recommendation here is to enable `envelopeEncryption` for older versions, or alternatively enable `disableEnvelopeEncryption`
before upgrading to v9.0. However, the latter is probably going to be removed in one of the next releases, so we hugely
encourage to move on with envelope encryption.
Find [here]({{< relref "../setup-grafana/configure-security/configure-database-encryption/" >}}) more details and some
possible workarounds in case you end up in an undesired situation.
## A note on Grafana Enterprise licensing
When we release Grafana 9.0 on June 14th, Grafana will no longer enforce viewers and editor-admins differently. That means that regardless of whether your Grafana Enterprise license is tiered or combined, instead of seeing this on the Stats & Licensing page:
+3 -3
View File
@@ -53,7 +53,7 @@ require (
github.com/grafana/cuetsy v0.0.1
github.com/grafana/grafana-aws-sdk v0.10.3
github.com/grafana/grafana-azure-sdk-go v1.2.0
github.com/grafana/grafana-plugin-sdk-go v0.137.0
github.com/grafana/grafana-plugin-sdk-go v0.138.0
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-hclog v1.0.0
@@ -84,7 +84,7 @@ require (
github.com/prometheus/prometheus v1.8.2-0.20211011171444-354d8d2ecfac
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.1.1
github.com/stretchr/testify v1.7.1
github.com/stretchr/testify v1.7.2
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f
github.com/uber/jaeger-client-go v2.29.1+incompatible
@@ -117,7 +117,7 @@ require (
gopkg.in/mail.v2 v2.3.1
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
gopkg.in/yaml.v3 v3.0.1
xorm.io/builder v0.3.6
xorm.io/core v0.7.3
xorm.io/xorm v0.8.2
+6 -4
View File
@@ -1453,8 +1453,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW3
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/grafana-plugin-sdk-go v0.125.0/go.mod h1:9YiJ5GUxIsIEUC0qR9+BJVP5M7mCSP6uc6Ne62YKkgc=
github.com/grafana/grafana-plugin-sdk-go v0.129.0/go.mod h1:4edtosZepfQF9jkQwRywJsNSJzXTHmzbmcVcAl8MEQc=
github.com/grafana/grafana-plugin-sdk-go v0.137.0 h1:4AFA4O5ViGjjktXKmmpyCc1kzRaSHYUHjJWNqUbpW6E=
github.com/grafana/grafana-plugin-sdk-go v0.137.0/go.mod h1:jmrxelOJKrIK0yrsIzcotS8pbqPZozbmJgGy7k3hK1k=
github.com/grafana/grafana-plugin-sdk-go v0.138.0 h1:uJWNwHL4RoQF3axoi3RDSwoNu/KHy5F+5dnrK0fx5yc=
github.com/grafana/grafana-plugin-sdk-go v0.138.0/go.mod h1:Y+Ps2sesZ62AyCnX+hzrYnyDQYe/ZZl+A8yKLOBm12c=
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa h1:+pXjAxavVR2FKKNsuuCXGCWEj8XGc1Af6SPiyBpzU2A=
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa/go.mod h1:0O8o/juxNSKN/e+DzWDTRkl7Zm8CkZcz0NDqEdojlrk=
github.com/grafana/saml v0.0.0-20211007135653-aed1b2edd86b h1:YiSGp34F4V0G08HHx1cJBf2GVgwYAkXQjzuVs1t8jYk=
@@ -2582,8 +2582,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@@ -3826,8 +3827,9 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
@@ -126,10 +126,15 @@ export const getWeekdayIndex = (day: string) => {
return moment.weekdays().findIndex((wd) => wd.toLowerCase() === day.toLowerCase());
};
export const getWeekdayIndexByEnglishName = (day: string) =>
['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].findIndex(
(wd) => wd.toLowerCase() === day.toLowerCase()
);
export const setWeekStart = (weekStart?: string) => {
const suffix = '-weekStart';
const language = getLocale().replace(suffix, '');
const dow = weekStart ? getWeekdayIndex(weekStart) : -1;
const dow = weekStart ? getWeekdayIndexByEnglishName(weekStart) : -1;
if (dow !== -1) {
moment.locale(language + suffix, {
parentLocale: language,
@@ -90,7 +90,7 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
background: colors.mode === 'dark' ? 'rgba(63, 62, 62, 0.45)' : 'rgba(208, 209, 211, 0.24)',
},
sidemenu: {
width: 48,
width: 57,
},
menuTabs: {
height: 41,
@@ -58,4 +58,5 @@ export interface FeatureToggles {
prometheusStreamingJSONParser?: boolean;
validateDashboardsOnSave?: boolean;
prometheusWideSeries?: boolean;
logRequestsInstrumentedAsUnknown?: boolean;
}
@@ -1,4 +1,4 @@
import { currency } from './symbolFormatters';
import { currency, SIPrefix } from './symbolFormatters';
describe('currency', () => {
const symbol = '@';
@@ -8,6 +8,7 @@ describe('currency', () => {
it.each`
value | expectedSuffix | expectedText
${0} | ${''} | ${'0'}
${999} | ${''} | ${'999'}
${1000} | ${'K'} | ${'1'}
${1000000} | ${'M'} | ${'1'}
@@ -33,6 +34,7 @@ describe('currency', () => {
it.each`
value | expectedSuffix | expectedText
${0} | ${'@'} | ${'0'}
${999} | ${'@'} | ${'999'}
${1000} | ${'K@'} | ${'1'}
${1000000} | ${'M@'} | ${'1'}
@@ -53,3 +55,30 @@ describe('currency', () => {
});
});
});
describe('SIPrefix', () => {
const symbol = 'V';
const fmtFunc = SIPrefix(symbol);
it.each`
value | expectedSuffix | expectedText
${0} | ${' V'} | ${'0'}
${999} | ${' V'} | ${'999'}
${1000} | ${' kV'} | ${'1'}
${1000000} | ${' MV'} | ${'1'}
${1000000000} | ${' GV'} | ${'1'}
${1000000000000} | ${' TV'} | ${'1'}
${1000000000000000} | ${' PV'} | ${'1'}
${-1000000000000} | ${' TV'} | ${'-1'}
${-1000000000} | ${' GV'} | ${'-1'}
${-1000000} | ${' MV'} | ${'-1'}
${-1000} | ${' kV'} | ${'-1'}
${-999} | ${' V'} | ${'-999'}
`('when called with value:{$value}', ({ value, expectedSuffix, expectedText }) => {
const { prefix, suffix, text } = fmtFunc(value);
expect(prefix).toEqual(undefined);
expect(suffix).toEqual(expectedSuffix);
expect(text).toEqual(expectedText);
});
});
@@ -54,6 +54,7 @@ describe('valueFormats', () => {
${'b'} | ${0} | ${1532.82} | ${'1533 b'}
${'prefix:b'} | ${undefined} | ${1532.82} | ${'b1533'}
${'suffix:d'} | ${undefined} | ${1532.82} | ${'1533 d'}
${'si:µF'} | ${2} | ${0} | ${'0.00 µF'}
${'si:µF'} | ${2} | ${1234} | ${'1.23 mF'}
${'si:µF'} | ${2} | ${1234000000} | ${'1.23 kF'}
${'si:µF'} | ${2} | ${1234000000000000} | ${'1.23 GF'}
@@ -143,7 +143,7 @@ export function scaledUnits(factor: number, extArray: string[], offset = 0): Val
return { text: size.toLocaleString() };
}
const siIndex = Math.floor(logb(factor, Math.abs(size)));
const siIndex = size === 0 ? 0 : Math.floor(logb(factor, Math.abs(size)));
const suffix = extArray[clamp(offset + siIndex, 0, extArray.length - 1)];
return {
@@ -272,6 +272,7 @@ export interface VizLegendOptions {
placement: LegendPlacement;
sortBy?: string;
sortDesc?: boolean;
width?: number;
}
export enum BarGaugeDisplayMode {
@@ -11,5 +11,6 @@ VizLegendOptions: {
isVisible?: bool
sortBy?: string
sortDesc?: bool
width?: number
calcs: [...string]
} @cuetsy(kind="interface")
@@ -77,13 +77,9 @@ Object {
"pub": [Function],
},
"key": "__global_",
"match": Array [
[Function],
[Function],
],
"scales": Array [
"x",
null,
"__fixed/na-na/na-na/auto/linear/na",
],
},
},
@@ -82,6 +82,7 @@ export class UnThemedLogMessageAnsi extends PureComponent<Props, State> {
return chunks.map((chunk, index) => {
const chunkText = this.props.highlight?.searchWords ? (
<Highlighter
key={index}
textToHighlight={chunk.text}
searchWords={this.props.highlight.searchWords}
findChunks={findHighlightChunksInText}
@@ -137,10 +137,12 @@ export const LogRowContextGroup: React.FunctionComponent<LogRowContextGroupProps
const listContainerRef = useRef<HTMLDivElement>() as React.RefObject<HTMLDivElement>;
useLayoutEffect(() => {
if (shouldScrollToBottom && listContainerRef.current) {
// We want to scroll to bottom only when we receive first 10 log lines
const shouldScrollRows = rows.length > 0 && rows.length <= 10;
if (shouldScrollToBottom && shouldScrollRows && listContainerRef.current) {
setScrollTop(listContainerRef.current.offsetHeight);
}
}, [shouldScrollToBottom]);
}, [shouldScrollToBottom, rows]);
const headerProps = {
row,
@@ -8,6 +8,7 @@ export type ActionMeta = SelectActionMeta<{}>;
export type InputActionMeta = {
action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close';
};
export type LoadOptionsCallback<T> = (options: Array<SelectableValue<T>>) => void;
export interface SelectCommonProps<T> {
/** Aria label applied to the input field */
@@ -87,8 +88,10 @@ export interface SelectCommonProps<T> {
export interface SelectAsyncProps<T> {
/** When specified as boolean the loadOptions will execute when component is mounted */
defaultOptions?: boolean | Array<SelectableValue<T>>;
/** Asynchronously load select options */
loadOptions?: (query: string) => Promise<Array<SelectableValue<T>>>;
loadOptions?: (query: string, cb?: LoadOptionsCallback<T>) => Promise<Array<SelectableValue<T>>> | void;
/** If cacheOptions is true, then the loaded data will be cached. The cache will remain until cacheOptions changes value. */
cacheOptions?: boolean;
/** Message to display when options are loading */
@@ -465,10 +465,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
return true;
},
},
// ??? setSeries: syncMode === DashboardCursorSync.Tooltip,
//TODO: remove any once https://github.com/leeoniya/uPlot/pull/611 got merged or the typing is fixed
scales: [xScaleKey, null as any],
match: [() => true, () => true],
scales: [xScaleKey, yScaleKey],
// match: [() => true, (a, b) => a === b],
};
}
@@ -68,6 +68,11 @@ export const VizLayout: VizLayoutComponentType = ({ width, height, legend, child
if (legendMeasure) {
size = { width: width - legendMeasure.width, height };
}
if (legend.props.width) {
legendStyle.width = legend.props.width;
size = { width: width - legend.props.width, height };
}
break;
}
@@ -115,6 +120,7 @@ export interface VizLayoutLegendProps {
children: React.ReactNode;
maxHeight?: string;
maxWidth?: string;
width?: number;
}
/**
@@ -36,6 +36,15 @@ export function addLegendOptions<T extends OptionsWithLegend>(
],
},
showIf: (c) => c.legend.displayMode !== LegendDisplayMode.Hidden,
})
.addNumberInput({
path: 'legend.width',
name: 'Width',
category: ['Legend'],
settings: {
placeholder: 'Auto',
},
showIf: (c) => c.legend.displayMode !== LegendDisplayMode.Hidden && c.legend.placement === 'right',
});
if (includeLegendCalcs) {
+5 -5
View File
@@ -431,11 +431,11 @@ func (hs *HTTPServer) registerRoutes() {
// Playlist
apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
playlistRoute.Get("/", routing.Wrap(hs.SearchPlaylists))
playlistRoute.Get("/:id", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylist))
playlistRoute.Get("/:id/items", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems))
playlistRoute.Get("/:id/dashboards", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylistDashboards))
playlistRoute.Delete("/:id", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.DeletePlaylist))
playlistRoute.Put("/:id", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist))
playlistRoute.Get("/:uid", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylist))
playlistRoute.Get("/:uid/items", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems))
playlistRoute.Get("/:uid/dashboards", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylistDashboards))
playlistRoute.Delete("/:uid", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.DeletePlaylist))
playlistRoute.Put("/:uid", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist))
playlistRoute.Post("/", reqEditorRole, routing.Wrap(hs.CreatePlaylist))
})
+2 -8
View File
@@ -75,19 +75,13 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
loginInfo.AuthModule = name
provider := hs.SocialService.GetOAuthInfoProvider(name)
if provider == nil {
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{
HttpStatus: http.StatusNotFound,
PublicMessage: "OAuth not enabled",
})
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, errors.New("OAuth not enabled"))
return
}
connect, err := hs.SocialService.GetConnector(name)
if err != nil {
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{
HttpStatus: http.StatusNotFound,
PublicMessage: fmt.Sprintf("No OAuth with name %s configured", name),
})
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, fmt.Errorf("no OAuth with name %s configured", name))
return
}
+11 -8
View File
@@ -9,6 +9,8 @@ import (
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -31,11 +33,12 @@ func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux {
sqlStore := sqlstore.InitTestDB(t)
hs := &HTTPServer{
Cfg: cfg,
License: &licensing.OSSLicensingService{Cfg: cfg},
SQLStore: sqlStore,
SocialService: social.ProvideService(cfg),
HooksService: hooks.ProvideService(),
Cfg: cfg,
License: &licensing.OSSLicensingService{Cfg: cfg},
SQLStore: sqlStore,
SocialService: social.ProvideService(cfg),
HooksService: hooks.ProvideService(),
SecretsService: fakes.NewFakeSecretsService(),
}
m := web.New()
@@ -55,9 +58,9 @@ func TestOAuthLogin_UnknownProvider(t *testing.T) {
recorder := httptest.NewRecorder()
m.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusNotFound, recorder.Code)
assert.Contains(t, recorder.Body.String(), "OAuth not enabled")
// expect to be redirected to /login
assert.Equal(t, http.StatusFound, recorder.Code)
assert.Equal(t, "/login", recorder.Header().Get("Location"))
}
func TestOAuthLogin_Base(t *testing.T) {
+2 -3
View File
@@ -19,13 +19,12 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) {
setting.AutoAssignOrg = true
setting.AutoAssignOrgId = int(testOrgID)
sqlStore.Cfg.AutoAssignOrg = true
sqlStore.Cfg.AutoAssignOrgId = int(testOrgID)
_, err := sqlStore.CreateUser(context.Background(), models.CreateUserCommand{Email: "testUser@grafana.com", Login: testUserLogin})
require.NoError(t, err)
+19 -39
View File
@@ -3,7 +3,6 @@ package api
import (
"context"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
@@ -11,13 +10,9 @@ import (
)
func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) {
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
c.JsonApiErr(http.StatusBadRequest, "id is invalid", nil)
return
}
query := models.GetPlaylistByIdQuery{Id: id}
err = hs.SQLStore.GetPlaylist(c.Req.Context(), &query)
uid := web.Params(c.Req)[":uid"]
query := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId}
err := hs.SQLStore.GetPlaylist(c.Req.Context(), &query)
if err != nil {
c.JsonApiErr(404, "Playlist not found", err)
@@ -58,20 +53,18 @@ func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response {
}
func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response {
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
cmd := models.GetPlaylistByIdQuery{Id: id}
uid := web.Params(c.Req)[":uid"]
cmd := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId}
if err := hs.SQLStore.GetPlaylist(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Playlist not found", err)
}
playlistDTOs, _ := hs.LoadPlaylistItemDTOs(c.Req.Context(), id)
playlistDTOs, _ := hs.LoadPlaylistItemDTOs(c.Req.Context(), uid, c.OrgId)
dto := &models.PlaylistDTO{
Id: cmd.Result.Id,
UID: cmd.Result.UID,
Name: cmd.Result.Name,
Interval: cmd.Result.Interval,
OrgId: cmd.Result.OrgId,
@@ -81,8 +74,8 @@ func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, dto)
}
func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, id int64) ([]models.PlaylistItemDTO, error) {
playlistitems, err := hs.LoadPlaylistItems(ctx, id)
func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItemDTO, error) {
playlistitems, err := hs.LoadPlaylistItems(ctx, uid, orgId)
if err != nil {
return nil, err
@@ -104,8 +97,8 @@ func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, id int64) ([]mod
return playlistDTOs, nil
}
func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, id int64) ([]models.PlaylistItem, error) {
itemQuery := models.GetPlaylistItemsByIdQuery{PlaylistId: id}
func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItem, error) {
itemQuery := models.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: orgId}
if err := hs.SQLStore.GetPlaylistItem(ctx, &itemQuery); err != nil {
return nil, err
}
@@ -114,12 +107,9 @@ func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, id int64) ([]models
}
func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response {
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
uid := web.Params(c.Req)[":uid"]
playlistDTOs, err := hs.LoadPlaylistItemDTOs(c.Req.Context(), id)
playlistDTOs, err := hs.LoadPlaylistItemDTOs(c.Req.Context(), uid, c.OrgId)
if err != nil {
return response.Error(500, "Could not load playlist items", err)
@@ -129,12 +119,9 @@ func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response {
}
func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Response {
playlistID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
playlistUID := web.Params(c.Req)[":uid"]
playlists, err := hs.LoadPlaylistDashboards(c.Req.Context(), c.OrgId, c.SignedInUser, playlistID)
playlists, err := hs.LoadPlaylistDashboards(c.Req.Context(), c.OrgId, c.SignedInUser, playlistUID)
if err != nil {
return response.Error(500, "Could not load dashboards", err)
}
@@ -143,12 +130,9 @@ func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Respo
}
func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response {
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
uid := web.Params(c.Req)[":uid"]
cmd := models.DeletePlaylistCommand{Id: id, OrgId: c.OrgId}
cmd := models.DeletePlaylistCommand{UID: uid, OrgId: c.OrgId}
if err := hs.SQLStore.DeletePlaylist(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to delete playlist", err)
}
@@ -176,17 +160,13 @@ func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.OrgId = c.OrgId
var err error
cmd.Id, err = strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
cmd.UID = web.Params(c.Req)[":uid"]
if err := hs.SQLStore.UpdatePlaylist(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to save playlist", err)
}
playlistDTOs, err := hs.LoadPlaylistItemDTOs(c.Req.Context(), cmd.Id)
playlistDTOs, err := hs.LoadPlaylistItemDTOs(c.Req.Context(), cmd.UID, c.OrgId)
if err != nil {
return response.Error(500, "Failed to save playlist", err)
}
+2 -2
View File
@@ -65,8 +65,8 @@ func (hs *HTTPServer) populateDashboardsByTag(ctx context.Context, orgID int64,
return result
}
func (hs *HTTPServer) LoadPlaylistDashboards(ctx context.Context, orgID int64, signedInUser *models.SignedInUser, playlistID int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := hs.LoadPlaylistItems(ctx, playlistID)
func (hs *HTTPServer) LoadPlaylistDashboards(ctx context.Context, orgID int64, signedInUser *models.SignedInUser, playlistUID string) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := hs.LoadPlaylistItems(ctx, playlistUID, orgID)
dashboardByIDs := make([]int64, 0)
dashboardByTag := make([]string, 0)
+24 -40
View File
@@ -3,9 +3,9 @@ package metrics
import (
"runtime"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
// ExporterName is used as namespace for exposing prometheus metrics
@@ -199,40 +199,40 @@ func init() {
Namespace: ExporterName,
})
MPageStatus = newCounterVecStartingAtZero(
MPageStatus = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "page_response_status_total",
Help: "page http response status",
Namespace: ExporterName,
}, []string{"code"}, httpStatusCodes...)
}, []string{"code"}, map[string][]string{"code": httpStatusCodes})
MApiStatus = newCounterVecStartingAtZero(
MApiStatus = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "api_response_status_total",
Help: "api http response status",
Namespace: ExporterName,
}, []string{"code"}, httpStatusCodes...)
}, []string{"code"}, map[string][]string{"code": httpStatusCodes})
MProxyStatus = newCounterVecStartingAtZero(
MProxyStatus = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "proxy_response_status_total",
Help: "proxy http response status",
Namespace: ExporterName,
}, []string{"code"}, httpStatusCodes...)
}, []string{"code"}, map[string][]string{"code": httpStatusCodes})
MApiUserSignUpStarted = newCounterStartingAtZero(prometheus.CounterOpts{
MApiUserSignUpStarted = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_started_total",
Help: "amount of users who started the signup flow",
Namespace: ExporterName,
})
MApiUserSignUpCompleted = newCounterStartingAtZero(prometheus.CounterOpts{
MApiUserSignUpCompleted = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_completed_total",
Help: "amount of users who completed the signup flow",
Namespace: ExporterName,
})
MApiUserSignUpInvite = newCounterStartingAtZero(prometheus.CounterOpts{
MApiUserSignUpInvite = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_invite_total",
Help: "amount of users who have been invited",
Namespace: ExporterName,
@@ -259,55 +259,55 @@ func init() {
Namespace: ExporterName,
})
MApiAdminUserCreate = newCounterStartingAtZero(prometheus.CounterOpts{
MApiAdminUserCreate = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_admin_user_created_total",
Help: "api admin user created counter",
Namespace: ExporterName,
})
MApiLoginPost = newCounterStartingAtZero(prometheus.CounterOpts{
MApiLoginPost = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_post_total",
Help: "api login post counter",
Namespace: ExporterName,
})
MApiLoginOAuth = newCounterStartingAtZero(prometheus.CounterOpts{
MApiLoginOAuth = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_oauth_total",
Help: "api login oauth counter",
Namespace: ExporterName,
})
MApiLoginSAML = newCounterStartingAtZero(prometheus.CounterOpts{
MApiLoginSAML = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_saml_total",
Help: "api login saml counter",
Namespace: ExporterName,
})
MApiOrgCreate = newCounterStartingAtZero(prometheus.CounterOpts{
MApiOrgCreate = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_org_create_total",
Help: "api org created counter",
Namespace: ExporterName,
})
MApiDashboardSnapshotCreate = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardSnapshotCreate = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_create_total",
Help: "dashboard snapshots created",
Namespace: ExporterName,
})
MApiDashboardSnapshotExternal = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardSnapshotExternal = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_external_total",
Help: "external dashboard snapshots created",
Namespace: ExporterName,
})
MApiDashboardSnapshotGet = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardSnapshotGet = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_get_total",
Help: "loaded dashboards",
Namespace: ExporterName,
})
MApiDashboardInsert = newCounterStartingAtZero(prometheus.CounterOpts{
MApiDashboardInsert = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_models_dashboard_insert_total",
Help: "dashboards inserted ",
Namespace: ExporterName,
@@ -331,25 +331,25 @@ func init() {
Namespace: ExporterName,
}, []string{"type"})
MAwsCloudWatchGetMetricStatistics = newCounterStartingAtZero(prometheus.CounterOpts{
MAwsCloudWatchGetMetricStatistics = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_get_metric_statistics_total",
Help: "counter for getting metric statistics from aws",
Namespace: ExporterName,
})
MAwsCloudWatchListMetrics = newCounterStartingAtZero(prometheus.CounterOpts{
MAwsCloudWatchListMetrics = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_list_metrics_total",
Help: "counter for getting list of metrics from aws",
Namespace: ExporterName,
})
MAwsCloudWatchGetMetricData = newCounterStartingAtZero(prometheus.CounterOpts{
MAwsCloudWatchGetMetricData = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_get_metric_data_total",
Help: "counter for getting metric data time series from aws",
Namespace: ExporterName,
})
MDBDataSourceQueryByID = newCounterStartingAtZero(prometheus.CounterOpts{
MDBDataSourceQueryByID = metricutil.NewCounterStartingAtZero(prometheus.CounterOpts{
Name: "db_datasource_query_by_id_total",
Help: "counter for getting datasource by id",
Namespace: ExporterName,
@@ -646,19 +646,3 @@ func initMetricVars() {
StatsTotalDataKeys,
)
}
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
counter := prometheus.NewCounterVec(opts, labels)
for _, label := range labelValues {
counter.WithLabelValues(label).Add(0)
}
return counter
}
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
counter := prometheus.NewCounter(opts)
counter.Add(0)
return counter
}
+55
View File
@@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
// SanitizeLabelName removes all invalid chars from the label name.
@@ -29,3 +31,56 @@ func SanitizeLabelName(name string) (string, error) {
return out.String(), nil
}
// NewCounterStartingAtZero initializes a new Prometheus counter with an initial
// observation of zero. Used for to guarantee the existence of the specific metric.
func NewCounterStartingAtZero(opts prometheus.CounterOpts) prometheus.Counter {
counter := prometheus.NewCounter(opts)
counter.Add(0)
return counter
}
// NewCounterVecStartingAtZero initializes a new Prometheus counter with an initial
// observation of zero for every possible value of each label. Used for the sake of
// consistency among all the possible labels and values.
func NewCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues map[string][]string) *prometheus.CounterVec {
counter := prometheus.NewCounterVec(opts, labels)
for _, ls := range buildLabelSets(labels, labelValues) {
counter.With(ls).Add(0)
}
return counter
}
func buildLabelSets(labels []string, labelValues map[string][]string) []prometheus.Labels {
var labelSets []prometheus.Labels
var n func(i int, ls prometheus.Labels)
n = func(i int, ls prometheus.Labels) {
if i == len(labels) {
labelSets = append(labelSets, ls)
return
}
label := labels[i]
values := labelValues[label]
for _, v := range values {
lsCopy := copyLabelSet(ls)
lsCopy[label] = v
n(i+1, lsCopy)
}
}
n(0, prometheus.Labels{})
return labelSets
}
func copyLabelSet(ls prometheus.Labels) prometheus.Labels {
newLs := make(prometheus.Labels, len(ls))
for l, v := range ls {
newLs[l] = v
}
return newLs
}
@@ -3,6 +3,7 @@ package metricutil
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -30,3 +31,89 @@ func TestLabelNameSanitization(t *testing.T) {
}
}
}
func Test_buildLabelSets(t *testing.T) {
testcases := map[string]struct {
labels []string
labelValues map[string][]string
expected []prometheus.Labels
}{
"single label, single value": {
labels: []string{"operation"},
labelValues: map[string][]string{
"operation": {"insert"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert"},
},
},
"single label, multiple values": {
labels: []string{"operation"},
labelValues: map[string][]string{
"operation": {"insert", "delete"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert"},
map[string]string{"operation": "delete"},
},
},
"multiple label, single value": {
labels: []string{"operation", "success"},
labelValues: map[string][]string{
"operation": {"insert"},
"success": {"true"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert", "success": "true"},
},
},
"multiple label, multiple values": {
labels: []string{"operation", "success"},
labelValues: map[string][]string{
"operation": {"insert", "delete"},
"success": {"true", "false"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert", "success": "true"},
map[string]string{"operation": "insert", "success": "false"},
map[string]string{"operation": "delete", "success": "true"},
map[string]string{"operation": "delete", "success": "false"},
},
},
"irregular labels and values": {
labels: []string{"operation", "success", "environment"},
labelValues: map[string][]string{
"operation": {"insert", "update", "delete"},
"success": {"true", "false"},
"environment": {"dev", "test", "staging"},
},
expected: []prometheus.Labels{
map[string]string{"operation": "insert", "success": "true", "environment": "dev"},
map[string]string{"operation": "insert", "success": "true", "environment": "test"},
map[string]string{"operation": "insert", "success": "true", "environment": "staging"},
map[string]string{"operation": "insert", "success": "false", "environment": "dev"},
map[string]string{"operation": "insert", "success": "false", "environment": "test"},
map[string]string{"operation": "insert", "success": "false", "environment": "staging"},
map[string]string{"operation": "update", "success": "true", "environment": "dev"},
map[string]string{"operation": "update", "success": "true", "environment": "test"},
map[string]string{"operation": "update", "success": "true", "environment": "staging"},
map[string]string{"operation": "update", "success": "false", "environment": "dev"},
map[string]string{"operation": "update", "success": "false", "environment": "test"},
map[string]string{"operation": "update", "success": "false", "environment": "staging"},
map[string]string{"operation": "delete", "success": "true", "environment": "dev"},
map[string]string{"operation": "delete", "success": "true", "environment": "test"},
map[string]string{"operation": "delete", "success": "true", "environment": "staging"},
map[string]string{"operation": "delete", "success": "false", "environment": "dev"},
map[string]string{"operation": "delete", "success": "false", "environment": "test"},
map[string]string{"operation": "delete", "success": "false", "environment": "staging"},
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
got := buildLabelSets(tc.labels, tc.labelValues)
assert.Equal(t, tc.expected, got)
})
}
}
+18 -11
View File
@@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -46,12 +47,9 @@ func init() {
// RequestMetrics is a middleware handler that instruments the request.
func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
if strings.HasPrefix(c.Req.URL.Path, "/public/") || c.Req.URL.Path == "robots.txt" || c.Req.URL.Path == "/metrics" {
c.Next()
return
}
log := log.New("middleware.request-metrics")
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
rw := res.(web.ResponseWriter)
now := time.Now()
httpRequestsInFlight.Inc()
@@ -59,15 +57,24 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
c.Map(c.Req)
c.Next()
handler := "unknown"
if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists {
handler = routeOperation
}
status := rw.Status()
code := sanitizeCode(status)
handler := "unknown"
if routeOperation, exists := routeOperationName(c.Req); exists {
handler = routeOperation
} else {
// if grafana does not recognize the handler and returns 404 we should register it as `notfound`
if status == http.StatusNotFound {
handler = "notfound"
} else {
// log requests where we could not identify handler so we can register them.
if features.IsEnabled(featuremgmt.FlagLogRequestsInstrumentedAsUnknown) {
log.Warn("request instrumented as unknown", "path", c.Req.URL.Path, "status_code", status)
}
}
}
// avoiding the sanitize functions for in the new instrumentation
// since they dont make much sense. We should remove them later.
histogram := httpRequestDurationHistogram.
+40
View File
@@ -0,0 +1,40 @@
package middleware
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCanGetRouteNameFromContext(t *testing.T) {
tcs := []struct {
namedHandler string
path string
expected string
}{
{namedHandler: "", path: "/public/img/apple-touch-icon.png", expected: "public-assets"},
{namedHandler: "", path: "/favicon.ico", expected: "public-assets"},
{namedHandler: "", path: "/robots.txt", expected: "/robots.txt"},
{namedHandler: "", path: "/debug/pprof/heap", expected: "/debug/pprof-handlers"},
{namedHandler: "", path: "/debug/pprof/allocs", expected: "/debug/pprof-handlers"},
{namedHandler: "", path: "/debug/pprof/threadcreate", expected: "/debug/pprof-handlers"},
{namedHandler: "/api/dashboard/:uid", path: "/api/dashboard/ddfgeasdfr", expected: "/api/dashboard/:uid"},
{namedHandler: "", path: "/metrics", expected: "/metrics"},
}
for _, tc := range tcs {
req, err := http.NewRequest(http.MethodPost, "https://ops.grafana.net"+tc.path, nil)
if tc.namedHandler != "" {
req = req.WithContext(context.WithValue(context.Background(), routeOperationNameKey, tc.namedHandler))
}
assert.NoError(t, err)
handler, _ := routeOperationName(req)
assert.Equal(t, tc.expected, handler)
}
}
+26 -5
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
@@ -31,20 +32,40 @@ func ProvideRouteOperationName(name string) web.Handler {
}
}
// RouteOperationNameFromContext receives the route operation name from context, if set.
func RouteOperationNameFromContext(ctx context.Context) (string, bool) {
if val := ctx.Value(routeOperationNameKey); val != nil {
var unnamedHandlers = []struct {
pathPattern *regexp.Regexp
handler string
}{
{handler: "public-assets", pathPattern: regexp.MustCompile("^/favicon.ico")},
{handler: "public-assets", pathPattern: regexp.MustCompile("^/public/")},
{handler: "/metrics", pathPattern: regexp.MustCompile("^/metrics")},
{handler: "/healthz", pathPattern: regexp.MustCompile("^/healthz")},
{handler: "/robots.txt", pathPattern: regexp.MustCompile("^/robots.txt$")},
// bundle all pprof endpoints under the same handler name
{handler: "/debug/pprof-handlers", pathPattern: regexp.MustCompile("^/debug/pprof")},
}
// routeOperationName receives the route operation name from context, if set.
func routeOperationName(req *http.Request) (string, bool) {
if val := req.Context().Value(routeOperationNameKey); val != nil {
op, ok := val.(string)
return op, ok
}
for _, hp := range unnamedHandlers {
if hp.pathPattern.Match([]byte(req.URL.Path)) {
return hp.handler, true
}
}
return "", false
}
func RequestTracing(tracer tracing.Tracer) web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
if strings.HasPrefix(c.Req.URL.Path, "/public/") ||
c.Req.URL.Path == "robots.txt" {
c.Req.URL.Path == "/robots.txt" ||
c.Req.URL.Path == "/favicon.ico" {
c.Next()
return
}
@@ -61,7 +82,7 @@ func RequestTracing(tracer tracing.Tracer) web.Handler {
// Only call span.Finish when a route operation name have been set,
// meaning that not set the span would not be reported.
if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists {
if routeOperation, exists := routeOperationName(c.Req); exists {
defer span.End()
span.SetName(fmt.Sprintf("HTTP %s %s", req.Method, routeOperation))
}
+13 -8
View File
@@ -6,12 +6,14 @@ import (
// Typed errors
var (
ErrPlaylistNotFound = errors.New("Playlist not found")
ErrPlaylistNotFound = errors.New("Playlist not found")
ErrPlaylistFailedGenerateUniqueUid = errors.New("failed to generate unique playlist UID")
)
// Playlist model
type Playlist struct {
Id int64 `json:"id"`
UID string `json:"uid" xorm:"uid"`
Name string `json:"name"`
Interval string `json:"interval"`
OrgId int64 `json:"-"`
@@ -19,6 +21,7 @@ type Playlist struct {
type PlaylistDTO struct {
Id int64 `json:"id"`
UID string `json:"uid"`
Name string `json:"name"`
Interval string `json:"interval"`
OrgId int64 `json:"-"`
@@ -51,7 +54,7 @@ type Playlists []*Playlist
type UpdatePlaylistCommand struct {
OrgId int64 `json:"-"`
Id int64 `json:"id"`
UID string `json:"uid"`
Name string `json:"name" binding:"Required"`
Interval string `json:"interval"`
Items []PlaylistItemDTO `json:"items"`
@@ -69,7 +72,7 @@ type CreatePlaylistCommand struct {
}
type DeletePlaylistCommand struct {
Id int64
UID string
OrgId int64
}
@@ -85,12 +88,14 @@ type GetPlaylistsQuery struct {
Result Playlists
}
type GetPlaylistByIdQuery struct {
Id int64
type GetPlaylistByUidQuery struct {
UID string
OrgId int64
Result *Playlist
}
type GetPlaylistItemsByIdQuery struct {
PlaylistId int64
Result *[]PlaylistItem
type GetPlaylistItemsByUidQuery struct {
PlaylistUID string
OrgId int64
Result *[]PlaylistItem
}
@@ -21,7 +21,6 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/star/starimpl"
"github.com/grafana/grafana/pkg/setting"
)
func TestIntegrationDashboardDataAccess(t *testing.T) {
@@ -661,9 +660,9 @@ func insertTestRule(t *testing.T, sqlStore *sqlstore.SQLStore, foderOrgID int64,
func CreateUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) models.User {
t.Helper()
setting.AutoAssignOrg = true
setting.AutoAssignOrgId = 1
setting.AutoAssignOrgRole = role
sqlStore.Cfg.AutoAssignOrg = true
sqlStore.Cfg.AutoAssignOrgId = 1
sqlStore.Cfg.AutoAssignOrgRole = role
currentUserCmd := models.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
currentUser, err := sqlStore.CreateUser(context.Background(), currentUserCmd)
require.NoError(t, err)
@@ -5,15 +5,13 @@ package permissions
import (
"context"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/require"
)
@@ -256,9 +254,9 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) {
func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) models.User {
t.Helper()
setting.AutoAssignOrg = true
setting.AutoAssignOrgId = 1
setting.AutoAssignOrgRole = role
sqlStore.Cfg.AutoAssignOrg = true
sqlStore.Cfg.AutoAssignOrgId = 1
sqlStore.Cfg.AutoAssignOrgRole = role
currentUserCmd := models.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
currentUser, err := sqlStore.CreateUser(context.Background(), currentUserCmd)
require.NoError(t, err)
@@ -86,6 +86,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte
ds, err := p.DataSourceCache.GetDatasource(c.Req.Context(), dsID, c.SignedInUser, c.SkipCache)
if err != nil {
toAPIError(c, err)
return
}
p.proxyDatasourceRequest(c, ds)
}
+9 -5
View File
@@ -210,11 +210,10 @@ var (
State: FeatureStateAlpha,
},
{
Name: "azureMonitorExperimentalUI",
Description: "Use grafana-experimental UI in Azure Monitor",
State: FeatureStateAlpha,
RequiresDevMode: true,
FrontendOnly: true,
Name: "azureMonitorExperimentalUI",
Description: "Use grafana-experimental UI in Azure Monitor",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "traceToMetrics",
@@ -238,5 +237,10 @@ var (
Description: "Enable wide series responses in the Prometheus datasource",
State: FeatureStateAlpha,
},
{
Name: "logRequestsInstrumentedAsUnknown",
Description: "Logs the path for requests that are instrumented as unknown",
State: FeatureStateAlpha,
},
}
)
+4
View File
@@ -174,4 +174,8 @@ const (
// FlagPrometheusWideSeries
// Enable wide series responses in the Prometheus datasource
FlagPrometheusWideSeries = "prometheusWideSeries"
// FlagLogRequestsInstrumentedAsUnknown
// Logs the path for requests that are instrumented as unknown
FlagLogRequestsInstrumentedAsUnknown = "logRequestsInstrumentedAsUnknown"
)
+12 -2
View File
@@ -82,7 +82,12 @@ func loadLibraryPanelsRecursively(elements map[string]libraryelements.LibraryEle
elementInDB, ok := elements[UID]
if !ok {
elem := parent.Get("panels").GetIndex(i)
elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap())
gridPos := panelAsJSON.Get("gridPos").MustMap()
if gridPos == nil {
elem.Del("gridPos")
} else {
elem.Set("gridPos", gridPos)
}
elem.Set("id", panelAsJSON.Get("id").MustInt64())
elem.Set("type", fmt.Sprintf("Library panel with UID: \"%s\"", UID))
elem.Set("libraryPanel", map[string]interface{}{
@@ -111,7 +116,12 @@ func loadLibraryPanelsRecursively(elements map[string]libraryelements.LibraryEle
// set dashboard specific props
elem := parent.Get("panels").GetIndex(i)
elem.Set("gridPos", panelAsJSON.Get("gridPos").MustMap())
gridPos := panelAsJSON.Get("gridPos").MustMap()
if gridPos == nil {
elem.Del("gridPos")
} else {
elem.Set("gridPos", gridPos)
}
elem.Set("id", panelAsJSON.Get("id").MustInt64())
elem.Set("libraryPanel", map[string]interface{}{
"uid": elementInDB.UID,
-1
View File
@@ -940,7 +940,6 @@ func (g *GrafanaLive) handleDatasourceScope(ctx context.Context, user *models.Si
// Publish sends the data to the channel without checking permissions etc.
func (g *GrafanaLive) Publish(orgID int64, channel string, data []byte) error {
logger.Debug("publish into channel", "channel", channel, "orgId", orgID, "data", string(data))
_, err := g.node.Publish(orgchannel.PrependOrgID(orgID, channel), data)
return err
}
@@ -206,6 +206,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.R
}
func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response {
mt.Provenance = alerting_models.ProvenanceAPI
created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgId)
if err != nil {
if errors.Is(err, provisioning.ErrValidation) {
@@ -219,6 +220,7 @@ func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definit
func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response {
name := pathParam(c, namePathParam)
mt.Name = name
mt.Provenance = alerting_models.ProvenanceAPI
updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgId)
if err != nil {
if errors.Is(err, provisioning.ErrValidation) {
File diff suppressed because it is too large Load Diff
@@ -24,11 +24,6 @@
// swagger:meta
package definitions
// swagger:model
type ValidationError struct {
Msg string `json:"msg"`
}
type Backend int
const (
@@ -12,12 +12,15 @@ import (
//
// Responses:
// 200: AlertRule
// 400: ValidationError
// 404: description: Not found.
// swagger:route POST /api/v1/provisioning/alert-rules provisioning stable RoutePostAlertRule
//
// Create a new alert rule.
//
// Consumes:
// - application/json
//
// Responses:
// 201: AlertRule
// 400: ValidationError
@@ -39,10 +42,10 @@ import (
//
// Responses:
// 204: description: The alert rule was deleted successfully.
// 400: ValidationError
// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule
type AlertRuleUIDReference struct {
// Alert rule UID
// in:path
UID string
}
@@ -12,8 +12,7 @@ import (
// Get all the contact points.
//
// Responses:
// 200: Route
// 400: ValidationError
// 200: ContactPoints
// swagger:route POST /api/v1/provisioning/contact-points provisioning stable RoutePostContactpoints
//
@@ -23,7 +22,7 @@ import (
// - application/json
//
// Responses:
// 202: Ack
// 202: EmbeddedContactPoint
// 400: ValidationError
// swagger:route PUT /api/v1/provisioning/contact-points/{UID} provisioning stable RoutePutContactpoint
@@ -45,12 +44,11 @@ import (
// - application/json
//
// Responses:
// 202: Ack
// 400: ValidationError
// 204: description: The contact point was deleted successfully.
// swagger:parameters RoutePutContactpoint RouteDeleteContactpoints
type ContactPointUIDReference struct {
// UID should be the contact point unique identifier
// UID is the contact point unique identifier
// in:path
UID string
}
@@ -61,8 +59,12 @@ type ContactPointPayload struct {
Body EmbeddedContactPoint
}
// swagger:model
type ContactPoints []EmbeddedContactPoint
// EmbeddedContactPoint is the contact point type that is used
// by grafanas embedded alertmanager implementation.
// swagger:model
type EmbeddedContactPoint struct {
// UID is the unique identifier of the contact point. The UID can be
// set by the user.
@@ -11,7 +11,6 @@ import (
//
// Responses:
// 200: MuteTimings
// 400: ValidationError
// swagger:route GET /api/v1/provisioning/mute-timings/{name} provisioning stable RouteGetMuteTiming
//
@@ -19,7 +18,7 @@ import (
//
// Responses:
// 200: MuteTimeInterval
// 400: ValidationError
// 404: description: Not found.
// swagger:route POST /api/v1/provisioning/mute-timings provisioning stable RoutePostMuteTiming
//
@@ -48,7 +47,7 @@ import (
// Delete a mute timing.
//
// Responses:
// 204: Ack
// 204: description: The mute timing was deleted successfully.
// swagger:route
@@ -57,7 +56,7 @@ type MuteTimings []MuteTimeInterval
// swagger:parameters RouteGetTemplate RouteGetMuteTiming RoutePutMuteTiming stable RouteDeleteMuteTiming
type RouteGetMuteTimingParam struct {
// Template Name
// Mute timing name
// in:path
Name string `json:"name"`
}
@@ -6,7 +6,7 @@ package definitions
//
// Responses:
// 200: Route
// 400: ValidationError
// description: The currently active notification routing tree
// swagger:route PUT /api/v1/provisioning/policies provisioning stable RoutePutPolicyTree
//
@@ -21,6 +21,7 @@ package definitions
// swagger:parameters RoutePutPolicyTree
type Policytree struct {
// The new notification routing tree to use
// in:body
Body Route
}
@@ -9,8 +9,8 @@ import (
// Get all message templates.
//
// Responses:
// 200: MessageTemplate
// 400: ValidationError
// 200: MessageTemplates
// 404: description: Not found.
// swagger:route GET /api/v1/provisioning/templates/{name} provisioning stable RouteGetTemplate
//
@@ -18,7 +18,7 @@ import (
//
// Responses:
// 200: MessageTemplate
// 404: NotFound
// 404: description: Not found.
// swagger:route PUT /api/v1/provisioning/templates/{name} provisioning stable RoutePutTemplate
//
@@ -28,7 +28,7 @@ import (
// - application/json
//
// Responses:
// 202: Ack
// 202: MessageTemplate
// 400: ValidationError
// swagger:route DELETE /api/v1/provisioning/templates/{name} provisioning stable RouteDeleteTemplate
@@ -36,7 +36,7 @@ import (
// Delete a template.
//
// Responses:
// 204: Ack
// 204: description: The template was deleted successfully.
// swagger:parameters RouteGetTemplate RoutePutTemplate RouteDeleteTemplate
type RouteGetTemplateParam struct {
@@ -47,8 +47,8 @@ type RouteGetTemplateParam struct {
// swagger:model
type MessageTemplate struct {
Name string
Template string
Name string `json:"name"`
Template string `json:"template"`
Provenance models.Provenance `json:"provenance,omitempty"`
}
@@ -56,7 +56,7 @@ type MessageTemplate struct {
type MessageTemplates []MessageTemplate
type MessageTemplateContent struct {
Template string
Template string `json:"template"`
}
// swagger:parameters RoutePutTemplate
@@ -5,3 +5,9 @@ type NotFound struct{}
// swagger:model
type Ack struct{}
// swagger:model
type ValidationError struct {
// example: error message
Msg string `json:"msg"`
}
+54 -88
View File
@@ -539,6 +539,13 @@
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"ContactPoints": {
"items": {
"$ref": "#/definitions/EmbeddedContactPoint"
},
"type": "array",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"CreateDashboardSnapshotCommand": {
"properties": {
"Result": {
@@ -1581,14 +1588,16 @@
},
"MessageTemplate": {
"properties": {
"Name": {
"type": "string"
},
"Template": {
"type": "string"
"name": {
"type": "string",
"x-go-name": "Name"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"template": {
"type": "string",
"x-go-name": "Template"
}
},
"type": "object",
@@ -1596,8 +1605,9 @@
},
"MessageTemplateContent": {
"properties": {
"Template": {
"type": "string"
"template": {
"type": "string",
"x-go-name": "Template"
}
},
"type": "object",
@@ -3176,6 +3186,7 @@
"ValidationError": {
"properties": {
"msg": {
"example": "error message",
"type": "string",
"x-go-name": "Msg"
}
@@ -3513,6 +3524,7 @@
"$ref": "#/definitions/Duration"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/definitions/labelSet"
@@ -3571,16 +3583,15 @@
"status",
"updatedAt"
],
"type": "object",
"x-go-name": "GettableAlert",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/definitions/gettableAlert"
},
"type": "array"
"type": "array",
"x-go-name": "GettableAlerts",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"gettableSilence": {
"properties": {
@@ -3815,6 +3826,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"name": {
"description": "name",
@@ -3825,9 +3837,7 @@
"required": [
"name"
],
"type": "object",
"x-go-name": "Receiver",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
"type": "object"
},
"silence": {
"description": "Silence silence",
@@ -5593,6 +5603,9 @@
},
"/api/v1/provisioning/alert-rules": {
"post": {
"consumes": [
"application/json"
],
"operationId": "RoutePostAlertRule",
"parameters": [
{
@@ -5628,6 +5641,7 @@
"operationId": "RouteDeleteAlertRule",
"parameters": [
{
"description": "Alert rule UID",
"in": "path",
"name": "UID",
"required": true,
@@ -5637,12 +5651,6 @@
"responses": {
"204": {
"description": " The alert rule was deleted successfully."
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
},
"summary": "Delete a specific alert rule by UID.",
@@ -5654,6 +5662,7 @@
"operationId": "RouteGetAlertRule",
"parameters": [
{
"description": "Alert rule UID",
"in": "path",
"name": "UID",
"required": true,
@@ -5667,11 +5676,8 @@
"$ref": "#/definitions/AlertRule"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"404": {
"description": " Not found."
}
},
"summary": "Get a specific alert rule by UID.",
@@ -5686,6 +5692,7 @@
"operationId": "RoutePutAlertRule",
"parameters": [
{
"description": "Alert rule UID",
"in": "path",
"name": "UID",
"required": true,
@@ -5724,15 +5731,9 @@
"operationId": "RouteGetContactpoints",
"responses": {
"200": {
"description": "Route",
"description": "ContactPoints",
"schema": {
"$ref": "#/definitions/Route"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
"$ref": "#/definitions/ContactPoints"
}
}
},
@@ -5757,9 +5758,9 @@
],
"responses": {
"202": {
"description": "Ack",
"description": "EmbeddedContactPoint",
"schema": {
"$ref": "#/definitions/Ack"
"$ref": "#/definitions/EmbeddedContactPoint"
}
},
"400": {
@@ -5791,17 +5792,8 @@
}
],
"responses": {
"202": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"204": {
"description": " The contact point was deleted successfully."
}
},
"summary": "Delete a contact point.",
@@ -5906,12 +5898,6 @@
"schema": {
"$ref": "#/definitions/MuteTimings"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
},
"summary": "Get all the mute timings.",
@@ -5958,7 +5944,7 @@
"operationId": "RouteDeleteMuteTiming",
"parameters": [
{
"description": "Template Name",
"description": "Mute timing name",
"in": "path",
"name": "name",
"required": true,
@@ -5968,10 +5954,7 @@
],
"responses": {
"204": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
"description": " The mute timing was deleted successfully."
}
},
"summary": "Delete a mute timing.",
@@ -5983,7 +5966,7 @@
"operationId": "RouteGetMuteTiming",
"parameters": [
{
"description": "Template Name",
"description": "Mute timing name",
"in": "path",
"name": "name",
"required": true,
@@ -5998,11 +5981,8 @@
"$ref": "#/definitions/MuteTimeInterval"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"404": {
"description": " Not found."
}
},
"summary": "Get a mute timing.",
@@ -6017,7 +5997,7 @@
"operationId": "RoutePutMuteTiming",
"parameters": [
{
"description": "Template Name",
"description": "Mute timing name",
"in": "path",
"name": "name",
"required": true,
@@ -6061,12 +6041,6 @@
"schema": {
"$ref": "#/definitions/Route"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
},
"summary": "Get the notification policy tree.",
@@ -6081,6 +6055,7 @@
"operationId": "RoutePutPolicyTree",
"parameters": [
{
"description": "The new notification routing tree to use",
"in": "body",
"name": "Body",
"schema": {
@@ -6113,16 +6088,13 @@
"operationId": "RouteGetTemplates",
"responses": {
"200": {
"description": "MessageTemplate",
"description": "MessageTemplates",
"schema": {
"$ref": "#/definitions/MessageTemplate"
"$ref": "#/definitions/MessageTemplates"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"404": {
"description": " Not found."
}
},
"summary": "Get all message templates.",
@@ -6146,10 +6118,7 @@
],
"responses": {
"204": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
"description": " The template was deleted successfully."
}
},
"summary": "Delete a template.",
@@ -6177,10 +6146,7 @@
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
"description": " Not found."
}
},
"summary": "Get a message template.",
@@ -6212,9 +6178,9 @@
],
"responses": {
"202": {
"description": "Ack",
"description": "MessageTemplate",
"schema": {
"$ref": "#/definitions/Ack"
"$ref": "#/definitions/MessageTemplate"
}
},
"400": {
+52 -86
View File
@@ -1669,6 +1669,9 @@
},
"/api/v1/provisioning/alert-rules": {
"post": {
"consumes": [
"application/json"
],
"tags": [
"provisioning",
"stable"
@@ -1711,6 +1714,7 @@
"parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
@@ -1723,11 +1727,8 @@
"$ref": "#/definitions/AlertRule"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"404": {
"description": " Not found."
}
}
},
@@ -1744,6 +1745,7 @@
"parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
@@ -1781,6 +1783,7 @@
"parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
@@ -1789,12 +1792,6 @@
"responses": {
"204": {
"description": " The alert rule was deleted successfully."
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
}
@@ -1809,15 +1806,9 @@
"operationId": "RouteGetContactpoints",
"responses": {
"200": {
"description": "Route",
"description": "ContactPoints",
"schema": {
"$ref": "#/definitions/Route"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
"$ref": "#/definitions/ContactPoints"
}
}
}
@@ -1843,9 +1834,9 @@
],
"responses": {
"202": {
"description": "Ack",
"description": "EmbeddedContactPoint",
"schema": {
"$ref": "#/definitions/Ack"
"$ref": "#/definitions/EmbeddedContactPoint"
}
},
"400": {
@@ -1919,17 +1910,8 @@
}
],
"responses": {
"202": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"204": {
"description": " The contact point was deleted successfully."
}
}
}
@@ -1996,12 +1978,6 @@
"schema": {
"$ref": "#/definitions/MuteTimings"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
@@ -2052,7 +2028,7 @@
{
"type": "string",
"x-go-name": "Name",
"description": "Template Name",
"description": "Mute timing name",
"name": "name",
"in": "path",
"required": true
@@ -2065,11 +2041,8 @@
"$ref": "#/definitions/MuteTimeInterval"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"404": {
"description": " Not found."
}
}
},
@@ -2087,7 +2060,7 @@
{
"type": "string",
"x-go-name": "Name",
"description": "Template Name",
"description": "Mute timing name",
"name": "name",
"in": "path",
"required": true
@@ -2126,7 +2099,7 @@
{
"type": "string",
"x-go-name": "Name",
"description": "Template Name",
"description": "Mute timing name",
"name": "name",
"in": "path",
"required": true
@@ -2134,10 +2107,7 @@
],
"responses": {
"204": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
"description": " The mute timing was deleted successfully."
}
}
}
@@ -2156,12 +2126,6 @@
"schema": {
"$ref": "#/definitions/Route"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
@@ -2177,6 +2141,7 @@
"operationId": "RoutePutPolicyTree",
"parameters": [
{
"description": "The new notification routing tree to use",
"name": "Body",
"in": "body",
"schema": {
@@ -2210,16 +2175,13 @@
"operationId": "RouteGetTemplates",
"responses": {
"200": {
"description": "MessageTemplate",
"description": "MessageTemplates",
"schema": {
"$ref": "#/definitions/MessageTemplate"
"$ref": "#/definitions/MessageTemplates"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
"404": {
"description": " Not found."
}
}
}
@@ -2250,10 +2212,7 @@
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
"description": " Not found."
}
}
},
@@ -2286,9 +2245,9 @@
],
"responses": {
"202": {
"description": "Ack",
"description": "MessageTemplate",
"schema": {
"$ref": "#/definitions/Ack"
"$ref": "#/definitions/MessageTemplate"
}
},
"400": {
@@ -2318,10 +2277,7 @@
],
"responses": {
"204": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
"description": " The template was deleted successfully."
}
}
}
@@ -2934,6 +2890,13 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"ContactPoints": {
"type": "array",
"items": {
"$ref": "#/definitions/EmbeddedContactPoint"
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"CreateDashboardSnapshotCommand": {
"type": "object",
"required": [
@@ -3981,14 +3944,16 @@
"MessageTemplate": {
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Template": {
"type": "string"
"name": {
"type": "string",
"x-go-name": "Name"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"template": {
"type": "string",
"x-go-name": "Template"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -3996,8 +3961,9 @@
"MessageTemplateContent": {
"type": "object",
"properties": {
"Template": {
"type": "string"
"template": {
"type": "string",
"x-go-name": "Template"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -5577,7 +5543,8 @@
"properties": {
"msg": {
"type": "string",
"x-go-name": "Msg"
"x-go-name": "Msg",
"example": "error message"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -5914,6 +5881,7 @@
"$ref": "#/definitions/Duration"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object",
"required": [
"labels",
@@ -5973,16 +5941,15 @@
"x-go-name": "UpdatedAt"
}
},
"x-go-name": "GettableAlert",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableAlert"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"type": "array",
"items": {
"$ref": "#/definitions/gettableAlert"
},
"x-go-name": "GettableAlerts",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableAlerts"
},
"gettableSilence": {
@@ -6221,6 +6188,7 @@
"$ref": "#/definitions/postableSilence"
},
"receiver": {
"description": "Receiver receiver",
"type": "object",
"required": [
"name"
@@ -6232,8 +6200,6 @@
"x-go-name": "Name"
}
},
"x-go-name": "Receiver",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/receiver"
},
"silence": {
@@ -34,7 +34,6 @@ type RuleStore interface {
ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) error
GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error)
InsertAlertRules(ctx context.Context, rule []models.AlertRule) (map[string]int64, error)
UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error
UpdateAlertRules(ctx context.Context, rule []store.UpdateRule) error
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
}
-12
View File
@@ -46,8 +46,6 @@ type RuleStore interface {
// GetRuleGroups returns the unique rule groups across all organizations.
GetRuleGroups(ctx context.Context, query *ngmodels.ListRuleGroupsQuery) error
GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error)
// UpdateRuleGroup will update the interval for all rules in the group.
UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error
GetUserVisibleNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error)
GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error)
// InsertAlertRules will insert all alert rules passed into the function
@@ -302,16 +300,6 @@ func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespa
})
}
func (st DBstore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Update(
ngmodels.AlertRule{IntervalSeconds: interval},
ngmodels.AlertRule{OrgID: orgID, RuleGroup: ruleGroup, NamespaceUID: namespaceUID},
)
return err
})
}
// GetNamespaces returns the folders that are visible to the user and have at least one alert in it
func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) {
namespaceMap := make(map[string]*models.Folder)
+11 -2
View File
@@ -2,6 +2,7 @@ package manager
import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/prometheus/client_golang/prometheus"
)
@@ -11,21 +12,29 @@ const (
)
var (
opsCounter = prometheus.NewCounterVec(
opsCounter = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Namespace: metrics.ExporterName,
Name: "encryption_ops_total",
Help: "A counter for encryption operations",
},
[]string{"success", "operation"},
map[string][]string{
"success": {"true", "false"},
"operation": {OpEncrypt, OpDecrypt},
},
)
cacheReadsCounter = prometheus.NewCounterVec(
cacheReadsCounter = metricutil.NewCounterVecStartingAtZero(
prometheus.CounterOpts{
Namespace: metrics.ExporterName,
Name: "encryption_cache_reads_total",
Help: "A counter for encryption cache reads",
},
[]string{"hit", "method"},
map[string][]string{
"hit": {"true", "false"},
"method": {"byId", "byName"},
},
)
)
+3 -3
View File
@@ -36,10 +36,10 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
store := sqlstore.InitTestDB(t)
svcmock := tests.ServiceAccountMock{}
autoAssignOrg := setting.AutoAssignOrg
setting.AutoAssignOrg = true
autoAssignOrg := store.Cfg.AutoAssignOrg
store.Cfg.AutoAssignOrg = true
defer func() {
setting.AutoAssignOrg = autoAssignOrg
store.Cfg.AutoAssignOrg = autoAssignOrg
}()
orgCmd := &models.CreateOrgCommand{Name: "Some Test Org"}
@@ -91,6 +91,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
accesscontrol.AddManagedPermissionsMigration(mg, accesscontrol.ManagedPermissionsMigrationID)
accesscontrol.AddManagedFolderAlertActionsMigration(mg)
accesscontrol.AddActionNameMigrator(mg)
addPlaylistUIDMigration(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
@@ -6,18 +6,8 @@ func addPlaylistMigrations(mg *Migrator) {
mg.AddMigration("Drop old table playlist table", NewDropTableMigration("playlist"))
mg.AddMigration("Drop old table playlist_item table", NewDropTableMigration("playlist_item"))
playlistV2 := Table{
Name: "playlist",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "interval", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
},
}
// create table
mg.AddMigration("create playlist table v2", NewAddTableMigration(playlistV2))
mg.AddMigration("create playlist table v2", NewAddTableMigration(playlistV2()))
playlistItemV2 := Table{
Name: "playlist_item",
@@ -44,3 +34,32 @@ func addPlaylistMigrations(mg *Migrator) {
{Name: "title", Type: DB_Text, Nullable: false},
}))
}
func addPlaylistUIDMigration(mg *Migrator) {
// Replacing auto-incremented playlistIDs with string UIDs
mg.AddMigration("Add UID column to playlist", NewAddColumnMigration(playlistV2(), &Column{
Name: "uid", Type: DB_NVarchar, Length: 80, Nullable: false, Default: "0",
}))
// copy the (string representation of) existing IDs into the new uid column.
mg.AddMigration("Update uid column values in playlist", NewRawSQLMigration("").
SQLite("UPDATE playlist SET uid=printf('%d',id);").
Postgres("UPDATE playlist SET uid=id::text;").
Mysql("UPDATE playlist SET uid=id;"))
mg.AddMigration("Add index for uid in playlist", NewAddIndexMigration(playlistV2(), &Index{
Cols: []string{"org_id", "uid"}, Type: UniqueIndex,
}))
}
func playlistV2() Table {
return Table{
Name: "playlist",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "interval", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
},
}
}
+2 -2
View File
@@ -375,7 +375,7 @@ func (m *SQLStoreMock) UpdatePlaylist(ctx context.Context, cmd *models.UpdatePla
return m.ExpectedError
}
func (m *SQLStoreMock) GetPlaylist(ctx context.Context, query *models.GetPlaylistByIdQuery) error {
func (m *SQLStoreMock) GetPlaylist(ctx context.Context, query *models.GetPlaylistByUidQuery) error {
return m.ExpectedError
}
@@ -387,7 +387,7 @@ func (m *SQLStoreMock) SearchPlaylists(ctx context.Context, query *models.GetPla
return m.ExpectedError
}
func (m *SQLStoreMock) GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByIdQuery) error {
func (m *SQLStoreMock) GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByUidQuery) error {
return m.ExpectedError
}
-47
View File
@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"xorm.io/xorm"
)
@@ -317,49 +316,3 @@ func (ss *SQLStore) getOrCreateOrg(sess *DBSession, orgName string) (int64, erro
return org.Id, nil
}
func getOrCreateOrg(sess *DBSession, orgName string) (int64, error) {
var org models.Org
if setting.AutoAssignOrg {
has, err := sess.Where("id=?", setting.AutoAssignOrgId).Get(&org)
if err != nil {
return 0, err
}
if has {
return org.Id, nil
}
if setting.AutoAssignOrgId != 1 {
sqlog.Error("Could not create user: organization ID does not exist", "orgID",
setting.AutoAssignOrgId)
return 0, fmt.Errorf("could not create user: organization ID %d does not exist",
setting.AutoAssignOrgId)
}
org.Name = MainOrgName
org.Id = int64(setting.AutoAssignOrgId)
} else {
org.Name = orgName
}
org.Created = time.Now()
org.Updated = time.Now()
if org.Id != 0 {
if _, err := sess.InsertId(&org); err != nil {
return 0, err
}
} else {
if _, err := sess.InsertOne(&org); err != nil {
return 0, err
}
}
sess.publishAfterCommit(&events.OrgCreated{
Timestamp: org.Created,
Id: org.Id,
Name: org.Name,
})
return org.Id, nil
}
+7 -8
View File
@@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/require"
)
@@ -80,9 +79,9 @@ func TestIntegrationAccountDataAccess(t *testing.T) {
})
t.Run("Given single org mode", func(t *testing.T) {
setting.AutoAssignOrg = true
setting.AutoAssignOrgId = 1
setting.AutoAssignOrgRole = "Viewer"
sqlStore.Cfg.AutoAssignOrg = true
sqlStore.Cfg.AutoAssignOrgId = 1
sqlStore.Cfg.AutoAssignOrgRole = "Viewer"
t.Run("Users should be added to default organization", func(t *testing.T) {
ac1cmd := models.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"}
@@ -107,9 +106,9 @@ func TestIntegrationAccountDataAccess(t *testing.T) {
t.Run("Given single org and 2 users inserted", func(t *testing.T) {
sqlStore = InitTestDB(t)
setting.AutoAssignOrg = true
setting.AutoAssignOrgId = 1
setting.AutoAssignOrgRole = "Viewer"
sqlStore.Cfg.AutoAssignOrg = true
sqlStore.Cfg.AutoAssignOrgId = 1
sqlStore.Cfg.AutoAssignOrgRole = "Viewer"
ac1cmd := models.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"}
ac2cmd := models.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"}
@@ -148,7 +147,7 @@ func TestIntegrationAccountDataAccess(t *testing.T) {
t.Run("Given two saved users", func(t *testing.T) {
sqlStore = InitTestDB(t)
setting.AutoAssignOrg = false
sqlStore.Cfg.AutoAssignOrg = false
ac1cmd := models.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"}
ac2cmd := models.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true}
+65 -24
View File
@@ -8,13 +8,19 @@ import (
func (ss *SQLStore) CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
uid, err := generateAndValidateNewPlaylistUid(sess, cmd.OrgId)
if err != nil {
return err
}
playlist := models.Playlist{
Name: cmd.Name,
Interval: cmd.Interval,
OrgId: cmd.OrgId,
UID: uid,
}
_, err := sess.Insert(&playlist)
_, err = sess.Insert(&playlist)
if err != nil {
return err
}
@@ -40,33 +46,34 @@ func (ss *SQLStore) CreatePlaylist(ctx context.Context, cmd *models.CreatePlayli
func (ss *SQLStore) UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
playlist := models.Playlist{
Id: cmd.Id,
UID: cmd.UID,
OrgId: cmd.OrgId,
Name: cmd.Name,
Interval: cmd.Interval,
}
existingPlaylist := sess.Where("id = ? AND org_id = ?", cmd.Id, cmd.OrgId).Find(models.Playlist{})
if existingPlaylist == nil {
return models.ErrPlaylistNotFound
existingPlaylist := models.Playlist{UID: cmd.UID, OrgId: cmd.OrgId}
_, err := sess.Get(&existingPlaylist)
if err != nil {
return err
}
playlist.Id = existingPlaylist.Id
cmd.Result = &models.PlaylistDTO{
Id: playlist.Id,
UID: playlist.UID,
OrgId: playlist.OrgId,
Name: playlist.Name,
Interval: playlist.Interval,
}
_, err := sess.ID(cmd.Id).Cols("name", "interval").Update(&playlist)
_, err = sess.Where("id=?", playlist.Id).Cols("name", "interval").Update(&playlist)
if err != nil {
return err
}
rawSQL := "DELETE FROM playlist_item WHERE playlist_id = ?"
_, err = sess.Exec(rawSQL, cmd.Id)
_, err = sess.Exec(rawSQL, playlist.Id)
if err != nil {
return err
@@ -85,20 +92,18 @@ func (ss *SQLStore) UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlayli
}
_, err = sess.Insert(&playlistItems)
return err
})
}
func (ss *SQLStore) GetPlaylist(ctx context.Context, query *models.GetPlaylistByIdQuery) error {
if query.Id == 0 {
func (ss *SQLStore) GetPlaylist(ctx context.Context, query *models.GetPlaylistByUidQuery) error {
if query.UID == "" || query.OrgId == 0 {
return models.ErrCommandValidationFailed
}
return ss.WithDbSession(ctx, func(sess *DBSession) error {
playlist := models.Playlist{}
_, err := sess.ID(query.Id).Get(&playlist)
playlist := models.Playlist{UID: query.UID, OrgId: query.OrgId}
_, err := sess.Get(&playlist)
query.Result = &playlist
return err
@@ -106,26 +111,35 @@ func (ss *SQLStore) GetPlaylist(ctx context.Context, query *models.GetPlaylistBy
}
func (ss *SQLStore) DeletePlaylist(ctx context.Context, cmd *models.DeletePlaylistCommand) error {
if cmd.Id == 0 || cmd.OrgId == 0 {
if cmd.UID == "" || cmd.OrgId == 0 {
return models.ErrCommandValidationFailed
}
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
var rawPlaylistSQL = "DELETE FROM playlist WHERE id = ? and org_id = ?"
_, err := sess.Exec(rawPlaylistSQL, cmd.Id, cmd.OrgId)
playlist := models.Playlist{UID: cmd.UID, OrgId: cmd.OrgId}
_, err := sess.Get(&playlist)
if err != nil {
return err
}
var rawPlaylistSQL = "DELETE FROM playlist WHERE uid = ? and org_id = ?"
_, err = sess.Exec(rawPlaylistSQL, cmd.UID, cmd.OrgId)
if err != nil {
return err
}
var rawItemSQL = "DELETE FROM playlist_item WHERE playlist_id = ?"
_, err2 := sess.Exec(rawItemSQL, cmd.Id)
_, err = sess.Exec(rawItemSQL, playlist.Id)
return err2
return err
})
}
func (ss *SQLStore) SearchPlaylists(ctx context.Context, query *models.GetPlaylistsQuery) error {
if query.OrgId == 0 {
return models.ErrCommandValidationFailed
}
return ss.WithDbSession(ctx, func(dbSess *DBSession) error {
var playlists = make(models.Playlists, 0)
@@ -143,17 +157,44 @@ func (ss *SQLStore) SearchPlaylists(ctx context.Context, query *models.GetPlayli
})
}
func (ss *SQLStore) GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByIdQuery) error {
func (ss *SQLStore) GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByUidQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
if query.PlaylistId == 0 {
if query.PlaylistUID == "" || query.OrgId == 0 {
return models.ErrCommandValidationFailed
}
var playlistItems = make([]models.PlaylistItem, 0)
err := sess.Where("playlist_id=?", query.PlaylistId).Find(&playlistItems)
// get the playlist Id
get := &models.GetPlaylistByUidQuery{UID: query.PlaylistUID, OrgId: query.OrgId}
err := ss.GetPlaylist(ctx, get)
if err != nil {
return err
}
var playlistItems = make([]models.PlaylistItem, 0)
err = sess.Where("playlist_id=?", get.Result.Id).Find(&playlistItems)
query.Result = &playlistItems
return err
})
}
// generateAndValidateNewPlaylistUid generates a playlistUID and verifies that
// the uid isn't already in use. This is deliberately overly cautious, since users
// can also specify playlist uids during provisioning.
func generateAndValidateNewPlaylistUid(sess *DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ {
uid := generateNewUid()
playlist := models.Playlist{OrgId: orgId, UID: uid}
exists, err := sess.Get(&playlist)
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", models.ErrPlaylistFailedGenerateUniqueUid
}
+17 -8
View File
@@ -7,8 +7,9 @@ import (
"context"
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
)
func TestIntegrationPlaylistDataAccess(t *testing.T) {
@@ -22,31 +23,39 @@ func TestIntegrationPlaylistDataAccess(t *testing.T) {
cmd := models.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items}
err := ss.CreatePlaylist(context.Background(), &cmd)
require.NoError(t, err)
uid := cmd.Result.UID
t.Run("Can get playlist items", func(t *testing.T) {
get := &models.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: 1}
err = ss.GetPlaylistItem(context.Background(), get)
require.NoError(t, err)
require.Equal(t, len(*get.Result), len(items))
})
t.Run("Can update playlist", func(t *testing.T) {
items := []models.PlaylistItemDTO{
{Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"},
{Title: "Backend response times", Value: "2", Type: "dashboard_by_id"},
}
query := models.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, Id: 1, Interval: "10s", Items: items}
query := models.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, UID: uid, Interval: "10s", Items: items}
err = ss.UpdatePlaylist(context.Background(), &query)
require.NoError(t, err)
})
t.Run("Can remove playlist", func(t *testing.T) {
deleteQuery := models.DeletePlaylistCommand{Id: 1, OrgId: 1}
deleteQuery := models.DeletePlaylistCommand{UID: uid, OrgId: 1}
err = ss.DeletePlaylist(context.Background(), &deleteQuery)
require.NoError(t, err)
getQuery := models.GetPlaylistByIdQuery{Id: 1}
getQuery := models.GetPlaylistByUidQuery{UID: uid, OrgId: 1}
err = ss.GetPlaylist(context.Background(), &getQuery)
require.NoError(t, err)
require.Equal(t, int64(0), getQuery.Result.Id, "playlist should've been removed")
require.Equal(t, uid, getQuery.Result.UID, "playlist should've been removed")
})
})
t.Run("Delete playlist that doesn't exist", func(t *testing.T) {
deleteQuery := models.DeletePlaylistCommand{Id: 1, OrgId: 1}
deleteQuery := models.DeletePlaylistCommand{UID: "654312", OrgId: 1}
err := ss.DeletePlaylist(context.Background(), &deleteQuery)
require.NoError(t, err)
})
@@ -57,8 +66,8 @@ func TestIntegrationPlaylistDataAccess(t *testing.T) {
cmd models.DeletePlaylistCommand
}{
{desc: "none", cmd: models.DeletePlaylistCommand{}},
{desc: "no OrgId", cmd: models.DeletePlaylistCommand{Id: 1}},
{desc: "no Id", cmd: models.DeletePlaylistCommand{OrgId: 1}},
{desc: "no OrgId", cmd: models.DeletePlaylistCommand{UID: "1"}},
{desc: "no Uid", cmd: models.DeletePlaylistCommand{OrgId: 1}},
}
for _, tc := range testCases {
+2 -2
View File
@@ -173,12 +173,12 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser() error {
// ensure admin user
if !ss.Cfg.DisableInitAdminCreation {
ss.log.Debug("Creating default admin user")
if _, err := ss.createUser(ctx, sess, userCreationArgs{
if _, err := ss.createUser(ctx, sess, models.CreateUserCommand{
Login: ss.Cfg.AdminUser,
Email: ss.Cfg.AdminUser + "@localhost",
Password: ss.Cfg.AdminPassword,
IsAdmin: true,
}, false); err != nil {
}); err != nil {
return fmt.Errorf("failed to create admin user: %s", err)
}
+2 -2
View File
@@ -80,10 +80,10 @@ type Store interface {
GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error
CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error
UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error
GetPlaylist(ctx context.Context, query *models.GetPlaylistByIdQuery) error
GetPlaylist(ctx context.Context, query *models.GetPlaylistByUidQuery) error
DeletePlaylist(ctx context.Context, cmd *models.DeletePlaylistCommand) error
SearchPlaylists(ctx context.Context, query *models.GetPlaylistsQuery) error
GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByIdQuery) error
GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByUidQuery) error
GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error
GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error
HandleAlertsQuery(ctx context.Context, query *models.GetAlertsQuery) error
+13 -138
View File
@@ -11,51 +11,15 @@ import (
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func getOrgIdForNewUser(sess *DBSession, cmd models.CreateUserCommand) (int64, error) {
if cmd.SkipOrgSetup {
return -1, nil
}
if setting.AutoAssignOrg && cmd.OrgId != 0 {
err := verifyExistingOrg(sess, cmd.OrgId)
if err != nil {
func (ss *SQLStore) getOrgIDForNewUser(sess *DBSession, args models.CreateUserCommand) (int64, error) {
if ss.Cfg.AutoAssignOrg && args.OrgId != 0 {
if err := verifyExistingOrg(sess, args.OrgId); err != nil {
return -1, err
}
return cmd.OrgId, nil
}
orgName := cmd.OrgName
if len(orgName) == 0 {
orgName = util.StringsFallback2(cmd.Email, cmd.Login)
}
return getOrCreateOrg(sess, orgName)
}
type userCreationArgs struct {
Login string
Email string
Name string
Company string
Password string
IsAdmin bool
IsDisabled bool
EmailVerified bool
OrgID int64
OrgName string
DefaultOrgRole string
}
func (ss *SQLStore) getOrgIDForNewUser(sess *DBSession, args userCreationArgs) (int64, error) {
if ss.Cfg.AutoAssignOrg && args.OrgID != 0 {
if err := verifyExistingOrg(sess, args.OrgID); err != nil {
return -1, err
}
return args.OrgID, nil
return args.OrgId, nil
}
orgName := args.OrgName
@@ -67,10 +31,10 @@ func (ss *SQLStore) getOrgIDForNewUser(sess *DBSession, args userCreationArgs) (
}
// createUser creates a user in the database
func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args userCreationArgs, skipOrgSetup bool) (models.User, error) {
func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args models.CreateUserCommand) (models.User, error) {
var user models.User
var orgID int64 = -1
if !skipOrgSetup {
if !args.SkipOrgSetup {
var err error
orgID, err = ss.getOrgIDForNewUser(sess, args)
if err != nil {
@@ -103,7 +67,7 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args userCr
Created: time.Now(),
Updated: time.Now(),
LastSeenAt: time.Now().AddDate(-10, 0, 0),
IsServiceAccount: false,
IsServiceAccount: args.IsServiceAccount,
}
salt, err := util.GetRandomString(10)
@@ -140,7 +104,7 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args userCr
})
// create org user link
if !skipOrgSetup {
if !args.SkipOrgSetup {
orgUser := models.OrgUser{
OrgId: orgID,
UserId: user.Id,
@@ -166,101 +130,12 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args userCr
}
func (ss *SQLStore) CreateUser(ctx context.Context, cmd models.CreateUserCommand) (*models.User, error) {
var user *models.User
err := ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
orgId, err := getOrgIdForNewUser(sess, cmd)
if err != nil {
return err
}
if cmd.Email == "" {
cmd.Email = cmd.Login
}
exists, err := sess.Where("email=? OR login=?", cmd.Email, cmd.Login).Get(&models.User{})
if err != nil {
return err
}
if exists {
return models.ErrUserAlreadyExists
}
// create user
user = &models.User{
Email: cmd.Email,
Name: cmd.Name,
Login: cmd.Login,
Company: cmd.Company,
IsAdmin: cmd.IsAdmin,
IsDisabled: cmd.IsDisabled,
OrgId: orgId,
EmailVerified: cmd.EmailVerified,
Created: time.Now(),
Updated: time.Now(),
LastSeenAt: time.Now().AddDate(-10, 0, 0),
IsServiceAccount: cmd.IsServiceAccount,
}
salt, err := util.GetRandomString(10)
if err != nil {
return err
}
user.Salt = salt
rands, err := util.GetRandomString(10)
if err != nil {
return err
}
user.Rands = rands
if len(cmd.Password) > 0 {
encodedPassword, err := util.EncodePassword(cmd.Password, user.Salt)
if err != nil {
return err
}
user.Password = encodedPassword
}
sess.UseBool("is_admin")
if _, err := sess.Insert(user); err != nil {
return err
}
sess.publishAfterCommit(&events.UserCreated{
Timestamp: user.Created,
Id: user.Id,
Name: user.Name,
Login: user.Login,
Email: user.Email,
})
// create org user link
if !cmd.SkipOrgSetup {
orgUser := models.OrgUser{
OrgId: orgId,
UserId: user.Id,
Role: models.ROLE_ADMIN,
Created: time.Now(),
Updated: time.Now(),
}
if setting.AutoAssignOrg && !user.IsAdmin {
if len(cmd.DefaultOrgRole) > 0 {
orgUser.Role = models.RoleType(cmd.DefaultOrgRole)
} else {
orgUser.Role = models.RoleType(setting.AutoAssignOrgRole)
}
}
if _, err = sess.Insert(&orgUser); err != nil {
return err
}
}
return nil
var user models.User
createErr := ss.WithTransactionalDbSession(ctx, func(sess *DBSession) (err error) {
user, err = ss.createUser(ctx, sess, cmd)
return
})
return user, err
return &user, createErr
}
func notServiceAccountFilter(ss *SQLStore) string {
+3 -4
View File
@@ -9,7 +9,6 @@ import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -78,10 +77,10 @@ func TestIntegrationUserDataAccess(t *testing.T) {
t.Run("Testing DB - create user assigned to other organization", func(t *testing.T) {
ss = InitTestDB(t)
autoAssignOrg := setting.AutoAssignOrg
setting.AutoAssignOrg = true
autoAssignOrg := ss.Cfg.AutoAssignOrg
ss.Cfg.AutoAssignOrg = true
defer func() {
setting.AutoAssignOrg = autoAssignOrg
ss.Cfg.AutoAssignOrg = autoAssignOrg
}()
orgCmd := &models.CreateOrgCommand{Name: "Some Test Org"}
+110 -8
View File
@@ -489,7 +489,7 @@ func TestAlertingEnabled(t *testing.T) {
},
},
{
desc: "when legacy alerting is enabled and unified is invalid (or not defined) [OSS]",
desc: "when legacy alerting is enabled and unified is not defined [OSS]",
legacyAlertingEnabled: "true",
unifiedAlertingEnabled: "",
isEnterprise: false,
@@ -507,7 +507,24 @@ func TestAlertingEnabled(t *testing.T) {
},
},
{
desc: "when legacy alerting is enabled and unified is invalid (or not defined) [Enterprise]",
desc: "when legacy alerting is enabled and unified is invalid [OSS]",
legacyAlertingEnabled: "true",
unifiedAlertingEnabled: "invalid",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
err = cfg.readFeatureToggles(f)
require.NoError(t, err)
err = cfg.ReadUnifiedAlertingSettings(f)
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
assert.NotNil(t, AlertingEnabled)
assert.Equal(t, false, *AlertingEnabled)
},
},
{
desc: "when legacy alerting is enabled and unified is not defined [Enterprise]",
legacyAlertingEnabled: "true",
unifiedAlertingEnabled: "",
isEnterprise: true,
@@ -525,10 +542,27 @@ func TestAlertingEnabled(t *testing.T) {
},
},
{
desc: "when legacy alerting is disabled and unified is invalid (or not defined) [OSS]",
legacyAlertingEnabled: "false",
desc: "when legacy alerting is enabled and unified is invalid [Enterprise]",
legacyAlertingEnabled: "true",
unifiedAlertingEnabled: "invalid",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
err = cfg.readFeatureToggles(f)
require.NoError(t, err)
err = cfg.ReadUnifiedAlertingSettings(f)
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
assert.NotNil(t, AlertingEnabled)
assert.Equal(t, false, *AlertingEnabled)
},
},
{
desc: "when legacy alerting is disabled and unified is not defined [OSS]",
legacyAlertingEnabled: "false",
unifiedAlertingEnabled: "",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
@@ -543,9 +577,26 @@ func TestAlertingEnabled(t *testing.T) {
},
},
{
desc: "when legacy alerting is disabled and unified is invalid (or not defined) [Enterprise]",
desc: "when legacy alerting is disabled and unified is invalid [OSS]",
legacyAlertingEnabled: "false",
unifiedAlertingEnabled: "invalid",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
err = cfg.readFeatureToggles(f)
require.NoError(t, err)
err = cfg.ReadUnifiedAlertingSettings(f)
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
assert.NotNil(t, AlertingEnabled)
assert.Equal(t, false, *AlertingEnabled)
},
},
{
desc: "when legacy alerting is disabled and unified is not defined [Enterprise]",
legacyAlertingEnabled: "false",
unifiedAlertingEnabled: "",
isEnterprise: true,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
@@ -561,10 +612,27 @@ func TestAlertingEnabled(t *testing.T) {
},
},
{
desc: "when both are invalid (or not defined) [OSS]",
legacyAlertingEnabled: "invalid",
desc: "when legacy alerting is disabled and unified is invalid [Enterprise]",
legacyAlertingEnabled: "false",
unifiedAlertingEnabled: "invalid",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
err = cfg.readFeatureToggles(f)
require.NoError(t, err)
err = cfg.ReadUnifiedAlertingSettings(f)
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
assert.NotNil(t, AlertingEnabled)
assert.Equal(t, false, *AlertingEnabled)
},
},
{
desc: "when both are not defined [OSS]",
legacyAlertingEnabled: "",
unifiedAlertingEnabled: "",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
@@ -578,9 +646,26 @@ func TestAlertingEnabled(t *testing.T) {
},
},
{
desc: "when both are invalid (or not defined) [Enterprise]",
desc: "when both are not invalid [OSS]",
legacyAlertingEnabled: "invalid",
unifiedAlertingEnabled: "invalid",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
err = cfg.readFeatureToggles(f)
require.NoError(t, err)
err = cfg.ReadUnifiedAlertingSettings(f)
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
assert.NotNil(t, AlertingEnabled)
assert.Equal(t, false, *AlertingEnabled)
},
},
{
desc: "when both are not defined [Enterprise]",
legacyAlertingEnabled: "",
unifiedAlertingEnabled: "",
isEnterprise: true,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
@@ -594,6 +679,23 @@ func TestAlertingEnabled(t *testing.T) {
assert.Nil(t, AlertingEnabled)
},
},
{
desc: "when both are not invalid [Enterprise]",
legacyAlertingEnabled: "invalid",
unifiedAlertingEnabled: "invalid",
isEnterprise: false,
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
err := readAlertingSettings(f)
require.NoError(t, err)
err = cfg.readFeatureToggles(f)
require.NoError(t, err)
err = cfg.ReadUnifiedAlertingSettings(f)
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
assert.NotNil(t, AlertingEnabled)
assert.Equal(t, false, *AlertingEnabled)
},
},
{
desc: "when both are false",
legacyAlertingEnabled: "false",
+14 -6
View File
@@ -103,31 +103,39 @@ func (cfg *Cfg) readUnifiedAlertingEnabledSetting(section *ini.Section) (*bool,
// At present an invalid value is considered the same as no value. This means that a
// spelling mistake in the string "false" could enable unified alerting rather
// than disable it. This issue can be found here
unifiedAlerting, err := section.Key("enabled").Bool()
if err != nil {
hasEnabled := section.Key("enabled").Value() != ""
if !hasEnabled {
// TODO: Remove in Grafana v9
if cfg.IsFeatureToggleEnabled("ngalert") {
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
// feature flag overrides the legacy alerting setting
legacyAlerting := false
AlertingEnabled = &legacyAlerting
unifiedAlerting = true
unifiedAlerting := true
return &unifiedAlerting, nil
}
// if legacy alerting has not been configured then enable unified alerting
if AlertingEnabled == nil {
unifiedAlerting = true
unifiedAlerting := true
return &unifiedAlerting, nil
}
// enable unified alerting and disable legacy alerting
legacyAlerting := false
AlertingEnabled = &legacyAlerting
unifiedAlerting = true
unifiedAlerting := true
return &unifiedAlerting, nil
}
unifiedAlerting, err := section.Key("enabled").Bool()
if err != nil {
// the value for unified alerting is invalid so disable all alerting
legacyAlerting := false
AlertingEnabled = &legacyAlerting
return nil, fmt.Errorf("invalid value %s, should be either true or false", section.Key("enabled"))
}
// If both legacy and unified alerting are enabled then return an error
if AlertingEnabled != nil && *AlertingEnabled && unifiedAlerting {
return nil, errors.New("legacy and unified alerting cannot both be enabled at the same time, please disable one of them and restart Grafana")
@@ -149,7 +157,7 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error {
ua := iniFile.Section("unified_alerting")
uaCfg.Enabled, err = cfg.readUnifiedAlertingEnabledSetting(ua)
if err != nil {
return err
return fmt.Errorf("failed to read unified alerting enabled setting: %w", err)
}
uaCfg.DisabledOrgs = make(map[int64]struct{})
@@ -2743,6 +2743,9 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st
func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCommand) int64 {
t.Helper()
store.Cfg.AutoAssignOrg = true
store.Cfg.AutoAssignOrgId = 1
u, err := store.CreateUser(context.Background(), cmd)
require.NoError(t, err)
return u.Id
@@ -92,6 +92,10 @@ func TestDashboardQuota(t *testing.T) {
func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCommand) int64 {
t.Helper()
store.Cfg.AutoAssignOrg = true
store.Cfg.AutoAssignOrgId = 1
u, err := store.CreateUser(context.Background(), cmd)
require.NoError(t, err)
return u.Id
+8 -1
View File
@@ -95,7 +95,11 @@ func TestPlugins(t *testing.T) {
if !same {
if updateSnapshotFlag {
t.Log("updating snapshot results")
updateRespSnapshot(t, tc.expRespPath, string(b))
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, b, "", " "); err != nil {
t.FailNow()
}
updateRespSnapshot(t, tc.expRespPath, prettyJSON.String())
}
t.FailNow()
}
@@ -107,6 +111,9 @@ func TestPlugins(t *testing.T) {
func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCommand) {
t.Helper()
store.Cfg.AutoAssignOrg = true
store.Cfg.AutoAssignOrgId = 1
_, err := store.CreateUser(context.Background(), cmd)
require.NoError(t, err)
}
File diff suppressed because one or more lines are too long
+7 -3
View File
@@ -156,13 +156,17 @@ func (s *Service) doRandomWalk(query backend.DataQuery) backend.DataResponse {
}
func (s *Service) doSearchQuery(ctx context.Context, req *backend.QueryDataRequest, query backend.DataQuery) backend.DataResponse {
q := searchV2.DashboardQuery{}
err := json.Unmarshal(query.JSON, &q)
m := requestModel{}
err := json.Unmarshal(query.JSON, &m)
if err != nil {
return backend.DataResponse{
Error: err,
}
}
return *s.search.DoDashboardQuery(ctx, req.PluginContext.User, req.PluginContext.OrgID, m.Search)
}
return *s.search.DoDashboardQuery(ctx, req.PluginContext.User, req.PluginContext.OrgID, q)
type requestModel struct {
QueryType string `json:"queryType"`
Search searchV2.DashboardQuery `json:"search,omitempty"`
}
+106 -20
View File
@@ -1,11 +1,15 @@
package resource
import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/httpclient"
@@ -16,8 +20,54 @@ import (
)
type Resource struct {
provider *client.Provider
log log.Logger
provider *client.Provider
log log.Logger
customHeaders map[string]string
}
// Hop-by-hop headers. These are removed when sent to the backend.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
var hopHeaders = []string{
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te", // canonicalized version of "TE"
"Trailers",
"Transfer-Encoding",
"Upgrade",
}
// The following headers will be removed from the request
var stopHeaders = []string{
"cookie",
"Cookie",
}
func delHopHeaders(header http.Header) {
for _, h := range hopHeaders {
header.Del(h)
}
}
func delStopHeaders(header http.Header) {
for _, h := range stopHeaders {
header.Del(h)
}
}
func addHeaders(header http.Header, toAdd map[string]string) {
for k, v := range toAdd {
header.Add(k, v)
}
}
func normalizeReqHeaders(headers map[string][]string) map[string]string {
h := make(map[string]string, len(headers))
for k, v := range headers {
h[k] = strings.Join(v, ",")
}
return h
}
func New(
@@ -34,14 +84,46 @@ func New(
p := client.NewProvider(settings, jsonData, httpClientProvider, cfg, features, plog)
customHeaders := make(map[string]string)
var jsonDataMap map[string]interface{}
err := json.Unmarshal(settings.JSONData, &jsonDataMap)
if err != nil {
return nil, err
}
index := 1
for {
headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index)
headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index)
key := jsonDataMap[headerNameSuffix]
if key == nil {
// No (more) header values are available
break
}
if val, ok := settings.DecryptedSecureJSONData[headerValueSuffix]; ok {
switch k := key.(type) {
case string:
customHeaders[k] = val
}
}
index++
}
return &Resource{
log: plog,
provider: p,
log: plog,
provider: p,
customHeaders: customHeaders,
}, nil
}
func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest) (int, []byte, error) {
client, err := r.provider.GetClient(reqHeaders(req.Headers))
delHopHeaders(req.Headers)
delStopHeaders(req.Headers)
addHeaders(req.Headers, r.customHeaders)
client, err := r.provider.GetClient(normalizeReqHeaders(req.Headers))
if err != nil {
return 500, nil, err
}
@@ -65,25 +147,29 @@ func (r *Resource) fetch(ctx context.Context, client *client.Client, req *backen
return statusCode, nil, err
}
defer resp.Body.Close() //nolint (we don't care about the error being returned by resp.Body.Close())
defer func() {
err = resp.Body.Close()
}()
data, err := ioutil.ReadAll(resp.Body)
// Check that the server actually sent compressed data
var reader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(resp.Body)
defer func() {
err = reader.Close()
}()
if err != nil {
return 500, nil, err
}
default:
reader = resp.Body
}
data, err := ioutil.ReadAll(reader)
if err != nil {
return 500, nil, err
}
return resp.StatusCode, data, err
}
func reqHeaders(headers map[string][]string) map[string]string {
// Keep only the authorization header, incase downstream the authorization header is required.
// Strip all the others out as appropriate headers will be applied to speak with prometheus.
h := make(map[string]string)
accessValues := headers["Authorization"]
if len(accessValues) > 0 {
h["Authorization"] = accessValues[0]
}
return h
}
@@ -206,7 +206,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
zIndex: theme.zIndex.sidemenu,
padding: `${theme.spacing(1)} 0`,
position: 'relative',
width: `calc(${theme.spacing(7)} + 1px)`,
width: theme.components.sidemenu.width,
borderRight: `1px solid ${theme.colors.border.weak}`,
[theme.breakpoints.down('md')]: {
@@ -5,7 +5,7 @@ import { Role, OrgRole } from 'app/types';
import { RolePickerInput } from './RolePickerInput';
import { RolePickerMenu } from './RolePickerMenu';
import { MENU_MAX_HEIGHT } from './constants';
import { MENU_MAX_HEIGHT, ROLE_PICKER_WIDTH } from './constants';
export interface Props {
builtInRole?: OrgRole;
@@ -37,7 +37,7 @@ export const RolePicker = ({
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(builtInRole);
const [query, setQuery] = useState('');
const [offset, setOffset] = useState(0);
const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 });
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -50,14 +50,22 @@ export const RolePicker = ({
if (!dimensions || !isOpen) {
return;
}
const { bottom, top } = dimensions;
const { bottom, top, left, right } = dimensions;
const distance = window.innerHeight - bottom;
const offset = bottom - top + 10; // Add extra 10px to offset to account for border and outline
const offsetVertical = bottom - top + 10; // Add extra 10px to offset to account for border and outline
const offsetHorizontal = right - left;
let horizontal = -offsetHorizontal;
let vertical = -offsetVertical;
if (distance < MENU_MAX_HEIGHT + 20) {
setOffset(offset);
} else {
setOffset(-offset);
vertical = offsetVertical;
}
if (window.innerWidth - right < ROLE_PICKER_WIDTH) {
horizontal = offsetHorizontal;
}
setOffset({ horizontal, vertical });
}, [isOpen, selectedRoles]);
const onOpen = useCallback(
@@ -42,7 +42,7 @@ interface RolePickerMenuProps {
onUpdate: (newRoles: Role[], newBuiltInRole?: OrgRole) => void;
onClear?: () => void;
updateDisabled?: boolean;
offset: number;
offset: { vertical: number; horizontal: number };
}
export const RolePickerMenu = ({
@@ -181,9 +181,10 @@ export const RolePickerMenu = ({
className={cx(
styles.menu,
customStyles.menuWrapper,
{ [customStyles.menuLeft]: offset.horizontal > 0 },
css`
bottom: ${offset > 0 ? `${offset}px` : 'unset'};
top: ${offset < 0 ? `${Math.abs(offset)}px` : 'unset'};
bottom: ${offset.vertical > 0 ? `${offset.vertical}px` : 'unset'};
top: ${offset.vertical < 0 ? `${Math.abs(offset.vertical)}px` : 'unset'};
`
)}
>
@@ -226,6 +227,7 @@ export const RolePickerMenu = ({
selectedOptions={selectedOptions}
onSelect={onChange}
onClear={onClearSubMenu}
showOnLeft={offset.horizontal > 0}
/>
)}
</RoleMenuGroupOption>
@@ -278,7 +280,7 @@ export const RolePickerMenu = ({
</HorizontalGroup>
</div>
</div>
<div ref={subMenuNode}></div>
<div ref={subMenuNode} />
</div>
);
};
@@ -317,6 +319,7 @@ interface RolePickerSubMenuProps {
disabledOptions?: Role[];
onSelect: (option: Role) => void;
onClear?: () => void;
showOnLeft?: boolean;
}
export const RolePickerSubMenu = ({
@@ -325,6 +328,7 @@ export const RolePickerSubMenu = ({
disabledOptions,
onSelect,
onClear,
showOnLeft,
}: RolePickerSubMenuProps): JSX.Element => {
const theme = useTheme2();
const styles = getSelectStyles(theme);
@@ -337,7 +341,10 @@ export const RolePickerSubMenu = ({
};
return (
<div className={customStyles.subMenu} aria-label="Role picker submenu">
<div
className={cx(customStyles.subMenu, { [customStyles.subMenuLeft]: showOnLeft })}
aria-label="Role picker submenu"
>
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack>
<div className={styles.optionBody}>
{options.map((option, i) => (
@@ -506,7 +513,7 @@ export const RoleMenuGroupOption = React.forwardRef<HTMLDivElement, RoleMenuGrou
/>
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
<span>{data.displayName || data.name}</span>
<span className={customStyles.menuOptionExpand}></span>
<span className={customStyles.menuOptionExpand} />
</div>
{root && children && (
<Portal className={customStyles.subMenuPortal} root={root}>
@@ -552,19 +559,25 @@ export const getStyles = (theme: GrafanaTheme2) => {
padding-top: ${theme.spacing(1)};
}
`,
menuLeft: css`
right: 0;
flex-direction: row-reverse;
`,
subMenu: css`
height: 100%;
min-width: 260px;
display: flex;
flex-direction: column;
border-left-style: solid;
border-left-width: 1px;
border-left-color: ${theme.components.input.borderColor};
border-left: 1px solid ${theme.components.input.borderColor};
& > div {
padding-top: ${theme.spacing(1)};
}
`,
subMenuLeft: css`
border-right: 1px solid ${theme.components.input.borderColor};
border-left: unset;
`,
groupHeader: css`
padding: ${theme.spacing(0, 4)};
display: flex;
@@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import { AppEvents, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { ActionMeta, AsyncSelect } from '@grafana/ui';
import { ActionMeta, AsyncSelect, LoadOptionsCallback } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { createFolder, getFolderById, searchFolders } from 'app/features/manage-dashboards/state/actions';
import { DashboardSearchHit } from 'app/features/search/types';
@@ -52,7 +52,7 @@ export class FolderPicker extends PureComponent<Props, State> {
folder: null,
};
this.debouncedSearch = debounce(this.getOptions, 300, {
this.debouncedSearch = debounce(this.loadOptions, 300, {
leading: true,
trailing: true,
});
@@ -82,7 +82,13 @@ export class FolderPicker extends PureComponent<Props, State> {
await this.loadInitialValue();
};
getOptions = async (query: string) => {
// when debouncing, we must use the callback form of react-select's loadOptions so we don't
// drop results for user input. This must not return a promise/use await.
loadOptions = (query: string, callback: LoadOptionsCallback<number>): void => {
this.searchFolders(query).then(callback);
};
private searchFolders = async (query: string) => {
const {
rootName,
enableReset,
@@ -159,7 +165,7 @@ export class FolderPicker extends PureComponent<Props, State> {
const resetFolder: SelectableValue<number> = { label: initialTitle, value: undefined };
const rootFolder: SelectableValue<number> = { label: rootName, value: 0 };
const options = await this.getOptions('');
const options = await this.searchFolders('');
let folder: SelectableValue<number> | null = null;
+11 -1
View File
@@ -702,7 +702,17 @@ export function queryLogsVolume<T extends DataQuery>(
observer.complete();
},
next: (dataQueryResponse: DataQueryResponse) => {
rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame));
const { error } = dataQueryResponse;
if (error !== undefined) {
observer.next({
state: LoadingState.Error,
error,
data: [],
});
observer.error(error);
} else {
rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame));
}
},
error: (error) => {
observer.next({
@@ -235,12 +235,12 @@ export function getAnnotationsFromData(
* Opt out of using the default mapping functionality on frontend.
*/
export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
return datasource.type !== 'prometheus';
return datasource.type !== 'prometheus' && datasource.type !== 'grafana-opensearch-datasource';
}
/**
* Use legacy runner. Used only as an escape hatch for easier transition to React based annotation editor.
*/
export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean {
return datasource.type === 'prometheus';
return datasource.type === 'prometheus' || datasource.type === 'grafana-opensearch-datasource';
}
@@ -19,7 +19,7 @@ jest.mock('@grafana/runtime', () => ({
}));
setBackendSrv({
get: jest.fn().mockResolvedValue({}),
get: jest.fn().mockResolvedValue([]),
} as any);
describe('DashboardSettings', () => {
@@ -5,11 +5,16 @@ import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { byRole } from 'testing-library-selector';
import { selectors } from '@grafana/e2e-selectors';
import { setBackendSrv } from '@grafana/runtime';
import { DashboardModel } from '../../state';
import { GeneralSettingsUnconnected as GeneralSettings, Props } from './GeneralSettings';
setBackendSrv({
get: jest.fn().mockResolvedValue([]),
} as any);
const setupTestContext = (options: Partial<Props>) => {
const defaults: Props = {
dashboard: {
@@ -1,12 +1,13 @@
import { css } from '@emotion/css';
import { sumBy } from 'lodash';
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { Modal, ConfirmModal, Button } from '@grafana/ui';
import { config } from 'app/core/config';
import { DashboardModel, PanelModel } from '../../state';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { cleanUpDashboardAndVariables } from 'app/features/dashboard/state/actions';
import { useDashboardDelete } from './useDashboardDelete';
@@ -15,9 +16,17 @@ type DeleteDashboardModalProps = {
dashboard: DashboardModel;
};
export const DeleteDashboardModal: React.FC<DeleteDashboardModalProps> = ({ hideModal, dashboard }) => {
const mapDispatchToProps = {
cleanUpDashboardAndVariables,
};
const connector = connect(null, mapDispatchToProps);
type Props = DeleteDashboardModalProps & ConnectedProps<typeof connector>;
const DeleteDashboardModalUnconnected: React.FC<Props> = ({ hideModal, cleanUpDashboardAndVariables, dashboard }) => {
const isProvisioned = dashboard.meta.provisioned;
const { onDeleteDashboard } = useDashboardDelete(dashboard.uid);
const { onDeleteDashboard } = useDashboardDelete(dashboard.uid, cleanUpDashboardAndVariables);
const [, onConfirm] = useAsyncFn(async () => {
await onDeleteDashboard();
@@ -98,3 +107,5 @@ const ProvisionedDeleteModal = ({ hideModal, provisionedId }: { hideModal(): voi
</Modal.ButtonRow>
</Modal>
);
export const DeleteDashboardModal = connector(DeleteDashboardModalUnconnected);
@@ -5,16 +5,17 @@ import { locationService } from '@grafana/runtime';
import { useAppNotification } from 'app/core/copy/appNotification';
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
export const useDashboardDelete = (uid: string) => {
export const useDashboardDelete = (uid: string, cleanUpDashboardAndVariables: () => void) => {
const [state, onDeleteDashboard] = useAsyncFn(() => deleteDashboard(uid, false), []);
const notifyApp = useAppNotification();
useEffect(() => {
if (state.value) {
cleanUpDashboardAndVariables();
locationService.replace('/');
notifyApp.success('Dashboard Deleted', `${state.value.title} has been deleted`);
}
}, [state, notifyApp]);
}, [state, notifyApp, cleanUpDashboardAndVariables]);
return { state, onDeleteDashboard };
};
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
import { CoreApp, DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { Drawer, Tab, TabsBar } from '@grafana/ui';
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
@@ -90,6 +90,7 @@ export const InspectContent: React.FC<Props> = ({
options={dataOptions}
onOptionsChange={onDataOptionsChange}
timeZone={dashboard.timezone}
app={CoreApp.Dashboard}
/>
)}
{data && activeTab === InspectTab.Meta && (
@@ -1,7 +1,7 @@
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { TimeZone } from '@grafana/data';
import { CoreApp, TimeZone } from '@grafana/data';
import { TabbedContainer, TabConfig } from '@grafana/ui';
import { ExploreDrawer } from 'app/features/explore/ExploreDrawer';
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
@@ -51,6 +51,7 @@ export function ExploreQueryInspector(props: Props) {
isLoading={loading}
options={{ withTransforms: false, withFieldConfig: false }}
timeZone={timeZone}
app={CoreApp.Explore}
/>
),
};
+24 -12
View File
@@ -2,7 +2,7 @@ import React, { lazy, PureComponent, RefObject, Suspense } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { DataSourceInstanceSettings, RawTimeRange } from '@grafana/data';
import { config, DataSourcePicker } from '@grafana/runtime';
import { config, DataSourcePicker, reportInteraction } from '@grafana/runtime';
import {
defaultIntervals,
PageToolbar,
@@ -186,17 +186,28 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
{hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
{(controls) => (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
)}
{(c) => {
const controls = {
...c,
start: () => {
reportInteraction('grafana_explore_logs_result_displayed', {
datasourceType: this.props.datasourceType,
});
c.start();
},
};
return (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
);
}}
</LiveTailControls>
)}
</ToolbarButtonRow>
@@ -217,6 +228,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
return {
datasourceMissing,
datasourceName: datasourceInstance?.name,
datasourceType: datasourceInstance?.type,
loading,
range,
timeZone: getTimeZone(state.user),
+6
View File
@@ -21,6 +21,7 @@ import {
GrafanaTheme2,
LoadingState,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { TooltipDisplayMode } from '@grafana/schema';
import {
RadioButtonGroup,
@@ -64,6 +65,7 @@ interface Props extends Themeable2 {
scanning?: boolean;
scanRange?: RawTimeRange;
exploreId: ExploreId;
datasourceType?: string;
showContextToggle?: (row?: LogRowModel) => boolean;
onChangeTime: (range: AbsoluteTimeRange) => void;
onClickFilterLabel?: (key: string, value: string) => void;
@@ -138,6 +140,10 @@ class UnthemedLogs extends PureComponent<Props, State> {
};
onChangeDedup = (dedupStrategy: LogsDedupStrategy) => {
reportInteraction('grafana_explore_logs_deduplication_clicked', {
deduplicationType: dedupStrategy,
datasourceType: this.props.datasourceType,
});
this.setState({ dedupStrategy });
};

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