diff --git a/.betterer.results b/.betterer.results index 584acbcd0f1..747f18df0cc 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4543,12 +4543,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], "public/app/features/dashboard/components/SubMenu/SubMenu.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -4849,7 +4843,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "24"], [0, 0, 0, "Do not use any type assertions.", "25"], [0, 0, 0, "Unexpected any. Specify a different type.", "26"], - [0, 0, 0, "Unexpected any. Specify a different type.", "27"] + [0, 0, 0, "Unexpected any. Specify a different type.", "27"], + [0, 0, 0, "Unexpected any. Specify a different type.", "28"] ], "public/app/features/dashboard/state/TimeModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6229,9 +6224,6 @@ exports[`better eslint`] = { "public/app/features/transformers/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/users/UsersActionBar.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/users/__mocks__/userMocks.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -7861,7 +7853,20 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"] + [0, 0, 0, "Unexpected any. Specify a different type.", "13"], + [0, 0, 0, "Unexpected any. Specify a different type.", "14"], + [0, 0, 0, "Unexpected any. Specify a different type.", "15"], + [0, 0, 0, "Unexpected any. Specify a different type.", "16"], + [0, 0, 0, "Unexpected any. Specify a different type.", "17"], + [0, 0, 0, "Unexpected any. Specify a different type.", "18"], + [0, 0, 0, "Unexpected any. Specify a different type.", "19"], + [0, 0, 0, "Unexpected any. Specify a different type.", "20"], + [0, 0, 0, "Unexpected any. Specify a different type.", "21"], + [0, 0, 0, "Unexpected any. Specify a different type.", "22"], + [0, 0, 0, "Unexpected any. Specify a different type.", "23"], + [0, 0, 0, "Unexpected any. Specify a different type.", "24"], + [0, 0, 0, "Unexpected any. Specify a different type.", "25"], + [0, 0, 0, "Unexpected any. Specify a different type.", "26"] ], "public/app/plugins/datasource/loki/datasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ef3115678..85a4cbfb5c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ + + +# 9.0.6 (2022-08-01) + +### Features and enhancements + +- **Access Control:** Allow org admins to invite new users to their organization. [#52904](https://github.com/grafana/grafana/pull/52904), [@IevaVasiljeva](https://github.com/IevaVasiljeva) + +### Bug fixes + +- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52927](https://github.com/grafana/grafana/pull/52927), [@academo](https://github.com/academo) +- **Prometheus:** Fix adding of multiple values for regex operator. [#52978](https://github.com/grafana/grafana/pull/52978), [@ivanahuckova](https://github.com/ivanahuckova) +- **UI/Card:** Fix card items always having pointer cursor. [#52809](https://github.com/grafana/grafana/pull/52809), [@gillesdemey](https://github.com/gillesdemey) + + # 9.0.5 (2022-07-26) diff --git a/conf/defaults.ini b/conf/defaults.ini index 397f49bed26..175dbbec8fa 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -125,7 +125,7 @@ path = grafana.db # For "sqlite3" only. cache mode setting used for connecting to the database cache_mode = private -# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0. +# For "mysql" only if migrationLocking feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0. locking_attempt_timeout_sec = 0 #################################### Cache server ############################# diff --git a/conf/sample.ini b/conf/sample.ini index b5c2e2eae2f..bea2e245500 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -126,7 +126,7 @@ # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) ;cache_mode = private -# For "mysql" only if lockingMigration feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0. +# For "mysql" only if migrationLocking feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0. ;locking_attempt_timeout_sec = 0 ################################### Data sources ######################### diff --git a/contribute/localisation.md b/contribute/localization.md similarity index 98% rename from contribute/localisation.md rename to contribute/localization.md index 97f4ed8d284..97a220732b8 100644 --- a/contribute/localisation.md +++ b/contribute/localization.md @@ -191,4 +191,4 @@ import { Plural } from "@lingui/macro" ## Documentation -[Grafana's documentation](https://grafana.com/docs/grafana/latest/) is not yet open for translation and should be authored in English only. +[Grafana's documentation](https://grafana.com/docs/grafana/latest/) is not yet open for translation and should be authored in American English only. diff --git a/docs/sources/administration/api-keys/_index.md b/docs/sources/administration/api-keys/_index.md index c7e556b5477..630058d9c85 100644 --- a/docs/sources/administration/api-keys/_index.md +++ b/docs/sources/administration/api-keys/_index.md @@ -14,24 +14,18 @@ weight: 700 # API keys -API keys can be used to interact with Grafana HTTP APIs. - -We recommend using service accounts instead of API keys if you are on Grafana 8.5+, for more information refer to [About service accounts]({{< relref "../service-accounts/about-service-accounts/#" >}}). - -{{< section >}} - -## About API keys - An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs. -When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. For more information about creating API keys, refer to [Create an API key]({{< relref "create-api-key/#" >}}). +When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. + +> **Note:** If you use Grafana v8.5 or newer, use service accounts instead of API keys. For more information, refer to [Service accounts]({{< relref "../service-accounts/" >}}). + +{{< section >}} ## Create an API key Create an API key when you want to manage your computed workload with a user. -For more information about API keys, refer to [About API keys in Grafana]({{< relref "about-api-keys/" >}}). - This topic shows you how to create an API key using the Grafana UI. You can also create an API key using the Grafana HTTP API. For more information about creating API keys via the API, refer to [Create API key via API]({{< relref "../../developers/http_api/create-api-tokens-for-org/#how-to-create-a-new-organization-and-an-api-token" >}}). ### Before you begin: diff --git a/docs/sources/datasources/azuremonitor/deprecated-application-insights.md b/docs/sources/datasources/azuremonitor/deprecated-application-insights.md index 68c836a83af..f81ee4f8d89 100644 --- a/docs/sources/datasources/azuremonitor/deprecated-application-insights.md +++ b/docs/sources/datasources/azuremonitor/deprecated-application-insights.md @@ -18,7 +18,9 @@ weight: 999 # Deprecated Application Insights and Insights Analytics -Application Insights and Insights Analytics are two ways to query the same Azure Application Insights data, which can also be queried from Metrics and Logs. In Grafana 8.0, Application Insights and Insights Analytics are deprecated and made read-only in favor of querying this data through Metrics and Logs. Existing queries will continue to work, but you cannot edit them. New panels are not able to use Application Insights or Insights Analytics. +Application Insights and Insights Analytics are two ways to query the same Azure Application Insights data, which can also be queried from Metrics and Logs. In Grafana 8.0, Application Insights and Insights Analytics were deprecated and made read-only in favor of querying this data through Metrics and Logs. + +These query methods were completely removed in Grafana 9.0. Azure Monitor Metrics and Azure Monitor Logs do not use Application Insights API keys, so make sure the data source is configured with an Azure AD app registration that has access to Application Insights. diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 796d7d6009e..b181446fe9e 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -10,6 +10,7 @@ weight: 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 9.0.6]({{< relref "release-notes-9-0-6" >}}) - [Release notes for 9.0.5]({{< relref "release-notes-9-0-5" >}}) - [Release notes for 9.0.4]({{< relref "release-notes-9-0-4" >}}) - [Release notes for 9.0.3]({{< relref "release-notes-9-0-3" >}}) diff --git a/docs/sources/release-notes/release-notes-9-0-6.md b/docs/sources/release-notes/release-notes-9-0-6.md new file mode 100644 index 00000000000..3d7f470aa5d --- /dev/null +++ b/docs/sources/release-notes/release-notes-9-0-6.md @@ -0,0 +1,18 @@ ++++ +title = "Release notes for Grafana 9.0.6" +hide_menu = true ++++ + + + +# Release notes for Grafana 9.0.6 + +### Features and enhancements + +- **Access Control:** Allow org admins to invite new users to their organization. [#52904](https://github.com/grafana/grafana/pull/52904), [@IevaVasiljeva](https://github.com/IevaVasiljeva) + +### Bug fixes + +- **Grafana/toolkit:** Fix incorrect image and font generation for plugin builds. [#52927](https://github.com/grafana/grafana/pull/52927), [@academo](https://github.com/academo) +- **Prometheus:** Fix adding of multiple values for regex operator. [#52978](https://github.com/grafana/grafana/pull/52978), [@ivanahuckova](https://github.com/ivanahuckova) +- **UI/Card:** Fix card items always having pointer cursor. [#52809](https://github.com/grafana/grafana/pull/52809), [@gillesdemey](https://github.com/gillesdemey) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index a871ce5ca14..1546b71ebfd 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -324,7 +324,7 @@ Sets the maximum amount of time a connection may be reused. The default is 14400 ### locking_attempt_timeout_sec -For "mysql", if `lockingMigration` feature toggle is set, specify the time (in seconds) to wait before failing to lock the database for the migrations. Default is 0. +For "mysql", if the `migrationLocking` feature toggle is set, specify the time (in seconds) to wait before failing to lock the database for the migrations. Default is 0. ### log_queries @@ -1204,11 +1204,11 @@ The interval string is a possibly signed sequence of decimal numbers, followed b ### ha_listen_address -Listen address/hostname and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port. The default value is `0.0.0.0:9094`. +Listen IP address and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port. The default value is `0.0.0.0:9094`. ### ha_advertise_address -Explicit address/hostname and port to advertise other Grafana instances. The port is used for both TCP and UDP. +Explicit IP address and port to advertise other Grafana instances. The port is used for both TCP and UDP. ### ha_peers diff --git a/docs/sources/visualizations/bar-chart.md b/docs/sources/visualizations/bar-chart.md index b03d53d07b8..bd2bbd6fc21 100644 --- a/docs/sources/visualizations/bar-chart.md +++ b/docs/sources/visualizations/bar-chart.md @@ -21,7 +21,7 @@ This panel visualization allows you to graph categorical data. ## Supported data formats -Only one data frame is supported and it needs to have at least one string field that will be used as the category for an X or Y axis and one or more numerical fields. +Only one data frame is supported and it must have at least one string field that will be used as the category for an X or Y axis and one or more numerical fields. Example: @@ -107,6 +107,8 @@ Gradient color is generated based on the hue of the line color. Choose which of the [standard calculations]({{< relref "../panels/calculation-types/" >}}) to show in the legend. You can have more than one. +For more information about the legend, refer to [Configure a legend](../configure-legend/). + ## Text size Enter a **Value** to change the size of the text on your bar chart. diff --git a/docs/sources/panels/working-with-panels/configure-legend.md b/docs/sources/visualizations/configure-legend/index.md similarity index 85% rename from docs/sources/panels/working-with-panels/configure-legend.md rename to docs/sources/visualizations/configure-legend/index.md index 008bd996556..b219806ca2d 100644 --- a/docs/sources/panels/working-with-panels/configure-legend.md +++ b/docs/sources/visualizations/configure-legend/index.md @@ -1,9 +1,9 @@ --- aliases: - /docs/grafana/latest/panels/working-with-panels/configure-legend/ - - /docs/sources/panels/working-with-panels/configure-legend/ + - /docs/grafana/latest/visualizations/configure-legend/ title: Configure a legend -weight: 70 +weight: 1300 --- # Configure a legend @@ -41,13 +41,7 @@ This topic currently applies to the following visualizations: As way to add more context to a visualization, you can add series data values to a legend. You can add as many values as you'd like; after you apply your changes, you can horizontally scroll the legend to see all values. -### Before you begin - -- Add a panel - -**To add values to a legend**: - -1. Open a panel. +1. Edit a panel. 1. In the panel display options pane, locate the **Legend** section. @@ -61,13 +55,7 @@ As way to add more context to a visualization, you can add series data values to By default, Grafana specifies the color of your series data, which you can change. -### Before you begin - -- Add a panel - -**To change a series color, perform the following steps**: - -1. Open the panel. +1. Edit a panel. 1. In the legend, click the color bar associated with the series. @@ -79,7 +67,7 @@ By default, Grafana specifies the color of your series data, which you can chang ## Sort series -Change legend mode to **Table** and choose [calculations]({{< relref "../calculation-types/" >}}) to be displayed in the legend. Click the calculation name header in the legend table to sort the values in the table in ascending or descending order. +You can change legend mode to **Table** and choose [calculations]({{< relref "../../panels/calculation-types/" >}}) to be displayed in the legend. Click the calculation name header in the legend table to sort the values in the table in ascending or descending order. The sort order affects the positions of the bars in the Bar chart panel as well as the order of stacked series in the Time series and Bar chart panels. diff --git a/docs/sources/visualizations/pie-chart-panel.md b/docs/sources/visualizations/pie-chart-panel.md index a83c5e3a7f5..103e45baa66 100644 --- a/docs/sources/visualizations/pie-chart-panel.md +++ b/docs/sources/visualizations/pie-chart-panel.md @@ -80,5 +80,7 @@ The following example shows a pie chart with **Name** and **Percent** labels dis Select values to display in the legend. You can select more than one. -**Percent -** The percentage of the whole. -**Value -** The raw numerical value. +- **Percent:** The percentage of the whole. +- **Value:** The raw numerical value. + +For more information about the legend, refer to [Configure a legend](../configure-legend/). diff --git a/docs/sources/visualizations/time-series/_index.md b/docs/sources/visualizations/time-series/_index.md index fa215ddf346..45a394ccdde 100644 --- a/docs/sources/visualizations/time-series/_index.md +++ b/docs/sources/visualizations/time-series/_index.md @@ -33,6 +33,8 @@ These options are available whether you are graphing your time series as lines, Choose which of the [standard calculations]({{< relref "../../panels/calculation-types/" >}}) to show in the legend. You can have more than one. +For more information about the legend, refer to [Configure a legend](../configure-legend/). + ## Graph styles Use these options to choose how to display your time series data. diff --git a/lerna.json b/lerna.json index 89ce6b595f4..afefc2c8716 100644 --- a/lerna.json +++ b/lerna.json @@ -4,5 +4,5 @@ "packages": [ "packages/*" ], - "version": "9.0.5" + "version": "9.0.6" } diff --git a/package.json b/package.json index 91332f36811..b0f9961e9f6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "9.0.5", + "version": "9.0.6", "repository": "github:grafana/grafana", "scripts": { "api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index d5e2d14781a..5bf982874ec 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "9.0.5", + "version": "9.0.6", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -22,7 +22,7 @@ }, "dependencies": { "@braintree/sanitize-url": "6.0.0", - "@grafana/schema": "9.0.5", + "@grafana/schema": "9.0.6", "@types/d3-interpolate": "^1.4.0", "d3-interpolate": "1.4.0", "date-fns": "2.28.0", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 905a77f13ac..c1ea8c6e688 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "9.0.5", + "version": "9.0.6", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 5830966d431..163f0acf9c4 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e", - "version": "9.0.5", + "version": "9.0.6", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -48,7 +48,7 @@ "@babel/core": "7.17.8", "@babel/preset-env": "7.17.10", "@cypress/webpack-preprocessor": "5.11.1", - "@grafana/e2e-selectors": "9.0.5", + "@grafana/e2e-selectors": "9.0.6", "@grafana/tsconfig": "^1.2.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "8.2.5", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 5c823c6a14a..673ebb5cdc3 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "9.0.5", + "version": "9.0.6", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -22,9 +22,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@grafana/data": "9.0.5", - "@grafana/e2e-selectors": "9.0.5", - "@grafana/ui": "9.0.5", + "@grafana/data": "9.0.6", + "@grafana/e2e-selectors": "9.0.6", + "@grafana/ui": "9.0.6", "@sentry/browser": "6.19.7", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 9176832f07f..621108dc1ca 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "9.0.5", + "version": "9.0.6", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index a5d6720066c..9673d3aa291 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/toolkit", - "version": "9.0.5", + "version": "9.0.6", "description": "Grafana Toolkit", "keywords": [ "grafana", @@ -38,10 +38,10 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", - "@grafana/data": "9.0.5", + "@grafana/data": "9.0.6", "@grafana/eslint-config": "^4.0.0", "@grafana/tsconfig": "^1.2.0-rc1", - "@grafana/ui": "9.0.5", + "@grafana/ui": "9.0.6", "@jest/core": "27.5.1", "@types/command-exists": "^1.2.0", "@types/eslint": "8.4.1", diff --git a/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts b/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts index 40d8eb0072f..53078e4185e 100644 --- a/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts +++ b/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts @@ -5,7 +5,7 @@ import rimrafCallback from 'rimraf'; import { promisify } from 'util'; import { getPluginId } from '../../config/utils/getPluginId'; -import { getPluginJson } from '../../config/utils/pluginValidation'; +import { assertRootUrlIsValid, getPluginJson } from '../../config/utils/pluginValidation'; import { getJobFolder, writeJobStats, @@ -140,7 +140,8 @@ const packagePluginRunner: TaskRunner = async ({ signatureType, if (signatureType) { manifest.signatureType = signatureType; } - if (rootUrls) { + if (rootUrls && rootUrls.length > 0) { + rootUrls.forEach(assertRootUrlIsValid); manifest.rootUrls = rootUrls; } const signedManifest = await signManifest(manifest); diff --git a/packages/grafana-toolkit/src/cli/tasks/plugin.sign.ts b/packages/grafana-toolkit/src/cli/tasks/plugin.sign.ts index 7424bdeb1fb..bec898243f1 100644 --- a/packages/grafana-toolkit/src/cli/tasks/plugin.sign.ts +++ b/packages/grafana-toolkit/src/cli/tasks/plugin.sign.ts @@ -1,5 +1,6 @@ import path from 'path'; +import { assertRootUrlIsValid } from '../../config/utils/pluginValidation'; import { buildManifest, signManifest, saveManifest } from '../../plugins/manifest'; import { getToolkitVersion } from './plugin.utils'; @@ -21,7 +22,8 @@ const pluginSignRunner: TaskRunner = async ({ signatureType, if (signatureType) { manifest.signatureType = signatureType; } - if (rootUrls) { + if (rootUrls && rootUrls.length > 0) { + rootUrls.forEach(assertRootUrlIsValid); manifest.rootUrls = rootUrls; } diff --git a/packages/grafana-toolkit/src/config/utils/pluginValidation.ts b/packages/grafana-toolkit/src/config/utils/pluginValidation.ts index f2ff2cdf7df..80b88ce8f13 100644 --- a/packages/grafana-toolkit/src/config/utils/pluginValidation.ts +++ b/packages/grafana-toolkit/src/config/utils/pluginValidation.ts @@ -36,3 +36,11 @@ export const getPluginJson = (path: string): PluginMeta => { return pluginJson as PluginMeta; }; + +export const assertRootUrlIsValid = (rootUrl: string) => { + try { + new URL(rootUrl); + } catch (err) { + throw new Error(`${rootUrl} is not a valid URL`); + } +}; diff --git a/packages/grafana-toolkit/src/config/webpack/loaders.ts b/packages/grafana-toolkit/src/config/webpack/loaders.ts index e558dba405e..da71d380443 100644 --- a/packages/grafana-toolkit/src/config/webpack/loaders.ts +++ b/packages/grafana-toolkit/src/config/webpack/loaders.ts @@ -1,6 +1,8 @@ import fs from 'fs'; import path from 'path'; +import { getPluginId } from '../utils/getPluginId'; + const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const supportedExtensions = ['css', 'scss', 'less', 'sass']; @@ -109,7 +111,7 @@ export const getFileLoaders = () => { test: /\.(png|jpe?g|gif|svg)$/, type: 'asset/resource', generator: { - publicPath: `img/`, + publicPath: `public/plugins/${getPluginId()}/img/`, outputPath: 'img/', }, }, @@ -117,7 +119,7 @@ export const getFileLoaders = () => { test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/, type: 'asset/resource', generator: { - publicPath: `fonts/`, + publicPath: `public/plugins/${getPluginId()}/fonts/`, outputPath: 'fonts/', }, }, diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index afe2b540ba1..962e7976c1b 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "9.0.5", + "version": "9.0.6", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -33,9 +33,9 @@ "@emotion/css": "11.9.0", "@emotion/react": "11.9.0", "@grafana/aws-sdk": "0.0.36", - "@grafana/data": "9.0.5", - "@grafana/e2e-selectors": "9.0.5", - "@grafana/schema": "9.0.5", + "@grafana/data": "9.0.6", + "@grafana/e2e-selectors": "9.0.6", + "@grafana/schema": "9.0.6", "@grafana/slate-react": "0.22.10-grafana", "@monaco-editor/react": "4.3.1", "@popperjs/core": "2.11.5", diff --git a/packages/grafana-ui/src/components/PageLayout/PageToolbar.test.tsx b/packages/grafana-ui/src/components/PageLayout/PageToolbar.test.tsx new file mode 100644 index 00000000000..6fa71287d09 --- /dev/null +++ b/packages/grafana-ui/src/components/PageLayout/PageToolbar.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { PageToolbar } from '..'; + +describe('PageToolbar', () => { + it('renders left items when title is not set', () => { + const leftItemContent = 'Left Item!'; + render({leftItemContent}]} />); + + expect(screen.getByText(leftItemContent)).toBeInTheDocument(); + }); +}); diff --git a/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx b/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx index 2af2412a28a..34298aebbd6 100644 --- a/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx +++ b/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx @@ -61,12 +61,6 @@ export const PageToolbar: FC = React.memo( className ); - const leftItemChildren = leftItems?.map((child, index) => ( -
- {child} -
- )); - const titleEl = ( <> {title} @@ -112,22 +106,29 @@ export const PageToolbar: FC = React.memo( )} - {title && ( + {(title || leftItems?.length) && (
-

- {titleHref ? ( - - {titleEl} - - ) : ( -
{titleEl}
- )} -

- {leftItemChildren} + {title && ( +

+ {titleHref ? ( + + {titleEl} + + ) : ( +
{titleEl}
+ )} +

+ )} + + {leftItems?.map((child, index) => ( +
+ {child} +
+ ))}
)} diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index ba21da3c431..fbab5158cbc 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@jaegertracing/jaeger-ui-components", - "version": "9.0.5", + "version": "9.0.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,10 +28,10 @@ }, "dependencies": { "@emotion/css": "11.9.0", - "@grafana/data": "9.0.5", - "@grafana/e2e-selectors": "9.0.5", - "@grafana/runtime": "9.0.5", - "@grafana/ui": "9.0.5", + "@grafana/data": "9.0.6", + "@grafana/e2e-selectors": "9.0.6", + "@grafana/runtime": "9.0.6", + "@grafana/ui": "9.0.6", "chance": "^1.0.10", "classnames": "^2.2.5", "combokeys": "^3.0.0", diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 53c3cebed62..c023a669c4f 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -153,14 +153,17 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Response { key := web.Params(c.Req)[":key"] if len(key) == 0 { - return response.Error(404, "Snapshot not found", nil) + return response.Error(http.StatusBadRequest, "Empty snapshot key", nil) } query := &models.GetDashboardSnapshotQuery{Key: key} err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) if err != nil { - return response.Error(500, "Failed to get dashboard snapshot", err) + if errors.Is(err, models.ErrDashboardSnapshotNotFound) { + return response.Error(http.StatusNotFound, "Failed to find dashboard snapshot", err) + } + return response.Error(http.StatusInternalServerError, "Failed to get dashboard snapshot", err) } snapshot := query.Result @@ -227,7 +230,10 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r query := &models.GetDashboardSnapshotQuery{DeleteKey: key} err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) if err != nil { - return response.Error(500, "Failed to get dashboard snapshot", err) + if errors.Is(err, models.ErrDashboardSnapshotNotFound) { + return response.Error(http.StatusNotFound, "Failed to find dashboard snapshot", err) + } + return response.Error(http.StatusInternalServerError, "Failed to get dashboard snapshot", err) } if query.Result.External { @@ -260,7 +266,10 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) if err != nil { - return response.Error(500, "Failed to get dashboard snapshot", err) + if errors.Is(err, models.ErrDashboardSnapshotNotFound) { + return response.Error(http.StatusNotFound, "Failed to find dashboard snapshot", err) + } + return response.Error(http.StatusInternalServerError, "Failed to get dashboard snapshot", err) } if query.Result == nil { return response.Error(404, "Failed to get dashboard snapshot", nil) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index cdab0462bb5..5f5c209d483 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -1,6 +1,7 @@ package api import ( + "errors" "fmt" "net/http" "net/http/httptest" @@ -229,3 +230,71 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { }, sqlmock) }) } + +func TestGetDashboardSnapshotNotFound(t *testing.T) { + sqlmock := mockstore.NewSQLStoreMock() + sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{} + sqlmock.ExpectedError = models.ErrDashboardSnapshotNotFound + hs := &HTTPServer{DashboardsnapshotsService: &dashboardsnapshots.Service{SQLStore: sqlmock}} + + loggedInUserScenarioWithRole(t, + "GET /snapshots/{key} should return 404 when the snapshot does not exist", "GET", + "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = hs.GetDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, http.StatusNotFound, sc.resp.Code) + }, sqlmock) + + loggedInUserScenarioWithRole(t, + "DELETE /snapshots/{key} should return 404 when the snapshot does not exist", "DELETE", + "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = hs.DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, http.StatusNotFound, sc.resp.Code) + }, sqlmock) + + loggedInUserScenarioWithRole(t, + "GET /snapshots-delete/{deleteKey} should return 404 when the snapshot does not exist", "DELETE", + "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", models.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec() + + assert.Equal(t, http.StatusNotFound, sc.resp.Code) + }, sqlmock) +} + +func TestGetDashboardSnapshotFailure(t *testing.T) { + sqlmock := mockstore.NewSQLStoreMock() + sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{} + sqlmock.ExpectedError = errors.New("something went wrong") + hs := &HTTPServer{DashboardsnapshotsService: &dashboardsnapshots.Service{SQLStore: sqlmock}} + + loggedInUserScenarioWithRole(t, + "GET /snapshots/{key} should return 404 when the snapshot does not exist", "GET", + "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = hs.GetDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, http.StatusInternalServerError, sc.resp.Code) + }, sqlmock) + + loggedInUserScenarioWithRole(t, + "DELETE /snapshots/{key} should return 404 when the snapshot does not exist", "DELETE", + "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = hs.DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, http.StatusInternalServerError, sc.resp.Code) + }, sqlmock) + + loggedInUserScenarioWithRole(t, + "GET /snapshots-delete/{deleteKey} should return 404 when the snapshot does not exist", "DELETE", + "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", models.ROLE_EDITOR, func(sc *scenarioContext) { + sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec() + + assert.Equal(t, http.StatusInternalServerError, sc.resp.Code) + }, sqlmock) +} diff --git a/pkg/api/docs/definitions/snapshot.go b/pkg/api/docs/definitions/snapshot.go index 4926595a722..c1549ccefd9 100644 --- a/pkg/api/docs/definitions/snapshot.go +++ b/pkg/api/docs/definitions/snapshot.go @@ -30,6 +30,7 @@ import ( // // Responses: // 200: snapshotResponse +// 400: badRequestError // 404: notFoundError // 500: internalServerError diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 2a654451438..64d74aa638b 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -27,7 +27,12 @@ SELECT DISTINCT , (SELECT COUNT(connection_id) FROM ` + models.LibraryElementConnectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards` ) -const deleteInvalidConnections = "DELETE FROM library_element_connection WHERE connection_id IN (SELECT connection_id as id FROM library_element_connection WHERE element_id=? EXCEPT SELECT id from dashboard)" +const deleteInvalidConnections = ` +DELETE FROM library_element_connection +WHERE connection_id IN ( + SELECT connection_id as id FROM library_element_connection + WHERE element_id=? AND connection_id NOT IN (SELECT id as connection_id from dashboard) +)` func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string { user := dialect.Quote("user") @@ -589,7 +594,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser * return connections, err } -//getElementsForDashboardID gets all elements for a specific dashboard +// getElementsForDashboardID gets all elements for a specific dashboard func (l *LibraryElementService) getElementsForDashboardID(c context.Context, dashboardID int64) (map[string]LibraryElementDTO, error) { libraryElementMap := make(map[string]LibraryElementDTO) err := l.SQLStore.WithDbSession(c, func(session *sqlstore.DBSession) error { diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 6654ba21485..41cc56c4b91 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -25,7 +25,7 @@ import ( ) var searchRegex = regexp.MustCompile(`\{(\w+)\}`) - +var errInvalidRecipientFormat = errors.New("invalid recipient (datasource) identifier format. Only integer is expected") var NotImplementedResp = ErrResp(http.StatusNotImplemented, errors.New("endpoint not implemented"), "") func toMacaronPath(path string) string { @@ -37,7 +37,8 @@ func toMacaronPath(path string) string { func backendTypeByUID(ctx *models.ReqContext, cache datasources.CacheService) (apimodels.Backend, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] - if ds, err := cache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache); err == nil { + ds, err := cache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache) + if err == nil { switch ds.Type { case "loki", "prometheus": return apimodels.LoTexRulerBackend, nil @@ -47,7 +48,7 @@ func backendTypeByUID(ctx *models.ReqContext, cache datasources.CacheService) (a return 0, fmt.Errorf("unexpected backend type (%v)", ds.Type) } } - return 0, fmt.Errorf("unexpected backend type (%v)", datasourceUID) + return 0, errors.New("no datasource was found matching the given UID") } // macaron unsafely asserts the http.ResponseWriter is an http.CloseNotifier, which will panic. @@ -100,7 +101,7 @@ func (p *AlertingProxy) withReq( if datasourceID != "" { recipient, err := strconv.ParseInt(web.Params(ctx.Req)[":DatasourceID"], 10, 64) if err != nil { - return ErrResp(http.StatusBadRequest, err, "DatasourceID is invalid") + return ErrResp(http.StatusBadRequest, errInvalidRecipientFormat, "") } p.DataProxy.ProxyDatasourceRequestWithID(newCtx, recipient) diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go index 6f8fbb1cf92..df8679d1dfe 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go @@ -658,12 +658,14 @@ func addConfigData(frames data.Frames, dl string, unit string) data.Frames { if frames[i].Fields[1].Config == nil { frames[i].Fields[1].Config = &data.FieldConfig{} } - deepLink := data.DataLink{ - Title: "View in Metrics Explorer", - TargetBlank: true, - URL: dl, + if len(dl) > 0 { + deepLink := data.DataLink{ + Title: "View in Metrics Explorer", + TargetBlank: true, + URL: dl, + } + frames[i].Fields[1].Config.Links = append(frames[i].Fields[1].Config.Links, deepLink) } - frames[i].Fields[1].Config.Links = append(frames[i].Fields[1].Config.Links, deepLink) if len(unit) > 0 { if val, ok := cloudMonitoringUnitMappings[unit]; ok { frames[i].Fields[1].Config.Unit = val diff --git a/pkg/tsdb/cloudmonitoring/time_series_filter_test.go b/pkg/tsdb/cloudmonitoring/time_series_filter_test.go index 72812087e51..9981b38ed12 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_filter_test.go +++ b/pkg/tsdb/cloudmonitoring/time_series_filter_test.go @@ -459,6 +459,19 @@ func TestTimeSeriesFilter(t *testing.T) { }, *res.Frames[0].Meta) }) }) + + t.Run("when data comes from a slo query, it should skip the link", func(t *testing.T) { + data, err := loadTestFile("./test-data/3-series-response-distribution-exponential.json") + require.NoError(t, err) + assert.Equal(t, 1, len(data.TimeSeries)) + + res := &backend.DataResponse{} + query := &cloudMonitoringTimeSeriesFilter{Params: url.Values{}, Slo: "yes"} + err = query.parseResponse(res, data, "") + require.NoError(t, err) + frames := res.Frames + assert.Equal(t, len(frames[0].Fields[1].Config.Links), 0) + }) } func loadTestFile(path string) (cloudMonitoringResponse, error) { diff --git a/pkg/tsdb/cloudwatch/metrics.go b/pkg/tsdb/cloudwatch/metrics.go index 30d5b31b35e..a061799e88d 100644 --- a/pkg/tsdb/cloudwatch/metrics.go +++ b/pkg/tsdb/cloudwatch/metrics.go @@ -398,6 +398,7 @@ var metricsMap = map[string][]string{ "AWS/Rekognition": {"DetectedFaceCount", "DetectedLabelCount", "ResponseTime", "ServerErrorCount", "SuccessfulRequestCount", "ThrottledCount", "UserErrorCount"}, "AWS/Cassandra": {"AccountMaxReads", "AccountMaxTableLevelReads", "AccountMaxTableLevelWrites", "AccountMaxWrites", "AccountProvisionedReadCapacityUtilization", "AccountProvisionedWriteCapacityUtilization", "ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "MaxProvisionedTableReadCapacityUtilization", "MaxProvisionedTableWriteCapacityUtilization", "ReturnedItemCount", "ReturnedItemCountBySelect", "SuccessfulRequestCount", "SuccessfulRequestLatency", "SystemErrors", "UserErrors"}, "AWS/AmplifyHosting": {"Requests", "BytesDownloaded", "BytesUploaded", "4XXErrors", "5XXErrors", "Latency"}, + "CloudWatchSynthetics": {"SuccessPercent", "Duration", "2xx", "4xx", "5xx", "Failed", "Failed requests", "VisualMonitoringSuccessPercent", "VisualMonitoringTotalComparisons"}, } var dimensionsMap = map[string][]string{ @@ -513,6 +514,7 @@ var dimensionsMap = map[string][]string{ "AWS/Rekognition": {}, "AWS/Cassandra": {"Keyspace", "Operation", "TableName"}, "AWS/AmplifyHosting": {"App"}, + "CloudWatchSynthetics": {"CanaryName"}, } // Known AWS regions. diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index 9d846d31df1..88d6a51fe5c 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@grafana-plugins/input-datasource", - "version": "9.0.5", + "version": "9.0.6", "description": "Input Datasource", "private": true, "repository": { @@ -15,15 +15,15 @@ }, "author": "Grafana Labs", "devDependencies": { - "@grafana/toolkit": "9.0.5", + "@grafana/toolkit": "9.0.6", "@types/jest": "26.0.15", "@types/lodash": "4.14.149", "@types/react": "17.0.30", "lodash": "4.17.21" }, "dependencies": { - "@grafana/data": "9.0.5", - "@grafana/ui": "9.0.5", + "@grafana/data": "9.0.6", + "@grafana/ui": "9.0.6", "jquery": "3.5.1", "react": "17.0.1", "react-dom": "17.0.1", diff --git a/public/api-merged.json b/public/api-merged.json index 269c5a11e88..90900c0e2b1 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -6898,6 +6898,9 @@ "200": { "$ref": "#/responses/snapshotResponse" }, + "400": { + "$ref": "#/responses/badRequestError" + }, "404": { "$ref": "#/responses/notFoundError" }, diff --git a/public/api-spec.json b/public/api-spec.json index a42630925d2..453591541df 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -6898,6 +6898,9 @@ "200": { "$ref": "#/responses/snapshotResponse" }, + "400": { + "$ref": "#/responses/badRequestError" + }, "404": { "$ref": "#/responses/notFoundError" }, diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index e850cf1a355..c3fff8a8e57 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -131,7 +131,7 @@ export const RolePicker = ({ } return ( -
+
= (props) => { @@ -23,13 +24,7 @@ export const DashboardLinksDashboard: React.FC = (props) => { const [dropdownCssClass, setDropdownCssClass] = useState('invisible'); const [opened, setOpened] = useState(0); const resolvedLinks = useResolvedLinks(props, opened); - - const buttonStyle = useStyles2( - (theme) => - css` - color: ${theme.colors.text.primary}; - ` - ); + const styles = useStyles2(getStyles); useLayoutEffect(() => { setDropdownCssClass(getDropdownLocationCssClass(listRef.current)); @@ -41,17 +36,22 @@ export const DashboardLinksDashboard: React.FC = (props) => { <> setOpened(Date.now())} - className={cx('gf-form-label gf-form-label--dashlink', buttonStyle)} + className={cx('gf-form-label gf-form-label--dashlink', styles.button)} data-placement="bottom" data-toggle="dropdown" aria-expanded={!!opened} aria-controls="dropdown-list" aria-haspopup="menu" > - + {linkInfo.title} -