diff --git a/.betterer.results b/.betterer.results
index 9d094e27732..2ddef2a2f99 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -110,9 +110,6 @@ exports[`no enzyme tests`] = {
"public/app/features/alerting/TestRuleResult.test.tsx:2358420489": [
[0, 19, 13, "RegExp match", "2409514259"]
],
- "public/app/features/api-keys/ApiKeysAddedModal.test.tsx:3246264379": [
- [0, 20, 13, "RegExp match", "2409514259"]
- ],
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:2357087833": [
[0, 35, 13, "RegExp match", "2409514259"]
],
@@ -146,9 +143,6 @@ exports[`no enzyme tests`] = {
"public/app/features/org/OrgDetailsPage.test.tsx:3835042085": [
[0, 19, 13, "RegExp match", "2409514259"]
],
- "public/app/features/teams/TeamSettings.test.tsx:2043271249": [
- [0, 19, 13, "RegExp match", "2409514259"]
- ],
"public/app/features/users/UsersListPage.test.tsx:2518052139": [
[0, 19, 13, "RegExp match", "2409514259"]
],
diff --git a/.drone.yml b/.drone.yml
index afd93a2702e..6c02cf66b77 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -141,7 +141,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-backend
- commands:
- - ./bin/grabpl test-backend --edition oss
+ - go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -812,7 +812,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-backend
- commands:
- - ./bin/grabpl test-backend --edition oss
+ - go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -912,7 +912,6 @@ steps:
params:
- SOURCE_BUILD_NUMBER=${DRONE_COMMIT}
- SOURCE_COMMIT=${DRONE_COMMIT}
- - OSS_PULL_REQUEST=${DRONE_PULL_REQUEST}
repositories:
- grafana/grafana-enterprise@main
server: https://drone.grafana.net
@@ -1505,7 +1504,7 @@ type: docker
---
depends_on: []
kind: pipeline
-name: oss-build-e2e-publish-release
+name: release-oss-build-e2e-publish
node:
type: no-parallel
platform:
@@ -1802,7 +1801,7 @@ volumes:
---
depends_on: []
kind: pipeline
-name: oss-test-release
+name: release-oss-test
node:
type: no-parallel
platform:
@@ -1884,7 +1883,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-frontend
- commands:
- - ./bin/grabpl test-backend --edition oss
+ - go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -1920,7 +1919,7 @@ volumes:
---
depends_on: []
kind: pipeline
-name: oss-integration-tests-release
+name: release-oss-integration-tests
node:
type: no-parallel
platform:
@@ -2010,11 +2009,11 @@ volumes:
medium: memory
---
depends_on:
-- oss-build-e2e-publish-release
-- oss-test-release
-- oss-integration-tests-release
+- release-oss-build-e2e-publish
+- release-oss-test
+- release-oss-integration-tests
kind: pipeline
-name: oss-windows-release
+name: release-oss-windows
platform:
arch: amd64
os: windows
@@ -2076,7 +2075,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-build-e2e-publish-release
+name: release-enterprise-build-e2e-publish
node:
type: no-parallel
platform:
@@ -2424,7 +2423,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-test-release
+name: release-enterprise-test
node:
type: no-parallel
platform:
@@ -2587,7 +2586,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-integration-tests-release
+name: release-enterprise-integration-tests
node:
type: no-parallel
platform:
@@ -2728,13 +2727,13 @@ volumes:
clone:
disable: true
depends_on:
-- enterprise-build-e2e-publish-release
-- enterprise-test-release
-- enterprise-integration-tests-release
+- release-enterprise-build-e2e-publish
+- release-enterprise-test
+- release-enterprise-integration-tests
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-windows-release
+name: release-enterprise-windows
platform:
arch: amd64
os: windows
@@ -3317,7 +3316,7 @@ volumes:
---
depends_on: []
kind: pipeline
-name: oss-build-e2e-publish-release-branch
+name: release-branch-oss-build-e2e-publish
node:
type: no-parallel
platform:
@@ -3584,7 +3583,7 @@ volumes:
---
depends_on: []
kind: pipeline
-name: oss-test-release-branch
+name: release-branch-oss-test
node:
type: no-parallel
platform:
@@ -3666,7 +3665,7 @@ steps:
image: grafana/build-container:1.5.5
name: lint-frontend
- commands:
- - ./bin/grabpl test-backend --edition oss
+ - go test -short -covermode=atomic -timeout=30m ./pkg/...
depends_on:
- wire-install
image: grafana/build-container:1.5.5
@@ -3696,7 +3695,7 @@ volumes:
---
depends_on: []
kind: pipeline
-name: oss-integration-tests-release-branch
+name: release-branch-oss-integration-tests
node:
type: no-parallel
platform:
@@ -3780,11 +3779,11 @@ volumes:
medium: memory
---
depends_on:
-- oss-build-e2e-publish-release-branch
-- oss-test-release-branch
-- oss-integration-tests-release-branch
+- release-branch-oss-build-e2e-publish
+- release-branch-oss-test
+- release-branch-oss-integration-tests
kind: pipeline
-name: oss-windows-release-branch
+name: release-branch-oss-windows
platform:
arch: amd64
os: windows
@@ -3835,7 +3834,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-build-e2e-publish-release-branch
+name: release-branch-enterprise-build-e2e-publish
node:
type: no-parallel
platform:
@@ -4181,7 +4180,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-test-release-branch
+name: release-branch-enterprise-test
node:
type: no-parallel
platform:
@@ -4335,7 +4334,7 @@ depends_on: []
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-integration-tests-release-branch
+name: release-branch-enterprise-integration-tests
node:
type: no-parallel
platform:
@@ -4467,13 +4466,13 @@ volumes:
clone:
disable: true
depends_on:
-- enterprise-build-e2e-publish-release-branch
-- enterprise-test-release-branch
-- enterprise-integration-tests-release-branch
+- release-branch-enterprise-build-e2e-publish
+- release-branch-enterprise-test
+- release-branch-enterprise-integration-tests
image_pull_secrets:
- dockerconfigjson
kind: pipeline
-name: enterprise-windows-release-branch
+name: release-branch-enterprise-windows
platform:
arch: amd64
os: windows
@@ -4683,6 +4682,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
-hmac: 378147a306d077b0566f5353c9e5c236c40dbb8d81392091a1346ed275b86496
+hmac: 5f90961f151491580770a354e6f60ceddf35557f1094ec0ee94369fe52285697
...
diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
index 3a147755400..e202f47c2e4 100644
--- a/.github/workflows/bump-version.yml
+++ b/.github/workflows/bump-version.yml
@@ -74,7 +74,7 @@ jobs:
repository: "grafana/grafana-github-actions"
path: ./actions
ref: main
- - uses: actions/setup-node@v3.2.0
+ - uses: actions/setup-node@v3.3.0
with:
node-version: '16'
- name: Install Actions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 6fd5101abc8..c563770cff2 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -41,7 +41,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,4 +50,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/detect-breaking-changes-report.yml b/.github/workflows/detect-breaking-changes-report.yml
index 82f7bbc6a0b..6ea4074c4e4 100644
--- a/.github/workflows/detect-breaking-changes-report.yml
+++ b/.github/workflows/detect-breaking-changes-report.yml
@@ -104,7 +104,7 @@ jobs:
- name: Post to Slack
id: slack
if: ${{ steps.levitate-run.outputs.exit_code == 1 && steps.does-label-exist.outputs.result == 0 }}
- uses: slackapi/slack-github-action@v1.18.0
+ uses: slackapi/slack-github-action@v1.19.0
with:
payload: |
{
diff --git a/.github/workflows/pr-codeql-analysis-go.yml b/.github/workflows/pr-codeql-analysis-go.yml
index d80c62f4f5c..7dfad430af6 100644
--- a/.github/workflows/pr-codeql-analysis-go.yml
+++ b/.github/workflows/pr-codeql-analysis-go.yml
@@ -21,9 +21,9 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
languages: "go"
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/pr-codeql-analysis-javascript.yml b/.github/workflows/pr-codeql-analysis-javascript.yml
index 9fc371a61ad..ac5bd159206 100644
--- a/.github/workflows/pr-codeql-analysis-javascript.yml
+++ b/.github/workflows/pr-codeql-analysis-javascript.yml
@@ -23,9 +23,9 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
languages: "javascript"
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/pr-codeql-analysis-python.yml b/.github/workflows/pr-codeql-analysis-python.yml
index d9fc18b4f68..cd4c47ce945 100644
--- a/.github/workflows/pr-codeql-analysis-python.yml
+++ b/.github/workflows/pr-codeql-analysis-python.yml
@@ -21,9 +21,9 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
languages: "python"
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 0f2f55a065b..6a6b6387ac3 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- run: git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync
- name: generate-packages-docs
- uses: actions/setup-node@v3.2.0
+ uses: actions/setup-node@v3.3.0
id: generate-docs
with:
node-version: '16'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 362060af120..f14efc90861 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -72,7 +72,7 @@ Migration should just be a matter of going from
to
-````tsx
+```tsx
{
diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md
index 9803ae16ed7..b8bf41fc564 100644
--- a/docs/sources/alerting/_index.md
+++ b/docs/sources/alerting/_index.md
@@ -8,7 +8,7 @@ weight: 114
# Grafana Alerting
-Grafana alerts allow you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to:
+Grafana Alerting allows you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to:
- Create and manage Grafana alerts
- Create and manage Grafana Mimir and Loki managed alerts
diff --git a/docs/sources/alerting/migrating-alerts/_index.md b/docs/sources/alerting/migrating-alerts/_index.md
index c2b1a057a0f..12a1710194d 100644
--- a/docs/sources/alerting/migrating-alerts/_index.md
+++ b/docs/sources/alerting/migrating-alerts/_index.md
@@ -1,27 +1,33 @@
---
aliases:
- /docs/grafana/latest/alerting/migrating-alerts/
-description: Migrate Grafana alerts
-title: Migrate to Grafana Alerting
-weight: 113
+description: Upgrade Grafana alerts
+title: Upgrade to Grafana Alerting
+weight: 101
---
-# Migrate to Grafana Alerting
+# Upgrade to Grafana Alerting
-Grafana Alerting is the default for new Cloud, Enterprise, and OSS installations. The new installations will only show the Grafana Alerting icon in the left navigation panel.
+Grafana Alerting is enabled by default for new installations or existing installations whether or not legacy alerting is configured.
-Existing installations that upgrade to v9.0 will have Grafana Alerting enabled by default.
+> **Note**: We recommend that Grafana Enterprise customers with more than a dozen Grafana dashboard alert rules do not upgrade and remain on legacy alerting for now by [opting out]({{< relref "opt-out/" >}}). If you do want to upgrade to Grafana Alerting, contact customer support.
-| Grafana instance upgraded to v 90 | |
-| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Cloud | Existing Cloud installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting plugin icon and the new Grafana Alerting icon. During upgrade, existing alerts from the Cloud alerting plugin are migrated to Grafana Alerting. Once migration is complete, you can access aman manage the older alerts from the new alerting Grafana Alerting icon in the navigation panel. The (older) Cloud alerting plugin is uninstalled from your cloud instance. Contact customer support if you **do not wish** to migrate to Grafana Alerting for your Cloud stack. If you choose to use legacy alerting, use the You will see the new Grafana Alerting icon as well as the old Cloud alerting plugin in the left navigation panel. |
-| Enterprise | Existing Enterprise instances using legacy alerting will have both the old (marked as legacy) and the new alerting icons in the navigation panel. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
-| OSS | Existing OSS installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting icon (marked as legacy) and the new Grafana Alerting icon. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
+Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration.
-During migration from legacy alerting to unified alerting, the legacy alerts are updated to the new alerts type, as a result, the user does not lose alerts or alerting data. However, if a user rolls back to legacy alerting after having migrated to unified alerting, they will only get the legacy alerts they had right before migration.
+Likewise, existing installations that use legacy alerting will be automatically upgraded to Grafana Alerting unless you have [opted out]({{< relref "opt-out/" >}}) of Grafana Alerting before migration takes place. During the upgrade, legacy alerts are migrated to the new alerts type and no alerts or alerting data are lost.
-## Roll back to legacy alerting
+Once the upgrade has taken place, you still have the option to [roll back]({{< relref "roll-back/" >}}) to legacy alerting. However, we do not recommend choosing this option. If you do choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place. All new alerts and changes made exclusively in Grafana Alerting will be deleted.
-Although we encourage you to use Grafana Alerting, roll back to legacy alerting is supported in Grafana 9. Rolling back can result in data loss (you will loose all alerts that you created using Grafana Alerting). This is applicable to the fresh installation as well as upgraded setups.
+> **Note**: Cloud customers, who do not want to upgrade to Grafana Alerting, should contact customer support.
+
+If you have opted out or rolled back, you can always choose to [opt in]({{< relref "opt-in/" >}}) to Grafana Alerting at a later point in time.
+
+The following table provides details on the upgrade for Cloud, Enterprise, and OSS installations and the new Grafana Alerting UI.
+
+| Grafana instance upgraded to 9.0 | |
+| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Cloud | Existing Cloud installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting plugin icon and the new Grafana Alerting icon. During upgrade, existing alerts from the Cloud alerting plugin are migrated to Grafana Alerting. Once migration is complete, you can access and manage the older alerts from the new alerting Grafana Alerting icon in the navigation panel. The (older) Cloud alerting plugin is uninstalled from your cloud instance. Contact customer support if you **do not wish** to migrate to Grafana Alerting for your Cloud stack. If you choose to use legacy alerting, use the You will see the new Grafana Alerting icon as well as the old Cloud alerting plugin in the left navigation panel. |
+| Enterprise | Existing Enterprise instances using legacy alerting will have both the old (marked as legacy) and the new alerting icons in the navigation panel. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
+| OSS | Existing OSS installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting icon (marked as legacy) and the new Grafana Alerting icon. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
> **Note:** Legacy alerting will be deprecated in a future release (v10).
diff --git a/docs/sources/alerting/migrating-alerts/disable-alerting.md b/docs/sources/alerting/migrating-alerts/disable-alerting.md
new file mode 100644
index 00000000000..0f759504417
--- /dev/null
+++ b/docs/sources/alerting/migrating-alerts/disable-alerting.md
@@ -0,0 +1,23 @@
+---
+aliases:
+ - /docs/grafana/latest/alerting/migrating-alerts/disable-alerting/
+ - /docs/grafana/latest/alerting/disable-alerting/
+ - /docs/grafana/latest/alerting/unified-alerting/disable-alerting/
+description: Disable alerting in Grafana
+title: Disable alerting in Grafana
+weight: 105
+---
+
+# Disable alerting in Grafana
+
+To disable alerting in Grafana entirely (including both legacy and Grafana Alerting), enter the following in your configuration:
+
+```
+[alerting]
+enabled = false
+
+[unified_alerting]
+enabled = false
+```
+
+If at any time you want to turn alerting back on, you can opt in.
diff --git a/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md
index e517e3e90fa..6a94dd9a459 100644
--- a/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md
+++ b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md
@@ -4,13 +4,14 @@ aliases:
- /docs/grafana/latest/alerting/migrating-legacy-alerts/
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
description: Migrate legacy dashboard alerts
-title: Migrating legacy dashboard alerts
-weight: 114
+title: Differences and limitations
+weight: 106
---
-# Migrating legacy dashboard alerts
+# Differences and limitations
-When Grafana Alerting is enabled or Grafana is upgraded to the latest version, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts. This topic explains how legacy dashboard alerts are migrated and some limitations.
+When Grafana Alerting is enabled or upgraded to Grafana 9.0 or later, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts.
+This topic explains how legacy dashboard alerts are migrated and some limitations of the migration.
> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Cloud stack.
@@ -24,5 +25,7 @@ Read and write access to legacy dashboard alerts and Grafana alerts are governed
Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route.
+## Limitations
+
Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts.
Silences (expiring after one year) are created for all paused dashboard alerts.
diff --git a/docs/sources/alerting/migrating-alerts/opt-in.md b/docs/sources/alerting/migrating-alerts/opt-in.md
new file mode 100644
index 00000000000..00a62c2385b
--- /dev/null
+++ b/docs/sources/alerting/migrating-alerts/opt-in.md
@@ -0,0 +1,25 @@
+---
+aliases:
+ - /docs/grafana/latest/alerting/migrating-alerts/opt-in/
+ - /docs/grafana/latest/alerting/opt-in/
+ - /docs/grafana/latest/alerting/unified-alerting/opt-in/
+description: Opt in to Grafana Alerting
+title: Opt in to Grafana Alerting
+weight: 104
+---
+
+# Opt in to Grafana Alerting
+
+If you have previously disabled alerting in Grafana, or opted out of Grafana Alerting and have decided that you would now like to use Grafana Alerting, you can choose to opt in at any time.
+
+If you have been using legacy alerting up until now your existing alerts will be migrated to the new alerts type and no alerts or alerting data are lost. Even if you choose to opt in to Grafana Alerting, you can roll back to legacy alerting at any time.
+
+To opt in to Grafana Alerting, enter the following in your configuration:
+
+```
+[alerting]
+enabled = false
+
+[unified_alerting]
+enabled = true
+```
diff --git a/docs/sources/alerting/migrating-alerts/opt-out.md b/docs/sources/alerting/migrating-alerts/opt-out.md
index 72edb664d98..49cf9e64fb7 100644
--- a/docs/sources/alerting/migrating-alerts/opt-out.md
+++ b/docs/sources/alerting/migrating-alerts/opt-out.md
@@ -3,41 +3,47 @@ aliases:
- /docs/grafana/latest/alerting/migrating-alerts/opt-out/
- /docs/grafana/latest/alerting/opt-in/
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
-description: Disable Grafana alerts
-title: Opt-out of Grafana Alerting
-weight: 113
+description: Opt out of Grafana Alerting
+title: Opt out of Grafana Alerting
+weight: 102
---
-# Opt-out to Grafana Alerting in OSS
+# Opt out of Grafana Alerting
-This topic discusses how to disable Grafana Alerting and migrate to legacy dashboard alerting. It also provides guidance on how to enable Grafana Alerting once you are ready to migrate to Grafana Alerting.
+If you have an existing installation, you can opt out of alerting in its entirety or opt out of Grafana Alerting in favor of using legacy alerting.
+
+Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration. To keep alerting disabled:
+
+1. Go to your custom configuration file ($WORKING_DIR/conf/custom.ini).
+1. Enter the following in your configuration:
+
+```
+[alerting]
+enabled = false
+
+[unified_alerting]
+enabled = false
+```
+
+3. Restart Grafana for the configuration changes to take effect.
+
+If at any time you want to turn alerting back on, you can do so.
+
+Existing installations that use legacy alerting will automatically be upgraded to Grafana Alerting unless you have opted-out of Grafana Alerting before migration takes place. During the upgrade, legacy alerts are migrated to the new alerts type and no alerts or alerting data are lost. To keep using legacy alerting and disable Grafana Alerting:
+
+1. Go to your custom configuration file ($WORKING_DIR/conf/custom.ini).
+2. Enter the following in your configuration:
+
+```
+[alerting]
+enabled = false
+
+[unified_alerting]
+enabled = true
+```
+
+Installations that have been migrated to Grafana Alerting can [roll back]({{< relref "roll-back/" >}}) to legacy alerting at any time.
> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Grafana Cloud stack.
-## Before you begin
-
-We recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5.
-
-## Opt-out of Grafana alerts
-
-To opt-out of Grafana alerts and roll back to legacy dashboard alerting:
-
-1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana Alerting]({{< relref "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section.
-1. Set the `enabled` property to `false`.
-1. For [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#alerting" >}}), set the `enabled` flag to `true`.
-1. Restart Grafana for the configuration changes to take effect.
-
-> **Note:** Rolling back from Grafana to legacy alerting can result in data loss. This is applicable to the fresh installation as well as upgraded setups.
-
-## Opt-in to Grafana Alerting
-
-When you are ready to make the switch, the following procedure will help you migrate to Grafana Alerting.
-
-To opt-in to Grafana alerts:
-
-1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section.
-1. Set the `enabled` property to `true`.
-1. Next, for [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#alerting" >}}), set the `enabled` flag to `false`.
-1. Restart Grafana for the configuration changes to take effect.
-
-> **Note:** The `ngalert` toggle previously used to enable or disable Grafana Alerting is no longer available.
+The `ngalert` toggle previously used to enable or disable Grafana Alerting is no longer available.
diff --git a/docs/sources/alerting/migrating-alerts/roll-back.md b/docs/sources/alerting/migrating-alerts/roll-back.md
new file mode 100644
index 00000000000..319db9b3921
--- /dev/null
+++ b/docs/sources/alerting/migrating-alerts/roll-back.md
@@ -0,0 +1,29 @@
+---
+aliases:
+ - /docs/grafana/latest/alerting/migrating-alerts/roll-back/
+ - /docs/grafana/latest/alerting/opt-in/
+ - /docs/grafana/latest/alerting/unified-alerting/roll-back/
+description: Roll back to legacy alerting
+title: Roll back to legacy alerting
+weight: 103
+---
+
+# Roll back to legacy alerting
+
+Once the upgrade has taken place, you still have the option to roll back to legacy alerting. If you choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place.
+
+All new alerts and changes made exclusively in Grafana Alerting will be deleted.
+
+To roll back to legacy alerting, enter the following in your configuration:
+
+```
+force_migration = true
+
+[alerting]
+enabled = true
+
+[unified_alerting]
+enabled = false
+```
+
+> **Note**: We do not recommend this option. If you choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place. All new alerts and changes made exclusively in Grafana Alerting will be deleted.
diff --git a/docs/sources/alerting/performance.md b/docs/sources/alerting/performance.md
index c4a2802e90f..3c1f4279034 100644
--- a/docs/sources/alerting/performance.md
+++ b/docs/sources/alerting/performance.md
@@ -2,7 +2,7 @@
title = "Performance considerations"
description = "Understanding alerting performance"
keywords = ["grafana", "alerting", "performance"]
-weight = 100
+weight = 555
+++
# Alerting performance considerations
diff --git a/docs/sources/dashboards/dashboard-folders.md b/docs/sources/dashboards/dashboard-folders.md
index 0d7d3a265df..4c5e067dba1 100644
--- a/docs/sources/dashboards/dashboard-folders.md
+++ b/docs/sources/dashboards/dashboard-folders.md
@@ -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
diff --git a/docs/sources/dashboards/export-import.md b/docs/sources/dashboards/export-import.md
index 8f1f4c7d376..220a9bfbd5a 100644
--- a/docs/sources/dashboards/export-import.md
+++ b/docs/sources/dashboards/export-import.md
@@ -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" >}}
diff --git a/docs/sources/developers/http_api/_index.md b/docs/sources/developers/http_api/_index.md
index 03546de0e01..65827a1aeee 100644
--- a/docs/sources/developers/http_api/_index.md
+++ b/docs/sources/developers/http_api/_index.md
@@ -22,8 +22,7 @@ dashboards, creating users, and updating data sources.
## HTTP APIs
- [Admin API]({{< relref "admin/" >}})
-- [Alerting Notification Channels API]({{< relref "alerting_notification_channels/" >}})
-- [Alerting API]({{< relref "alerting/" >}})
+- [Alerting Provisioning API]({{< relref "alerting_provisioning/" >}})
- [Annotations API]({{< relref "annotations/" >}})
- [Authentication API]({{< relref "auth/" >}})
- [Dashboard API]({{< relref "dashboard/" >}})
@@ -39,10 +38,16 @@ dashboards, creating users, and updating data sources.
- [Playlists API]({{< relref "playlist/" >}})
- [Preferences API]({{< relref "preferences/" >}})
- [Short URL API]({{< relref "short_url/" >}})
+- [Query history API]({{< relref "query_history/" >}})
- [Snapshot API]({{< relref "snapshot/" >}})
- [Team API]({{< relref "team/" >}})
- [User API]({{< relref "user/" >}})
+## Deprecated HTTP APIs
+
+- [Alerting Notification Channels API]({{< relref "alerting_notification_channels/" >}})
+- [Alerting API]({{< relref "alerting/" >}})
+
## Grafana Enterprise HTTP APIs
Grafana Enterprise includes all of the Grafana OSS APIs as well as those that follow:
diff --git a/docs/sources/developers/http_api/alerting.md b/docs/sources/developers/http_api/alerting.md
index 465e6deb6b9..239b12d9317 100644
--- a/docs/sources/developers/http_api/alerting.md
+++ b/docs/sources/developers/http_api/alerting.md
@@ -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.
diff --git a/docs/sources/developers/http_api/alerting_notification_channels.md b/docs/sources/developers/http_api/alerting_notification_channels.md
index fda3224f777..06329181a35 100644
--- a/docs/sources/developers/http_api/alerting_notification_channels.md
+++ b/docs/sources/developers/http_api/alerting_notification_channels.md
@@ -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`
diff --git a/docs/sources/developers/http_api/alerting_provisioning.md b/docs/sources/developers/http_api/alerting_provisioning.md
new file mode 100644
index 00000000000..1abcb73107f
--- /dev/null
+++ b/docs/sources/developers/http_api/alerting_provisioning.md
@@ -0,0 +1,1050 @@
+---
+aliases:
+ - /docs/grafana/latest/developers/http_api/alerting_provisioning/
+ - /docs/grafana/latest/http_api/alerting_provisioning/
+description: Grafana Alerts HTTP API
+keywords:
+ - grafana
+ - http
+ - documentation
+ - api
+ - alerting
+ - alerts
+title: 'Alerting Provisioning HTTP API '
+---
+
+# Alerting provisioning API
+
+## Information
+
+### Version
+
+1.0.0
+
+## Content negotiation
+
+### Consumes
+
+- application/json
+
+### Produces
+
+- application/json
+
+## All endpoints
+
+### Alert rules
+
+| Method | URI | Name | Summary |
+| ------ | ----------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------ |
+| GET | /api/v1/provisioning/alert-rules/{UID} | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. |
+| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. |
+| PUT | /api/v1/provisioning/alert-rules/{UID} | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. |
+| PUT | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group. |
+| DELETE | /api/v1/provisioning/alert-rules/{UID} | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. |
+
+### Contact points
+
+| Method | URI | Name | Summary |
+| ------ | ----------------------------------------- | --------------------------------------------------------- | --------------------------------- |
+| GET | /api/v1/provisioning/contact-points | [route get contactpoints](#route-get-contactpoints) | Get all the contact points. |
+| POST | /api/v1/provisioning/contact-points | [route post contactpoints](#route-post-contactpoints) | Create a contact point. |
+| PUT | /api/v1/provisioning/contact-points/{UID} | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. |
+| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. |
+
+### Notification policies
+
+| Method | URI | Name | Summary |
+| ------ | ----------------------------- | ----------------------------------------------- | ---------------------------------- |
+| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. |
+| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. |
+
+### Mute timings
+
+| Method | URI | Name | Summary |
+| ------ | ---------------------------------------- | ----------------------------------------------------- | -------------------------------- |
+| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. |
+| GET | /api/v1/provisioning/mute-timings/{name} | [route get mute timing](#route-get-mute-timing) | Get a mute timing. |
+| POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. |
+| PUT | /api/v1/provisioning/mute-timings/{name} | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. |
+| DELETE | /api/v1/provisioning/mute-timings/{name} | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. |
+
+### Templates
+
+| Method | URI | Name | Summary |
+| ------ | ------------------------------------- | ----------------------------------------------- | ------------------------------ |
+| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all message templates. |
+| GET | /api/v1/provisioning/templates/{name} | [route get template](#route-get-template) | Get a message template. |
+| PUT | /api/v1/provisioning/templates/{name} | [route put template](#route-put-template) | Creates or updates a template. |
+| DELETE | /api/v1/provisioning/templates/{name} | [route delete template](#route-delete-template) | Delete a template. |
+
+## Paths
+
+### Delete a specific alert rule by UID. (_RouteDeleteAlertRule_)
+
+```
+DELETE /api/v1/provisioning/alert-rules/{UID}
+```
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- |
+| UID | `path` | string | `string` | | ✓ | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ----------------------------------- | ----------- | ---------------------------------------- | :---------: | --------------------------------------------- |
+| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) |
+| [400](#route-delete-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-delete-alert-rule-400-schema) |
+
+#### Responses
+
+##### 204 - The alert rule was deleted successfully.
+
+Status: No Content
+
+###### Schema
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Delete a contact point. (_RouteDeleteContactpoints_)
+
+```
+DELETE /api/v1/provisioning/contact-points/{UID}
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------------- |
+| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| -------------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------------ |
+| [202](#route-delete-contactpoints-202) | Accepted | Ack | | [schema](#route-delete-contactpoints-202-schema) |
+| [400](#route-delete-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-delete-contactpoints-400-schema) |
+
+#### Responses
+
+##### 202 - Ack
+
+Status: Accepted
+
+###### Schema
+
+[Ack](#ack)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Delete a mute timing. (_RouteDeleteMuteTiming_)
+
+```
+DELETE /api/v1/provisioning/mute-timings/{name}
+```
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- |
+| name | `path` | string | `string` | | ✓ | | Template Name |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ------------------------------------ | ---------- | ----------- | :---------: | ---------------------------------------------- |
+| [204](#route-delete-mute-timing-204) | No Content | Ack | | [schema](#route-delete-mute-timing-204-schema) |
+
+#### Responses
+
+##### 204 - Ack
+
+Status: No Content
+
+###### Schema
+
+[Ack](#ack)
+
+### Delete a template. (_RouteDeleteTemplate_)
+
+```
+DELETE /api/v1/provisioning/templates/{name}
+```
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- |
+| name | `path` | string | `string` | | ✓ | | Template Name |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| --------------------------------- | ---------- | ----------- | :---------: | ------------------------------------------- |
+| [204](#route-delete-template-204) | No Content | Ack | | [schema](#route-delete-template-204-schema) |
+
+#### Responses
+
+##### 204 - Ack
+
+Status: No Content
+
+###### Schema
+
+[Ack](#ack)
+
+### Get a specific alert rule by UID. (_RouteGetAlertRule_)
+
+```
+GET /api/v1/provisioning/alert-rules/{UID}
+```
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- |
+| UID | `path` | string | `string` | | ✓ | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ |
+| [200](#route-get-alert-rule-200) | OK | AlertRule | | [schema](#route-get-alert-rule-200-schema) |
+| [400](#route-get-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-get-alert-rule-400-schema) |
+
+#### Responses
+
+##### 200 - AlertRule
+
+Status: OK
+
+###### Schema
+
+[AlertRule](#alert-rule)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Get all the contact points. (_RouteGetContactpoints_)
+
+```
+GET /api/v1/provisioning/contact-points
+```
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ----------------------------------- | ----------- | --------------- | :---------: | --------------------------------------------- |
+| [200](#route-get-contactpoints-200) | OK | Route | | [schema](#route-get-contactpoints-200-schema) |
+| [400](#route-get-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-get-contactpoints-400-schema) |
+
+#### Responses
+
+##### 200 - Route
+
+Status: OK
+
+###### Schema
+
+[Route](#route)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Get a mute timing. (_RouteGetMuteTiming_)
+
+```
+GET /api/v1/provisioning/mute-timings/{name}
+```
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- |
+| name | `path` | string | `string` | | ✓ | | Template Name |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| --------------------------------- | ----------- | ---------------- | :---------: | ------------------------------------------- |
+| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) |
+| [400](#route-get-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timing-400-schema) |
+
+#### Responses
+
+##### 200 - MuteTimeInterval
+
+Status: OK
+
+###### Schema
+
+[MuteTimeInterval](#mute-time-interval)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Get all the mute timings. (_RouteGetMuteTimings_)
+
+```
+GET /api/v1/provisioning/mute-timings
+```
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ---------------------------------- | ----------- | --------------- | :---------: | -------------------------------------------- |
+| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) |
+| [400](#route-get-mute-timings-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timings-400-schema) |
+
+#### Responses
+
+##### 200 - MuteTimings
+
+Status: OK
+
+###### Schema
+
+[MuteTimings](#mute-timings)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Get the notification policy tree. (_RouteGetPolicyTree_)
+
+```
+GET /api/v1/provisioning/policies
+```
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- |
+| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) |
+| [400](#route-get-policy-tree-400) | Bad Request | ValidationError | | [schema](#route-get-policy-tree-400-schema) |
+
+#### Responses
+
+##### 200 - Route
+
+Status: OK
+
+###### Schema
+
+[Route](#route)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Get a message template. (_RouteGetTemplate_)
+
+```
+GET /api/v1/provisioning/templates/{name}
+```
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- |
+| name | `path` | string | `string` | | ✓ | | Template Name |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ------------------------------ | --------- | --------------- | :---------: | ---------------------------------------- |
+| [200](#route-get-template-200) | OK | MessageTemplate | | [schema](#route-get-template-200-schema) |
+| [404](#route-get-template-404) | Not Found | NotFound | | [schema](#route-get-template-404-schema) |
+
+#### Responses
+
+##### 200 - MessageTemplate
+
+Status: OK
+
+###### Schema
+
+[MessageTemplate](#message-template)
+
+##### 404 - NotFound
+
+Status: Not Found
+
+###### Schema
+
+[NotFound](#not-found)
+
+### Get all message templates. (_RouteGetTemplates_)
+
+```
+GET /api/v1/provisioning/templates
+```
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ------------------------------- | ----------- | --------------- | :---------: | ----------------------------------------- |
+| [200](#route-get-templates-200) | OK | MessageTemplate | | [schema](#route-get-templates-200-schema) |
+| [400](#route-get-templates-400) | Bad Request | ValidationError | | [schema](#route-get-templates-400-schema) |
+
+#### Responses
+
+##### 200 - MessageTemplate
+
+Status: OK
+
+###### Schema
+
+[MessageTemplate](#message-template)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Create a new alert rule. (_RoutePostAlertRule_)
+
+```
+POST /api/v1/provisioning/alert-rules
+```
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- |
+| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- |
+| [201](#route-post-alert-rule-201) | Created | AlertRule | | [schema](#route-post-alert-rule-201-schema) |
+| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) |
+
+#### Responses
+
+##### 201 - AlertRule
+
+Status: Created
+
+###### Schema
+
+[AlertRule](#alert-rule)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Create a contact point. (_RoutePostContactpoints_)
+
+```
+POST /api/v1/provisioning/contact-points
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ----------- |
+| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ------------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------------- |
+| [202](#route-post-contactpoints-202) | Accepted | Ack | | [schema](#route-post-contactpoints-202-schema) |
+| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) |
+
+#### Responses
+
+##### 202 - Ack
+
+Status: Accepted
+
+###### Schema
+
+[Ack](#ack)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Create a new mute timing. (_RoutePostMuteTiming_)
+
+```
+POST /api/v1/provisioning/mute-timings
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ----------- |
+| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ---------------------------------- | ----------- | ---------------- | :---------: | -------------------------------------------- |
+| [201](#route-post-mute-timing-201) | Created | MuteTimeInterval | | [schema](#route-post-mute-timing-201-schema) |
+| [400](#route-post-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-post-mute-timing-400-schema) |
+
+#### Responses
+
+##### 201 - MuteTimeInterval
+
+Status: Created
+
+###### Schema
+
+[MuteTimeInterval](#mute-time-interval)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Update an existing alert rule. (_RoutePutAlertRule_)
+
+```
+PUT /api/v1/provisioning/alert-rules/{UID}
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- |
+| UID | `path` | string | `string` | | ✓ | | |
+| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ |
+| [200](#route-put-alert-rule-200) | OK | AlertRule | | [schema](#route-put-alert-rule-200-schema) |
+| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) |
+
+#### Responses
+
+##### 200 - AlertRule
+
+Status: OK
+
+###### Schema
+
+[AlertRule](#alert-rule)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Update the interval of a rule group. (_RoutePutAlertRuleGroup_)
+
+```
+PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| --------- | ------ | ----------------------------------- | ----------------------- | --------- | :------: | ------- | ----------- |
+| FolderUID | `path` | string | `string` | | ✓ | | |
+| Group | `path` | string | `string` | | ✓ | | |
+| Body | `body` | [AlertRuleGroup](#alert-rule-group) | `models.AlertRuleGroup` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| -------------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------------ |
+| [200](#route-put-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-put-alert-rule-group-200-schema) |
+| [400](#route-put-alert-rule-group-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-group-400-schema) |
+
+#### Responses
+
+##### 200 - AlertRuleGroup
+
+Status: OK
+
+###### Schema
+
+[AlertRuleGroup](#alert-rule-group)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Update an existing contact point. (_RoutePutContactpoint_)
+
+```
+PUT /api/v1/provisioning/contact-points/{UID}
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------------- |
+| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier |
+| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ---------------------------------- | ----------- | --------------- | :---------: | -------------------------------------------- |
+| [202](#route-put-contactpoint-202) | Accepted | Ack | | [schema](#route-put-contactpoint-202-schema) |
+| [400](#route-put-contactpoint-400) | Bad Request | ValidationError | | [schema](#route-put-contactpoint-400-schema) |
+
+#### Responses
+
+##### 202 - Ack
+
+Status: Accepted
+
+###### Schema
+
+[Ack](#ack)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Replace an existing mute timing. (_RoutePutMuteTiming_)
+
+```
+PUT /api/v1/provisioning/mute-timings/{name}
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ------------- |
+| name | `path` | string | `string` | | ✓ | | Template Name |
+| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| --------------------------------- | ----------- | ---------------- | :---------: | ------------------------------------------- |
+| [200](#route-put-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-put-mute-timing-200-schema) |
+| [400](#route-put-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-put-mute-timing-400-schema) |
+
+#### Responses
+
+##### 200 - MuteTimeInterval
+
+Status: OK
+
+###### Schema
+
+[MuteTimeInterval](#mute-time-interval)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Sets the notification policy tree. (_RoutePutPolicyTree_)
+
+```
+PUT /api/v1/provisioning/policies
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ----------- |
+| Body | `body` | [Route](#route) | `models.Route` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- |
+| [202](#route-put-policy-tree-202) | Accepted | Ack | | [schema](#route-put-policy-tree-202-schema) |
+| [400](#route-put-policy-tree-400) | Bad Request | ValidationError | | [schema](#route-put-policy-tree-400-schema) |
+
+#### Responses
+
+##### 202 - Ack
+
+Status: Accepted
+
+###### Schema
+
+[Ack](#ack)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### Updates an existing template. (_RoutePutTemplate_)
+
+```
+PUT /api/v1/provisioning/templates/{name}
+```
+
+#### Consumes
+
+- application/json
+
+#### Parameters
+
+| Name | Source | Type | Go type | Separator | Required | Default | Description |
+| ---- | ------ | --------------------------------------------------- | ------------------------------- | --------- | :------: | ------- | ------------- |
+| name | `path` | string | `string` | | ✓ | | Template Name |
+| Body | `body` | [MessageTemplateContent](#message-template-content) | `models.MessageTemplateContent` | | | | |
+
+#### All responses
+
+| Code | Status | Description | Has headers | Schema |
+| ------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------- |
+| [202](#route-put-template-202) | Accepted | Ack | | [schema](#route-put-template-202-schema) |
+| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) |
+
+#### Responses
+
+##### 202 - Ack
+
+Status: Accepted
+
+###### Schema
+
+[Ack](#ack)
+
+##### 400 - ValidationError
+
+Status: Bad Request
+
+###### Schema
+
+[ValidationError](#validation-error)
+
+### AlertQuery
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | -------------------------------------------------------------------------------------------------- | ------- |
+| DatasourceUID | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | |
+| Model | object | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | |
+| QueryType | string | `string` | | | QueryType is an optional identifier for the type of query. |
+| It can be used to distinguish different types of queries. | |
+| RefID | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | |
+| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | |
+
+### AlertRule
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` |
+| Condition | string | `string` | ✓ | | | `A` |
+| Data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` |
+| ExecErrState | string | `string` | ✓ | | Allowed values: "OK", "Alerting", "Error" | |
+| FolderUID | string | `string` | ✓ | | | `project_x` |
+| ID | int64 (formatted integer) | `int64` | | | | |
+| Labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` |
+| NoDataState | string | `string` | ✓ | | Allowed values: "OK", "NoData", "Error" | |
+| OrgID | int64 (formatted integer) | `int64` | ✓ | | | |
+| RuleGroup | string | `string` | ✓ | | | `eval_group_1` |
+| Title | string | `string` | ✓ | | | `Always firing` |
+| UID | string | `string` | | | | |
+| Updated | date-time (formatted string) | `strfmt.DateTime` | | | | |
+| for | [Duration](#duration) | `Duration` | ✓ | | | |
+| provenance | string | `Provenance` | | | | |
+
+### AlertRuleGroup
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| -------- | ------------------------- | ------- | :------: | ------- | ----------- | ------- |
+| Interval | int64 (formatted integer) | `int64` | | | | |
+
+### DayOfMonthRange
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- |
+| Begin | int64 (formatted integer) | `int64` | | | | |
+| End | int64 (formatted integer) | `int64` | | | | |
+
+### Duration
+
+| Name | Type | Go type | Default | Description | Example |
+| -------- | ------------------------- | ------- | ------- | ----------- | ------- |
+| Duration | int64 (formatted integer) | int64 | | | |
+
+### EmbeddedContactPoint
+
+> EmbeddedContactPoint is the contact point type that is used
+> by grafanas embedded alertmanager implementation.
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| --------------------- | ------- | -------- | :------: | ------- | ---------------------------------------------------------------------------------------------------- | ----------------------- |
+| DisableResolveMessage | boolean | `bool` | | | | `false` |
+| Name | string | `string` | ✓ | | Name is used as grouping key in the UI. Contact points with the same name will be grouped in the UI. | `webhook_1` |
+| Provenance | string | `string` | | | | |
+| Type | string | `string` | ✓ | | | `webhook` |
+| UID | string | `string` | | | UID is the unique identifier of the contact point. The UID can be set by the user. | `my_external_reference` |
+| settings | object | `JSON` | ✓ | | | |
+
+### MatchType
+
+| Name | Type | Go type | Default | Description | Example |
+| --------- | ------------------------- | ------- | ------- | ---------------------------------------------------------------------- | ------- |
+| MatchType | int64 (formatted integer) | int64 | | 0 = MatchEqual, 1 = MatchNotEqual, 2 = MatchRegexp, 3 = MatchNotRegexp | |
+
+### Matcher
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----- | ------------------------ | ----------- | :------: | ------- | ----------- | ------- |
+| Name | string | `string` | | | | |
+| Type | [MatchType](#match-type) | `MatchType` | | | | |
+| Value | string | `string` | | | | |
+
+### Matchers
+
+> Matchers is a slice of Matchers that is sortable, implements Stringer, and
+> provides a Matches method to match a LabelSet against all Matchers in the
+> slice. Note that some users of Matchers might require it to be sorted.
+
+[][matcher](#matcher)
+
+### MessageTemplate
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ---------- | ------ | ------------ | :------: | ------- | ----------- | ------- |
+| Name | string | `string` | | | | |
+| Template | string | `string` | | | | |
+| provenance | string | `Provenance` | | | | |
+
+### MessageTemplateContent
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| -------- | ------ | -------- | :------: | ------- | ----------- | ------- |
+| Template | string | `string` | | | | |
+
+### MonthRange
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- |
+| Begin | int64 (formatted integer) | `int64` | | | | |
+| End | int64 (formatted integer) | `int64` | | | | |
+
+### MuteTimeInterval
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- |
+| Name | string | `string` | | | | |
+| TimeIntervals | [][timeinterval](#time-interval) | `[]*TimeInterval` | | | | |
+
+### MuteTimings
+
+[][mutetimeinterval](#mute-time-interval)
+
+### NotFound
+
+[interface{}](#interface)
+
+### ObjectMatchers
+
+[Matchers](#matchers)
+
+#### Inlined models
+
+### RelativeTimeRange
+
+> RelativeTimeRange is the per query start and end time
+> for requests.
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ---- | --------------------- | ---------- | :------: | ------- | ----------- | ------- |
+| from | [Duration](#duration) | `Duration` | | | | |
+| to | [Duration](#duration) | `Duration` | | | | |
+
+### Route
+
+> A Route is a node that contains definitions of how to handle alerts.
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----------------- | ---------------------------------- | ---------------- | :------: | ------- | ----------- | ------- |
+| Continue | boolean | `bool` | | | | |
+| GroupByStr | []string | `[]string` | | | | |
+| MuteTimeIntervals | []string | `[]string` | | | | |
+| Receiver | string | `string` | | | | |
+| Routes | [][route](#route) | `[]*Route` | | | | |
+| group_interval | [Duration](#duration) | `Duration` | | | | |
+| group_wait | [Duration](#duration) | `Duration` | | | | |
+| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | |
+| provenance | string | `Provenance` | | | | |
+| repeat_interval | [Duration](#duration) | `Duration` | | | | |
+
+### TimeInterval
+
+> TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained
+> within the interval.
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----------- | ---------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- |
+| DaysOfMonth | [][dayofmonthrange](#day-of-month-range) | `[]*DayOfMonthRange` | | | | |
+| Months | [][monthrange](#month-range) | `[]*MonthRange` | | | | |
+| Times | [][timerange](#time-range) | `[]*TimeRange` | | | | |
+| Weekdays | [][weekdayrange](#weekday-range) | `[]*WeekdayRange` | | | | |
+| Years | [][yearrange](#year-range) | `[]*YearRange` | | | | |
+
+### TimeRange
+
+> For example, 4:00PM to End of the day would Begin at 1020 and End at 1440.
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----------- | ------------------------- | ------- | :------: | ------- | ----------- | ------- |
+| EndMinute | int64 (formatted integer) | `int64` | | | | |
+| StartMinute | int64 (formatted integer) | `int64` | | | | |
+
+### ValidationError
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ---- | ------ | -------- | :------: | ------- | ----------- | ------- |
+| Msg | string | `string` | | | | |
+
+### WeekdayRange
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- |
+| Begin | int64 (formatted integer) | `int64` | | | | |
+| End | int64 (formatted integer) | `int64` | | | | |
+
+### YearRange
+
+**Properties**
+
+| Name | Type | Go type | Required | Default | Description | Example |
+| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- |
+| Begin | int64 (formatted integer) | `int64` | | | | |
+| End | int64 (formatted integer) | `int64` | | | | |
diff --git a/docs/sources/developers/http_api/query_history.md b/docs/sources/developers/http_api/query_history.md
index b7616fb1ba8..2f1dd8cb71f 100644
--- a/docs/sources/developers/http_api/query_history.md
+++ b/docs/sources/developers/http_api/query_history.md
@@ -82,7 +82,8 @@ Status codes:
- **200** – OK
- **400** - Errors (invalid JSON, missing or invalid fields)
-- **500** – Unable to add query to the database
+- **401** – Unauthorized
+- **500** – Internal error
## Query history search
@@ -144,7 +145,8 @@ Content-Type: application/json
Status codes:
- **200** – OK
-- **500** – Unable to add query to the database
+- **401** – Unauthorized
+- **500** – Internal error
## Delete query from Query history by UID
@@ -176,7 +178,8 @@ Content-Type: application/json
Status codes:
- **200** – OK
-- **500** – Unable to delete query from the database
+- **401** – Unauthorized
+- **500** – Internal error
## Update comment of query in Query history by UID
@@ -232,7 +235,8 @@ Status codes:
- **200** – OK
- **400** - Errors (invalid JSON, missing or invalid fields)
-- **500** – Unable to update comment of query in the database
+- **401** – Unauthorized
+- **500** – Internal error
## Star query in Query history
@@ -280,7 +284,8 @@ Content-Type: application/json
Status codes:
- **200** – OK
-- **500** – Unable to star query in the database
+- **401** – Unauthorized
+- **500** – Internal error
## Unstar query in Query history
@@ -328,4 +333,64 @@ Content-Type: application/json
Status codes:
- **200** – OK
-- **500** – Unable to unstar query in the database
+- **401** – Unauthorized
+- **500** – Internal error
+
+## Migrate queries to Query history
+
+`POST /api/query-history/migrate`
+
+Migrates multiple queries in to query history.
+
+**Example request:**
+
+```http
+POST /api/query-history HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+{
+ "queries": [
+ {
+ "datasourceUid": "PE1C5CBDA0504A6A3",
+ "queries": [
+ {
+ "refId": "A",
+ "key": "Q-87fed8e3-62ba-4eb2-8d2a-4129979bb4de-0",
+ "scenarioId": "csv_content",
+ "datasource": {
+ "type": "testdata",
+ "uid": "PD8C576611E62080A"
+ }
+ }
+ ],
+ "starred": false,
+ "createdAt": 1643630762,
+ "comment": "debugging"
+ }
+ ]
+}
+```
+
+JSON body schema:
+
+- **queries** – JSON of query history items.
+
+**Example response:**
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+{
+ "message": "Query history successfully migrated",
+ "totalCount": 105,
+ "starredCount": 10
+}
+```
+
+Status codes:
+
+- **200** – OK
+- **400** - Errors (invalid JSON, missing or invalid fields)
+- **401** – Unauthorized
+- **500** – Internal error
diff --git a/docs/sources/developers/plugins/data-frames.md b/docs/sources/developers/plugins/data-frames.md
index 1619d42d65a..b70e1654212 100644
--- a/docs/sources/developers/plugins/data-frames.md
+++ b/docs/sources/developers/plugins/data-frames.md
@@ -46,7 +46,7 @@ interface Field {
}
```
-Let's look an example. The table below demonstrates a data frame with two fields, _time_ and _temperature_.
+Let's look at an example. The table below demonstrates a data frame with two fields, _time_ and _temperature_.
| time | temperature |
| ------------------- | ----------- |
diff --git a/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md b/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md
index cfa180ec896..780f407539a 100644
--- a/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md
+++ b/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md
@@ -47,12 +47,7 @@ You can take advantage of your current authentication provider to manage user an
For example:
1. Map SAML, LDAP, or Oauth roles to Grafana basic roles (viewer, editor, or admin).
-2. Use the Grafana Enterprise team sync feature to synchronize teams from your SAML, LDAP, or Oauth provider to Grafana.
-
- - If a team does not exist in Grafana, team sync creates it.
- - If a team exists in Grafana, team sync updates its membership.
-
- For more information about team sync, refer to [Team sync]({{< relref "../../setup-grafana/configure-security/configure-team-sync/" >}}).
+2. Use the Grafana Enterprise team sync feature to synchronize teams from your SAML, LDAP, or Oauth provider to Grafana. For more information about team sync, refer to [Team sync]({{< relref "../../setup-grafana/configure-security/configure-team-sync/" >}}).
3. Within Grafana, assign RBAC permissions to users and teams.
diff --git a/docs/sources/getting-started/build-first-dashboard.md b/docs/sources/getting-started/build-first-dashboard.md
index da7fd4c8cd2..9c5d7ed2bb6 100644
--- a/docs/sources/getting-started/build-first-dashboard.md
+++ b/docs/sources/getting-started/build-first-dashboard.md
@@ -34,7 +34,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.
diff --git a/docs/sources/panels/library-panels/add-library-panel.md b/docs/sources/panels/library-panels/add-library-panel.md
index 24be42ccf89..49daa67e8fc 100644
--- a/docs/sources/panels/library-panels/add-library-panel.md
+++ b/docs/sources/panels/library-panels/add-library-panel.md
@@ -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" >}}
diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md
index 55f95c700ca..ed821e6eddc 100644
--- a/docs/sources/setup-grafana/configure-grafana/_index.md
+++ b/docs/sources/setup-grafana/configure-grafana/_index.md
@@ -1189,7 +1189,7 @@ For more information about the Grafana alerts, refer to [About Grafana Alerting]
### enabled
-Enable the Unified Alerting sub-system and interface. When enabled we'll migrate all of your alert rules and notification channels to the new system. New alert rules will be created and your notification channels will be converted into an Alertmanager configuration. Previous data is preserved to enable backwards compatibility but new data is removed. The default value is `false`.
+Enable or disable Grafana Alerting. If enabled, we’ll migrate all your alert rules and notification channels to the new system as alert rules and notification channels you had previously defined will be converted into an Alertmanager configuration. Legacy alerting data is preserved to enable backwards compatibility. If disabled, all your legacy alerting data will be available again, but the data you created using Grafana Alerting will be deleted. Set force_migration=true to avoid deletion of data. The default value is `true`.
Alerting Rules migrated from dashboards and panels will include a link back via the `annotations`.
diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml.md
index 85f33f00d74..98fdd86bfd5 100644
--- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml.md
+++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml.md
@@ -412,7 +412,7 @@ The keys you provide should look like:
When the user logs in using SAML and gets presented with "origin not allowed", the user might be issuing the login from an IdP (identity provider) service or the user is behind a reverse proxy. This potentially happens as Grafana's CSRF checks deem the requests to be invalid. For more information [CSRF](https://owasp.org/www-community/attacks/csrf).
-To solve this issue, you can configure either the [`csrf_trusted_origins`]({{< relref "../../configure-grafana/enterprise-configuration/#csrf-trusted-origins" >}}) or [`csrf_origin_headers`]({{< relref "../../configure-grafana/enterprise-configuration/#csrf-origin-headers" >}}) option in the SAML configuration.
+To solve this issue, you can configure either the [`csrf_trusted_origins`]({{< relref "../../configure-grafana/#csrf-trusted-origins" >}}) or [`csrf_additional_headers`]({{< relref "../../configure-grafana/#csrf_additional_headers" >}}) option in the SAML configuration.
Example of a configuration file:
@@ -421,6 +421,6 @@ Example of a configuration file:
...
[security]
csrf_trusted_origins = https://grafana.example.com
-csrf_origin_headers = X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Port
+csrf_additional_headers = X-Forwarded-Host
...
```
diff --git a/docs/sources/setup-grafana/set-up-for-high-availability.md b/docs/sources/setup-grafana/set-up-for-high-availability.md
index a5c94b9012d..d4893f9df9e 100644
--- a/docs/sources/setup-grafana/set-up-for-high-availability.md
+++ b/docs/sources/setup-grafana/set-up-for-high-availability.md
@@ -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
diff --git a/docs/sources/shared/alerts/grafana-managed-alerts.md b/docs/sources/shared/alerts/grafana-managed-alerts.md
index 77187d96cc4..8f3dfa3b1f7 100644
--- a/docs/sources/shared/alerts/grafana-managed-alerts.md
+++ b/docs/sources/shared/alerts/grafana-managed-alerts.md
@@ -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 |
| ------------------------------------------- | --------- | ---------------------------------------------------------------------------------------- |
diff --git a/docs/sources/whatsnew/whats-new-in-v7-4.md b/docs/sources/whatsnew/whats-new-in-v7-4.md
index bfd8b0e087a..f74c6104e24 100644
--- a/docs/sources/whatsnew/whats-new-in-v7-4.md
+++ b/docs/sources/whatsnew/whats-new-in-v7-4.md
@@ -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
diff --git a/package.json b/package.json
index 1df77b45723..4d91df3602c 100644
--- a/package.json
+++ b/package.json
@@ -102,9 +102,9 @@
"@lingui/macro": "3.13.3",
"@microsoft/api-extractor": "7.25.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.6",
- "@react-types/button": "3.4.5",
- "@react-types/menu": "3.5.3",
- "@react-types/overlays": "3.5.5",
+ "@react-types/button": "3.5.0",
+ "@react-types/menu": "3.6.0",
+ "@react-types/overlays": "3.6.0",
"@react-types/shared": "3.12.0",
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
"@swc/core": "1.2.187",
@@ -181,14 +181,14 @@
"cypress": "9.5.1",
"enzyme": "3.11.0",
"enzyme-to-json": "3.6.2",
- "eslint": "8.15.0",
+ "eslint": "8.17.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "26.2.2",
"eslint-plugin-jsdoc": "39.3.0",
"eslint-plugin-lodash": "7.4.0",
"eslint-plugin-react": "7.29.4",
- "eslint-plugin-react-hooks": "4.3.0",
+ "eslint-plugin-react-hooks": "4.5.0",
"eslint-webpack-plugin": "3.1.1",
"expose-loader": "4.0.0",
"file-loader": "6.2.0",
@@ -221,7 +221,7 @@
"postcss-scss": "4.0.4",
"prettier": "2.6.2",
"raw-loader": "4.0.2",
- "react-refresh": "0.11.0",
+ "react-refresh": "0.13.0",
"react-select-event": "5.5.0",
"react-test-renderer": "17.0.2",
"redux-mock-store": "1.5.4",
@@ -269,16 +269,16 @@
"@opentelemetry/exporter-collector": "0.25.0",
"@opentelemetry/semantic-conventions": "1.3.1",
"@popperjs/core": "2.11.5",
- "@react-aria/button": "3.4.4",
- "@react-aria/dialog": "3.1.9",
- "@react-aria/focus": "3.5.5",
- "@react-aria/interactions": "3.8.4",
- "@react-aria/menu": "3.4.4",
- "@react-aria/overlays": "3.8.2",
- "@react-aria/utils": "3.12.0",
- "@react-stately/collections": "3.3.8",
- "@react-stately/menu": "3.2.7",
- "@react-stately/tree": "3.2.4",
+ "@react-aria/button": "3.5.0",
+ "@react-aria/dialog": "3.2.0",
+ "@react-aria/focus": "3.6.0",
+ "@react-aria/interactions": "3.9.0",
+ "@react-aria/menu": "3.5.0",
+ "@react-aria/overlays": "3.9.0",
+ "@react-aria/utils": "3.13.0",
+ "@react-stately/collections": "3.4.0",
+ "@react-stately/menu": "3.3.0",
+ "@react-stately/tree": "3.3.0",
"@reduxjs/toolkit": "1.8.1",
"@sentry/browser": "6.19.7",
"@sentry/types": "6.19.7",
diff --git a/packages/grafana-data/src/themes/createComponents.ts b/packages/grafana-data/src/themes/createComponents.ts
index 314b9ba218e..7646160521d 100644
--- a/packages/grafana-data/src/themes/createComponents.ts
+++ b/packages/grafana-data/src/themes/createComponents.ts
@@ -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,
diff --git a/packages/grafana-data/src/types/dataFrameTypes.ts b/packages/grafana-data/src/types/dataFrameTypes.ts
index 1e615e57232..00874cb51ce 100644
--- a/packages/grafana-data/src/types/dataFrameTypes.ts
+++ b/packages/grafana-data/src/types/dataFrameTypes.ts
@@ -13,17 +13,19 @@ export enum DataFrameType {
DirectoryListing = 'directory-listing',
/**
- * First field is X, the rest are bucket values
+ * First field is X, the rest are ordinal values used as rows in the heatmap
*/
- HeatmapBuckets = 'heatmap-buckets',
+ HeatmapRows = 'heatmap-rows',
/**
* Explicit fields for:
* xMin, yMin, count, ...
*
* All values in the grid exist and have regular spacing
+ *
+ * If the y value is actually ordinal, use `meta.custom` to specify the bucket lookup values
*/
- HeatmapScanlines = 'heatmap-scanlines',
+ HeatmapCells = 'heatmap-cells',
/**
* WIP sparse heatmap support
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index e3b794c181a..33708fe1626 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -36,6 +36,7 @@ export interface FeatureToggles {
showFeatureFlagsInUI?: boolean;
publicDashboards?: boolean;
lokiLive?: boolean;
+ lokiDataframeApi?: boolean;
swaggerUi?: boolean;
featureHighlights?: boolean;
dashboardComments?: boolean;
@@ -55,7 +56,11 @@ export interface FeatureToggles {
traceToMetrics?: boolean;
prometheusStreamingJSONParser?: boolean;
validateDashboardsOnSave?: boolean;
+ autoMigrateGraphPanels?: boolean;
prometheusWideSeries?: boolean;
canvasPanelNesting?: boolean;
cloudMonitoringExperimentalUI?: boolean;
+ logRequestsInstrumentedAsUnknown?: boolean;
+ dataConnectionsConsole?: boolean;
+ internationalization?: boolean;
}
diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index c580a71a296..1d860a5088b 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -39,12 +39,12 @@
"@grafana/slate-react": "0.22.10-grafana",
"@monaco-editor/react": "4.3.1",
"@popperjs/core": "2.11.5",
- "@react-aria/button": "3.4.4",
- "@react-aria/dialog": "3.1.9",
- "@react-aria/focus": "3.5.5",
- "@react-aria/menu": "3.4.4",
- "@react-aria/overlays": "3.8.2",
- "@react-stately/menu": "3.2.7",
+ "@react-aria/button": "3.5.0",
+ "@react-aria/dialog": "3.2.0",
+ "@react-aria/focus": "3.6.0",
+ "@react-aria/menu": "3.5.0",
+ "@react-aria/overlays": "3.9.0",
+ "@react-stately/menu": "3.3.0",
"@sentry/browser": "6.19.7",
"ansicolor": "1.1.100",
"calculate-size": "1.1.1",
@@ -97,6 +97,7 @@
"@babel/core": "7.18.2",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mdx-js/react": "1.6.22",
+ "@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "22.0.0",
"@rollup/plugin-node-resolve": "13.3.0",
"@storybook/addon-a11y": "6.4.21",
diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts
index 9a1d647c43c..b5751895103 100644
--- a/packages/grafana-ui/rollup.config.ts
+++ b/packages/grafana-ui/rollup.config.ts
@@ -1,3 +1,4 @@
+import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import svg from 'rollup-plugin-svg-import';
@@ -39,6 +40,9 @@ const buildCjsPackage = ({ env }) => {
'@emotion/css',
],
plugins: [
+ // rc-time-picker has a transitive dependency on component-indexof which
+ // when bundled via `component-classes` imports a nonexistent `indexof` module.
+ alias({ entries: [{ find: 'indexof', replacement: 'component-indexof' }] }),
commonjs({
include: /node_modules/,
ignoreTryCatch: false,
diff --git a/packages/grafana-ui/src/components/Logs/LogMessageAnsi.tsx b/packages/grafana-ui/src/components/Logs/LogMessageAnsi.tsx
index b7633b714b5..918ff631cc3 100644
--- a/packages/grafana-ui/src/components/Logs/LogMessageAnsi.tsx
+++ b/packages/grafana-ui/src/components/Logs/LogMessageAnsi.tsx
@@ -82,6 +82,7 @@ export class UnThemedLogMessageAnsi extends PureComponent {
return chunks.map((chunk, index) => {
const chunkText = this.props.highlight?.searchWords ? (
() as React.RefObject;
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,
diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts
index e2ecb59178a..b7bd318463d 100644
--- a/packages/grafana-ui/src/components/Select/types.ts
+++ b/packages/grafana-ui/src/components/Select/types.ts
@@ -8,6 +8,7 @@ export type ActionMeta = SelectActionMeta<{}>;
export type InputActionMeta = {
action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close';
};
+export type LoadOptionsCallback = (options: Array>) => void;
export interface SelectCommonProps {
/** Aria label applied to the input field */
@@ -87,8 +88,10 @@ export interface SelectCommonProps {
export interface SelectAsyncProps {
/** When specified as boolean the loadOptions will execute when component is mounted */
defaultOptions?: boolean | Array>;
+
/** Asynchronously load select options */
- loadOptions?: (query: string) => Promise>>;
+ loadOptions?: (query: string, cb?: LoadOptionsCallback) => Promise>> | 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 */
diff --git a/pkg/api/docs/definitions/query_history.go b/pkg/api/docs/definitions/query_history.go
new file mode 100644
index 00000000000..12fc438477a
--- /dev/null
+++ b/pkg/api/docs/definitions/query_history.go
@@ -0,0 +1,179 @@
+package definitions
+
+import (
+ "github.com/grafana/grafana/pkg/services/queryhistory"
+)
+
+// swagger:route GET /query-history query_history searchQueries
+//
+// Query history search.
+//
+// Returns a list of queries in the query history that matches the search criteria.
+// Query history search supports pagination. Use the `limit` parameter to control the maximum number of queries returned; the default limit is 100.
+// You can also use the `page` query parameter to fetch queries from any page other than the first one.
+//
+// Responses:
+// 200: getQueryHistorySearchResponse
+// 401: unauthorisedError
+// 500: internalServerError
+
+// swagger:route POST /query-history query_history createQuery
+//
+// Add query to query history.
+//
+// Adds new query to query history.
+//
+// Responses:
+// 200: getQueryHistoryResponse
+// 400: badRequestError
+// 401: unauthorisedError
+// 500: internalServerError
+
+// swagger:route POST /query-history/star/{query_history_uid} query_history starQuery
+//
+// Add star to query in query history.
+//
+// Adds star to query in query history as specified by the UID.
+//
+// Responses:
+// 200: getQueryHistoryResponse
+// 401: unauthorisedError
+// 500: internalServerError
+
+// swagger:route POST /query-history/migrate query_history migrateQueries
+//
+// Migrate queries to query history.
+//
+// Adds multiple queries to query history.
+//
+// Responses:
+// 200: getQueryHistoryMigrationResponse
+// 400: badRequestError
+// 401: unauthorisedError
+// 500: internalServerError
+
+// swagger:route PATCH /query-history/{query_history_uid} query_history patchQueryComment
+//
+// Update comment for query in query history.
+//
+// Updates comment for query in query history as specified by the UID.
+//
+// Responses:
+// 200: getQueryHistoryResponse
+// 400: badRequestError
+// 401: unauthorisedError
+// 500: internalServerError
+
+// swagger:route DELETE /query-history/{query_history_uid} query_history deleteQuery
+//
+// Delete query in query history.
+//
+// Deletes an existing query in query history as specified by the UID. This operation cannot be reverted.
+//
+// Responses:
+// 200: getQueryHistoryDeleteQueryResponse
+// 401: unauthorisedError
+// 500: internalServerError
+
+// swagger:route DELETE /query-history/star/{query_history_uid} query_history unstarQuery
+//
+// Remove star to query in query history.
+//
+// Removes star from query in query history as specified by the UID.
+//
+// Responses:
+// 200: getQueryHistoryResponse
+// 401: unauthorisedError
+// 500: internalServerError
+
+// swagger:parameters starQuery patchQueryComment deleteQuery unstarQuery
+type QueryHistoryByUID struct {
+ // in:path
+ // required:true
+ UID string `json:"query_history_uid"`
+}
+
+// swagger:parameters searchQueries
+type SearchQueriesParams struct {
+ // List of data source UIDs to search for
+ // in:query
+ // required: false
+ // type: array
+ // collectionFormat: multi
+ DatasourceUid []string `json:"datasourceUid"`
+ // Text inside query or comments that is searched for
+ // in:query
+ // required: false
+ SearchString string `json:"searchString"`
+ // Flag indicating if only starred queries should be returned
+ // in:query
+ // required: false
+ OnlyStarred bool `json:"onlyStarred"`
+ // Sort method
+ // in:query
+ // required: false
+ // default: time-desc
+ // Enum: time-desc,time-asc
+ Sort string `json:"sort"`
+ // Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size.
+ // in:query
+ // required: false
+ Page int `json:"page"`
+ // Limit the number of returned results
+ // in:query
+ // required: false
+ Limit int `json:"limit"`
+ // From range for the query history search
+ // in:query
+ // required: false
+ From int64 `json:"from"`
+ // To range for the query history search
+ // in:query
+ // required: false
+ To int64 `json:"to"`
+}
+
+// swagger:parameters createQuery
+type CreateQueryParams struct {
+ // in:body
+ // required:true
+ Body queryhistory.CreateQueryInQueryHistoryCommand `json:"body"`
+}
+
+// swagger:parameters patchQueryComment
+type PatchQueryCommentParams struct {
+ // in:body
+ // required:true
+ Body queryhistory.PatchQueryCommentInQueryHistoryCommand `json:"body"`
+}
+
+// swagger:parameters migrateQueries
+type MigrateQueriesParams struct {
+ // in:body
+ // required:true
+ Body queryhistory.MigrateQueriesToQueryHistoryCommand `json:"body"`
+}
+
+//swagger:response getQueryHistorySearchResponse
+type GetQueryHistorySearchResponse struct {
+ // in: body
+ Body queryhistory.QueryHistorySearchResponse `json:"body"`
+}
+
+// swagger:response getQueryHistoryResponse
+type GetQueryHistoryResponse struct {
+ // in: body
+ Body queryhistory.QueryHistoryResponse `json:"body"`
+}
+
+// swagger:response getQueryHistoryDeleteQueryResponse
+type GetQueryHistoryDeleteQueryResponse struct {
+ // in: body
+ Body queryhistory.QueryHistoryDeleteQueryResponse `json:"body"`
+}
+
+// swagger:response getQueryHistoryMigrationResponse
+type GetQueryHistoryMigrationResponse struct {
+ // in: body
+ Body queryhistory.QueryHistoryMigrationResponse `json:"body"`
+}
diff --git a/pkg/api/docs/tags.json b/pkg/api/docs/tags.json
index 17b6bf9aa82..b46177db930 100644
--- a/pkg/api/docs/tags.json
+++ b/pkg/api/docs/tags.json
@@ -31,6 +31,10 @@
{
"name": "library_elements",
"description": "The identifier (ID) of a library element is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a library element uniquely identifies library elements between multiple Grafana installs. It’s automatically generated unless you specify it during library element creation. The UID provides consistent URLs for accessing library elements and when syncing library elements between multiple Grafana installs.\nThe maximum length of a UID is 40 characters."
+ },
+ {
+ "name": "query_history",
+ "description": "The identifier (ID) of a query in query history is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a query history uniquely identifies queries in query history between multiple Grafana installs. It’s automatically generated. The UID provides consistent URLs for accessing queries in query history."
},
{
"name": "orgs",
diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go
index caba326a2a8..d951bcfc87a 100644
--- a/pkg/api/dtos/index.go
+++ b/pkg/api/dtos/index.go
@@ -42,6 +42,7 @@ const (
WeightDashboard
WeightExplore
WeightAlerting
+ WeightDataConnections
WeightPlugin
WeightConfig
WeightAdmin
@@ -61,7 +62,7 @@ type NavLink struct {
Description string `json:"description,omitempty"`
Section string `json:"section,omitempty"`
SubTitle string `json:"subTitle,omitempty"`
- Icon string `json:"icon,omitempty"`
+ Icon string `json:"icon,omitempty"` // Available icons can be browsed in Storybook: https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 8aa9a327461..945840a2936 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -245,6 +245,10 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
navTree = append(navTree, hs.buildAlertNavLinks(c)...)
}
+ if hs.Features.IsEnabled(featuremgmt.FlagDataConnectionsConsole) {
+ navTree = append(navTree, hs.buildDataConnectionsNavLink(c))
+ }
+
appLinks, err := hs.getAppLinks(c)
if err != nil {
return nil, err
@@ -456,6 +460,11 @@ func (hs *HTTPServer) buildStarredItemsNavLinks(c *models.ReqContext, prefs *pre
}
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
+ hasAccess := ac.HasAccess(hs.AccessControl, c)
+ hasEditPermInAnyFolder := func(c *models.ReqContext) bool {
+ return hasEditPerm
+ }
+
dashboardChildNavs := []*dtos.NavLink{}
if !hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
@@ -492,19 +501,26 @@ func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
})
- dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
- Text: "New dashboard", Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "new-dashboard", ShowIconInNavbar: true,
- })
- if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
+
+ if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
+ dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
+ Text: "New dashboard", Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "new-dashboard", ShowIconInNavbar: true,
+ })
+ }
+
+ if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalPermission(dashboards.ActionFoldersCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "New folder", SubTitle: "Create a new folder to organize your dashboards", Id: "new-folder",
Icon: "plus", Url: hs.Cfg.AppSubURL + "/dashboards/folder/new", HideFromTabs: true, ShowIconInNavbar: true,
})
}
- dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
- Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "plus",
- Url: hs.Cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, ShowIconInNavbar: true,
- })
+
+ if hasAccess(hasEditPermInAnyFolder, ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
+ dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
+ Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "plus",
+ Url: hs.Cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, ShowIconInNavbar: true,
+ })
+ }
}
return dashboardChildNavs
}
@@ -630,6 +646,58 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink
return children
}
+func (hs *HTTPServer) buildDataConnectionsNavLink(c *models.ReqContext) *dtos.NavLink {
+ var children []*dtos.NavLink
+ var navLink *dtos.NavLink
+
+ baseId := "data-connections"
+ baseUrl := hs.Cfg.AppSubURL + "/" + baseId
+
+ children = append(children, &dtos.NavLink{
+ Id: baseId + "-datasources",
+ Text: "Data sources",
+ Icon: "database",
+ Description: "Add and configure data sources",
+ Url: baseUrl + "/datasources",
+ })
+
+ children = append(children, &dtos.NavLink{
+ Id: baseId + "-plugins",
+ Text: "Plugins",
+ Icon: "plug",
+ Description: "Manage plugins",
+ Url: baseUrl + "/plugins",
+ })
+
+ children = append(children, &dtos.NavLink{
+ Id: baseId + "-cloud-integrations",
+ Text: "Cloud integrations",
+ Icon: "bolt",
+ Description: "Manage your cloud integrations",
+ Url: baseUrl + "/cloud-integrations",
+ })
+
+ children = append(children, &dtos.NavLink{
+ Id: baseId + "-recorded-queries",
+ Text: "Recorded queries",
+ Icon: "record-audio",
+ Description: "Manage your recorded queries",
+ Url: baseUrl + "/recorded-queries",
+ })
+
+ navLink = &dtos.NavLink{
+ Text: "Data Connections",
+ Icon: "link",
+ Id: baseId,
+ Url: baseUrl,
+ Children: children,
+ Section: dtos.NavSectionCore,
+ SortWeight: dtos.WeightDataConnections,
+ }
+
+ return navLink
+}
+
func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
hasAccess := ac.HasAccess(hs.AccessControl, c)
hasGlobalAccess := ac.HasGlobalAccess(hs.AccessControl, c)
diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go
index 1803c168e95..62c0d6e966d 100644
--- a/pkg/api/login_oauth.go
+++ b/pkg/api/login_oauth.go
@@ -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
}
diff --git a/pkg/api/login_oauth_test.go b/pkg/api/login_oauth_test.go
index 2f67bfdfbcc..cd5b2e6f2f2 100644
--- a/pkg/api/login_oauth_test.go
+++ b/pkg/api/login_oauth_test.go
@@ -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) {
diff --git a/pkg/infra/filestorage/fs_integration_test.go b/pkg/infra/filestorage/fs_integration_test.go
index 48cc7285051..f2ea34e0479 100644
--- a/pkg/infra/filestorage/fs_integration_test.go
+++ b/pkg/infra/filestorage/fs_integration_test.go
@@ -170,6 +170,9 @@ func runTests(createCases func() []fsTestCase, t *testing.T) {
}
func TestIntegrationFsStorage(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
//skipTest := true
emptyContents := make([]byte, 0)
pngImage, _ := base64.StdEncoding.DecodeString(pngImageBase64)
diff --git a/pkg/infra/httpclient/httpclientprovider/forwarded_cookie_middleware_test.go b/pkg/infra/httpclient/httpclientprovider/forwarded_cookie_middleware_test.go
new file mode 100644
index 00000000000..07753e0c0f5
--- /dev/null
+++ b/pkg/infra/httpclient/httpclientprovider/forwarded_cookie_middleware_test.go
@@ -0,0 +1,65 @@
+package httpclientprovider_test
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
+ "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
+ "github.com/stretchr/testify/require"
+)
+
+func TestForwardedCookiesMiddleware(t *testing.T) {
+ tcs := []struct {
+ desc string
+ allowedCookies []string
+ expectedCookieHeader string
+ }{
+ {
+ desc: "With nil allowedCookies should not populate Cookie header",
+ allowedCookies: nil,
+ expectedCookieHeader: "",
+ },
+ {
+ desc: "With empty allowed cookies should not populate Cookie header",
+ allowedCookies: []string{},
+ expectedCookieHeader: "",
+ },
+ {
+ desc: "When provided with allowed cookies should populate Cookie header",
+ allowedCookies: []string{"c1", "c3"},
+ expectedCookieHeader: "c1=1; c3=3",
+ },
+ }
+
+ for _, tc := range tcs {
+ t.Run(tc.desc, func(t *testing.T) {
+ ctx := &testContext{}
+ finalRoundTripper := ctx.createRoundTripper()
+ forwarded := []*http.Cookie{
+ {Name: "c1", Value: "1"},
+ {Name: "c2", Value: "2"},
+ {Name: "c3", Value: "3"},
+ }
+ mw := httpclientprovider.ForwardedCookiesMiddleware(forwarded, tc.allowedCookies)
+ opts := httpclient.Options{}
+ rt := mw.CreateMiddleware(opts, finalRoundTripper)
+ require.NotNil(t, rt)
+ middlewareName, ok := mw.(httpclient.MiddlewareName)
+ require.True(t, ok)
+ require.Equal(t, "forwarded-cookies", middlewareName.MiddlewareName())
+
+ req, err := http.NewRequest(http.MethodGet, "http://", nil)
+ require.NoError(t, err)
+ res, err := rt.RoundTrip(req)
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ if res.Body != nil {
+ require.NoError(t, res.Body.Close())
+ }
+ require.Len(t, ctx.callChain, 1)
+ require.ElementsMatch(t, []string{"final"}, ctx.callChain)
+ require.Equal(t, tc.expectedCookieHeader, ctx.req.Header.Get("Cookie"))
+ })
+ }
+}
diff --git a/pkg/infra/httpclient/httpclientprovider/forwarded_cookies_middleware.go b/pkg/infra/httpclient/httpclientprovider/forwarded_cookies_middleware.go
new file mode 100644
index 00000000000..7d0ef6f2ef5
--- /dev/null
+++ b/pkg/infra/httpclient/httpclientprovider/forwarded_cookies_middleware.go
@@ -0,0 +1,24 @@
+package httpclientprovider
+
+import (
+ "net/http"
+
+ "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
+ "github.com/grafana/grafana/pkg/util/proxyutil"
+)
+
+const ForwardedCookiesMiddlewareName = "forwarded-cookies"
+
+// ForwardedCookiesMiddleware middleware that sets Cookie header on the
+// outgoing request, if forwarded cookies configured/provided.
+func ForwardedCookiesMiddleware(forwardedCookies []*http.Cookie, allowedCookies []string) httpclient.Middleware {
+ return httpclient.NamedMiddlewareFunc(ForwardedCookiesMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
+ return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
+ for _, cookie := range forwardedCookies {
+ req.AddCookie(cookie)
+ }
+ proxyutil.ClearCookieHeader(req, allowedCookies)
+ return next.RoundTrip(req)
+ })
+ })
+}
diff --git a/pkg/infra/httpclient/httpclientprovider/forwarded_oauth_identity_middleware.go b/pkg/infra/httpclient/httpclientprovider/forwarded_oauth_identity_middleware.go
new file mode 100644
index 00000000000..d5c78e794d7
--- /dev/null
+++ b/pkg/infra/httpclient/httpclientprovider/forwarded_oauth_identity_middleware.go
@@ -0,0 +1,31 @@
+package httpclientprovider
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
+ "golang.org/x/oauth2"
+)
+
+const ForwardedOAuthIdentityMiddlewareName = "forwarded-oauth-identity"
+
+// ForwardedOAuthIdentityMiddleware middleware that sets Authorization/X-ID-Token
+// headers on the outgoing request if an OAuth Token is provided
+func ForwardedOAuthIdentityMiddleware(token *oauth2.Token) httpclient.Middleware {
+ return httpclient.NamedMiddlewareFunc(ForwardedOAuthIdentityMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
+ if token == nil {
+ return next
+ }
+ return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
+ req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
+
+ idToken, ok := token.Extra("id_token").(string)
+ if ok && idToken != "" {
+ req.Header.Set("X-ID-Token", idToken)
+ }
+
+ return next.RoundTrip(req)
+ })
+ })
+}
diff --git a/pkg/infra/httpclient/httpclientprovider/forwarded_oauth_identity_middleware_test.go b/pkg/infra/httpclient/httpclientprovider/forwarded_oauth_identity_middleware_test.go
new file mode 100644
index 00000000000..1cd65d1bf09
--- /dev/null
+++ b/pkg/infra/httpclient/httpclientprovider/forwarded_oauth_identity_middleware_test.go
@@ -0,0 +1,82 @@
+package httpclientprovider_test
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
+ "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/oauth2"
+)
+
+func TestForwardedOAuthIdentityMiddleware(t *testing.T) {
+ at := &oauth2.Token{
+ AccessToken: "access-token",
+ }
+ tcs := []struct {
+ desc string
+ token *oauth2.Token
+ expectedAuthorizationHeader string
+ expectedIDTokenHeader string
+ }{
+ {
+ desc: "With nil token should not populate Cookie headers",
+ token: nil,
+ expectedAuthorizationHeader: "",
+ expectedIDTokenHeader: "",
+ },
+ {
+ desc: "With access token set should populate Authorization header",
+ token: at,
+ expectedAuthorizationHeader: "Bearer access-token",
+ expectedIDTokenHeader: "",
+ },
+ {
+ desc: "With Authorization and X-ID-Token header set should populate Authorization and X-Id-Token header",
+ token: at.WithExtra(map[string]interface{}{"id_token": "id-token"}),
+ expectedAuthorizationHeader: "Bearer access-token",
+ expectedIDTokenHeader: "id-token",
+ },
+ }
+
+ for _, tc := range tcs {
+ t.Run(tc.desc, func(t *testing.T) {
+ ctx := &testContext{}
+ finalRoundTripper := ctx.createRoundTripper()
+ mw := httpclientprovider.ForwardedOAuthIdentityMiddleware(tc.token)
+ opts := httpclient.Options{}
+ rt := mw.CreateMiddleware(opts, finalRoundTripper)
+ require.NotNil(t, rt)
+ middlewareName, ok := mw.(httpclient.MiddlewareName)
+ require.True(t, ok)
+ require.Equal(t, "forwarded-oauth-identity", middlewareName.MiddlewareName())
+
+ req, err := http.NewRequest(http.MethodGet, "http://", nil)
+ require.NoError(t, err)
+ res, err := rt.RoundTrip(req)
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ if res.Body != nil {
+ require.NoError(t, res.Body.Close())
+ }
+ require.Len(t, ctx.callChain, 1)
+ require.ElementsMatch(t, []string{"final"}, ctx.callChain)
+ require.Equal(t, tc.expectedAuthorizationHeader, ctx.req.Header.Get("Authorization"))
+ require.Equal(t, tc.expectedIDTokenHeader, ctx.req.Header.Get("X-ID-Token"))
+ })
+ }
+}
+
+type testContext struct {
+ callChain []string
+ req *http.Request
+}
+
+func (c *testContext) createRoundTripper() http.RoundTripper {
+ return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
+ c.callChain = append(c.callChain, "final")
+ c.req = req
+ return &http.Response{StatusCode: http.StatusOK}, nil
+ })
+}
diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go
index 0c6944cffba..b09b50c7b8f 100644
--- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go
+++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go
@@ -5,12 +5,11 @@ import (
"net/http"
"time"
- "github.com/grafana/grafana/pkg/models"
-
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/infra/tracing"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/mwitkow/go-conntrack"
)
diff --git a/pkg/infra/kvstore/kvstore_test.go b/pkg/infra/kvstore/kvstore_test.go
index 8467f31e4df..78350342ccf 100644
--- a/pkg/infra/kvstore/kvstore_test.go
+++ b/pkg/infra/kvstore/kvstore_test.go
@@ -37,6 +37,9 @@ func (t *TestCase) Value() string {
}
func TestIntegrationKVStore(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
kv := createTestableKVStore(t)
ctx := context.Background()
diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go
index 3eac92d8987..85d4e628297 100644
--- a/pkg/infra/metrics/metrics.go
+++ b/pkg/infra/metrics/metrics.go
@@ -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
-}
diff --git a/pkg/infra/metrics/metricutil/utils.go b/pkg/infra/metrics/metricutil/utils.go
index 7bb3b471333..c26f5363f8a 100644
--- a/pkg/infra/metrics/metricutil/utils.go
+++ b/pkg/infra/metrics/metricutil/utils.go
@@ -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
+}
diff --git a/pkg/infra/metrics/metricutil/utils_test.go b/pkg/infra/metrics/metricutil/utils_test.go
index d4862529085..b703b9f6811 100644
--- a/pkg/infra/metrics/metricutil/utils_test.go
+++ b/pkg/infra/metrics/metricutil/utils_test.go
@@ -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)
+ })
+ }
+}
diff --git a/pkg/infra/serverlock/serverlock_integration_test.go b/pkg/infra/serverlock/serverlock_integration_test.go
index 9e839645c19..49e45de280b 100644
--- a/pkg/infra/serverlock/serverlock_integration_test.go
+++ b/pkg/infra/serverlock/serverlock_integration_test.go
@@ -9,6 +9,9 @@ import (
)
func TestIntegrationServerLok(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sl := createTestableServerLock(t)
counter := 0
diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go
index 654271e92cd..fb6c7a57117 100644
--- a/pkg/middleware/request_metrics.go
+++ b/pkg/middleware/request_metrics.go
@@ -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,6 +47,8 @@ func init() {
// RequestMetrics is a middleware handler that instruments the request.
func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
+ log := log.New("middleware.request-metrics")
+
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()
@@ -58,15 +61,18 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
defer httpRequestsInFlight.Dec()
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 := RouteOperationNameFromContext(c.Req.Context()); exists {
+ handler = routeOperation
+ } else {
+ 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.
diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go
index f410ac2a916..226975937d6 100644
--- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go
+++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go
@@ -20,265 +20,6 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
-type SecretsGetRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
-}
-
-func (x *SecretsGetRequest) Reset() {
- *x = SecretsGetRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_secretsmanager_proto_msgTypes[0]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *SecretsGetRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SecretsGetRequest) ProtoMessage() {}
-
-func (x *SecretsGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_secretsmanager_proto_msgTypes[0]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use SecretsGetRequest.ProtoReflect.Descriptor instead.
-func (*SecretsGetRequest) Descriptor() ([]byte, []int) {
- return file_secretsmanager_proto_rawDescGZIP(), []int{0}
-}
-
-func (x *SecretsGetRequest) GetKeyDescriptor() *Key {
- if x != nil {
- return x.KeyDescriptor
- }
- return nil
-}
-
-type SecretsSetRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
- Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
-}
-
-func (x *SecretsSetRequest) Reset() {
- *x = SecretsSetRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_secretsmanager_proto_msgTypes[1]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *SecretsSetRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SecretsSetRequest) ProtoMessage() {}
-
-func (x *SecretsSetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_secretsmanager_proto_msgTypes[1]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use SecretsSetRequest.ProtoReflect.Descriptor instead.
-func (*SecretsSetRequest) Descriptor() ([]byte, []int) {
- return file_secretsmanager_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *SecretsSetRequest) GetKeyDescriptor() *Key {
- if x != nil {
- return x.KeyDescriptor
- }
- return nil
-}
-
-func (x *SecretsSetRequest) GetValue() string {
- if x != nil {
- return x.Value
- }
- return ""
-}
-
-type SecretsDelRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
-}
-
-func (x *SecretsDelRequest) Reset() {
- *x = SecretsDelRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_secretsmanager_proto_msgTypes[2]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *SecretsDelRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SecretsDelRequest) ProtoMessage() {}
-
-func (x *SecretsDelRequest) ProtoReflect() protoreflect.Message {
- mi := &file_secretsmanager_proto_msgTypes[2]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use SecretsDelRequest.ProtoReflect.Descriptor instead.
-func (*SecretsDelRequest) Descriptor() ([]byte, []int) {
- return file_secretsmanager_proto_rawDescGZIP(), []int{2}
-}
-
-func (x *SecretsDelRequest) GetKeyDescriptor() *Key {
- if x != nil {
- return x.KeyDescriptor
- }
- return nil
-}
-
-type SecretsKeysRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
- AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"`
-}
-
-func (x *SecretsKeysRequest) Reset() {
- *x = SecretsKeysRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_secretsmanager_proto_msgTypes[3]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *SecretsKeysRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SecretsKeysRequest) ProtoMessage() {}
-
-func (x *SecretsKeysRequest) ProtoReflect() protoreflect.Message {
- mi := &file_secretsmanager_proto_msgTypes[3]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use SecretsKeysRequest.ProtoReflect.Descriptor instead.
-func (*SecretsKeysRequest) Descriptor() ([]byte, []int) {
- return file_secretsmanager_proto_rawDescGZIP(), []int{3}
-}
-
-func (x *SecretsKeysRequest) GetKeyDescriptor() *Key {
- if x != nil {
- return x.KeyDescriptor
- }
- return nil
-}
-
-func (x *SecretsKeysRequest) GetAllOrganizations() bool {
- if x != nil {
- return x.AllOrganizations
- }
- return false
-}
-
-type SecretsRenameRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
- NewNamespace string `protobuf:"bytes,4,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"`
-}
-
-func (x *SecretsRenameRequest) Reset() {
- *x = SecretsRenameRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_secretsmanager_proto_msgTypes[4]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *SecretsRenameRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SecretsRenameRequest) ProtoMessage() {}
-
-func (x *SecretsRenameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_secretsmanager_proto_msgTypes[4]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use SecretsRenameRequest.ProtoReflect.Descriptor instead.
-func (*SecretsRenameRequest) Descriptor() ([]byte, []int) {
- return file_secretsmanager_proto_rawDescGZIP(), []int{4}
-}
-
-func (x *SecretsRenameRequest) GetKeyDescriptor() *Key {
- if x != nil {
- return x.KeyDescriptor
- }
- return nil
-}
-
-func (x *SecretsRenameRequest) GetNewNamespace() string {
- if x != nil {
- return x.NewNamespace
- }
- return ""
-}
-
type Key struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -292,7 +33,7 @@ type Key struct {
func (x *Key) Reset() {
*x = Key{}
if protoimpl.UnsafeEnabled {
- mi := &file_secretsmanager_proto_msgTypes[5]
+ mi := &file_secretsmanager_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -305,7 +46,7 @@ func (x *Key) String() string {
func (*Key) ProtoMessage() {}
func (x *Key) ProtoReflect() protoreflect.Message {
- mi := &file_secretsmanager_proto_msgTypes[5]
+ mi := &file_secretsmanager_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -318,7 +59,7 @@ func (x *Key) ProtoReflect() protoreflect.Message {
// Deprecated: Use Key.ProtoReflect.Descriptor instead.
func (*Key) Descriptor() ([]byte, []int) {
- return file_secretsmanager_proto_rawDescGZIP(), []int{5}
+ return file_secretsmanager_proto_rawDescGZIP(), []int{0}
}
func (x *Key) GetOrgId() int64 {
@@ -342,16 +83,275 @@ func (x *Key) GetType() string {
return ""
}
-type SecretsErrorResponse struct {
+type GetSecretRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
+ KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
}
-func (x *SecretsErrorResponse) Reset() {
- *x = SecretsErrorResponse{}
+func (x *GetSecretRequest) Reset() {
+ *x = GetSecretRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_secretsmanager_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetSecretRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSecretRequest) ProtoMessage() {}
+
+func (x *GetSecretRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_secretsmanager_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSecretRequest.ProtoReflect.Descriptor instead.
+func (*GetSecretRequest) Descriptor() ([]byte, []int) {
+ return file_secretsmanager_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GetSecretRequest) GetKeyDescriptor() *Key {
+ if x != nil {
+ return x.KeyDescriptor
+ }
+ return nil
+}
+
+type GetSecretResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
+ DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"`
+ Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"`
+}
+
+func (x *GetSecretResponse) Reset() {
+ *x = GetSecretResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_secretsmanager_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetSecretResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSecretResponse) ProtoMessage() {}
+
+func (x *GetSecretResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_secretsmanager_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSecretResponse.ProtoReflect.Descriptor instead.
+func (*GetSecretResponse) Descriptor() ([]byte, []int) {
+ return file_secretsmanager_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *GetSecretResponse) GetUserFriendlyError() string {
+ if x != nil {
+ return x.UserFriendlyError
+ }
+ return ""
+}
+
+func (x *GetSecretResponse) GetDecryptedValue() string {
+ if x != nil {
+ return x.DecryptedValue
+ }
+ return ""
+}
+
+func (x *GetSecretResponse) GetExists() bool {
+ if x != nil {
+ return x.Exists
+ }
+ return false
+}
+
+type SetSecretRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
+ Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+}
+
+func (x *SetSecretRequest) Reset() {
+ *x = SetSecretRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_secretsmanager_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SetSecretRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetSecretRequest) ProtoMessage() {}
+
+func (x *SetSecretRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_secretsmanager_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SetSecretRequest.ProtoReflect.Descriptor instead.
+func (*SetSecretRequest) Descriptor() ([]byte, []int) {
+ return file_secretsmanager_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *SetSecretRequest) GetKeyDescriptor() *Key {
+ if x != nil {
+ return x.KeyDescriptor
+ }
+ return nil
+}
+
+func (x *SetSecretRequest) GetValue() string {
+ if x != nil {
+ return x.Value
+ }
+ return ""
+}
+
+type SetSecretResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
+}
+
+func (x *SetSecretResponse) Reset() {
+ *x = SetSecretResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_secretsmanager_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SetSecretResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetSecretResponse) ProtoMessage() {}
+
+func (x *SetSecretResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_secretsmanager_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SetSecretResponse.ProtoReflect.Descriptor instead.
+func (*SetSecretResponse) Descriptor() ([]byte, []int) {
+ return file_secretsmanager_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *SetSecretResponse) GetUserFriendlyError() string {
+ if x != nil {
+ return x.UserFriendlyError
+ }
+ return ""
+}
+
+type DelSecretRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
+}
+
+func (x *DelSecretRequest) Reset() {
+ *x = DelSecretRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_secretsmanager_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *DelSecretRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DelSecretRequest) ProtoMessage() {}
+
+func (x *DelSecretRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_secretsmanager_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DelSecretRequest.ProtoReflect.Descriptor instead.
+func (*DelSecretRequest) Descriptor() ([]byte, []int) {
+ return file_secretsmanager_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *DelSecretRequest) GetKeyDescriptor() *Key {
+ if x != nil {
+ return x.KeyDescriptor
+ }
+ return nil
+}
+
+type DelSecretResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
+}
+
+func (x *DelSecretResponse) Reset() {
+ *x = DelSecretResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_secretsmanager_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -359,13 +359,13 @@ func (x *SecretsErrorResponse) Reset() {
}
}
-func (x *SecretsErrorResponse) String() string {
+func (x *DelSecretResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*SecretsErrorResponse) ProtoMessage() {}
+func (*DelSecretResponse) ProtoMessage() {}
-func (x *SecretsErrorResponse) ProtoReflect() protoreflect.Message {
+func (x *DelSecretResponse) ProtoReflect() protoreflect.Message {
mi := &file_secretsmanager_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -377,30 +377,29 @@ func (x *SecretsErrorResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use SecretsErrorResponse.ProtoReflect.Descriptor instead.
-func (*SecretsErrorResponse) Descriptor() ([]byte, []int) {
+// Deprecated: Use DelSecretResponse.ProtoReflect.Descriptor instead.
+func (*DelSecretResponse) Descriptor() ([]byte, []int) {
return file_secretsmanager_proto_rawDescGZIP(), []int{6}
}
-func (x *SecretsErrorResponse) GetError() string {
+func (x *DelSecretResponse) GetUserFriendlyError() string {
if x != nil {
- return x.Error
+ return x.UserFriendlyError
}
return ""
}
-type SecretsGetResponse struct {
+type ListSecretsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
- DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"`
- Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"`
+ KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
+ AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"`
}
-func (x *SecretsGetResponse) Reset() {
- *x = SecretsGetResponse{}
+func (x *ListSecretsRequest) Reset() {
+ *x = ListSecretsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_secretsmanager_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -408,13 +407,13 @@ func (x *SecretsGetResponse) Reset() {
}
}
-func (x *SecretsGetResponse) String() string {
+func (x *ListSecretsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*SecretsGetResponse) ProtoMessage() {}
+func (*ListSecretsRequest) ProtoMessage() {}
-func (x *SecretsGetResponse) ProtoReflect() protoreflect.Message {
+func (x *ListSecretsRequest) ProtoReflect() protoreflect.Message {
mi := &file_secretsmanager_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -426,43 +425,36 @@ func (x *SecretsGetResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use SecretsGetResponse.ProtoReflect.Descriptor instead.
-func (*SecretsGetResponse) Descriptor() ([]byte, []int) {
+// Deprecated: Use ListSecretsRequest.ProtoReflect.Descriptor instead.
+func (*ListSecretsRequest) Descriptor() ([]byte, []int) {
return file_secretsmanager_proto_rawDescGZIP(), []int{7}
}
-func (x *SecretsGetResponse) GetError() string {
+func (x *ListSecretsRequest) GetKeyDescriptor() *Key {
if x != nil {
- return x.Error
+ return x.KeyDescriptor
}
- return ""
+ return nil
}
-func (x *SecretsGetResponse) GetDecryptedValue() string {
+func (x *ListSecretsRequest) GetAllOrganizations() bool {
if x != nil {
- return x.DecryptedValue
- }
- return ""
-}
-
-func (x *SecretsGetResponse) GetExists() bool {
- if x != nil {
- return x.Exists
+ return x.AllOrganizations
}
return false
}
-type SecretsKeysResponse struct {
+type ListSecretsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
- Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
+ UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
+ Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
}
-func (x *SecretsKeysResponse) Reset() {
- *x = SecretsKeysResponse{}
+func (x *ListSecretsResponse) Reset() {
+ *x = ListSecretsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_secretsmanager_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -470,13 +462,13 @@ func (x *SecretsKeysResponse) Reset() {
}
}
-func (x *SecretsKeysResponse) String() string {
+func (x *ListSecretsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*SecretsKeysResponse) ProtoMessage() {}
+func (*ListSecretsResponse) ProtoMessage() {}
-func (x *SecretsKeysResponse) ProtoReflect() protoreflect.Message {
+func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message {
mi := &file_secretsmanager_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -488,118 +480,231 @@ func (x *SecretsKeysResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use SecretsKeysResponse.ProtoReflect.Descriptor instead.
-func (*SecretsKeysResponse) Descriptor() ([]byte, []int) {
+// Deprecated: Use ListSecretsResponse.ProtoReflect.Descriptor instead.
+func (*ListSecretsResponse) Descriptor() ([]byte, []int) {
return file_secretsmanager_proto_rawDescGZIP(), []int{8}
}
-func (x *SecretsKeysResponse) GetError() string {
+func (x *ListSecretsResponse) GetUserFriendlyError() string {
if x != nil {
- return x.Error
+ return x.UserFriendlyError
}
return ""
}
-func (x *SecretsKeysResponse) GetKeys() []*Key {
+func (x *ListSecretsResponse) GetKeys() []*Key {
if x != nil {
return x.Keys
}
return nil
}
+type RenameSecretRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
+ NewNamespace string `protobuf:"bytes,2,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"`
+}
+
+func (x *RenameSecretRequest) Reset() {
+ *x = RenameSecretRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_secretsmanager_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RenameSecretRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RenameSecretRequest) ProtoMessage() {}
+
+func (x *RenameSecretRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_secretsmanager_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RenameSecretRequest.ProtoReflect.Descriptor instead.
+func (*RenameSecretRequest) Descriptor() ([]byte, []int) {
+ return file_secretsmanager_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *RenameSecretRequest) GetKeyDescriptor() *Key {
+ if x != nil {
+ return x.KeyDescriptor
+ }
+ return nil
+}
+
+func (x *RenameSecretRequest) GetNewNamespace() string {
+ if x != nil {
+ return x.NewNamespace
+ }
+ return ""
+}
+
+type RenameSecretResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
+}
+
+func (x *RenameSecretResponse) Reset() {
+ *x = RenameSecretResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_secretsmanager_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RenameSecretResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RenameSecretResponse) ProtoMessage() {}
+
+func (x *RenameSecretResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_secretsmanager_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RenameSecretResponse.ProtoReflect.Descriptor instead.
+func (*RenameSecretResponse) Descriptor() ([]byte, []int) {
+ return file_secretsmanager_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *RenameSecretResponse) GetUserFriendlyError() string {
+ if x != nil {
+ return x.UserFriendlyError
+ }
+ return ""
+}
+
var File_secretsmanager_proto protoreflect.FileDescriptor
var file_secretsmanager_proto_rawDesc = []byte{
0x0a, 0x14, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d,
- 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x54, 0x0a, 0x11,
- 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x4d, 0x0a, 0x03,
+ 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d,
+ 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61,
+ 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x47,
+ 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+ 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
+ 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65,
+ 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72,
+ 0x22, 0x81, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72,
+ 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45,
+ 0x72, 0x72, 0x6f, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
+ 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65,
+ 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06,
+ 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78,
+ 0x69, 0x73, 0x74, 0x73, 0x22, 0x69, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65,
+ 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44,
+ 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72,
+ 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44,
+ 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c,
+ 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22,
+ 0x41, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65,
+ 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72,
+ 0x6f, 0x72, 0x22, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73,
+ 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
+ 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c,
+ 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73,
+ 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x22, 0x41, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x53, 0x65,
+ 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11,
+ 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f,
+ 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69,
+ 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x81, 0x01, 0x0a, 0x12, 0x4c,
+ 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65,
0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e,
0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
- 0x6f, 0x72, 0x22, 0x6a, 0x0a, 0x11, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x53, 0x65, 0x74,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65,
- 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19,
+ 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x61, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, 0x6c,
+ 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x72,
+ 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69,
+ 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72,
+ 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
+ 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65,
+ 0x79, 0x73, 0x22, 0x7a, 0x0a, 0x13, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x63, 0x72,
+ 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79,
+ 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
+ 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79,
+ 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x22, 0x0a, 0x0c, 0x6e, 0x65,
+ 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0c, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x44,
+ 0x0a, 0x14, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72,
+ 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x11, 0x75, 0x73, 0x65, 0x72, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x45,
+ 0x72, 0x72, 0x6f, 0x72, 0x32, 0xd6, 0x03, 0x0a, 0x0e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
+ 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x26,
0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70,
- 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65,
- 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
- 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x54,
- 0x0a, 0x11, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x44, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69,
- 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63,
- 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69,
- 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69,
- 0x70, 0x74, 0x6f, 0x72, 0x22, 0x81, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
- 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b,
- 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61,
- 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b,
- 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x10,
- 0x61, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e,
- 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x7b, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x72,
- 0x65, 0x74, 0x73, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f,
- 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
- 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b,
- 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f,
- 0x72, 0x12, 0x22, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
- 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65,
- 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x4d, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
- 0x6f, 0x72, 0x67, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x67,
- 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
- 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
- 0x74, 0x79, 0x70, 0x65, 0x22, 0x2c, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45,
- 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05,
- 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72,
- 0x6f, 0x72, 0x22, 0x6a, 0x0a, 0x12, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x47, 0x65, 0x74,
- 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f,
- 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x26,
- 0x0a, 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
- 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73,
- 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x5a,
- 0x0a, 0x13, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x04, 0x6b,
- 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72,
- 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
- 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x32, 0xe7, 0x03, 0x0a, 0x14, 0x52,
- 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4d, 0x61, 0x6e, 0x61,
- 0x67, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x27, 0x2e, 0x73, 0x65, 0x63,
- 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69,
- 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e,
- 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65,
- 0x74, 0x73, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a,
- 0x03, 0x53, 0x65, 0x74, 0x12, 0x27, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61,
- 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72,
- 0x65, 0x74, 0x73, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e,
- 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c,
- 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x72, 0x72, 0x6f,
- 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x03, 0x44, 0x65, 0x6c,
- 0x12, 0x27, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x44,
- 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x65, 0x63, 0x72,
- 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
- 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x04, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x28, 0x2e,
- 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c,
- 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x73,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
- 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53,
- 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x12, 0x60, 0x0a, 0x06, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x2e, 0x73,
- 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75,
- 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x6e, 0x61, 0x6d,
- 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65,
- 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e,
- 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x19, 0x5a, 0x17, 0x2e, 0x2f, 0x3b, 0x73, 0x65, 0x63, 0x72, 0x65,
- 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x62,
- 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
+ 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65,
+ 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x56, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x12, 0x26, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
+ 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65,
+ 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27,
+ 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70,
+ 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x03, 0x44, 0x65, 0x6c, 0x12, 0x26,
+ 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70,
+ 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
+ 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x44, 0x65,
+ 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x5b, 0x0a, 0x04, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x28, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
+ 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4c,
+ 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x29, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63,
+ 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x06,
+ 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
+ 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x52, 0x65,
+ 0x6e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53,
+ 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x19, 0x5a,
+ 0x17, 0x2e, 0x2f, 0x3b, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -614,35 +719,37 @@ func file_secretsmanager_proto_rawDescGZIP() []byte {
return file_secretsmanager_proto_rawDescData
}
-var file_secretsmanager_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
+var file_secretsmanager_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_secretsmanager_proto_goTypes = []interface{}{
- (*SecretsGetRequest)(nil), // 0: secretsmanagerplugin.SecretsGetRequest
- (*SecretsSetRequest)(nil), // 1: secretsmanagerplugin.SecretsSetRequest
- (*SecretsDelRequest)(nil), // 2: secretsmanagerplugin.SecretsDelRequest
- (*SecretsKeysRequest)(nil), // 3: secretsmanagerplugin.SecretsKeysRequest
- (*SecretsRenameRequest)(nil), // 4: secretsmanagerplugin.SecretsRenameRequest
- (*Key)(nil), // 5: secretsmanagerplugin.Key
- (*SecretsErrorResponse)(nil), // 6: secretsmanagerplugin.SecretsErrorResponse
- (*SecretsGetResponse)(nil), // 7: secretsmanagerplugin.SecretsGetResponse
- (*SecretsKeysResponse)(nil), // 8: secretsmanagerplugin.SecretsKeysResponse
+ (*Key)(nil), // 0: secretsmanagerplugin.Key
+ (*GetSecretRequest)(nil), // 1: secretsmanagerplugin.GetSecretRequest
+ (*GetSecretResponse)(nil), // 2: secretsmanagerplugin.GetSecretResponse
+ (*SetSecretRequest)(nil), // 3: secretsmanagerplugin.SetSecretRequest
+ (*SetSecretResponse)(nil), // 4: secretsmanagerplugin.SetSecretResponse
+ (*DelSecretRequest)(nil), // 5: secretsmanagerplugin.DelSecretRequest
+ (*DelSecretResponse)(nil), // 6: secretsmanagerplugin.DelSecretResponse
+ (*ListSecretsRequest)(nil), // 7: secretsmanagerplugin.ListSecretsRequest
+ (*ListSecretsResponse)(nil), // 8: secretsmanagerplugin.ListSecretsResponse
+ (*RenameSecretRequest)(nil), // 9: secretsmanagerplugin.RenameSecretRequest
+ (*RenameSecretResponse)(nil), // 10: secretsmanagerplugin.RenameSecretResponse
}
var file_secretsmanager_proto_depIdxs = []int32{
- 5, // 0: secretsmanagerplugin.SecretsGetRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
- 5, // 1: secretsmanagerplugin.SecretsSetRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
- 5, // 2: secretsmanagerplugin.SecretsDelRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
- 5, // 3: secretsmanagerplugin.SecretsKeysRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
- 5, // 4: secretsmanagerplugin.SecretsRenameRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
- 5, // 5: secretsmanagerplugin.SecretsKeysResponse.keys:type_name -> secretsmanagerplugin.Key
- 0, // 6: secretsmanagerplugin.RemoteSecretsManager.Get:input_type -> secretsmanagerplugin.SecretsGetRequest
- 1, // 7: secretsmanagerplugin.RemoteSecretsManager.Set:input_type -> secretsmanagerplugin.SecretsSetRequest
- 2, // 8: secretsmanagerplugin.RemoteSecretsManager.Del:input_type -> secretsmanagerplugin.SecretsDelRequest
- 3, // 9: secretsmanagerplugin.RemoteSecretsManager.Keys:input_type -> secretsmanagerplugin.SecretsKeysRequest
- 4, // 10: secretsmanagerplugin.RemoteSecretsManager.Rename:input_type -> secretsmanagerplugin.SecretsRenameRequest
- 7, // 11: secretsmanagerplugin.RemoteSecretsManager.Get:output_type -> secretsmanagerplugin.SecretsGetResponse
- 6, // 12: secretsmanagerplugin.RemoteSecretsManager.Set:output_type -> secretsmanagerplugin.SecretsErrorResponse
- 6, // 13: secretsmanagerplugin.RemoteSecretsManager.Del:output_type -> secretsmanagerplugin.SecretsErrorResponse
- 8, // 14: secretsmanagerplugin.RemoteSecretsManager.Keys:output_type -> secretsmanagerplugin.SecretsKeysResponse
- 6, // 15: secretsmanagerplugin.RemoteSecretsManager.Rename:output_type -> secretsmanagerplugin.SecretsErrorResponse
+ 0, // 0: secretsmanagerplugin.GetSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
+ 0, // 1: secretsmanagerplugin.SetSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
+ 0, // 2: secretsmanagerplugin.DelSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
+ 0, // 3: secretsmanagerplugin.ListSecretsRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
+ 0, // 4: secretsmanagerplugin.ListSecretsResponse.keys:type_name -> secretsmanagerplugin.Key
+ 0, // 5: secretsmanagerplugin.RenameSecretRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
+ 1, // 6: secretsmanagerplugin.SecretsManager.Get:input_type -> secretsmanagerplugin.GetSecretRequest
+ 3, // 7: secretsmanagerplugin.SecretsManager.Set:input_type -> secretsmanagerplugin.SetSecretRequest
+ 5, // 8: secretsmanagerplugin.SecretsManager.Del:input_type -> secretsmanagerplugin.DelSecretRequest
+ 7, // 9: secretsmanagerplugin.SecretsManager.Keys:input_type -> secretsmanagerplugin.ListSecretsRequest
+ 9, // 10: secretsmanagerplugin.SecretsManager.Rename:input_type -> secretsmanagerplugin.RenameSecretRequest
+ 2, // 11: secretsmanagerplugin.SecretsManager.Get:output_type -> secretsmanagerplugin.GetSecretResponse
+ 4, // 12: secretsmanagerplugin.SecretsManager.Set:output_type -> secretsmanagerplugin.SetSecretResponse
+ 6, // 13: secretsmanagerplugin.SecretsManager.Del:output_type -> secretsmanagerplugin.DelSecretResponse
+ 8, // 14: secretsmanagerplugin.SecretsManager.Keys:output_type -> secretsmanagerplugin.ListSecretsResponse
+ 10, // 15: secretsmanagerplugin.SecretsManager.Rename:output_type -> secretsmanagerplugin.RenameSecretResponse
11, // [11:16] is the sub-list for method output_type
6, // [6:11] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
@@ -657,66 +764,6 @@ func file_secretsmanager_proto_init() {
}
if !protoimpl.UnsafeEnabled {
file_secretsmanager_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsGetRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_secretsmanager_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsSetRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_secretsmanager_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsDelRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_secretsmanager_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsKeysRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_secretsmanager_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsRenameRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_secretsmanager_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Key); i {
case 0:
return &v.state
@@ -728,8 +775,68 @@ func file_secretsmanager_proto_init() {
return nil
}
}
+ file_secretsmanager_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetSecretRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_secretsmanager_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetSecretResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_secretsmanager_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SetSecretRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_secretsmanager_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*SetSecretResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_secretsmanager_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*DelSecretRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
file_secretsmanager_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsErrorResponse); i {
+ switch v := v.(*DelSecretResponse); i {
case 0:
return &v.state
case 1:
@@ -741,7 +848,7 @@ func file_secretsmanager_proto_init() {
}
}
file_secretsmanager_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsGetResponse); i {
+ switch v := v.(*ListSecretsRequest); i {
case 0:
return &v.state
case 1:
@@ -753,7 +860,31 @@ func file_secretsmanager_proto_init() {
}
}
file_secretsmanager_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SecretsKeysResponse); i {
+ switch v := v.(*ListSecretsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_secretsmanager_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RenameSecretRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_secretsmanager_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RenameSecretResponse); i {
case 0:
return &v.state
case 1:
@@ -771,7 +902,7 @@ func file_secretsmanager_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_secretsmanager_proto_rawDesc,
NumEnums: 0,
- NumMessages: 9,
+ NumMessages: 11,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.proto b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.proto
index 0ab75cdf9d7..6fc90534b50 100644
--- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.proto
+++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.proto
@@ -3,54 +3,62 @@ package secretsmanagerplugin;
option go_package = "./;secretsmanagerplugin";
-message SecretsGetRequest {
- Key keyDescriptor = 1;
-}
-
-message SecretsSetRequest {
- Key keyDescriptor = 1;
- string value = 2;
-}
-
-message SecretsDelRequest {
- Key keyDescriptor = 1;
-}
-
-message SecretsKeysRequest {
- Key keyDescriptor = 1;
- bool allOrganizations = 2;
-}
-
-message SecretsRenameRequest {
- Key keyDescriptor = 1;
- string newNamespace = 2;
-}
-
message Key {
int64 orgId = 1;
string namespace = 2;
string type = 3;
}
-message SecretsErrorResponse {
- string error = 1;
+message GetSecretRequest {
+ Key keyDescriptor = 1;
}
-message SecretsGetResponse {
- string error = 1;
+message GetSecretResponse {
+ string userFriendlyError = 1;
string decryptedValue = 2;
bool exists = 3;
}
-message SecretsKeysResponse {
- string error = 1;
+message SetSecretRequest {
+ Key keyDescriptor = 1;
+ string value = 2;
+}
+
+message SetSecretResponse {
+ string userFriendlyError = 1;
+}
+
+message DelSecretRequest {
+ Key keyDescriptor = 1;
+}
+
+message DelSecretResponse {
+ string userFriendlyError = 1;
+}
+
+message ListSecretsRequest {
+ Key keyDescriptor = 1;
+ bool allOrganizations = 2;
+}
+
+message ListSecretsResponse {
+ string userFriendlyError = 1;
repeated Key keys = 2;
}
-service RemoteSecretsManager {
- rpc Get(SecretsGetRequest) returns (SecretsGetResponse);
- rpc Set(SecretsSetRequest) returns (SecretsErrorResponse);
- rpc Del(SecretsDelRequest) returns (SecretsErrorResponse);
- rpc Keys(SecretsKeysRequest) returns (SecretsKeysResponse);
- rpc Rename(SecretsRenameRequest) returns (SecretsErrorResponse);
+message RenameSecretRequest {
+ Key keyDescriptor = 1;
+ string newNamespace = 2;
+}
+
+message RenameSecretResponse {
+ string userFriendlyError = 1;
+}
+
+service SecretsManager {
+ rpc Get(GetSecretRequest) returns (GetSecretResponse);
+ rpc Set(SetSecretRequest) returns (SetSecretResponse);
+ rpc Del(DelSecretRequest) returns (DelSecretResponse);
+ rpc Keys(ListSecretsRequest) returns (ListSecretsResponse);
+ rpc Rename(RenameSecretRequest) returns (RenameSecretResponse);
}
\ No newline at end of file
diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grcp_plugin.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grcp_plugin.go
index 6be57d4bc42..66ca722e9ce 100644
--- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grcp_plugin.go
+++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grcp_plugin.go
@@ -8,7 +8,7 @@ import (
)
type SecretsManagerPlugin interface {
- RemoteSecretsManagerClient
+ SecretsManagerClient
}
type SecretsManagerGRPCPlugin struct {
@@ -20,37 +20,37 @@ func (p *SecretsManagerGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc
}
func (p *SecretsManagerGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
- return &SecretsManagerGRPCClient{NewRemoteSecretsManagerClient(c)}, nil
+ return &SecretsManagerGRPCClient{NewSecretsManagerClient(c)}, nil
}
type SecretsManagerGRPCClient struct {
- RemoteSecretsManagerClient
+ SecretsManagerClient
}
// Get an item from the store
-func (sm *SecretsManagerGRPCClient) Get(ctx context.Context, req *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error) {
- return sm.RemoteSecretsManagerClient.Get(ctx, req)
+func (sm *SecretsManagerGRPCClient) Get(ctx context.Context, req *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) {
+ return sm.SecretsManagerClient.Get(ctx, req)
}
// Set an item in the store
-func (sm *SecretsManagerGRPCClient) Set(ctx context.Context, req *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
- return sm.RemoteSecretsManagerClient.Set(ctx, req)
+func (sm *SecretsManagerGRPCClient) Set(ctx context.Context, req *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error) {
+ return sm.SecretsManagerClient.Set(ctx, req)
}
// Del deletes an item from the store.
-func (sm *SecretsManagerGRPCClient) Del(ctx context.Context, req *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
- return sm.RemoteSecretsManagerClient.Del(ctx, req)
+func (sm *SecretsManagerGRPCClient) Del(ctx context.Context, req *DelSecretRequest, opts ...grpc.CallOption) (*DelSecretResponse, error) {
+ return sm.SecretsManagerClient.Del(ctx, req)
}
// Keys get all keys for a given namespace.
-func (sm *SecretsManagerGRPCClient) Keys(ctx context.Context, req *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error) {
- return sm.RemoteSecretsManagerClient.Keys(ctx, req)
+func (sm *SecretsManagerGRPCClient) Keys(ctx context.Context, req *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error) {
+ return sm.SecretsManagerClient.Keys(ctx, req)
}
// Rename an item in the store
-func (sm *SecretsManagerGRPCClient) Rename(ctx context.Context, req *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
- return sm.RemoteSecretsManagerClient.Rename(ctx, req)
+func (sm *SecretsManagerGRPCClient) Rename(ctx context.Context, req *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error) {
+ return sm.SecretsManagerClient.Rename(ctx, req)
}
-var _ RemoteSecretsManagerClient = &SecretsManagerGRPCClient{}
+var _ SecretsManagerClient = &SecretsManagerGRPCClient{}
var _ plugin.GRPCPlugin = &SecretsManagerGRPCPlugin{}
diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go
index 9d0459c8971..5e71c97635b 100644
--- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go
+++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go
@@ -18,230 +18,230 @@ import (
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
-// RemoteSecretsManagerClient is the client API for RemoteSecretsManager service.
+// SecretsManagerClient is the client API for SecretsManager service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
-type RemoteSecretsManagerClient interface {
- Get(ctx context.Context, in *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error)
- Set(ctx context.Context, in *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
- Del(ctx context.Context, in *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
- Keys(ctx context.Context, in *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error)
- Rename(ctx context.Context, in *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
+type SecretsManagerClient interface {
+ Get(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error)
+ Set(ctx context.Context, in *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error)
+ Del(ctx context.Context, in *DelSecretRequest, opts ...grpc.CallOption) (*DelSecretResponse, error)
+ Keys(ctx context.Context, in *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error)
+ Rename(ctx context.Context, in *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error)
}
-type remoteSecretsManagerClient struct {
+type secretsManagerClient struct {
cc grpc.ClientConnInterface
}
-func NewRemoteSecretsManagerClient(cc grpc.ClientConnInterface) RemoteSecretsManagerClient {
- return &remoteSecretsManagerClient{cc}
+func NewSecretsManagerClient(cc grpc.ClientConnInterface) SecretsManagerClient {
+ return &secretsManagerClient{cc}
}
-func (c *remoteSecretsManagerClient) Get(ctx context.Context, in *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error) {
- out := new(SecretsGetResponse)
- err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Get", in, out, opts...)
+func (c *secretsManagerClient) Get(ctx context.Context, in *GetSecretRequest, opts ...grpc.CallOption) (*GetSecretResponse, error) {
+ out := new(GetSecretResponse)
+ err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Get", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
-func (c *remoteSecretsManagerClient) Set(ctx context.Context, in *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
- out := new(SecretsErrorResponse)
- err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Set", in, out, opts...)
+func (c *secretsManagerClient) Set(ctx context.Context, in *SetSecretRequest, opts ...grpc.CallOption) (*SetSecretResponse, error) {
+ out := new(SetSecretResponse)
+ err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Set", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
-func (c *remoteSecretsManagerClient) Del(ctx context.Context, in *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
- out := new(SecretsErrorResponse)
- err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Del", in, out, opts...)
+func (c *secretsManagerClient) Del(ctx context.Context, in *DelSecretRequest, opts ...grpc.CallOption) (*DelSecretResponse, error) {
+ out := new(DelSecretResponse)
+ err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Del", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
-func (c *remoteSecretsManagerClient) Keys(ctx context.Context, in *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error) {
- out := new(SecretsKeysResponse)
- err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Keys", in, out, opts...)
+func (c *secretsManagerClient) Keys(ctx context.Context, in *ListSecretsRequest, opts ...grpc.CallOption) (*ListSecretsResponse, error) {
+ out := new(ListSecretsResponse)
+ err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Keys", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
-func (c *remoteSecretsManagerClient) Rename(ctx context.Context, in *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
- out := new(SecretsErrorResponse)
- err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Rename", in, out, opts...)
+func (c *secretsManagerClient) Rename(ctx context.Context, in *RenameSecretRequest, opts ...grpc.CallOption) (*RenameSecretResponse, error) {
+ out := new(RenameSecretResponse)
+ err := c.cc.Invoke(ctx, "/secretsmanagerplugin.SecretsManager/Rename", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
-// RemoteSecretsManagerServer is the server API for RemoteSecretsManager service.
-// All implementations must embed UnimplementedRemoteSecretsManagerServer
+// SecretsManagerServer is the server API for SecretsManager service.
+// All implementations must embed UnimplementedSecretsManagerServer
// for forward compatibility
-type RemoteSecretsManagerServer interface {
- Get(context.Context, *SecretsGetRequest) (*SecretsGetResponse, error)
- Set(context.Context, *SecretsSetRequest) (*SecretsErrorResponse, error)
- Del(context.Context, *SecretsDelRequest) (*SecretsErrorResponse, error)
- Keys(context.Context, *SecretsKeysRequest) (*SecretsKeysResponse, error)
- Rename(context.Context, *SecretsRenameRequest) (*SecretsErrorResponse, error)
- mustEmbedUnimplementedRemoteSecretsManagerServer()
+type SecretsManagerServer interface {
+ Get(context.Context, *GetSecretRequest) (*GetSecretResponse, error)
+ Set(context.Context, *SetSecretRequest) (*SetSecretResponse, error)
+ Del(context.Context, *DelSecretRequest) (*DelSecretResponse, error)
+ Keys(context.Context, *ListSecretsRequest) (*ListSecretsResponse, error)
+ Rename(context.Context, *RenameSecretRequest) (*RenameSecretResponse, error)
+ mustEmbedUnimplementedSecretsManagerServer()
}
-// UnimplementedRemoteSecretsManagerServer must be embedded to have forward compatible implementations.
-type UnimplementedRemoteSecretsManagerServer struct {
+// UnimplementedSecretsManagerServer must be embedded to have forward compatible implementations.
+type UnimplementedSecretsManagerServer struct {
}
-func (UnimplementedRemoteSecretsManagerServer) Get(context.Context, *SecretsGetRequest) (*SecretsGetResponse, error) {
+func (UnimplementedSecretsManagerServer) Get(context.Context, *GetSecretRequest) (*GetSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
}
-func (UnimplementedRemoteSecretsManagerServer) Set(context.Context, *SecretsSetRequest) (*SecretsErrorResponse, error) {
+func (UnimplementedSecretsManagerServer) Set(context.Context, *SetSecretRequest) (*SetSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Set not implemented")
}
-func (UnimplementedRemoteSecretsManagerServer) Del(context.Context, *SecretsDelRequest) (*SecretsErrorResponse, error) {
+func (UnimplementedSecretsManagerServer) Del(context.Context, *DelSecretRequest) (*DelSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Del not implemented")
}
-func (UnimplementedRemoteSecretsManagerServer) Keys(context.Context, *SecretsKeysRequest) (*SecretsKeysResponse, error) {
+func (UnimplementedSecretsManagerServer) Keys(context.Context, *ListSecretsRequest) (*ListSecretsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Keys not implemented")
}
-func (UnimplementedRemoteSecretsManagerServer) Rename(context.Context, *SecretsRenameRequest) (*SecretsErrorResponse, error) {
+func (UnimplementedSecretsManagerServer) Rename(context.Context, *RenameSecretRequest) (*RenameSecretResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Rename not implemented")
}
-func (UnimplementedRemoteSecretsManagerServer) mustEmbedUnimplementedRemoteSecretsManagerServer() {}
+func (UnimplementedSecretsManagerServer) mustEmbedUnimplementedSecretsManagerServer() {}
-// UnsafeRemoteSecretsManagerServer may be embedded to opt out of forward compatibility for this service.
-// Use of this interface is not recommended, as added methods to RemoteSecretsManagerServer will
+// UnsafeSecretsManagerServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to SecretsManagerServer will
// result in compilation errors.
-type UnsafeRemoteSecretsManagerServer interface {
- mustEmbedUnimplementedRemoteSecretsManagerServer()
+type UnsafeSecretsManagerServer interface {
+ mustEmbedUnimplementedSecretsManagerServer()
}
-func RegisterRemoteSecretsManagerServer(s grpc.ServiceRegistrar, srv RemoteSecretsManagerServer) {
- s.RegisterService(&RemoteSecretsManager_ServiceDesc, srv)
+func RegisterSecretsManagerServer(s grpc.ServiceRegistrar, srv SecretsManagerServer) {
+ s.RegisterService(&SecretsManager_ServiceDesc, srv)
}
-func _RemoteSecretsManager_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(SecretsGetRequest)
+func _SecretsManager_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
- return srv.(RemoteSecretsManagerServer).Get(ctx, in)
+ return srv.(SecretsManagerServer).Get(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Get",
+ FullMethod: "/secretsmanagerplugin.SecretsManager/Get",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(RemoteSecretsManagerServer).Get(ctx, req.(*SecretsGetRequest))
+ return srv.(SecretsManagerServer).Get(ctx, req.(*GetSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
-func _RemoteSecretsManager_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(SecretsSetRequest)
+func _SecretsManager_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SetSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
- return srv.(RemoteSecretsManagerServer).Set(ctx, in)
+ return srv.(SecretsManagerServer).Set(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Set",
+ FullMethod: "/secretsmanagerplugin.SecretsManager/Set",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(RemoteSecretsManagerServer).Set(ctx, req.(*SecretsSetRequest))
+ return srv.(SecretsManagerServer).Set(ctx, req.(*SetSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
-func _RemoteSecretsManager_Del_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(SecretsDelRequest)
+func _SecretsManager_Del_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DelSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
- return srv.(RemoteSecretsManagerServer).Del(ctx, in)
+ return srv.(SecretsManagerServer).Del(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Del",
+ FullMethod: "/secretsmanagerplugin.SecretsManager/Del",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(RemoteSecretsManagerServer).Del(ctx, req.(*SecretsDelRequest))
+ return srv.(SecretsManagerServer).Del(ctx, req.(*DelSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
-func _RemoteSecretsManager_Keys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(SecretsKeysRequest)
+func _SecretsManager_Keys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListSecretsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
- return srv.(RemoteSecretsManagerServer).Keys(ctx, in)
+ return srv.(SecretsManagerServer).Keys(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Keys",
+ FullMethod: "/secretsmanagerplugin.SecretsManager/Keys",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(RemoteSecretsManagerServer).Keys(ctx, req.(*SecretsKeysRequest))
+ return srv.(SecretsManagerServer).Keys(ctx, req.(*ListSecretsRequest))
}
return interceptor(ctx, in, info, handler)
}
-func _RemoteSecretsManager_Rename_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(SecretsRenameRequest)
+func _SecretsManager_Rename_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RenameSecretRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
- return srv.(RemoteSecretsManagerServer).Rename(ctx, in)
+ return srv.(SecretsManagerServer).Rename(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Rename",
+ FullMethod: "/secretsmanagerplugin.SecretsManager/Rename",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(RemoteSecretsManagerServer).Rename(ctx, req.(*SecretsRenameRequest))
+ return srv.(SecretsManagerServer).Rename(ctx, req.(*RenameSecretRequest))
}
return interceptor(ctx, in, info, handler)
}
-// RemoteSecretsManager_ServiceDesc is the grpc.ServiceDesc for RemoteSecretsManager service.
+// SecretsManager_ServiceDesc is the grpc.ServiceDesc for SecretsManager service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
-var RemoteSecretsManager_ServiceDesc = grpc.ServiceDesc{
- ServiceName: "secretsmanagerplugin.RemoteSecretsManager",
- HandlerType: (*RemoteSecretsManagerServer)(nil),
+var SecretsManager_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "secretsmanagerplugin.SecretsManager",
+ HandlerType: (*SecretsManagerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Get",
- Handler: _RemoteSecretsManager_Get_Handler,
+ Handler: _SecretsManager_Get_Handler,
},
{
MethodName: "Set",
- Handler: _RemoteSecretsManager_Set_Handler,
+ Handler: _SecretsManager_Set_Handler,
},
{
MethodName: "Del",
- Handler: _RemoteSecretsManager_Del_Handler,
+ Handler: _SecretsManager_Del_Handler,
},
{
MethodName: "Keys",
- Handler: _RemoteSecretsManager_Keys_Handler,
+ Handler: _SecretsManager_Keys_Handler,
},
{
MethodName: "Rename",
- Handler: _RemoteSecretsManager_Rename_Handler,
+ Handler: _SecretsManager_Rename_Handler,
},
},
Streams: []grpc.StreamDesc{},
diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go
index a4fc6f3335a..3aaade7aa19 100644
--- a/pkg/plugins/manager/manager_integration_test.go
+++ b/pkg/plugins/manager/manager_integration_test.go
@@ -79,7 +79,7 @@ func TestPluginManager_int_init(t *testing.T) {
es := elasticsearch.ProvideService(hcp)
grap := graphite.ProvideService(hcp, tracer)
idb := influxdb.ProvideService(hcp)
- lk := loki.ProvideService(hcp, tracer)
+ lk := loki.ProvideService(hcp, features, tracer)
otsdb := opentsdb.ProvideService(hcp)
pr := prometheus.ProvideService(hcp, cfg, features, tracer)
tmpo := tempo.ProvideService(hcp)
diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go
index c0a11f41280..fc1b35036c7 100644
--- a/pkg/services/alerting/engine_integration_test.go
+++ b/pkg/services/alerting/engine_integration_test.go
@@ -18,6 +18,9 @@ import (
)
func TestIntegrationEngineTimeouts(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
usMock := &usagestats.UsageStatsMock{T: t}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
diff --git a/pkg/services/alerting/ticker.go b/pkg/services/alerting/ticker.go
index a992a579ef7..d0f73ebb7be 100644
--- a/pkg/services/alerting/ticker.go
+++ b/pkg/services/alerting/ticker.go
@@ -32,7 +32,7 @@ func NewTicker(c clock.Clock, interval time.Duration, metric *metrics.Ticker) *T
t := &Ticker{
C: make(chan time.Time),
clock: c,
- last: c.Now(),
+ last: getStartTick(c, interval),
interval: interval,
metrics: metric,
stopCh: make(chan struct{}),
@@ -42,9 +42,14 @@ func NewTicker(c clock.Clock, interval time.Duration, metric *metrics.Ticker) *T
return t
}
+func getStartTick(clk clock.Clock, interval time.Duration) time.Time {
+ nano := clk.Now().UnixNano()
+ return time.Unix(0, nano-(nano%interval.Nanoseconds()))
+}
+
func (t *Ticker) run() {
logger := log.New("ticker")
- logger.Info("starting")
+ logger.Info("starting", "first_tick", t.last.Add(t.interval))
LOOP:
for {
next := t.last.Add(t.interval) // calculate the time of the next tick
diff --git a/pkg/services/alerting/ticker_test.go b/pkg/services/alerting/ticker_test.go
index 1994939b5ec..f759d5019ab 100644
--- a/pkg/services/alerting/ticker_test.go
+++ b/pkg/services/alerting/ticker_test.go
@@ -35,9 +35,24 @@ func TestTicker(t *testing.T) {
}
return time.Time{}
}
- t.Run("should not drop ticks", func(t *testing.T) {
+ t.Run("should align with clock", func(t *testing.T) {
+ interval := 10 * time.Second
clk := clock.NewMock()
+ clk.Add(1 * time.Minute)
+
+ require.Equal(t, clk.Now(), getStartTick(clk, interval))
+ now := clk.Now()
+ for i := 0; i < 100; i++ {
+ delta := time.Duration(rand.Int63n(interval.Nanoseconds()))
+ clk.Set(now.Add(delta))
+ require.Equal(t, now, getStartTick(clk, interval))
+ }
+ })
+
+ t.Run("should not drop ticks", func(t *testing.T) {
interval := time.Duration(rand.Int63n(100)+10) * time.Second
+ clk := clock.NewMock()
+ clk.Add(interval) // align clock with the start tick
ticker := NewTicker(clk, interval, metrics.NewTickerMetrics(prometheus.NewRegistry()))
ticks := rand.Intn(9) + 1
@@ -119,7 +134,7 @@ func TestTicker(t *testing.T) {
interval := time.Duration(rand.Int63n(9)+1) * time.Second
registry := prometheus.NewPedanticRegistry()
ticker := NewTicker(clk, interval, metrics.NewTickerMetrics(registry))
- expectedTick := clk.Now().Add(interval)
+ expectedTick := getStartTick(clk, interval).Add(interval)
expectedMetricFmt := `# HELP grafana_alerting_ticker_interval_seconds Interval at which the ticker is meant to tick.
# TYPE grafana_alerting_ticker_interval_seconds gauge
diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go
index 2e823bc57b0..800fcdd9d81 100644
--- a/pkg/services/dashboards/database/acl_test.go
+++ b/pkg/services/dashboards/database/acl_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationDashboardAclDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
var sqlStore *sqlstore.SQLStore
var currentUser models.User
var savedFolder, childDash *models.Dashboard
diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go
index dbca6013052..ebf06887b0d 100644
--- a/pkg/services/dashboards/database/database_folder_test.go
+++ b/pkg/services/dashboards/database/database_folder_test.go
@@ -12,6 +12,9 @@ import (
)
func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("Testing DB", func(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var folder, dashInRoot, childDash *models.Dashboard
diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go
index 1b9d3813df6..f882ca64cc3 100644
--- a/pkg/services/dashboards/database/database_provisioning_test.go
+++ b/pkg/services/dashboards/database/database_provisioning_test.go
@@ -14,6 +14,9 @@ import (
)
func TestIntegrationDashboardProvisioningTest(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go
index fedd4cd3726..ecb8e1aeaf4 100644
--- a/pkg/services/dashboards/database/database_test.go
+++ b/pkg/services/dashboards/database/database_test.go
@@ -21,6 +21,9 @@ import (
)
func TestIntegrationDashboardDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
var sqlStore *sqlstore.SQLStore
var savedFolder, savedDash, savedDash2 *models.Dashboard
var dashboardStore *DashboardStore
@@ -476,6 +479,9 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
}
func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
pluginId := "test-app"
@@ -495,6 +501,9 @@ func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *test
}
func TestIntegrationDashboard_SortingOptions(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
@@ -541,6 +550,9 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) {
}
func TestIntegrationDashboard_Filter(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore)
insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false)
diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go
index fb7c6101323..0cf92da3839 100644
--- a/pkg/services/dashboards/service/dashboard_service_integration_test.go
+++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go
@@ -22,6 +22,9 @@ import (
const testOrgID int64 = 1
func TestIntegrationIntegratedDashboardService(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) {
// Basic validation tests
diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go
index 65a05ed8d2e..dc9dc3cfefc 100644
--- a/pkg/services/dashboards/service/dashboard_service_test.go
+++ b/pkg/services/dashboards/service/dashboard_service_test.go
@@ -17,6 +17,9 @@ import (
)
func TestIntegrationDashboardService(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("Dashboard service tests", func(t *testing.T) {
fakeStore := m.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t)
diff --git a/pkg/services/dashboards/service/folder_service_test.go b/pkg/services/dashboards/service/folder_service_test.go
index 16d811ba2a6..f0869be2ea4 100644
--- a/pkg/services/dashboards/service/folder_service_test.go
+++ b/pkg/services/dashboards/service/folder_service_test.go
@@ -23,6 +23,9 @@ var orgID = int64(1)
var user = &models.SignedInUser{UserId: 1}
func TestIntegrationProvideFolderService(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("should register scope resolvers", func(t *testing.T) {
cfg := setting.NewCfg()
ac := acmock.New()
@@ -34,6 +37,9 @@ func TestIntegrationProvideFolderService(t *testing.T) {
}
func TestIntegrationFolderService(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("Folder service tests", func(t *testing.T) {
store := &dashboards.FakeDashboardStore{}
cfg := setting.NewCfg()
diff --git a/pkg/services/dashboardversion/dashverimpl/store_test.go b/pkg/services/dashboardversion/dashverimpl/store_test.go
index 845856b8e0b..de11a60ca8a 100644
--- a/pkg/services/dashboardversion/dashverimpl/store_test.go
+++ b/pkg/services/dashboardversion/dashverimpl/store_test.go
@@ -16,6 +16,9 @@ import (
)
func TestIntegrationGetDashboardVersion(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := sqlstore.InitTestDB(t)
dashVerStore := sqlStore{db: ss}
@@ -59,6 +62,9 @@ func TestIntegrationGetDashboardVersion(t *testing.T) {
}
func TestIntegrationDeleteExpiredVersions(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
versionsToWrite := 10
ss := sqlstore.InitTestDB(t)
dashVerStore := sqlStore{db: ss}
@@ -80,6 +86,9 @@ func TestIntegrationDeleteExpiredVersions(t *testing.T) {
}
func TestIntegrationListDashboardVersions(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := sqlstore.InitTestDB(t)
dashVerStore := sqlStore{db: ss, dialect: ss.Dialect}
savedDash := insertTestDashboard(t, ss, "test dash 43", 1, 0, false, "diff-all")
diff --git a/pkg/services/dashboardversion/model.go b/pkg/services/dashboardversion/model.go
index 6de84dd4d53..4141430f529 100644
--- a/pkg/services/dashboardversion/model.go
+++ b/pkg/services/dashboardversion/model.go
@@ -13,7 +13,7 @@ var (
)
type DashboardVersion struct {
- ID int64 `json:"id"`
+ ID int64 `json:"id" xorm:"pk autoincr 'id'"`
DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
ParentVersion int `json:"parentVersion"`
RestoredFrom int `json:"restoredFrom"`
@@ -45,9 +45,9 @@ type ListDashboardVersionsQuery struct {
}
type DashboardVersionDTO struct {
- ID int64 `json:"id"`
- DashboardID int64 `json:"dashboardId"`
- DashboardUID string `json:"dashboardUid"`
+ ID int64 `json:"id" xorm:"id"`
+ DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
+ DashboardUID string `json:"dashboardUid" xorm:"dashboard_uid"`
ParentVersion int `json:"parentVersion"`
RestoredFrom int `json:"restoredFrom"`
Version int `json:"version"`
diff --git a/pkg/services/datasourceproxy/datasourceproxy.go b/pkg/services/datasourceproxy/datasourceproxy.go
index 9a8a003b2a7..6a1f196c62b 100644
--- a/pkg/services/datasourceproxy/datasourceproxy.go
+++ b/pkg/services/datasourceproxy/datasourceproxy.go
@@ -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)
}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 5a95129680b..620282f855d 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -2,6 +2,7 @@
// pkg/services/featuremgmt/registry.go
// Then run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
+// twice to generate and validate the feature flag files
package featuremgmt
@@ -116,6 +117,11 @@ var (
Description: "support websocket streaming for loki (early prototype)",
State: FeatureStateAlpha,
},
+ {
+ Name: "lokiDataframeApi",
+ Description: "use experimental loki api for websocket streaming (early prototype)",
+ State: FeatureStateAlpha,
+ },
{
Name: "swaggerUi",
Description: "Serves swagger UI",
@@ -198,11 +204,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",
@@ -221,6 +226,12 @@ var (
State: FeatureStateAlpha,
RequiresRestart: true,
},
+ {
+ Name: "autoMigrateGraphPanels",
+ Description: "Replace the angular graph panel with timeseries",
+ State: FeatureStateBeta,
+ FrontendOnly: true,
+ },
{
Name: "prometheusWideSeries",
Description: "Enable wide series responses in the Prometheus datasource",
@@ -238,5 +249,19 @@ var (
State: FeatureStateAlpha,
FrontendOnly: true,
},
+ {
+ Name: "logRequestsInstrumentedAsUnknown",
+ Description: "Logs the path for requests that are instrumented as unknown",
+ },
+ {
+ Name: "dataConnectionsConsole",
+ Description: "Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins.",
+ State: FeatureStateAlpha,
+ },
+ {
+ Name: "internationalization",
+ Description: "Enables work-in-progress internationalization",
+ State: FeatureStateAlpha,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index a911fcbacae..7e8fca720d2 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -87,6 +87,10 @@ const (
// support websocket streaming for loki (early prototype)
FlagLokiLive = "lokiLive"
+ // FlagLokiDataframeApi
+ // use experimental loki api for websocket streaming (early prototype)
+ FlagLokiDataframeApi = "lokiDataframeApi"
+
// FlagSwaggerUi
// Serves swagger UI
FlagSwaggerUi = "swaggerUi"
@@ -163,6 +167,10 @@ const (
// Validate dashboard JSON POSTed to api/dashboards/db
FlagValidateDashboardsOnSave = "validateDashboardsOnSave"
+ // FlagAutoMigrateGraphPanels
+ // Replace the angular graph panel with timeseries
+ FlagAutoMigrateGraphPanels = "autoMigrateGraphPanels"
+
// FlagPrometheusWideSeries
// Enable wide series responses in the Prometheus datasource
FlagPrometheusWideSeries = "prometheusWideSeries"
@@ -174,4 +182,16 @@ const (
// FlagCloudMonitoringExperimentalUI
// Use grafana-experimental UI in Cloud Monitoring
FlagCloudMonitoringExperimentalUI = "cloudMonitoringExperimentalUI"
+
+ // FlagLogRequestsInstrumentedAsUnknown
+ // Logs the path for requests that are instrumented as unknown
+ FlagLogRequestsInstrumentedAsUnknown = "logRequestsInstrumentedAsUnknown"
+
+ // FlagDataConnectionsConsole
+ // Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins.
+ FlagDataConnectionsConsole = "dataConnectionsConsole"
+
+ // FlagInternationalization
+ // Enables work-in-progress internationalization
+ FlagInternationalization = "internationalization"
)
diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go
index 9b4470224c6..ed0f785b2ee 100644
--- a/pkg/services/libraryelements/database.go
+++ b/pkg/services/libraryelements/database.go
@@ -125,7 +125,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
}
err := l.SQLStore.WithTransactionalDbSession(c, func(session *sqlstore.DBSession) error {
- if err := l.requirePermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
+ if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
return err
}
if _, err := session.Insert(&element); err != nil {
@@ -176,7 +176,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn
if err != nil {
return err
}
- if err := l.requirePermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
+ if err := l.requireEditPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
return err
}
var connectionIDs []struct {
@@ -422,13 +422,13 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme
// FolderID was provided in the PATCH request
if toFolderID != -1 && toFolderID != fromFolderID {
- if err := l.requirePermissionsOnFolder(ctx, user, toFolderID); err != nil {
+ if err := l.requireEditPermissionsOnFolder(ctx, user, toFolderID); err != nil {
return err
}
}
// Always check permissions for the folder where library element resides
- if err := l.requirePermissionsOnFolder(ctx, user, fromFolderID); err != nil {
+ if err := l.requireEditPermissionsOnFolder(ctx, user, fromFolderID); err != nil {
return err
}
@@ -638,6 +638,10 @@ func (l *LibraryElementService) getElementsForDashboardID(c context.Context, das
// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard.
func (l *LibraryElementService) connectElementsToDashboardID(c context.Context, signedInUser *models.SignedInUser, elementUIDs []string, dashboardID int64) error {
+ if err := l.requireEditPermissionsOnDashboard(c, signedInUser, dashboardID); err != nil {
+ return err
+ }
+
err := l.SQLStore.WithTransactionalDbSession(c, func(session *sqlstore.DBSession) error {
_, err := session.Exec("DELETE FROM "+models.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
if err != nil {
@@ -648,7 +652,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c context.Context,
if err != nil {
return err
}
- if err := l.requirePermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
+ if err := l.requireViewPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil {
return err
}
@@ -704,7 +708,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c context.Conte
folderID := folderUIDs[0].ID
- if err := l.requirePermissionsOnFolder(c, signedInUser, folderID); err != nil {
+ if err := l.requireEditPermissionsOnFolder(c, signedInUser, folderID); err != nil {
return err
}
var connectionIDs []struct {
diff --git a/pkg/services/libraryelements/guard.go b/pkg/services/libraryelements/guard.go
index 7265e3acc3d..3f7d776f6f7 100644
--- a/pkg/services/libraryelements/guard.go
+++ b/pkg/services/libraryelements/guard.go
@@ -23,7 +23,7 @@ func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) err
}
}
-func (l *LibraryElementService) requirePermissionsOnFolder(ctx context.Context, user *models.SignedInUser, folderID int64) error {
+func (l *LibraryElementService) requireEditPermissionsOnFolder(ctx context.Context, user *models.SignedInUser, folderID int64) error {
if isGeneralFolder(folderID) && user.HasRole(models.ROLE_EDITOR) {
return nil
}
@@ -48,3 +48,40 @@ func (l *LibraryElementService) requirePermissionsOnFolder(ctx context.Context,
return nil
}
+
+func (l *LibraryElementService) requireViewPermissionsOnFolder(ctx context.Context, user *models.SignedInUser, folderID int64) error {
+ if isGeneralFolder(folderID) && user.HasRole(models.ROLE_VIEWER) {
+ return nil
+ }
+
+ folder, err := l.folderService.GetFolderByID(ctx, user, folderID, user.OrgId)
+ if err != nil {
+ return err
+ }
+
+ g := guardian.New(ctx, folder.Id, user.OrgId, user)
+
+ canView, err := g.CanView()
+ if err != nil {
+ return err
+ }
+ if !canView {
+ return models.ErrFolderAccessDenied
+ }
+
+ return nil
+}
+
+func (l *LibraryElementService) requireEditPermissionsOnDashboard(ctx context.Context, user *models.SignedInUser, dashboardID int64) error {
+ g := guardian.New(ctx, dashboardID, user.OrgId, user)
+
+ canEdit, err := g.CanEdit()
+ if err != nil {
+ return err
+ }
+ if !canEdit {
+ return models.ErrDashboardUpdateAccessDenied
+ }
+
+ return nil
+}
diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go
index 362b98cd093..6375cf8a78f 100644
--- a/pkg/services/libraryelements/libraryelements_permissions_test.go
+++ b/pkg/services/libraryelements/libraryelements_permissions_test.go
@@ -48,6 +48,7 @@ func TestLibraryElementPermissions(t *testing.T) {
{models.ROLE_ADMIN, viewerOnlyPermissions, viewerOnlyDesc, 200},
{models.ROLE_ADMIN, everyonePermissions, everyoneDesc, 200},
{models.ROLE_ADMIN, noPermissions, noDesc, 200},
+
{models.ROLE_EDITOR, defaultPermissions, defaultDesc, 200},
{models.ROLE_EDITOR, adminOnlyPermissions, adminOnlyDesc, 403},
{models.ROLE_EDITOR, editorOnlyPermissions, editorOnlyDesc, 200},
@@ -55,6 +56,7 @@ func TestLibraryElementPermissions(t *testing.T) {
{models.ROLE_EDITOR, viewerOnlyPermissions, viewerOnlyDesc, 403},
{models.ROLE_EDITOR, everyonePermissions, everyoneDesc, 200},
{models.ROLE_EDITOR, noPermissions, noDesc, 403},
+
{models.ROLE_VIEWER, defaultPermissions, defaultDesc, 403},
{models.ROLE_VIEWER, adminOnlyPermissions, adminOnlyDesc, 403},
{models.ROLE_VIEWER, editorOnlyPermissions, editorOnlyDesc, 403},
diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go
index 95ec44e4b1d..65572164016 100644
--- a/pkg/services/librarypanels/librarypanels.go
+++ b/pkg/services/librarypanels/librarypanels.go
@@ -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,
diff --git a/pkg/services/live/database/tests/storage_test.go b/pkg/services/live/database/tests/storage_test.go
index 8f57af305b6..2a583a86ca9 100644
--- a/pkg/services/live/database/tests/storage_test.go
+++ b/pkg/services/live/database/tests/storage_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationLiveMessage(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
storage := SetupTestStorage(t)
getQuery := &models.GetLiveMessageQuery{
diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md
index 8399d105de4..1def4603cbb 100644
--- a/pkg/services/ngalert/CHANGELOG.md
+++ b/pkg/services/ngalert/CHANGELOG.md
@@ -45,7 +45,9 @@ Scopes must have an order to ensure consistency and ease of search, this helps u
## Grafana Alerting - main / unreleased
+- [FEATURE] use optimistic lock by version field when updating alert rules #50274
- [ENHANCEMENT] Scheduler: Drop ticks if rule evaluation is too slow and adds a metric grafana_alerting_schedule_rule_evaluations_missed_total to track missed evaluations per rule #48885
+- [ENHANCEMENT] Ticker to tick at predictable time #50197
## 9.0.0
diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go
index df29ebd480b..d6a103a11c7 100644
--- a/pkg/services/ngalert/api/api_provisioning.go
+++ b/pkg/services/ngalert/api/api_provisioning.go
@@ -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) {
@@ -256,6 +258,9 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definiti
return ErrResp(http.StatusBadRequest, err, "")
}
if err != nil {
+ if errors.Is(err, store.ErrOptimisticLock) {
+ return ErrResp(http.StatusConflict, err, "")
+ }
return ErrResp(http.StatusInternalServerError, err, "")
}
ar.ID = createdAlertRule.ID
@@ -273,6 +278,9 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar definitio
return ErrResp(http.StatusBadRequest, err, "")
}
if err != nil {
+ if errors.Is(err, store.ErrOptimisticLock) {
+ return ErrResp(http.StatusConflict, err, "")
+ }
return ErrResp(http.StatusInternalServerError, err, "")
}
ar.Updated = updatedAlertRule.Updated
@@ -293,6 +301,9 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag defi
folderUID := pathParam(c, folderUIDPathParam)
err := srv.alertRules.UpdateRuleGroup(c.Req.Context(), c.OrgId, folderUID, rulegroup, ag.Interval)
if err != nil {
+ if errors.Is(err, store.ErrOptimisticLock) {
+ return ErrResp(http.StatusConflict, err, "")
+ }
return ErrResp(http.StatusInternalServerError, err, "")
}
return response.JSON(http.StatusOK, ag)
diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go
index e9f409f1069..721638c98d4 100644
--- a/pkg/services/ngalert/api/api_ruler.go
+++ b/pkg/services/ngalert/api/api_ruler.go
@@ -467,6 +467,8 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
return ErrResp(http.StatusForbidden, err, "")
} else if errors.Is(err, ErrAuthorization) {
return ErrResp(http.StatusUnauthorized, err, "")
+ } else if errors.Is(err, store.ErrOptimisticLock) {
+ return ErrResp(http.StatusConflict, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
}
diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go
index 7e6de39c079..39a09f22321 100644
--- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go
+++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go
@@ -54,21 +54,43 @@ type AlertRulePayload struct {
}
type AlertRule struct {
- ID int64 `json:"id"`
- UID string `json:"uid"`
- OrgID int64 `json:"orgID"`
- FolderUID string `json:"folderUID"`
- RuleGroup string `json:"ruleGroup"`
- Title string `json:"title"`
- Condition string `json:"condition"`
- Data []models.AlertQuery `json:"data"`
- Updated time.Time `json:"updated,omitempty"`
- NoDataState models.NoDataState `json:"noDataState"`
+ ID int64 `json:"id"`
+ UID string `json:"uid"`
+ // required: true
+ OrgID int64 `json:"orgID"`
+ // required: true
+ // example: project_x
+ FolderUID string `json:"folderUID"`
+ // required: true
+ // minLength: 1
+ // maxLength: 190
+ // example: eval_group_1
+ RuleGroup string `json:"ruleGroup"`
+ // required: true
+ // minLength: 1
+ // maxLength: 190
+ // example: Always firing
+ Title string `json:"title"`
+ // required: true
+ // example: A
+ Condition string `json:"condition"`
+ // required: true
+ // example: [{"refId":"A","queryType":"","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}]
+ Data []models.AlertQuery `json:"data"`
+ // readonly: true
+ Updated time.Time `json:"updated,omitempty"`
+ // required: true
+ NoDataState models.NoDataState `json:"noDataState"`
+ // required: true
ExecErrState models.ExecutionErrorState `json:"execErrState"`
- For time.Duration `json:"for"`
- Annotations map[string]string `json:"annotations,omitempty"`
- Labels map[string]string `json:"labels,omitempty"`
- Provenance models.Provenance `json:"provenance,omitempty"`
+ // required: true
+ For time.Duration `json:"for"`
+ // example: {"runbook_url": "https://supercoolrunbook.com/page/13"}
+ Annotations map[string]string `json:"annotations,omitempty"`
+ // example: {"team": "sre-team-1"}
+ Labels map[string]string `json:"labels,omitempty"`
+ // readonly: true
+ Provenance models.Provenance `json:"provenance,omitempty"`
}
func (a *AlertRule) UpstreamModel() models.AlertRule {
diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go
index dbb1f502eb0..9d35fca6094 100644
--- a/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go
+++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go
@@ -50,7 +50,7 @@ import (
// swagger:parameters RoutePutContactpoint RouteDeleteContactpoints
type ContactPointUIDReference struct {
- // ContactPointUID should be the contact point UID identifier
+ // UID should be the contact point unique identifier
// in:path
UID string
}
@@ -64,16 +64,24 @@ type ContactPointPayload struct {
// EmbeddedContactPoint is the contact point type that is used
// by grafanas embedded alertmanager implementation.
type EmbeddedContactPoint struct {
- // UID is the unique identifier of the contact point. This will be
- // automatically set be the Grafana.
+ // UID is the unique identifier of the contact point. The UID can be
+ // set by the user.
+ // example: my_external_reference
UID string `json:"uid"`
// Name is used as grouping key in the UI. Contact points with the
// same name will be grouped in the UI.
- Name string `json:"name" binding:"required"`
- Type string `json:"type" binding:"required"`
- Settings *simplejson.Json `json:"settings" binding:"required"`
- DisableResolveMessage bool `json:"disableResolveMessage"`
- Provenance string `json:"provenance"`
+ // example: webhook_1
+ Name string `json:"name" binding:"required"`
+ // required: true
+ // example: webhook
+ // enum: alertmanager, dingding, discord, email, googlechat, kafka, line, opsgenie, pagerduty, pushover, sensugo, slack, teams, telegram, threema, victorops, webhook, wecom
+ Type string `json:"type" binding:"required"`
+ // required: true
+ Settings *simplejson.Json `json:"settings" binding:"required"`
+ // example: false
+ DisableResolveMessage bool `json:"disableResolveMessage"`
+ // readonly: true
+ Provenance string `json:"provenance,omitempty"`
}
const RedactedValue = "[REDACTED]"
diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json
index 7fecceb91ce..779cf127d35 100644
--- a/pkg/services/ngalert/api/tooling/post.json
+++ b/pkg/services/ngalert/api/tooling/post.json
@@ -200,14 +200,63 @@
"additionalProperties": {
"type": "string"
},
+ "example": {
+ "runbook_url": "https://supercoolrunbook.com/page/13"
+ },
"type": "object",
"x-go-name": "Annotations"
},
"condition": {
+ "example": "A",
"type": "string",
"x-go-name": "Condition"
},
"data": {
+ "example": [
+ {
+ "datasourceUid": "-100",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ 0
+ ],
+ "type": "gt"
+ },
+ "operator": {
+ "type": "and"
+ },
+ "query": {
+ "params": []
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__"
+ },
+ "expression": "1 == 1",
+ "hide": false,
+ "intervalMs": 1000,
+ "maxDataPoints": 43200,
+ "refId": "A",
+ "type": "math"
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 0,
+ "to": 0
+ }
+ }
+ ],
"items": {
"$ref": "#/definitions/AlertQuery"
},
@@ -215,9 +264,18 @@
"x-go-name": "Data"
},
"execErrState": {
- "$ref": "#/definitions/ExecutionErrorState"
+ "description": "\nAlerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
+ "enum": [
+ "Alerting",
+ "Error",
+ "OK"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
+ "x-go-name": "ExecErrState"
},
"folderUID": {
+ "example": "project_x",
"type": "string",
"x-go-name": "FolderUID"
},
@@ -233,11 +291,22 @@
"additionalProperties": {
"type": "string"
},
+ "example": {
+ "team": "sre-team-1"
+ },
"type": "object",
"x-go-name": "Labels"
},
"noDataState": {
- "$ref": "#/definitions/NoDataState"
+ "description": "\nAlerting Alerting\nNoData NoData\nOK OK",
+ "enum": [
+ "Alerting",
+ "NoData",
+ "OK"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "Alerting Alerting\nNoData NoData\nOK OK",
+ "x-go-name": "NoDataState"
},
"orgID": {
"format": "int64",
@@ -248,10 +317,16 @@
"$ref": "#/definitions/Provenance"
},
"ruleGroup": {
+ "example": "eval_group_1",
+ "maxLength": 190,
+ "minLength": 1,
"type": "string",
"x-go-name": "RuleGroup"
},
"title": {
+ "example": "Always firing",
+ "maxLength": 190,
+ "minLength": 1,
"type": "string",
"x-go-name": "Title"
},
@@ -261,10 +336,22 @@
},
"updated": {
"format": "date-time",
+ "readOnly": true,
"type": "string",
"x-go-name": "Updated"
}
},
+ "required": [
+ "orgID",
+ "folderUID",
+ "ruleGroup",
+ "title",
+ "condition",
+ "data",
+ "noDataState",
+ "execErrState",
+ "for"
+ ],
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
@@ -673,15 +760,18 @@
"description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.",
"properties": {
"disableResolveMessage": {
+ "example": false,
"type": "boolean",
"x-go-name": "DisableResolveMessage"
},
"name": {
"description": "Name is used as grouping key in the UI. Contact points with the\nsame name will be grouped in the UI.",
+ "example": "webhook_1",
"type": "string",
"x-go-name": "Name"
},
"provenance": {
+ "readOnly": true,
"type": "string",
"x-go-name": "Provenance"
},
@@ -689,15 +779,41 @@
"$ref": "#/definitions/Json"
},
"type": {
+ "enum": [
+ "alertmanager",
+ " dingding",
+ " discord",
+ " email",
+ " googlechat",
+ " kafka",
+ " line",
+ " opsgenie",
+ " pagerduty",
+ " pushover",
+ " sensugo",
+ " slack",
+ " teams",
+ " telegram",
+ " threema",
+ " victorops",
+ " webhook",
+ " wecom"
+ ],
+ "example": "webhook",
"type": "string",
"x-go-name": "Type"
},
"uid": {
- "description": "UID is the unique identifier of the contact point. This will be\nautomatically set be the Grafana.",
+ "description": "UID is the unique identifier of the contact point. The UID can be\nset by the user.",
+ "example": "my_external_reference",
"type": "string",
"x-go-name": "UID"
}
},
+ "required": [
+ "type",
+ "settings"
+ ],
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
@@ -748,10 +864,6 @@
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"EvalQueriesResponse": {},
- "ExecutionErrorState": {
- "type": "string",
- "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
- },
"ExtendedReceiver": {
"properties": {
"email_configs": {
@@ -1552,10 +1664,6 @@
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
- "NoDataState": {
- "type": "string",
- "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
- },
"NotFound": {
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -3255,7 +3363,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
- "description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@@ -3277,7 +3384,9 @@
"labels",
"receiver"
],
- "type": "object"
+ "type": "object",
+ "x-go-name": "AlertGroup",
+ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroups": {
"description": "AlertGroups alert groups",
@@ -3530,12 +3639,11 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"gettableSilences": {
+ "description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence"
},
- "type": "array",
- "x-go-name": "GettableSilences",
- "x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
+ "type": "array"
},
"labelSet": {
"additionalProperties": {
@@ -3664,6 +3772,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"postableSilence": {
+ "description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@@ -3703,9 +3812,7 @@
"matchers",
"startsAt"
],
- "type": "object",
- "x-go-name": "PostableSilence",
- "x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
+ "type": "object"
},
"receiver": {
"properties": {
@@ -5676,7 +5783,7 @@
"operationId": "RouteDeleteContactpoints",
"parameters": [
{
- "description": "ContactPointUID should be the contact point UID identifier",
+ "description": "UID should be the contact point unique identifier",
"in": "path",
"name": "UID",
"required": true,
@@ -5709,7 +5816,7 @@
"operationId": "RoutePutContactpoint",
"parameters": [
{
- "description": "ContactPointUID should be the contact point UID identifier",
+ "description": "UID should be the contact point unique identifier",
"in": "path",
"name": "UID",
"required": true,
diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json
index ee3e57b7a13..cdbeaa4b219 100644
--- a/pkg/services/ngalert/api/tooling/spec.json
+++ b/pkg/services/ngalert/api/tooling/spec.json
@@ -1871,7 +1871,7 @@
"parameters": [
{
"type": "string",
- "description": "ContactPointUID should be the contact point UID identifier",
+ "description": "UID should be the contact point unique identifier",
"name": "UID",
"in": "path",
"required": true
@@ -1912,7 +1912,7 @@
"parameters": [
{
"type": "string",
- "description": "ContactPointUID should be the contact point UID identifier",
+ "description": "UID should be the contact point unique identifier",
"name": "UID",
"in": "path",
"required": true
@@ -2591,31 +2591,100 @@
},
"AlertRule": {
"type": "object",
+ "required": [
+ "orgID",
+ "folderUID",
+ "ruleGroup",
+ "title",
+ "condition",
+ "data",
+ "noDataState",
+ "execErrState",
+ "for"
+ ],
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
},
- "x-go-name": "Annotations"
+ "x-go-name": "Annotations",
+ "example": {
+ "runbook_url": "https://supercoolrunbook.com/page/13"
+ }
},
"condition": {
"type": "string",
- "x-go-name": "Condition"
+ "x-go-name": "Condition",
+ "example": "A"
},
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertQuery"
},
- "x-go-name": "Data"
+ "x-go-name": "Data",
+ "example": [
+ {
+ "datasourceUid": "-100",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ 0
+ ],
+ "type": "gt"
+ },
+ "operator": {
+ "type": "and"
+ },
+ "query": {
+ "params": []
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__"
+ },
+ "expression": "1 == 1",
+ "hide": false,
+ "intervalMs": 1000,
+ "maxDataPoints": 43200,
+ "refId": "A",
+ "type": "math"
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 0,
+ "to": 0
+ }
+ }
+ ]
},
"execErrState": {
- "$ref": "#/definitions/ExecutionErrorState"
+ "description": "\nAlerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
+ "type": "string",
+ "enum": [
+ "Alerting",
+ "Error",
+ "OK"
+ ],
+ "x-go-enum-desc": "Alerting AlertingErrState\nError ErrorErrState\nOK OkErrState",
+ "x-go-name": "ExecErrState"
},
"folderUID": {
"type": "string",
- "x-go-name": "FolderUID"
+ "x-go-name": "FolderUID",
+ "example": "project_x"
},
"for": {
"$ref": "#/definitions/Duration"
@@ -2630,10 +2699,21 @@
"additionalProperties": {
"type": "string"
},
- "x-go-name": "Labels"
+ "x-go-name": "Labels",
+ "example": {
+ "team": "sre-team-1"
+ }
},
"noDataState": {
- "$ref": "#/definitions/NoDataState"
+ "description": "\nAlerting Alerting\nNoData NoData\nOK OK",
+ "type": "string",
+ "enum": [
+ "Alerting",
+ "NoData",
+ "OK"
+ ],
+ "x-go-enum-desc": "Alerting Alerting\nNoData NoData\nOK OK",
+ "x-go-name": "NoDataState"
},
"orgID": {
"type": "integer",
@@ -2645,11 +2725,17 @@
},
"ruleGroup": {
"type": "string",
- "x-go-name": "RuleGroup"
+ "maxLength": 190,
+ "minLength": 1,
+ "x-go-name": "RuleGroup",
+ "example": "eval_group_1"
},
"title": {
"type": "string",
- "x-go-name": "Title"
+ "maxLength": 190,
+ "minLength": 1,
+ "x-go-name": "Title",
+ "example": "Always firing"
},
"uid": {
"type": "string",
@@ -2658,7 +2744,8 @@
"updated": {
"type": "string",
"format": "date-time",
- "x-go-name": "Updated"
+ "x-go-name": "Updated",
+ "readOnly": true
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -3068,31 +3155,60 @@
"EmbeddedContactPoint": {
"description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.",
"type": "object",
+ "required": [
+ "type",
+ "settings"
+ ],
"properties": {
"disableResolveMessage": {
"type": "boolean",
- "x-go-name": "DisableResolveMessage"
+ "x-go-name": "DisableResolveMessage",
+ "example": false
},
"name": {
"description": "Name is used as grouping key in the UI. Contact points with the\nsame name will be grouped in the UI.",
"type": "string",
- "x-go-name": "Name"
+ "x-go-name": "Name",
+ "example": "webhook_1"
},
"provenance": {
"type": "string",
- "x-go-name": "Provenance"
+ "x-go-name": "Provenance",
+ "readOnly": true
},
"settings": {
"$ref": "#/definitions/Json"
},
"type": {
"type": "string",
- "x-go-name": "Type"
+ "enum": [
+ "alertmanager",
+ " dingding",
+ " discord",
+ " email",
+ " googlechat",
+ " kafka",
+ " line",
+ " opsgenie",
+ " pagerduty",
+ " pushover",
+ " sensugo",
+ " slack",
+ " teams",
+ " telegram",
+ " threema",
+ " victorops",
+ " webhook",
+ " wecom"
+ ],
+ "x-go-name": "Type",
+ "example": "webhook"
},
"uid": {
- "description": "UID is the unique identifier of the contact point. This will be\nautomatically set be the Grafana.",
+ "description": "UID is the unique identifier of the contact point. The UID can be\nset by the user.",
"type": "string",
- "x-go-name": "UID"
+ "x-go-name": "UID",
+ "example": "my_external_reference"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -3146,10 +3262,6 @@
"EvalQueriesResponse": {
"$ref": "#/definitions/EvalQueriesResponse"
},
- "ExecutionErrorState": {
- "type": "string",
- "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
- },
"ExtendedReceiver": {
"type": "object",
"properties": {
@@ -3951,10 +4063,6 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
- "NoDataState": {
- "type": "string",
- "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models"
- },
"NotFound": {
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -5654,7 +5762,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
- "description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@@ -5677,6 +5784,8 @@
"$ref": "#/definitions/receiver"
}
},
+ "x-go-name": "AlertGroup",
+ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
@@ -5934,12 +6043,11 @@
"$ref": "#/definitions/gettableSilence"
},
"gettableSilences": {
+ "description": "GettableSilences gettable silences",
"type": "array",
"items": {
"$ref": "#/definitions/gettableSilence"
},
- "x-go-name": "GettableSilences",
- "x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableSilences"
},
"labelSet": {
@@ -6069,6 +6177,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"postableSilence": {
+ "description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",
@@ -6109,8 +6218,6 @@
"x-go-name": "StartsAt"
}
},
- "x-go-name": "PostableSilence",
- "x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/postableSilence"
},
"receiver": {
diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go
index 15cffa0251a..4d6b0932138 100644
--- a/pkg/services/ngalert/models/alert_rule.go
+++ b/pkg/services/ngalert/models/alert_rule.go
@@ -24,6 +24,7 @@ var (
ErrAlertRuleUniqueConstraintViolation = errors.New("a conflicting alert rule is found: rule title under the same organisation and folder should be unique")
)
+// swagger:enum NoDataState
type NoDataState string
func (noDataState NoDataState) String() string {
@@ -49,6 +50,7 @@ const (
OK NoDataState = "OK"
)
+// swagger:enum ExecutionErrorState
type ExecutionErrorState string
func (executionErrorState ExecutionErrorState) String() string {
@@ -109,7 +111,7 @@ type AlertRule struct {
Data []AlertQuery
Updated time.Time
IntervalSeconds int64
- Version int64
+ Version int64 `xorm:"version"` // this tag makes xorm add optimistic lock (see https://xorm.io/docs/chapter-06/1.lock/)
UID string `xorm:"uid"`
NamespaceUID string `xorm:"namespace_uid"`
DashboardUID *string `xorm:"dashboard_uid"`
diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go
index 7de30f4824c..04e4e9a8987 100644
--- a/pkg/services/ngalert/provisioning/alert_rules.go
+++ b/pkg/services/ngalert/provisioning/alert_rules.go
@@ -53,6 +53,9 @@ func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64,
return *query.Result, provenance, nil
}
+// CreateAlertRule creates a new alert rule. This function will ignore any
+// interval that is set in the rule struct and use the already existing group
+// interval or the default one.
func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) {
if rule.UID == "" {
rule.UID = util.GenerateShortUID()
@@ -117,6 +120,9 @@ func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int6
})
}
+// CreateAlertRule creates a new alert rule. This function will ignore any
+// interval that is set in the rule struct and fetch the current group interval
+// from database.
func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) {
storedRule, storedProvenance, err := service.GetAlertRule(ctx, rule.OrgID, rule.UID)
if err != nil {
diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go
index c50204fa504..da940e39989 100644
--- a/pkg/services/ngalert/provisioning/persist.go
+++ b/pkg/services/ngalert/provisioning/persist.go
@@ -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
}
diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go
index b7181c3e1cd..18fbc9a59b5 100644
--- a/pkg/services/ngalert/store/alert_rule.go
+++ b/pkg/services/ngalert/store/alert_rule.go
@@ -34,6 +34,7 @@ type UpdateRule struct {
var (
ErrAlertRuleGroupNotFound = errors.New("rulegroup not found")
+ ErrOptimisticLock = errors.New("version conflict while updating a record in the database with optimistic locking")
)
// RuleStore is the interface for persisting alert rules and instances
@@ -47,8 +48,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
@@ -95,7 +94,7 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI
})
}
-// DeleteAlertInstanceByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
+// DeleteAlertInstancesByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
func (st DBstore) DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Exec("DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid = ?", orgID, ruleUID)
@@ -207,7 +206,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
for _, r := range rules {
var parentVersion int64
r.New.ID = r.Existing.ID
- r.New.Version = r.Existing.Version + 1
+ r.New.Version = r.Existing.Version // xorm will take care of increasing it (see https://xorm.io/docs/chapter-06/1.lock/)
if err := st.validateAlertRule(r.New); err != nil {
return err
}
@@ -215,11 +214,14 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
return err
}
// no way to update multiple rules at once
- if _, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil {
- if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
- return ngmodels.ErrAlertRuleUniqueConstraintViolation
+ if updated, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil || updated == 0 {
+ if err != nil {
+ if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
+ return ngmodels.ErrAlertRuleUniqueConstraintViolation
+ }
+ return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err)
}
- return fmt.Errorf("failed to update rule [%s] %s: %w", r.New.UID, r.New.Title, err)
+ return fmt.Errorf("%w: alert rule UID %s version %d", ErrOptimisticLock, r.New.UID, r.New.Version)
}
parentVersion = r.Existing.Version
ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{
@@ -228,7 +230,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
RuleNamespaceUID: r.New.NamespaceUID,
RuleGroup: r.New.RuleGroup,
ParentVersion: parentVersion,
- Version: r.New.Version,
+ Version: r.New.Version + 1,
Created: r.New.Updated,
Condition: r.New.Condition,
Title: r.New.Title,
@@ -250,7 +252,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro
})
}
-// GetOrgAlertRules is a handler for retrieving alert rules of specific organisation.
+// ListAlertRules is a handler for retrieving alert rules of specific organisation.
func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
q := sess.Table("alert_rule")
@@ -319,17 +321,7 @@ 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
+// GetUserVisibleNamespaces 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)
diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go
new file mode 100644
index 00000000000..ff6f039991e
--- /dev/null
+++ b/pkg/services/ngalert/store/alert_rule_test.go
@@ -0,0 +1,91 @@
+package store
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "golang.org/x/exp/rand"
+
+ "github.com/grafana/grafana/pkg/services/ngalert/models"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/util"
+)
+
+func TestUpdateAlertRules(t *testing.T) {
+ sqlStore := sqlstore.InitTestDB(t)
+ store := DBstore{
+ SQLStore: sqlStore,
+ BaseInterval: time.Duration(rand.Int63n(100)) * time.Second,
+ }
+ createRule := func(t *testing.T) *models.AlertRule {
+ t.Helper()
+ rule := models.AlertRuleGen(withIntervalMatching(store.BaseInterval))()
+ err := sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ _, err := sess.Table(models.AlertRule{}).InsertOne(rule)
+ if err != nil {
+ return err
+ }
+ dbRule := &models.AlertRule{}
+ exist, err := sess.Table(models.AlertRule{}).ID(rule.ID).Get(dbRule)
+ if err != nil {
+ return err
+ }
+ if !exist {
+ return errors.New("cannot read inserted record")
+ }
+ rule = dbRule
+ return nil
+ })
+ require.NoError(t, err)
+ return rule
+ }
+
+ t.Run("should increase version", func(t *testing.T) {
+ rule := createRule(t)
+ newRule := models.CopyRule(rule)
+ newRule.Title = util.GenerateShortUID()
+ err := store.UpdateAlertRules(context.Background(), []UpdateRule{{
+ Existing: rule,
+ New: *newRule,
+ },
+ })
+ require.NoError(t, err)
+
+ dbrule := &models.AlertRule{}
+ err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ exist, err := sess.Table(models.AlertRule{}).ID(rule.ID).Get(dbrule)
+ require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", rule.ID))
+ return err
+ })
+
+ require.NoError(t, err)
+ require.Equal(t, rule.Version+1, dbrule.Version)
+ })
+
+ t.Run("should fail due to optimistic locking if version does not match", func(t *testing.T) {
+ rule := createRule(t)
+ rule.Version-- // simulate version discrepancy
+
+ newRule := models.CopyRule(rule)
+ newRule.Title = util.GenerateShortUID()
+
+ err := store.UpdateAlertRules(context.Background(), []UpdateRule{{
+ Existing: rule,
+ New: *newRule,
+ },
+ })
+
+ require.ErrorIs(t, err, ErrOptimisticLock)
+ })
+}
+
+func withIntervalMatching(baseInterval time.Duration) func(*models.AlertRule) {
+ return func(rule *models.AlertRule) {
+ rule.IntervalSeconds = int64(baseInterval.Seconds()) * rand.Int63n(10)
+ rule.For = time.Duration(rule.IntervalSeconds*rand.Int63n(9)+1) * time.Second
+ }
+}
diff --git a/pkg/services/ngalert/store/alertmanager_test.go b/pkg/services/ngalert/store/alertmanager_test.go
index 575ff4415e7..b28d238bdfb 100644
--- a/pkg/services/ngalert/store/alertmanager_test.go
+++ b/pkg/services/ngalert/store/alertmanager_test.go
@@ -12,6 +12,9 @@ import (
)
func TestIntegrationAlertManagerHash(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := sqlstore.InitTestDB(t)
store := &DBstore{
SQLStore: sqlStore,
diff --git a/pkg/services/ngalert/store/image_test.go b/pkg/services/ngalert/store/image_test.go
index d7f84fb91c6..d026888cf10 100644
--- a/pkg/services/ngalert/store/image_test.go
+++ b/pkg/services/ngalert/store/image_test.go
@@ -38,6 +38,9 @@ func addToken(img *models.Image) *models.Image {
}
func TestIntegrationSaveAndGetImage(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
mockTimeNow()
ctx := context.Background()
_, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds)
@@ -91,6 +94,9 @@ func TestIntegrationSaveAndGetImage(t *testing.T) {
}
func TestIntegrationGetImages(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
mockTimeNow()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -141,6 +147,9 @@ func TestIntegrationGetImages(t *testing.T) {
}
func TestIntegrationDeleteExpiredImages(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
mockTimeNow()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
diff --git a/pkg/services/ngalert/store/instance_database_test.go b/pkg/services/ngalert/store/instance_database_test.go
index c305cce7e02..a194aac0351 100644
--- a/pkg/services/ngalert/store/instance_database_test.go
+++ b/pkg/services/ngalert/store/instance_database_test.go
@@ -25,6 +25,9 @@ func mockTimeNow() {
}
func TestIntegrationAlertInstanceOperations(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ctx := context.Background()
_, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds)
diff --git a/pkg/services/preference/prefimpl/store_test.go b/pkg/services/preference/prefimpl/store_test.go
index 361663860d7..374454be7b0 100644
--- a/pkg/services/preference/prefimpl/store_test.go
+++ b/pkg/services/preference/prefimpl/store_test.go
@@ -13,6 +13,9 @@ import (
)
func TestIntegrationPreferencesDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := sqlstore.InitTestDB(t)
prefStore := sqlStore{db: ss}
orgNavbarPreferences := pref.NavbarPreference{
diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go
index 082696f6691..206d06ed39e 100644
--- a/pkg/services/query/query.go
+++ b/pkg/services/query/query.go
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
+ "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@@ -22,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/util/proxyutil"
"github.com/grafana/grafana-plugin-sdk-go/backend"
+ "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
)
const (
@@ -136,6 +138,13 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
Queries: []backend.DataQuery{},
}
+ middlewares := []httpclient.Middleware{}
+ if parsedReq.httpRequest != nil {
+ middlewares = append(middlewares,
+ httpclientprovider.ForwardedCookiesMiddleware(parsedReq.httpRequest.Cookies(), ds.AllowedCookies()),
+ )
+ }
+
if s.oAuthTokenService.IsOAuthPassThruEnabled(ds) {
if token := s.oAuthTokenService.GetCurrentOAuthToken(ctx, user); token != nil {
req.Headers["Authorization"] = fmt.Sprintf("%s %s", token.Type(), token.AccessToken)
@@ -144,6 +153,7 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
if ok && idToken != "" {
req.Headers["X-ID-Token"] = idToken
}
+ middlewares = append(middlewares, httpclientprovider.ForwardedOAuthIdentityMiddleware(token))
}
}
@@ -162,6 +172,8 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
req.Queries = append(req.Queries, q.query)
}
+ ctx = httpclient.WithContextualMiddleware(ctx, middlewares...)
+
return s.pluginClient.QueryData(ctx, req)
}
diff --git a/pkg/services/queryhistory/api.go b/pkg/services/queryhistory/api.go
index eac4ca1667e..58881377ac1 100644
--- a/pkg/services/queryhistory/api.go
+++ b/pkg/services/queryhistory/api.go
@@ -25,6 +25,7 @@ func (s *QueryHistoryService) registerAPIEndpoints() {
})
}
+// createHandler handles POST /api/query-history
func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Response {
cmd := CreateQueryInQueryHistoryCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
@@ -39,6 +40,7 @@ func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Respo
return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query})
}
+// searchHandler handles GET /api/query-history
func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Response {
timeRange := legacydata.NewDataTimeRange(c.Query("from"), c.Query("to"))
@@ -61,6 +63,7 @@ func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Respo
return response.JSON(http.StatusOK, QueryHistorySearchResponse{Result: result})
}
+// deleteHandler handles DELETE /api/query-history/:uid
func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Response {
queryUID := web.Params(c.Req)[":uid"]
if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) {
@@ -72,12 +75,13 @@ func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Respo
return response.Error(http.StatusInternalServerError, "Failed to delete query from query history", err)
}
- return response.JSON(http.StatusOK, DeleteQueryFromQueryHistoryResponse{
+ return response.JSON(http.StatusOK, QueryHistoryDeleteQueryResponse{
Message: "Query deleted",
ID: id,
})
}
+// patchCommentHandler handles PATCH /api/query-history/:uid
func (s *QueryHistoryService) patchCommentHandler(c *models.ReqContext) response.Response {
queryUID := web.Params(c.Req)[":uid"]
if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) {
@@ -97,6 +101,7 @@ func (s *QueryHistoryService) patchCommentHandler(c *models.ReqContext) response
return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query})
}
+// starHandler handles POST /api/query-history/star/:uid
func (s *QueryHistoryService) starHandler(c *models.ReqContext) response.Response {
queryUID := web.Params(c.Req)[":uid"]
if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) {
@@ -111,6 +116,7 @@ func (s *QueryHistoryService) starHandler(c *models.ReqContext) response.Respons
return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query})
}
+// starHandler handles DELETE /api/query-history/star/:uid
func (s *QueryHistoryService) unstarHandler(c *models.ReqContext) response.Response {
queryUID := web.Params(c.Req)[":uid"]
if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) {
@@ -125,6 +131,7 @@ func (s *QueryHistoryService) unstarHandler(c *models.ReqContext) response.Respo
return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query})
}
+// starHandler handles POST /api/query-history/migrate
func (s *QueryHistoryService) migrateHandler(c *models.ReqContext) response.Response {
cmd := MigrateQueriesToQueryHistoryCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
diff --git a/pkg/services/queryhistory/database.go b/pkg/services/queryhistory/database.go
index 1831615eef9..1db7c202cc6 100644
--- a/pkg/services/queryhistory/database.go
+++ b/pkg/services/queryhistory/database.go
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)
+// createQuery adds a query into query history
func (s QueryHistoryService) createQuery(ctx context.Context, user *models.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error) {
queryHistory := QueryHistory{
OrgID: user.OrgId,
@@ -43,6 +44,7 @@ func (s QueryHistoryService) createQuery(ctx context.Context, user *models.Signe
return dto, nil
}
+// searchQueries searches for queries in query history based on provided parameters
func (s QueryHistoryService) searchQueries(ctx context.Context, user *models.SignedInUser, query SearchInQueryHistoryQuery) (QueryHistorySearchResult, error) {
var dtos []QueryHistoryDTO
var allQueries []interface{}
@@ -132,6 +134,7 @@ func (s QueryHistoryService) deleteQuery(ctx context.Context, user *models.Signe
return queryID, err
}
+// patchQueryComment searches updates comment for query in query history
func (s QueryHistoryService) patchQueryComment(ctx context.Context, user *models.SignedInUser, UID string, cmd PatchQueryCommentInQueryHistoryCommand) (QueryHistoryDTO, error) {
var queryHistory QueryHistory
var isStarred bool
@@ -176,6 +179,7 @@ func (s QueryHistoryService) patchQueryComment(ctx context.Context, user *models
return dto, nil
}
+// starQuery adds query into query_history_star table together with user_id and org_id
func (s QueryHistoryService) starQuery(ctx context.Context, user *models.SignedInUser, UID string) (QueryHistoryDTO, error) {
var queryHistory QueryHistory
var isStarred bool
@@ -225,6 +229,7 @@ func (s QueryHistoryService) starQuery(ctx context.Context, user *models.SignedI
return dto, nil
}
+// unstarQuery deletes query with with user_id and org_id from query_history_star table
func (s QueryHistoryService) unstarQuery(ctx context.Context, user *models.SignedInUser, UID string) (QueryHistoryDTO, error) {
var queryHistory QueryHistory
var isStarred bool
@@ -267,6 +272,7 @@ func (s QueryHistoryService) unstarQuery(ctx context.Context, user *models.Signe
return dto, nil
}
+// migrateQueries adds multiple queries into query history
func (s QueryHistoryService) migrateQueries(ctx context.Context, user *models.SignedInUser, cmd MigrateQueriesToQueryHistoryCommand) (int, int, error) {
queryHistories := make([]*QueryHistory, 0, len(cmd.Queries))
starredQueries := make([]*QueryHistoryStar, 0)
@@ -363,6 +369,7 @@ func (s QueryHistoryService) deleteStaleQueries(ctx context.Context, olderThan i
return int(rowsCount), nil
}
+// enforceQueryHistoryRowLimit is run in scheduled cleanup and it removes queries and stars that exceeded limit
func (s QueryHistoryService) enforceQueryHistoryRowLimit(ctx context.Context, limit int, starredQueries bool) (int, error) {
var deletedRowsCount int64
diff --git a/pkg/services/queryhistory/models.go b/pkg/services/queryhistory/models.go
index e930604725e..98d646365a6 100644
--- a/pkg/services/queryhistory/models.go
+++ b/pkg/services/queryhistory/models.go
@@ -12,6 +12,7 @@ var (
ErrQueryAlreadyStarred = errors.New("query was already starred")
)
+// QueryHistory is the model for query history definitions
type QueryHistory struct {
ID int64 `xorm:"pk autoincr 'id'"`
UID string `xorm:"uid"`
@@ -23,17 +24,13 @@ type QueryHistory struct {
Queries *simplejson.Json
}
+// QueryHistory is the model for query history star definitions
type QueryHistoryStar struct {
ID int64 `xorm:"pk autoincr 'id'"`
QueryUID string `xorm:"query_uid"`
UserID int64 `xorm:"user_id"`
}
-type CreateQueryInQueryHistoryCommand struct {
- DatasourceUID string `json:"datasourceUid"`
- Queries *simplejson.Json `json:"queries"`
-}
-
type SearchInQueryHistoryQuery struct {
DatasourceUIDs []string `json:"datasourceUids"`
SearchString string `json:"searchString"`
@@ -45,10 +42,6 @@ type SearchInQueryHistoryQuery struct {
To int64 `json:"to"`
}
-type PatchQueryCommentInQueryHistoryCommand struct {
- Comment string `json:"comment"`
-}
-
type QueryHistoryDTO struct {
UID string `json:"uid" xorm:"uid"`
DatasourceUID string `json:"datasourceUid" xorm:"datasource_uid"`
@@ -75,16 +68,12 @@ type QueryHistorySearchResponse struct {
Result QueryHistorySearchResult `json:"result"`
}
-// DeleteQueryFromQueryHistoryResponse is the response struct for deleting a query from query history
-type DeleteQueryFromQueryHistoryResponse struct {
+// QueryHistoryDeleteQueryResponse is the response struct for deleting a query from query history
+type QueryHistoryDeleteQueryResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
-type MigrateQueriesToQueryHistoryCommand struct {
- Queries []QueryToMigrate `json:"queries"`
-}
-
type QueryToMigrate struct {
DatasourceUID string `json:"datasourceUid"`
Queries *simplejson.Json `json:"queries"`
@@ -98,3 +87,28 @@ type QueryHistoryMigrationResponse struct {
TotalCount int `json:"totalCount"`
StarredCount int `json:"starredCount"`
}
+
+// CreateQueryInQueryHistoryCommand is the command for adding query history
+// swagger:model
+type CreateQueryInQueryHistoryCommand struct {
+ // UID of the data source for which are queries stored.
+ // example: PE1C5CBDA0504A6A3
+ DatasourceUID string `json:"datasourceUid"`
+ // The JSON model of queries.
+ // required: true
+ Queries *simplejson.Json `json:"queries"`
+}
+
+// PatchQueryCommentInQueryHistoryCommand is the command for updating comment for query in query history
+// swagger:model
+type PatchQueryCommentInQueryHistoryCommand struct {
+ // Updated comment
+ Comment string `json:"comment"`
+}
+
+// MigrateQueriesToQueryHistoryCommand is the command used for migration of old queries into query history
+// swagger:model
+type MigrateQueriesToQueryHistoryCommand struct {
+ // Array of queries to store in query history.
+ Queries []QueryToMigrate `json:"queries"`
+}
diff --git a/pkg/services/queryhistory/queryhistory_create_test.go b/pkg/services/queryhistory/queryhistory_create_test.go
index 00f891359eb..a04d967a64f 100644
--- a/pkg/services/queryhistory/queryhistory_create_test.go
+++ b/pkg/services/queryhistory/queryhistory_create_test.go
@@ -8,6 +8,9 @@ import (
)
func TestIntegrationCreateQueryInQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenario(t, "When users tries to create query in query history it should succeed",
func(t *testing.T, sc scenarioContext) {
command := CreateQueryInQueryHistoryCommand{
diff --git a/pkg/services/queryhistory/queryhistory_delete_stale_test.go b/pkg/services/queryhistory/queryhistory_delete_stale_test.go
index 84fbfd7e317..4d1a5e1a271 100644
--- a/pkg/services/queryhistory/queryhistory_delete_stale_test.go
+++ b/pkg/services/queryhistory/queryhistory_delete_stale_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationDeleteStaleQueryFromQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenarioWithQueryInQueryHistory(t, "Stale query history can be deleted",
func(t *testing.T, sc scenarioContext) {
olderThan := time.Now().Unix() + 60
diff --git a/pkg/services/queryhistory/queryhistory_delete_test.go b/pkg/services/queryhistory/queryhistory_delete_test.go
index bf8ff11d45c..379f3f7dc79 100644
--- a/pkg/services/queryhistory/queryhistory_delete_test.go
+++ b/pkg/services/queryhistory/queryhistory_delete_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationDeleteQueryFromQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenarioWithQueryInQueryHistory(t, "When users tries to delete query in query history that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.deleteHandler(sc.reqContext)
diff --git a/pkg/services/queryhistory/queryhistory_enforce_limit_test.go b/pkg/services/queryhistory/queryhistory_enforce_limit_test.go
index 85ef244dfae..4db257c32d2 100644
--- a/pkg/services/queryhistory/queryhistory_enforce_limit_test.go
+++ b/pkg/services/queryhistory/queryhistory_enforce_limit_test.go
@@ -8,6 +8,9 @@ import (
)
func TestIntegrationEnforceRowLimitInQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenarioWithQueryInQueryHistory(t, "Enforce limit for query_history",
func(t *testing.T, sc scenarioContext) {
limit := 0
diff --git a/pkg/services/queryhistory/queryhistory_migrate_test.go b/pkg/services/queryhistory/queryhistory_migrate_test.go
index c323f51e44f..dc4156dbc37 100644
--- a/pkg/services/queryhistory/queryhistory_migrate_test.go
+++ b/pkg/services/queryhistory/queryhistory_migrate_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationMigrateQueriesToQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenario(t, "When users tries to migrate 1 query in query history it should succeed",
func(t *testing.T, sc scenarioContext) {
command := MigrateQueriesToQueryHistoryCommand{
diff --git a/pkg/services/queryhistory/queryhistory_patch_test.go b/pkg/services/queryhistory/queryhistory_patch_test.go
index 2d299b992d5..14e0dabe37f 100644
--- a/pkg/services/queryhistory/queryhistory_patch_test.go
+++ b/pkg/services/queryhistory/queryhistory_patch_test.go
@@ -8,6 +8,9 @@ import (
)
func TestIntegrationPatchQueryCommentInQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenarioWithQueryInQueryHistory(t, "When user tries to patch comment of query in query history that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.patchCommentHandler(sc.reqContext)
diff --git a/pkg/services/queryhistory/queryhistory_search_test.go b/pkg/services/queryhistory/queryhistory_search_test.go
index d40291094ee..24ca3f67f11 100644
--- a/pkg/services/queryhistory/queryhistory_search_test.go
+++ b/pkg/services/queryhistory/queryhistory_search_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationGetQueriesFromQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenario(t, "When users tries to get query in empty query history, it should return empty result",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.Req.Form.Add("datasourceUid", "test")
diff --git a/pkg/services/queryhistory/queryhistory_star_test.go b/pkg/services/queryhistory/queryhistory_star_test.go
index 96980cadb9d..a5fab3430c9 100644
--- a/pkg/services/queryhistory/queryhistory_star_test.go
+++ b/pkg/services/queryhistory/queryhistory_star_test.go
@@ -8,6 +8,9 @@ import (
)
func TestIntegrationStarQueryInQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenarioWithQueryInQueryHistory(t, "When users tries to star query in query history that does not exists, it should fail",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.starHandler(sc.reqContext)
diff --git a/pkg/services/queryhistory/queryhistory_unstar_test.go b/pkg/services/queryhistory/queryhistory_unstar_test.go
index c9a7c0159ab..eff47541fdc 100644
--- a/pkg/services/queryhistory/queryhistory_unstar_test.go
+++ b/pkg/services/queryhistory/queryhistory_unstar_test.go
@@ -8,6 +8,9 @@ import (
)
func TestIntegrationUnstarQueryInQueryHistory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testScenarioWithQueryInQueryHistory(t, "When users tries to unstar query in query history that does not exists, it should fail",
func(t *testing.T, sc scenarioContext) {
resp := sc.service.starHandler(sc.reqContext)
diff --git a/pkg/services/secrets/kvstore/remote_plugin.go b/pkg/services/secrets/kvstore/remote_plugin.go
index 8216140c624..6e471d06685 100644
--- a/pkg/services/secrets/kvstore/remote_plugin.go
+++ b/pkg/services/secrets/kvstore/remote_plugin.go
@@ -19,7 +19,7 @@ type secretsKVStorePlugin struct {
// Get an item from the store
func (kv *secretsKVStorePlugin) Get(ctx context.Context, orgId int64, namespace string, typ string) (string, bool, error) {
- req := &smp.SecretsGetRequest{
+ req := &smp.GetSecretRequest{
KeyDescriptor: &smp.Key{
OrgId: orgId,
Namespace: namespace,
@@ -28,9 +28,9 @@ func (kv *secretsKVStorePlugin) Get(ctx context.Context, orgId int64, namespace
}
res, err := kv.secretsPlugin.Get(ctx, req)
if err != nil {
- return "", false, wrapSecretError(err)
- } else if res.Error != "" {
- err = fmt.Errorf(res.Error)
+ return "", false, err
+ } else if res.UserFriendlyError != "" {
+ err = fmt.Errorf(res.UserFriendlyError)
}
return res.DecryptedValue, res.Exists, wrapSecretError(err)
@@ -38,7 +38,7 @@ func (kv *secretsKVStorePlugin) Get(ctx context.Context, orgId int64, namespace
// Set an item in the store
func (kv *secretsKVStorePlugin) Set(ctx context.Context, orgId int64, namespace string, typ string, value string) error {
- req := &smp.SecretsSetRequest{
+ req := &smp.SetSecretRequest{
KeyDescriptor: &smp.Key{
OrgId: orgId,
Namespace: namespace,
@@ -48,8 +48,8 @@ func (kv *secretsKVStorePlugin) Set(ctx context.Context, orgId int64, namespace
}
res, err := kv.secretsPlugin.Set(ctx, req)
- if err == nil && res.Error != "" {
- err = fmt.Errorf(res.Error)
+ if err == nil && res.UserFriendlyError != "" {
+ err = fmt.Errorf(res.UserFriendlyError)
}
return wrapSecretError(err)
@@ -57,7 +57,7 @@ func (kv *secretsKVStorePlugin) Set(ctx context.Context, orgId int64, namespace
// Del deletes an item from the store.
func (kv *secretsKVStorePlugin) Del(ctx context.Context, orgId int64, namespace string, typ string) error {
- req := &smp.SecretsDelRequest{
+ req := &smp.DelSecretRequest{
KeyDescriptor: &smp.Key{
OrgId: orgId,
Namespace: namespace,
@@ -66,8 +66,8 @@ func (kv *secretsKVStorePlugin) Del(ctx context.Context, orgId int64, namespace
}
res, err := kv.secretsPlugin.Del(ctx, req)
- if err == nil && res.Error != "" {
- err = fmt.Errorf(res.Error)
+ if err == nil && res.UserFriendlyError != "" {
+ err = fmt.Errorf(res.UserFriendlyError)
}
return wrapSecretError(err)
@@ -76,7 +76,7 @@ func (kv *secretsKVStorePlugin) Del(ctx context.Context, orgId int64, namespace
// Keys get all keys for a given namespace. To query for all
// organizations the constant 'kvstore.AllOrganizations' can be passed as orgId.
func (kv *secretsKVStorePlugin) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) {
- req := &smp.SecretsKeysRequest{
+ req := &smp.ListSecretsRequest{
KeyDescriptor: &smp.Key{
OrgId: orgId,
Namespace: namespace,
@@ -87,9 +87,9 @@ func (kv *secretsKVStorePlugin) Keys(ctx context.Context, orgId int64, namespace
res, err := kv.secretsPlugin.Keys(ctx, req)
if err != nil {
- return nil, wrapSecretError(err)
- } else if res.Error != "" {
- err = fmt.Errorf(res.Error)
+ return nil, err
+ } else if res.UserFriendlyError != "" {
+ err = fmt.Errorf(res.UserFriendlyError)
}
return parseKeys(res.Keys), wrapSecretError(err)
@@ -97,7 +97,7 @@ func (kv *secretsKVStorePlugin) Keys(ctx context.Context, orgId int64, namespace
// Rename an item in the store
func (kv *secretsKVStorePlugin) Rename(ctx context.Context, orgId int64, namespace string, typ string, newNamespace string) error {
- req := &smp.SecretsRenameRequest{
+ req := &smp.RenameSecretRequest{
KeyDescriptor: &smp.Key{
OrgId: orgId,
Namespace: namespace,
@@ -107,8 +107,8 @@ func (kv *secretsKVStorePlugin) Rename(ctx context.Context, orgId int64, namespa
}
res, err := kv.secretsPlugin.Rename(ctx, req)
- if err == nil && res.Error != "" {
- err = fmt.Errorf(res.Error)
+ if err == nil && res.UserFriendlyError != "" {
+ err = fmt.Errorf(res.UserFriendlyError)
}
return wrapSecretError(err)
diff --git a/pkg/services/secrets/manager/metrics.go b/pkg/services/secrets/manager/metrics.go
index 8176bc61d5a..b435f71036d 100644
--- a/pkg/services/secrets/manager/metrics.go
+++ b/pkg/services/secrets/manager/metrics.go
@@ -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"},
+ },
)
)
diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go
index ceb18ae11fa..6bc2e4a83d2 100644
--- a/pkg/services/sqlstore/alert_notification_test.go
+++ b/pkg/services/sqlstore/alert_notification_test.go
@@ -14,6 +14,9 @@ import (
)
func TestIntegrationAlertNotificationSQLAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
var sqlStore *SQLStore
setup := func() { sqlStore = InitTestDB(t) }
diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go
index 51d23fa060d..cb5beb3ddb3 100644
--- a/pkg/services/sqlstore/alert_test.go
+++ b/pkg/services/sqlstore/alert_test.go
@@ -26,6 +26,9 @@ func resetTimeNow() {
}
func TestIntegrationAlertingDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
mockTimeNow()
defer resetTimeNow()
@@ -260,6 +263,9 @@ func TestIntegrationAlertingDataAccess(t *testing.T) {
}
func TestIntegrationPausingAlerts(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
mockTimeNow()
defer resetTimeNow()
diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go
index bb62758231f..d6c1c50b6c1 100644
--- a/pkg/services/sqlstore/annotation_test.go
+++ b/pkg/services/sqlstore/annotation_test.go
@@ -18,6 +18,9 @@ import (
)
func TestIntegrationAnnotations(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sql := sqlstore.InitTestDB(t)
repo := sqlstore.NewSQLAnnotationRepo(sql)
@@ -380,6 +383,9 @@ func TestIntegrationAnnotations(t *testing.T) {
}
func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sql := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{})
repo := sqlstore.NewSQLAnnotationRepo(sql)
dashboardStore := dashboardstore.ProvideDashboardStore(sql)
diff --git a/pkg/services/sqlstore/apikey_test.go b/pkg/services/sqlstore/apikey_test.go
index 6be90afeb9d..260f319ecda 100644
--- a/pkg/services/sqlstore/apikey_test.go
+++ b/pkg/services/sqlstore/apikey_test.go
@@ -14,6 +14,9 @@ import (
)
func TestIntegrationApiKeyDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
mockTimeNow()
defer resetTimeNow()
@@ -135,6 +138,9 @@ func TestIntegrationApiKeyDataAccess(t *testing.T) {
}
func TestIntegrationApiKeyErrors(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
mockTimeNow()
defer resetTimeNow()
@@ -171,6 +177,9 @@ type getApiKeysTestCase struct {
}
func TestIntegrationSQLStore_GetAPIKeys(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
tests := []getApiKeysTestCase{
{
desc: "expect all keys for wildcard scope",
diff --git a/pkg/services/sqlstore/dashboard_snapshot_test.go b/pkg/services/sqlstore/dashboard_snapshot_test.go
index e5ad58c69db..008dd8cbdc3 100644
--- a/pkg/services/sqlstore/dashboard_snapshot_test.go
+++ b/pkg/services/sqlstore/dashboard_snapshot_test.go
@@ -15,6 +15,9 @@ import (
)
func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlstore := InitTestDB(t)
origSecret := setting.SecretKey
@@ -142,6 +145,9 @@ func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) {
}
func TestIntegrationDeleteExpiredSnapshots(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlstore := InitTestDB(t)
t.Run("Testing dashboard snapshots clean up", func(t *testing.T) {
diff --git a/pkg/services/sqlstore/dashboard_thumbs_test.go b/pkg/services/sqlstore/dashboard_thumbs_test.go
index af7f364d2c4..61cfc7e6bf0 100644
--- a/pkg/services/sqlstore/dashboard_thumbs_test.go
+++ b/pkg/services/sqlstore/dashboard_thumbs_test.go
@@ -16,6 +16,9 @@ var theme = models.ThemeDark
var kind = models.ThumbnailKindDefault
func TestIntegrationSqlStorage(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
var sqlStore *SQLStore
var savedFolder *models.Dashboard
diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go
index 9d6fccf1eed..8c350fbe0fb 100644
--- a/pkg/services/sqlstore/datasource_test.go
+++ b/pkg/services/sqlstore/datasource_test.go
@@ -17,6 +17,9 @@ import (
)
func TestIntegrationDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
defaultAddDatasourceCommand := models.AddDataSourceCommand{
OrgId: 10,
Name: "nisse",
@@ -429,6 +432,9 @@ func TestIntegrationDataAccess(t *testing.T) {
}
func TestIntegrationGetDefaultDataSource(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
InitTestDB(t)
t.Run("should return error if there is no default datasource", func(t *testing.T) {
diff --git a/pkg/services/sqlstore/health_test.go b/pkg/services/sqlstore/health_test.go
index 5028842d033..4873ec37fbf 100644
--- a/pkg/services/sqlstore/health_test.go
+++ b/pkg/services/sqlstore/health_test.go
@@ -9,6 +9,9 @@ import (
)
func TestIntegrationGetDBHealthQuery(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
store := InitTestDB(t)
query := models.GetDBHealthQuery{}
diff --git a/pkg/services/sqlstore/login_attempt_test.go b/pkg/services/sqlstore/login_attempt_test.go
index c2b2d797246..4d6d123278a 100644
--- a/pkg/services/sqlstore/login_attempt_test.go
+++ b/pkg/services/sqlstore/login_attempt_test.go
@@ -16,6 +16,9 @@ func mockTime(mock time.Time) time.Time {
}
func TestIntegrationLoginAttempts(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
var beginningOfTime, timePlusOneMinute, timePlusTwoMinutes time.Time
var sqlStore *SQLStore
user := "user"
diff --git a/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go
index 425247d0314..756502be02d 100644
--- a/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go
+++ b/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go
@@ -57,17 +57,10 @@ func (sp *managedPermissionMigrator) Exec(sess *xorm.Session, mg *migrator.Migra
return errFindPermissions
}
- roleMap := make(map[int64]map[string]int64) // map[org_id][role_name] = role_id
permissionMap := make(map[int64]map[string]map[Permission]bool) // map[org_id][role_name][Permission] = toInsert
// for each managed permission make a map of which permissions need to be added to inheritors
for _, p := range managedPermissions {
- if _, ok := roleMap[p.OrgID]; !ok {
- roleMap[p.OrgID] = map[string]int64{p.RoleName: p.RoleID}
- } else {
- roleMap[p.OrgID][p.RoleName] = p.RoleID
- }
-
// this ensures we can use p as a key in the map between different permissions
// ensuring we're only comparing on the action and scope
roleName := p.RoleName
@@ -106,7 +99,8 @@ func (sp *managedPermissionMigrator) Exec(sess *xorm.Session, mg *migrator.Migra
for orgID, orgMap := range permissionMap {
for managedRole, permissions := range orgMap {
// ensure managed role exists, create and add to map if it doesn't
- ok, err := sess.Get(&accesscontrol.Role{Name: managedRole, OrgID: orgID})
+ foundRole := &accesscontrol.Role{Name: managedRole, OrgID: orgID}
+ ok, err := sess.Get(foundRole)
if err != nil {
return err
}
@@ -135,11 +129,15 @@ func (sp *managedPermissionMigrator) Exec(sess *xorm.Session, mg *migrator.Migra
return err
}
- roleMap[orgID][managedRole] = createdRole.ID
+ foundRole = &createdRole
}
// assign permissions if they don't exist to the role
- roleID := roleMap[orgID][managedRole]
+ roleID := foundRole.ID
+ if roleID == 0 {
+ logger.Warn("Unable to create managed permission, got role ID 0", "orgID", orgID, "managedRole", managedRole)
+ continue
+ }
for p, toInsert := range permissions {
if toInsert {
perm := accesscontrol.Permission{RoleID: roleID, Action: p.Action, Scope: p.Scope, Created: now, Updated: now}
diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go
index 9d8946e2ed6..e8dd6d728c6 100644
--- a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go
+++ b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go
@@ -1,6 +1,7 @@
package test
import (
+ "fmt"
"strconv"
"strings"
"testing"
@@ -14,19 +15,17 @@ import (
"xorm.io/xorm"
)
-func TestManagedPermissionsMigration(t *testing.T) {
- // Run initial migration to have a working DB
- x := setupTestDB(t)
+type inheritanceTestCase struct {
+ desc string
+ putRolePerms map[int64]map[string][]rawPermission
+ wantRolePerms map[int64]map[string][]rawPermission
+}
+func inheritanceTestCases() []inheritanceTestCase {
team1Scope := accesscontrol.Scope("teams", "id", "1")
team2Scope := accesscontrol.Scope("teams", "id", "2")
- type teamMigrationTestCase struct {
- desc string
- putRolePerms map[int64]map[string][]rawPermission
- wantRolePerms map[int64]map[string][]rawPermission
- }
- testCases := []teamMigrationTestCase{
+ return []inheritanceTestCase{
{
desc: "empty perms",
putRolePerms: map[int64]map[string][]rawPermission{},
@@ -74,6 +73,16 @@ func TestManagedPermissionsMigration(t *testing.T) {
{Action: "teams:write", Scope: team1Scope},
},
},
+ 3: {
+ "managed:builtins:editor:permissions": {
+ {Action: "teams.permissions:read", Scope: team1Scope},
+ {Action: "teams.permissions:write", Scope: team2Scope},
+ },
+ "managed:builtins:admin:permissions": {}, //Role existed with no permission
+ },
+ 4: {
+ "managed:builtins:admin:permissions": {}, //Role existed with no permission
+ },
},
wantRolePerms: map[int64]map[string][]rawPermission{
1: {
@@ -111,16 +120,37 @@ func TestManagedPermissionsMigration(t *testing.T) {
{Action: "teams:write", Scope: team1Scope},
},
},
+ 3: {
+ "managed:builtins:editor:permissions": {
+ {Action: "teams.permissions:read", Scope: team1Scope},
+ {Action: "teams.permissions:write", Scope: team2Scope},
+ },
+ "managed:builtins:admin:permissions": {
+ {Action: "teams.permissions:read", Scope: team1Scope},
+ {Action: "teams.permissions:write", Scope: team2Scope},
+ },
+ },
+ 4: {
+ "managed:builtins:admin:permissions": {}, //Role existed with no permission
+ },
},
},
}
+}
- for _, tc := range testCases {
+func TestManagedPermissionsMigration(t *testing.T) {
+ // Run initial migration to have a working DB
+ x := setupTestDB(t)
+
+ for _, tc := range inheritanceTestCases() {
t.Run(tc.desc, func(t *testing.T) {
// Remove migration
- _, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id = ?;
-DELETE FROM permission; DELETE FROM role`, acmig.ManagedPermissionsMigrationID)
+ _, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id = ?`, acmig.ManagedPermissionsMigrationID)
require.NoError(t, errDeleteMig)
+ _, errDeletePerm := x.Exec(`DELETE FROM permission`)
+ require.NoError(t, errDeletePerm)
+ _, errDeleteRole := x.Exec(`DELETE FROM role`)
+ require.NoError(t, errDeleteRole)
// put permissions
putTestPermissions(t, x, tc.putRolePerms)
@@ -163,6 +193,65 @@ DELETE FROM permission; DELETE FROM role`, acmig.ManagedPermissionsMigrationID)
}
}
+func TestManagedPermissionsMigrationRunTwice(t *testing.T) {
+ // Run initial migration to have a working DB
+ x := setupTestDB(t)
+
+ for _, tc := range inheritanceTestCases() {
+ t.Run(tc.desc, func(t *testing.T) {
+ // Remove migration
+ _, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id LIKE ?`, acmig.ManagedPermissionsMigrationID+"%")
+ require.NoError(t, errDeleteMig)
+ _, errDeletePerm := x.Exec(`DELETE FROM permission`)
+ require.NoError(t, errDeletePerm)
+ _, errDeleteRole := x.Exec(`DELETE FROM role`)
+ require.NoError(t, errDeleteRole)
+
+ for i := 0; i < 2; i++ {
+ if i == 0 {
+ // put permissions
+ putTestPermissions(t, x, tc.putRolePerms)
+ }
+
+ // Run accesscontrol migration (permissions insertion should not have conflicted)
+ acmigrator := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("acmigration.test")})
+ acmig.AddManagedPermissionsMigration(acmigrator, acmig.ManagedPermissionsMigrationID+fmt.Sprint(i))
+
+ errRunningMig := acmigrator.Start(false, 0)
+ require.NoError(t, errRunningMig)
+
+ // verify got == want
+ for orgID, roles := range tc.wantRolePerms {
+ for roleName := range roles {
+ // Check managed roles exist
+ role := accesscontrol.Role{}
+ hasRole, errManagedRoleSearch := x.Table("role").Where("org_id = ? AND name = ?", orgID, roleName).Get(&role)
+
+ require.NoError(t, errManagedRoleSearch)
+ require.True(t, hasRole, "expected role to exist", "orgID", orgID, "role", roleName)
+
+ // Check permissions associated with each role
+ perms := []accesscontrol.Permission{}
+ count, errManagedPermsSearch := x.Table("permission").Where("role_id = ?", role.ID).FindAndCount(&perms)
+
+ require.NoError(t, errManagedPermsSearch)
+ require.Equal(t, int64(len(tc.wantRolePerms[orgID][roleName])), count, "expected role to be tied to permissions", "orgID", orgID, "role", roleName)
+
+ gotRawPerms := convertToRawPermissions(perms)
+ require.ElementsMatch(t, gotRawPerms, tc.wantRolePerms[orgID][roleName], "expected role to have permissions", "orgID", orgID, "role", roleName)
+
+ // Check assignment of the roles
+ br := accesscontrol.BuiltinRole{}
+ has, errAssignmentSearch := x.Table("builtin_role").Where("role_id = ? AND role = ? AND org_id = ?", role.ID, acmig.ParseRoleFromName(roleName), orgID).Get(&br)
+ require.NoError(t, errAssignmentSearch)
+ require.True(t, has, "expected assignment of role to builtin role", "orgID", orgID, "role", roleName)
+ }
+ }
+ }
+ })
+ }
+}
+
func putTestPermissions(t *testing.T, x *xorm.Engine, rolePerms map[int64]map[string][]rawPermission) {
for orgID, roles := range rolePerms {
for roleName, perms := range roles {
@@ -190,13 +279,15 @@ func putTestPermissions(t *testing.T, x *xorm.Engine, rolePerms map[int64]map[st
require.NoError(t, err)
require.Equal(t, int64(1), brCount)
- permissions := []accesscontrol.Permission{}
- for _, p := range perms {
- permissions = append(permissions, p.toPermission(role.ID, now))
+ if len(perms) > 0 {
+ permissions := []accesscontrol.Permission{}
+ for _, p := range perms {
+ permissions = append(permissions, p.toPermission(role.ID, now))
+ }
+ permissionsCount, err := x.Insert(permissions)
+ require.NoError(t, err)
+ require.Equal(t, int64(len(perms)), permissionsCount)
}
- permissionsCount, err := x.Insert(permissions)
- require.NoError(t, err)
- require.Equal(t, int64(len(perms)), permissionsCount)
}
}
}
diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go
index 6665850e0d3..f4360d06bbc 100644
--- a/pkg/services/sqlstore/org_test.go
+++ b/pkg/services/sqlstore/org_test.go
@@ -16,6 +16,9 @@ import (
)
func TestIntegrationAccountDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("Testing Account DB Access", func(t *testing.T) {
sqlStore := InitTestDB(t)
testUser := &models.SignedInUser{
diff --git a/pkg/services/sqlstore/playlist_test.go b/pkg/services/sqlstore/playlist_test.go
index 0c5d4d45ecd..168245b7869 100644
--- a/pkg/services/sqlstore/playlist_test.go
+++ b/pkg/services/sqlstore/playlist_test.go
@@ -9,6 +9,9 @@ import (
)
func TestIntegrationPlaylistDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := InitTestDB(t)
t.Run("Can create playlist", func(t *testing.T) {
diff --git a/pkg/services/sqlstore/plugin_setting_test.go b/pkg/services/sqlstore/plugin_setting_test.go
index 100bf12868d..2899afb6aa2 100644
--- a/pkg/services/sqlstore/plugin_setting_test.go
+++ b/pkg/services/sqlstore/plugin_setting_test.go
@@ -11,6 +11,9 @@ import (
)
func TestIntegrationPluginSettings(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
store := InitTestDB(t)
t.Run("Existing plugin settings", func(t *testing.T) {
diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go
index ccb26eade0f..79b37903359 100644
--- a/pkg/services/sqlstore/quota_test.go
+++ b/pkg/services/sqlstore/quota_test.go
@@ -11,6 +11,9 @@ import (
)
func TestIntegrationQuotaCommandsAndQueries(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := InitTestDB(t)
userId := int64(1)
orgId := int64(0)
diff --git a/pkg/services/sqlstore/sqlbuilder_test.go b/pkg/services/sqlstore/sqlbuilder_test.go
index d86cadd0e1b..0da337cea16 100644
--- a/pkg/services/sqlstore/sqlbuilder_test.go
+++ b/pkg/services/sqlstore/sqlbuilder_test.go
@@ -14,6 +14,9 @@ import (
)
func TestIntegrationSQLBuilder(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("WriteDashboardPermissionFilter", func(t *testing.T) {
t.Run("user ACL", func(t *testing.T) {
test(t,
diff --git a/pkg/services/sqlstore/sqlstore_test.go b/pkg/services/sqlstore/sqlstore_test.go
index 0ba0b60ec63..0b04716f7a8 100644
--- a/pkg/services/sqlstore/sqlstore_test.go
+++ b/pkg/services/sqlstore/sqlstore_test.go
@@ -75,6 +75,9 @@ var sqlStoreTestCases = []sqlStoreTest{
}
func TestIntegrationSQLConnectionString(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
for _, testCase := range sqlStoreTestCases {
t.Run(testCase.name, func(t *testing.T) {
sqlstore := &SQLStore{}
diff --git a/pkg/services/sqlstore/stats_integration_test.go b/pkg/services/sqlstore/stats_integration_test.go
index ae3542bdd58..674c2674e91 100644
--- a/pkg/services/sqlstore/stats_integration_test.go
+++ b/pkg/services/sqlstore/stats_integration_test.go
@@ -9,6 +9,9 @@ import (
)
func TestIntegration_GetAdminStats(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := InitTestDB(t)
query := models.GetAdminStatsQuery{}
diff --git a/pkg/services/sqlstore/stats_test.go b/pkg/services/sqlstore/stats_test.go
index 8d5a08dff28..0fa194e01bc 100644
--- a/pkg/services/sqlstore/stats_test.go
+++ b/pkg/services/sqlstore/stats_test.go
@@ -11,6 +11,9 @@ import (
)
func TestIntegrationStatsDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
sqlStore := InitTestDB(t)
populateDB(t, sqlStore)
diff --git a/pkg/services/sqlstore/tags_test.go b/pkg/services/sqlstore/tags_test.go
index d7187a49f3b..e076c402592 100644
--- a/pkg/services/sqlstore/tags_test.go
+++ b/pkg/services/sqlstore/tags_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationSavingTags(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := InitTestDB(t)
tagPairs := []*models.Tag{
diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go
index 5dd4e2256fb..aff06437b73 100644
--- a/pkg/services/sqlstore/team_test.go
+++ b/pkg/services/sqlstore/team_test.go
@@ -14,6 +14,9 @@ import (
)
func TestIntegrationTeamCommandsAndQueries(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("Testing Team commands & queries", func(t *testing.T) {
sqlStore := InitTestDB(t)
testUser := &models.SignedInUser{
@@ -405,6 +408,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) {
}
func TestIntegrationSQLStore_SearchTeams(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
type searchTeamsTestCase struct {
desc string
query *models.SearchTeamsQuery
@@ -478,6 +484,9 @@ func TestIntegrationSQLStore_SearchTeams(t *testing.T) {
// TestSQLStore_GetTeamMembers_ACFilter tests the accesscontrol filtering of
// team members based on the signed in user permissions
func TestIntegrationSQLStore_GetTeamMembers_ACFilter(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
testOrgID := int64(2)
userIds := make([]int64, 4)
diff --git a/pkg/services/sqlstore/temp_user_test.go b/pkg/services/sqlstore/temp_user_test.go
index 04fd9b79263..fee989c22de 100644
--- a/pkg/services/sqlstore/temp_user_test.go
+++ b/pkg/services/sqlstore/temp_user_test.go
@@ -11,6 +11,9 @@ import (
)
func TestIntegrationTempUserCommandsAndQueries(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := InitTestDB(t)
cmd := models.CreateTempUserCommand{
OrgId: 2256,
diff --git a/pkg/services/sqlstore/transactions_test.go b/pkg/services/sqlstore/transactions_test.go
index bbf0be3c240..6d1a48ce9e8 100644
--- a/pkg/services/sqlstore/transactions_test.go
+++ b/pkg/services/sqlstore/transactions_test.go
@@ -13,6 +13,9 @@ import (
var ErrProvokedError = errors.New("testing error")
func TestIntegrationTransaction(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := InitTestDB(t)
cmd := &models.AddApiKeyCommand{Key: "secret-key", Name: "key", OrgId: 1}
@@ -54,6 +57,9 @@ func TestIntegrationTransaction(t *testing.T) {
}
func TestIntegrationReuseSessionWithTransaction(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := InitTestDB(t)
t.Run("top level transaction", func(t *testing.T) {
diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go
index 5377eec1635..a1a941797c7 100644
--- a/pkg/services/sqlstore/user_test.go
+++ b/pkg/services/sqlstore/user_test.go
@@ -11,6 +11,9 @@ import (
)
func TestIntegrationUserDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
ss := InitTestDB(t)
user := &models.SignedInUser{
OrgId: 1,
diff --git a/pkg/services/star/starimpl/store_test.go b/pkg/services/star/starimpl/store_test.go
index 7b5070a89ea..6b10acf4164 100644
--- a/pkg/services/star/starimpl/store_test.go
+++ b/pkg/services/star/starimpl/store_test.go
@@ -10,6 +10,9 @@ import (
)
func TestIntegrationUserStarsDataAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
t.Run("Testing User Stars Data Access", func(t *testing.T) {
ss := sqlstore.InitTestDB(t)
starStore := sqlStore{db: ss}
diff --git a/pkg/services/store/entity_events_test.go b/pkg/services/store/entity_events_test.go
index 4919dc3e1d3..ba81b8832b5 100644
--- a/pkg/services/store/entity_events_test.go
+++ b/pkg/services/store/entity_events_test.go
@@ -11,6 +11,9 @@ import (
)
func TestIntegrationEntityEventsService(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
var ctx context.Context
var service EntityEventsService
@@ -135,6 +138,9 @@ func TestIntegrationEntityEventsService(t *testing.T) {
}
func TestIntegrationCreateDatabaseEntityId(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
tests := []struct {
name string
entityType EntityType
diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go
index 826e6ad4e3b..92f2a42a54d 100644
--- a/pkg/setting/setting_test.go
+++ b/pkg/setting/setting_test.go
@@ -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",
diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go
index e0bcc3da0e3..5762760c38d 100644
--- a/pkg/setting/setting_unified_alerting.go
+++ b/pkg/setting/setting_unified_alerting.go
@@ -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{})
diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go
index d3b2ad8f12f..f9f460a5295 100644
--- a/pkg/tsdb/elasticsearch/client/client_test.go
+++ b/pkg/tsdb/elasticsearch/client/client_test.go
@@ -145,7 +145,7 @@ func TestClient_ExecuteMultisearch(t *testing.T) {
assert.Equal(t, "15000*@hostname", jBody.GetPath("aggs", "2", "aggs", "1", "avg", "script").MustString())
- assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "interval").MustString())
+ assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "fixed_interval").MustString())
assert.Equal(t, 200, res.Status)
require.Len(t, res.Responses, 1)
@@ -197,7 +197,7 @@ func TestClient_ExecuteMultisearch(t *testing.T) {
assert.Equal(t, "15000*@hostname", jBody.GetPath("aggs", "2", "aggs", "1", "avg", "script").MustString())
- assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "interval").MustString())
+ assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "fixed_interval").MustString())
assert.Equal(t, 200, res.Status)
require.Len(t, res.Responses, 1)
@@ -252,7 +252,7 @@ func TestClient_ExecuteMultisearch(t *testing.T) {
assert.Equal(t, "15000*@hostname", jBody.GetPath("aggs", "2", "aggs", "1", "avg", "script").MustString())
- assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "interval").MustString())
+ assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "fixed_interval").MustString())
assert.Equal(t, 200, res.Status)
require.Len(t, res.Responses, 1)
@@ -308,7 +308,7 @@ func TestClient_ExecuteMultisearch(t *testing.T) {
assert.Equal(t, "15000*@hostname", jBody.GetPath("aggs", "2", "aggs", "1", "avg", "script").MustString())
- assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "interval").MustString())
+ assert.Equal(t, "15s", jBody.GetPath("aggs", "2", "date_histogram", "fixed_interval").MustString())
assert.Equal(t, 200, res.Status)
require.Len(t, res.Responses, 1)
@@ -321,7 +321,7 @@ func createMultisearchForTest(t *testing.T, c Client) (*MultiSearchRequest, erro
msb := c.MultiSearch()
s := msb.Search(intervalv2.Interval{Value: 15 * time.Second, Text: "15s"})
s.Agg().DateHistogram("2", "@timestamp", func(a *DateHistogramAgg, ab AggBuilder) {
- a.Interval = "$__interval"
+ a.FixedInterval = "$__interval"
ab.Metric("1", "avg", "@hostname", func(a *MetricAggregation) {
a.Settings["script"] = "$__interval_ms*@hostname"
diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go
index ec5d9f68d67..72640132e61 100644
--- a/pkg/tsdb/elasticsearch/client/models.go
+++ b/pkg/tsdb/elasticsearch/client/models.go
@@ -238,7 +238,6 @@ type HistogramAgg struct {
// DateHistogramAgg represents a date histogram aggregation
type DateHistogramAgg struct {
Field string `json:"field"`
- Interval string `json:"interval,omitempty"`
FixedInterval string `json:"fixed_interval,omitempty"`
MinDocCount int `json:"min_doc_count"`
Missing *string `json:"missing,omitempty"`
diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go
index c1625adf4e5..24eae62df6a 100644
--- a/pkg/tsdb/elasticsearch/client/search_request.go
+++ b/pkg/tsdb/elasticsearch/client/search_request.go
@@ -342,11 +342,6 @@ func (b *aggBuilderImpl) DateHistogram(key, field string, fn func(a *DateHistogr
fn(innerAgg, builder)
}
- if b.version.Major() >= 8 {
- innerAgg.FixedInterval = innerAgg.Interval
- innerAgg.Interval = ""
- }
-
b.aggDefs = append(b.aggDefs, aggDef)
return b
diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go
index 18cedfd337b..29bef578882 100644
--- a/pkg/tsdb/elasticsearch/time_series_query.go
+++ b/pkg/tsdb/elasticsearch/time_series_query.go
@@ -245,13 +245,13 @@ func (bucketAgg BucketAgg) generateSettingsForDSL() map[string]interface{} {
func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFrom, timeTo int64) es.AggBuilder {
aggBuilder.DateHistogram(bucketAgg.ID, bucketAgg.Field, func(a *es.DateHistogramAgg, b es.AggBuilder) {
- a.Interval = bucketAgg.Settings.Get("interval").MustString("auto")
+ a.FixedInterval = bucketAgg.Settings.Get("interval").MustString("auto")
a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0)
a.ExtendedBounds = &es.ExtendedBounds{Min: timeFrom, Max: timeTo}
a.Format = bucketAgg.Settings.Get("format").MustString(es.DateFormatEpochMS)
- if a.Interval == "auto" {
- a.Interval = "$__interval"
+ if a.FixedInterval == "auto" {
+ a.FixedInterval = "$__interval"
}
if offset, err := bucketAgg.Settings.Get("offset").String(); err == nil {
diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go
index 333bebfb111..a1572f35c83 100644
--- a/pkg/tsdb/elasticsearch/time_series_query_test.go
+++ b/pkg/tsdb/elasticsearch/time_series_query_test.go
@@ -399,7 +399,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
require.Equal(t, firstLevel.Aggregation.Type, "date_histogram")
hAgg := firstLevel.Aggregation.Aggregation.(*es.DateHistogramAgg)
require.Equal(t, hAgg.Field, "@timestamp")
- require.Equal(t, hAgg.Interval, "$__interval")
+ require.Equal(t, hAgg.FixedInterval, "$__interval")
require.Equal(t, hAgg.MinDocCount, 2)
t.Run("Should not include time_zone when timeZone is utc", func(t *testing.T) {
@@ -1050,35 +1050,8 @@ func TestSettingsCasting(t *testing.T) {
})
t.Run("interval parameter", func(t *testing.T) {
- t.Run("Uses interval with ES < 8.0.0", func(t *testing.T) {
- c := newFakeClient("7.7.0")
- _, err := executeTsdbQuery(c, `{
- "timeField": "@timestamp",
- "bucketAggs": [
- {
- "type": "date_histogram",
- "field": "@timestamp",
- "id": "2",
- "settings": {
- "interval": "1d"
- }
- }
- ],
- "metrics": [
- { "id": "1", "type": "average", "field": "@value" }
- ]
- }`, from, to, 15*time.Second)
- assert.Nil(t, err)
- sr := c.multisearchRequests[0].Requests[0]
-
- dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
-
- assert.Zero(t, dateHistogramAgg.FixedInterval)
- assert.NotZero(t, dateHistogramAgg.Interval)
- })
-
- t.Run("Uses fixed_interval with ES >= 8.0.0", func(t *testing.T) {
- c := newFakeClient("8.0.0")
+ t.Run("Uses fixed_interval", func(t *testing.T) {
+ c := newFakeClient("7.10.0")
_, err := executeTsdbQuery(c, `{
"timeField": "@timestamp",
"bucketAggs": [
@@ -1101,7 +1074,6 @@ func TestSettingsCasting(t *testing.T) {
dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
assert.NotZero(t, dateHistogramAgg.FixedInterval)
- assert.Zero(t, dateHistogramAgg.Interval)
})
})
})
diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go
index 72e4ccd3d95..14938bfc7af 100644
--- a/pkg/tsdb/grafanads/grafana.go
+++ b/pkg/tsdb/grafanads/grafana.go
@@ -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"`
}
diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go
index 35a09e296c7..fb69e6cc11b 100644
--- a/pkg/tsdb/loki/loki.go
+++ b/pkg/tsdb/loki/loki.go
@@ -16,13 +16,15 @@ import (
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"go.opentelemetry.io/otel/attribute"
)
type Service struct {
- im instancemgmt.InstanceManager
- plog log.Logger
- tracer tracing.Tracer
+ im instancemgmt.InstanceManager
+ features featuremgmt.FeatureToggles
+ plog log.Logger
+ tracer tracing.Tracer
}
var (
@@ -31,11 +33,12 @@ var (
_ backend.CallResourceHandler = (*Service)(nil)
)
-func ProvideService(httpClientProvider httpclient.Provider, tracer tracing.Tracer) *Service {
+func ProvideService(httpClientProvider httpclient.Provider, features featuremgmt.FeatureToggles, tracer tracing.Tracer) *Service {
return &Service{
- im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
- plog: log.New("tsdb.loki"),
- tracer: tracer,
+ im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
+ features: features,
+ plog: log.New("tsdb.loki"),
+ tracer: tracer,
}
}
diff --git a/pkg/tsdb/loki/streaming.go b/pkg/tsdb/loki/streaming.go
index e2c6ff96180..078c08b3a99 100644
--- a/pkg/tsdb/loki/streaming.go
+++ b/pkg/tsdb/loki/streaming.go
@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
- "net/http"
"net/url"
"os"
"os/signal"
@@ -14,6 +13,7 @@ import (
"github.com/gorilla/websocket"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
)
func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
@@ -82,13 +82,13 @@ func (s *Service) RunStream(ctx context.Context, req *backend.RunStreamRequest,
params := url.Values{}
params.Add("query", query.Expr)
- isV1 := false
+ lokiDataframeApi := s.features.IsEnabled(featuremgmt.FlagLokiDataframeApi)
+
wsurl, _ := url.Parse(dsInfo.URL)
- // Check if the v2alpha endpoint exists
- wsurl.Path = "/loki/api/v2alpha/tail"
- if !is400(dsInfo.HTTPClient, wsurl) {
- isV1 = true
+ if lokiDataframeApi {
+ wsurl.Path = "/loki/api/v2alpha/tail"
+ } else {
wsurl.Path = "/loki/api/v1/tail"
}
@@ -131,7 +131,7 @@ func (s *Service) RunStream(ctx context.Context, req *backend.RunStreamRequest,
}
frame := &data.Frame{}
- if isV1 {
+ if !lokiDataframeApi {
frame, err = lokiBytesToLabeledFrame(message)
} else {
err = json.Unmarshal(message, &frame)
@@ -182,19 +182,3 @@ func (s *Service) PublishStream(_ context.Context, _ *backend.PublishStreamReque
Status: backend.PublishStreamStatusPermissionDenied,
}, nil
}
-
-// if the v2 endpoint exists it will give a 400 rather than 404/500
-func is400(client *http.Client, url *url.URL) bool {
- req, err := http.NewRequest("GET", url.String(), nil)
- if err != nil {
- return false
- }
- rsp, err := client.Do(req)
- if err != nil {
- return false
- }
- defer func() {
- _ = rsp.Body.Close()
- }()
- return rsp.StatusCode == 400 // will be true
-}
diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index 6499e1437f7..4d67e06fdd6 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -26,6 +26,9 @@ import (
// use to verify that the generated data are visualized as expected, see
// devenv/README.md for setup instructions.
func TestIntegrationMySQL(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
// change to true to run the MySQL tests
runMySQLTests := false
// runMySqlTests := true
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index c7724d086cd..f7092eb2d24 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -23,6 +23,9 @@ import (
// Test generateConnectionString.
func TestIntegrationGenerateConnectionString(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
cfg := setting.NewCfg()
cfg.DataPath = t.TempDir()
@@ -170,6 +173,9 @@ func TestIntegrationGenerateConnectionString(t *testing.T) {
// use to verify that the generated data are visualized as expected, see
// devenv/README.md for setup instructions.
func TestIntegrationPostgres(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
// change to true to run the PostgreSQL tests
const runPostgresTests = false
diff --git a/public/api-merged.json b/public/api-merged.json
index aa81d429ce8..2671e83809f 100644
--- a/public/api-merged.json
+++ b/public/api-merged.json
@@ -489,13 +489,6 @@
"summary": "Add a user role assignment.",
"operationId": "addUserRole",
"parameters": [
- {
- "type": "integer",
- "format": "int64",
- "name": "user_id",
- "in": "path",
- "required": true
- },
{
"name": "body",
"in": "body",
@@ -503,6 +496,13 @@
"schema": {
"$ref": "#/definitions/AddUserRoleCommand"
}
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "name": "user_id",
+ "in": "path",
+ "required": true
}
],
"responses": {
@@ -528,13 +528,6 @@
"summary": "Remove a user role assignment.",
"operationId": "removeUserRole",
"parameters": [
- {
- "type": "integer",
- "format": "int64",
- "name": "user_id",
- "in": "path",
- "required": true
- },
{
"type": "string",
"name": "roleUID",
@@ -546,6 +539,13 @@
"description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.",
"name": "global",
"in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "name": "user_id",
+ "in": "path",
+ "required": true
}
],
"responses": {
@@ -5790,6 +5790,262 @@
}
}
},
+ "/query-history": {
+ "get": {
+ "description": "Returns a list of queries in the query history that matches the search criteria.\nQuery history search supports pagination. Use the `limit` parameter to control the maximum number of queries returned; the default limit is 100.\nYou can also use the `page` query parameter to fetch queries from any page other than the first one.",
+ "tags": ["query_history"],
+ "summary": "Query history search.",
+ "operationId": "searchQueries",
+ "parameters": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "collectionFormat": "multi",
+ "description": "List of data source UIDs to search for",
+ "name": "datasourceUid",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Text inside query or comments that is searched for",
+ "name": "searchString",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "Flag indicating if only starred queries should be returned",
+ "name": "onlyStarred",
+ "in": "query"
+ },
+ {
+ "enum": ["time-desc", "time-asc"],
+ "type": "string",
+ "default": "time-desc",
+ "description": "Sort method",
+ "name": "sort",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size.",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Limit the number of returned results",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "From range for the query history search",
+ "name": "from",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "To range for the query history search",
+ "name": "to",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistorySearchResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "post": {
+ "description": "Adds new query to query history.",
+ "tags": ["query_history"],
+ "summary": "Add query to query history.",
+ "operationId": "createQuery",
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/CreateQueryInQueryHistoryCommand"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
+ "/query-history/migrate": {
+ "post": {
+ "description": "Adds multiple queries to query history.",
+ "tags": ["query_history"],
+ "summary": "Migrate queries to query history.",
+ "operationId": "migrateQueries",
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/MigrateQueriesToQueryHistoryCommand"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryMigrationResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
+ "/query-history/star/{query_history_uid}": {
+ "post": {
+ "description": "Adds star to query in query history as specified by the UID.",
+ "tags": ["query_history"],
+ "summary": "Add star to query in query history.",
+ "operationId": "starQuery",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "delete": {
+ "description": "Removes star from query in query history as specified by the UID.",
+ "tags": ["query_history"],
+ "summary": "Remove star to query in query history.",
+ "operationId": "unstarQuery",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
+ "/query-history/{query_history_uid}": {
+ "delete": {
+ "description": "Deletes an existing query in query history as specified by the UID. This operation cannot be reverted.",
+ "tags": ["query_history"],
+ "summary": "Delete query in query history.",
+ "operationId": "deleteQuery",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryDeleteQueryResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "patch": {
+ "description": "Updates comment for query in query history as specified by the UID.",
+ "tags": ["query_history"],
+ "summary": "Update comment for query in query history.",
+ "operationId": "patchQueryComment",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/PatchQueryCommentInQueryHistoryCommand"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
"/recording-rules": {
"get": {
"tags": ["recording_rules", "enterprise"],
@@ -9145,6 +9401,21 @@
}
}
},
+ "CreateQueryInQueryHistoryCommand": {
+ "description": "CreateQueryInQueryHistoryCommand is the command for adding query history",
+ "type": "object",
+ "required": ["queries"],
+ "properties": {
+ "datasourceUid": {
+ "description": "UID of the data source for which are queries stored.",
+ "type": "string",
+ "example": "PE1C5CBDA0504A6A3"
+ },
+ "queries": {
+ "$ref": "#/definitions/Json"
+ }
+ }
+ },
"CreateRoleWithPermissionsCommand": {
"type": "object",
"properties": {
@@ -9584,7 +9855,6 @@
}
},
"DashboardVersionDTO": {
- "description": "DashboardVersionDTO represents a dashboard version, without the dashboard\nmap.",
"type": "object",
"properties": {
"created": {
@@ -9680,7 +9950,7 @@
}
},
"DataResponse": {
- "description": "A map of RefIDs (unique query identifers) to this type makes up the Responses property of a QueryDataResponse.\nThe Error property is used to allow for partial success responses from the containing QueryDataResponse.",
+ "description": "A map of RefIDs (unique query identifiers) to this type makes up the Responses property of a QueryDataResponse.\nThe Error property is used to allow for partial success responses from the containing QueryDataResponse.",
"type": "object",
"title": "DataResponse contains the results from a DataQuery.",
"properties": {
@@ -10348,7 +10618,7 @@
"type": "string"
},
"pathSeparator": {
- "description": "PathSeparator defines the separator pattern to decode a hiearchy. The default separator is '/'.",
+ "description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.",
"type": "string"
},
"preferredVisualisationType": {
@@ -11583,6 +11853,19 @@
}
}
},
+ "MigrateQueriesToQueryHistoryCommand": {
+ "description": "MigrateQueriesToQueryHistoryCommand is the command used for migration of old queries into query history",
+ "type": "object",
+ "properties": {
+ "queries": {
+ "description": "Array of queries to store in query history.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/QueryToMigrate"
+ }
+ }
+ }
+ },
"MonthRange": {
"type": "object",
"title": "A MonthRange is an inclusive range between [1, 12] where 1 = January.",
@@ -12162,6 +12445,16 @@
}
}
},
+ "PatchQueryCommentInQueryHistoryCommand": {
+ "description": "PatchQueryCommentInQueryHistoryCommand is the command for updating comment for query in query history",
+ "type": "object",
+ "properties": {
+ "comment": {
+ "description": "Updated comment",
+ "type": "string"
+ }
+ }
+ },
"PauseAlertCommand": {
"type": "object",
"properties": {
@@ -12684,6 +12977,63 @@
}
}
},
+ "QueryHistoryDTO": {
+ "type": "object",
+ "properties": {
+ "comment": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "createdBy": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "datasourceUid": {
+ "type": "string"
+ },
+ "queries": {
+ "$ref": "#/definitions/Json"
+ },
+ "starred": {
+ "type": "boolean"
+ },
+ "uid": {
+ "type": "string"
+ }
+ }
+ },
+ "QueryHistoryDeleteQueryResponse": {
+ "description": "QueryHistoryDeleteQueryResponse is the response struct for deleting a query from query history",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ },
+ "QueryHistoryMigrationResponse": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "starredCount": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "totalCount": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ },
"QueryHistoryPreference": {
"type": "object",
"properties": {
@@ -12692,6 +13042,46 @@
}
}
},
+ "QueryHistoryResponse": {
+ "description": "QueryHistoryResponse is a response struct for QueryHistoryDTO",
+ "type": "object",
+ "properties": {
+ "result": {
+ "$ref": "#/definitions/QueryHistoryDTO"
+ }
+ }
+ },
+ "QueryHistorySearchResponse": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "$ref": "#/definitions/QueryHistorySearchResult"
+ }
+ }
+ },
+ "QueryHistorySearchResult": {
+ "type": "object",
+ "properties": {
+ "page": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "perPage": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "queryHistory": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/QueryHistoryDTO"
+ }
+ },
+ "totalCount": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ },
"QueryStat": {
"description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).",
"type": "object",
@@ -12777,6 +13167,27 @@
}
}
},
+ "QueryToMigrate": {
+ "type": "object",
+ "properties": {
+ "comment": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "datasourceUid": {
+ "type": "string"
+ },
+ "queries": {
+ "$ref": "#/definitions/Json"
+ },
+ "starred": {
+ "type": "boolean"
+ }
+ }
+ },
"Receiver": {
"type": "object",
"title": "Receiver configuration provides configuration on how to contact a receiver.",
@@ -16130,6 +16541,30 @@
"$ref": "#/definitions/Prefs"
}
},
+ "getQueryHistoryDeleteQueryResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistoryDeleteQueryResponse"
+ }
+ },
+ "getQueryHistoryMigrationResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistoryMigrationResponse"
+ }
+ },
+ "getQueryHistoryResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistoryResponse"
+ }
+ },
+ "getQueryHistorySearchResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistorySearchResponse"
+ }
+ },
"getQuotaResponse": {
"description": "",
"schema": {
@@ -16576,6 +17011,10 @@
"description": "The identifier (ID) of a library element is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a library element uniquely identifies library elements between multiple Grafana installs. It’s automatically generated unless you specify it during library element creation. The UID provides consistent URLs for accessing library elements and when syncing library elements between multiple Grafana installs.\nThe maximum length of a UID is 40 characters.",
"name": "library_elements"
},
+ {
+ "description": "The identifier (ID) of a query in query history is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a query history uniquely identifies queries in query history between multiple Grafana installs. It’s automatically generated. The UID provides consistent URLs for accessing queries in query history.",
+ "name": "query_history"
+ },
{
"description": "The Admin Organizations HTTP API does not currently work with an API Token. API Tokens are currently only linked to an organization and an organization role. They cannot be given the permission of server admin, only users can be given that permission. So in order to use these API calls you will have to use Basic Auth and the Grafana user must have the Grafana Admin permission (The default admin user is called `admin` and has permission to use this API).",
"name": "orgs"
diff --git a/public/api-spec.json b/public/api-spec.json
index 14595a213f9..7024b925234 100644
--- a/public/api-spec.json
+++ b/public/api-spec.json
@@ -489,13 +489,6 @@
"summary": "Add a user role assignment.",
"operationId": "addUserRole",
"parameters": [
- {
- "type": "integer",
- "format": "int64",
- "name": "user_id",
- "in": "path",
- "required": true
- },
{
"name": "body",
"in": "body",
@@ -503,6 +496,13 @@
"schema": {
"$ref": "#/definitions/AddUserRoleCommand"
}
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "name": "user_id",
+ "in": "path",
+ "required": true
}
],
"responses": {
@@ -528,13 +528,6 @@
"summary": "Remove a user role assignment.",
"operationId": "removeUserRole",
"parameters": [
- {
- "type": "integer",
- "format": "int64",
- "name": "user_id",
- "in": "path",
- "required": true
- },
{
"type": "string",
"name": "roleUID",
@@ -546,6 +539,13 @@
"description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.",
"name": "global",
"in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "name": "user_id",
+ "in": "path",
+ "required": true
}
],
"responses": {
@@ -5790,6 +5790,262 @@
}
}
},
+ "/query-history": {
+ "get": {
+ "description": "Returns a list of queries in the query history that matches the search criteria.\nQuery history search supports pagination. Use the `limit` parameter to control the maximum number of queries returned; the default limit is 100.\nYou can also use the `page` query parameter to fetch queries from any page other than the first one.",
+ "tags": ["query_history"],
+ "summary": "Query history search.",
+ "operationId": "searchQueries",
+ "parameters": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "collectionFormat": "multi",
+ "description": "List of data source UIDs to search for",
+ "name": "datasourceUid",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Text inside query or comments that is searched for",
+ "name": "searchString",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "Flag indicating if only starred queries should be returned",
+ "name": "onlyStarred",
+ "in": "query"
+ },
+ {
+ "enum": ["time-desc", "time-asc"],
+ "type": "string",
+ "default": "time-desc",
+ "description": "Sort method",
+ "name": "sort",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size.",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Limit the number of returned results",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "From range for the query history search",
+ "name": "from",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "To range for the query history search",
+ "name": "to",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistorySearchResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "post": {
+ "description": "Adds new query to query history.",
+ "tags": ["query_history"],
+ "summary": "Add query to query history.",
+ "operationId": "createQuery",
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/CreateQueryInQueryHistoryCommand"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
+ "/query-history/migrate": {
+ "post": {
+ "description": "Adds multiple queries to query history.",
+ "tags": ["query_history"],
+ "summary": "Migrate queries to query history.",
+ "operationId": "migrateQueries",
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/MigrateQueriesToQueryHistoryCommand"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryMigrationResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
+ "/query-history/star/{query_history_uid}": {
+ "post": {
+ "description": "Adds star to query in query history as specified by the UID.",
+ "tags": ["query_history"],
+ "summary": "Add star to query in query history.",
+ "operationId": "starQuery",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "delete": {
+ "description": "Removes star from query in query history as specified by the UID.",
+ "tags": ["query_history"],
+ "summary": "Remove star to query in query history.",
+ "operationId": "unstarQuery",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
+ "/query-history/{query_history_uid}": {
+ "delete": {
+ "description": "Deletes an existing query in query history as specified by the UID. This operation cannot be reverted.",
+ "tags": ["query_history"],
+ "summary": "Delete query in query history.",
+ "operationId": "deleteQuery",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryDeleteQueryResponse"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "patch": {
+ "description": "Updates comment for query in query history as specified by the UID.",
+ "tags": ["query_history"],
+ "summary": "Update comment for query in query history.",
+ "operationId": "patchQueryComment",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "query_history_uid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/PatchQueryCommentInQueryHistoryCommand"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getQueryHistoryResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
"/recording-rules": {
"get": {
"tags": ["recording_rules", "enterprise"],
@@ -8890,6 +9146,21 @@
}
}
},
+ "CreateQueryInQueryHistoryCommand": {
+ "description": "CreateQueryInQueryHistoryCommand is the command for adding query history",
+ "type": "object",
+ "required": ["queries"],
+ "properties": {
+ "datasourceUid": {
+ "description": "UID of the data source for which are queries stored.",
+ "type": "string",
+ "example": "PE1C5CBDA0504A6A3"
+ },
+ "queries": {
+ "$ref": "#/definitions/Json"
+ }
+ }
+ },
"CreateRoleWithPermissionsCommand": {
"type": "object",
"properties": {
@@ -9328,7 +9599,6 @@
}
},
"DashboardVersionDTO": {
- "description": "DashboardVersionDTO represents a dashboard version, without the dashboard\nmap.",
"type": "object",
"properties": {
"created": {
@@ -9424,7 +9694,7 @@
}
},
"DataResponse": {
- "description": "A map of RefIDs (unique query identifers) to this type makes up the Responses property of a QueryDataResponse.\nThe Error property is used to allow for partial success responses from the containing QueryDataResponse.",
+ "description": "A map of RefIDs (unique query identifiers) to this type makes up the Responses property of a QueryDataResponse.\nThe Error property is used to allow for partial success responses from the containing QueryDataResponse.",
"type": "object",
"title": "DataResponse contains the results from a DataQuery.",
"properties": {
@@ -9874,7 +10144,7 @@
"type": "string"
},
"pathSeparator": {
- "description": "PathSeparator defines the separator pattern to decode a hiearchy. The default separator is '/'.",
+ "description": "PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'.",
"type": "string"
},
"preferredVisualisationType": {
@@ -10433,6 +10703,19 @@
}
}
},
+ "MigrateQueriesToQueryHistoryCommand": {
+ "description": "MigrateQueriesToQueryHistoryCommand is the command used for migration of old queries into query history",
+ "type": "object",
+ "properties": {
+ "queries": {
+ "description": "Array of queries to store in query history.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/QueryToMigrate"
+ }
+ }
+ }
+ },
"NavLink": {
"type": "object",
"properties": {
@@ -10696,6 +10979,16 @@
}
}
},
+ "PatchQueryCommentInQueryHistoryCommand": {
+ "description": "PatchQueryCommentInQueryHistoryCommand is the command for updating comment for query in query history",
+ "type": "object",
+ "properties": {
+ "comment": {
+ "description": "Updated comment",
+ "type": "string"
+ }
+ }
+ },
"PauseAlertCommand": {
"type": "object",
"properties": {
@@ -10845,6 +11138,63 @@
}
}
},
+ "QueryHistoryDTO": {
+ "type": "object",
+ "properties": {
+ "comment": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "createdBy": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "datasourceUid": {
+ "type": "string"
+ },
+ "queries": {
+ "$ref": "#/definitions/Json"
+ },
+ "starred": {
+ "type": "boolean"
+ },
+ "uid": {
+ "type": "string"
+ }
+ }
+ },
+ "QueryHistoryDeleteQueryResponse": {
+ "description": "QueryHistoryDeleteQueryResponse is the response struct for deleting a query from query history",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ },
+ "QueryHistoryMigrationResponse": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "starredCount": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "totalCount": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ },
"QueryHistoryPreference": {
"type": "object",
"properties": {
@@ -10853,6 +11203,46 @@
}
}
},
+ "QueryHistoryResponse": {
+ "description": "QueryHistoryResponse is a response struct for QueryHistoryDTO",
+ "type": "object",
+ "properties": {
+ "result": {
+ "$ref": "#/definitions/QueryHistoryDTO"
+ }
+ }
+ },
+ "QueryHistorySearchResponse": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "$ref": "#/definitions/QueryHistorySearchResult"
+ }
+ }
+ },
+ "QueryHistorySearchResult": {
+ "type": "object",
+ "properties": {
+ "page": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "perPage": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "queryHistory": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/QueryHistoryDTO"
+ }
+ },
+ "totalCount": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ },
"QueryStat": {
"description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).",
"type": "object",
@@ -10938,6 +11328,27 @@
}
}
},
+ "QueryToMigrate": {
+ "type": "object",
+ "properties": {
+ "comment": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "datasourceUid": {
+ "type": "string"
+ },
+ "queries": {
+ "$ref": "#/definitions/Json"
+ },
+ "starred": {
+ "type": "boolean"
+ }
+ }
+ },
"RecordingRuleJSON": {
"description": "RecordingRuleJSON is the external representation of a recording rule",
"type": "object",
@@ -12757,6 +13168,30 @@
"$ref": "#/definitions/Prefs"
}
},
+ "getQueryHistoryDeleteQueryResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistoryDeleteQueryResponse"
+ }
+ },
+ "getQueryHistoryMigrationResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistoryMigrationResponse"
+ }
+ },
+ "getQueryHistoryResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistoryResponse"
+ }
+ },
+ "getQueryHistorySearchResponse": {
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/QueryHistorySearchResponse"
+ }
+ },
"getQuotaResponse": {
"description": "",
"schema": {
@@ -13203,6 +13638,10 @@
"description": "The identifier (ID) of a library element is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a library element uniquely identifies library elements between multiple Grafana installs. It’s automatically generated unless you specify it during library element creation. The UID provides consistent URLs for accessing library elements and when syncing library elements between multiple Grafana installs.\nThe maximum length of a UID is 40 characters.",
"name": "library_elements"
},
+ {
+ "description": "The identifier (ID) of a query in query history is an auto-incrementing numeric value that is unique per Grafana install.\nThe unique identifier (UID) of a query history uniquely identifies queries in query history between multiple Grafana installs. It’s automatically generated. The UID provides consistent URLs for accessing queries in query history.",
+ "name": "query_history"
+ },
{
"description": "The Admin Organizations HTTP API does not currently work with an API Token. API Tokens are currently only linked to an organization and an organization role. They cannot be given the permission of server admin, only users can be given that permission. So in order to use these API calls you will have to use Basic Auth and the Grafana user must have the Grafana Admin permission (The default admin user is called `admin` and has permission to use this API).",
"name": "orgs"
diff --git a/public/app/angular/angular_wrappers.ts b/public/app/angular/angular_wrappers.ts
index be939278a09..15159553cd7 100644
--- a/public/app/angular/angular_wrappers.ts
+++ b/public/app/angular/angular_wrappers.ts
@@ -21,7 +21,6 @@ import PageHeader from '../core/components/PageHeader/PageHeader';
import { MetricSelect } from '../core/components/Select/MetricSelect';
import { TagFilter } from '../core/components/TagFilter/TagFilter';
import { HelpModal } from '../core/components/help/HelpModal';
-import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
const { SecretFormField } = LegacyForms;
@@ -49,35 +48,6 @@ export function registerAngularDirectives() {
'infoBox',
'infoBoxTitle',
]);
- //Search
- react2AngularDirective('searchField', SearchField, [
- 'query',
- 'autoFocus',
- ['onChange', { watchDepth: 'reference' }],
- ['onKeyDown', { watchDepth: 'reference' }],
- ]);
- react2AngularDirective('searchResults', SearchResults, [
- 'results',
- 'editable',
- 'selectors',
- ['onSelectionChanged', { watchDepth: 'reference' }],
- ['onTagSelected', { watchDepth: 'reference' }],
- ['onFolderExpanding', { watchDepth: 'reference' }],
- ['onToggleSelection', { watchDepth: 'reference' }],
- ]);
- react2AngularDirective('searchFilters', SearchResultsFilter, [
- 'allChecked',
- 'canMove',
- 'canDelete',
- 'tagFilterOptions',
- 'selectedStarredFilter',
- 'selectedTagFilter',
- ['onSelectAllChanged', { watchDepth: 'reference' }],
- ['deleteItem', { watchDepth: 'reference' }],
- ['moveTo', { watchDepth: 'reference' }],
- ['onStarredFilterChange', { watchDepth: 'reference' }],
- ['onTagFilterChange', { watchDepth: 'reference' }],
- ]);
react2AngularDirective('tagFilter', TagFilter, [
'tags',
['onChange', { watchDepth: 'reference' }],
diff --git a/public/app/core/components/NavBar/Next/NavBarNext.tsx b/public/app/core/components/NavBar/Next/NavBarNext.tsx
index 6fbaee70c0f..fa1031aca37 100644
--- a/public/app/core/components/NavBar/Next/NavBarNext.tsx
+++ b/public/app/core/components/NavBar/Next/NavBarNext.tsx
@@ -205,7 +205,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')]: {
diff --git a/public/app/core/components/Select/FolderPicker.tsx b/public/app/core/components/Select/FolderPicker.tsx
index 878e7d9d9fb..b8d5c006372 100644
--- a/public/app/core/components/Select/FolderPicker.tsx
+++ b/public/app/core/components/Select/FolderPicker.tsx
@@ -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 {
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 {
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): void => {
+ this.searchFolders(query).then(callback);
+ };
+
+ private searchFolders = async (query: string) => {
const {
rootName,
enableReset,
@@ -159,7 +165,7 @@ export class FolderPicker extends PureComponent {
const resetFolder: SelectableValue = { label: initialTitle, value: undefined };
const rootFolder: SelectableValue = { label: rootName, value: 0 };
- const options = await this.getOptions('');
+ const options = await this.searchFolders('');
let folder: SelectableValue | null = null;
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts
index 7ab66c6f587..48facc8143f 100644
--- a/public/app/core/logs_model.ts
+++ b/public/app/core/logs_model.ts
@@ -702,7 +702,17 @@ export function queryLogsVolume(
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({
diff --git a/public/app/features/alerting/unified/RuleEditor.test.tsx b/public/app/features/alerting/unified/RuleEditor.test.tsx
index e5dd5cf7959..8efe878f59b 100644
--- a/public/app/features/alerting/unified/RuleEditor.test.tsx
+++ b/public/app/features/alerting/unified/RuleEditor.test.tsx
@@ -148,19 +148,21 @@ describe('RuleEditor', () => {
await renderRuleEditor();
await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled());
-
await waitFor(() => expect(mocks.api.discoverFeatures).toHaveBeenCalled());
- await userEvent.type(await ui.inputs.name.find(), 'my great new rule');
- await userEvent.click(await ui.buttons.lotexAlert.get());
+
+ await userEvent.click(await ui.buttons.lotexAlert.find());
+
const dataSourceSelect = ui.inputs.dataSource.get();
await userEvent.click(byRole('combobox').get(dataSourceSelect));
await clickSelectOption(dataSourceSelect, 'Prom (default)');
await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled());
+
+ await userEvent.type(await ui.inputs.expr.find(), 'up == 1');
+
+ await userEvent.type(ui.inputs.name.get(), 'my great new rule');
await clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
await clickSelectOption(ui.inputs.group.get(), 'group2');
- await userEvent.type(ui.inputs.expr.get(), 'up == 1');
-
await userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary');
await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
@@ -353,7 +355,7 @@ describe('RuleEditor', () => {
await clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
await clickSelectOption(ui.inputs.group.get(), 'group2');
- await userEvent.type(ui.inputs.expr.get(), 'up == 1');
+ await userEvent.type(await ui.inputs.expr.find(), 'up == 1');
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never });
diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts
index 743cb79be39..9f1114677bf 100644
--- a/public/app/features/annotations/standardAnnotationSupport.ts
+++ b/public/app/features/annotations/standardAnnotationSupport.ts
@@ -236,7 +236,9 @@ export function getAnnotationsFromData(
*/
export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
const { type } = datasource;
- return type !== 'prometheus' && type !== 'elasticsearch' && type !== 'loki';
+ return (
+ type !== 'prometheus' && type !== 'elasticsearch' && type !== 'loki' && type !== 'grafana-opensearch-datasource'
+ );
}
/**
@@ -244,5 +246,7 @@ export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
*/
export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean {
const { type } = datasource;
- return type === 'prometheus' || type === 'elasticsearch' || type === 'loki';
+ return (
+ type === 'prometheus' || type === 'elasticsearch' || type === 'loki' || type === 'grafana-opensearch-datasource'
+ );
}
diff --git a/public/app/features/api-keys/ApiKeysAddedModal.test.tsx b/public/app/features/api-keys/ApiKeysAddedModal.test.tsx
index 35e0a556e76..b6342923402 100644
--- a/public/app/features/api-keys/ApiKeysAddedModal.test.tsx
+++ b/public/app/features/api-keys/ApiKeysAddedModal.test.tsx
@@ -1,27 +1,41 @@
-import { shallow } from 'enzyme';
+import { render, screen } from '@testing-library/react';
import React from 'react';
import { ApiKeysAddedModal, Props } from './ApiKeysAddedModal';
-const setup = (propOverrides?: object) => {
+describe('ApiKeysAddedModal', () => {
const props: Props = {
onDismiss: jest.fn(),
- apiKey: 'api key test',
+ apiKey: 'myApiKey',
rootPath: 'test/path',
};
- Object.assign(props, propOverrides);
+ it('should render without throwing', () => {
+ expect(() => render()).not.toThrow();
+ });
- const wrapper = shallow();
+ it('displays the apiKey in a readOnly input', () => {
+ render();
+ const input = screen.getByRole('textbox');
+ expect(input).toHaveValue(props.apiKey);
+ expect(input).toHaveAttribute('readonly');
+ });
- return {
- wrapper,
- };
-};
+ it('has a `Copy to clipboard` button', () => {
+ render();
+ expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument();
+ });
-describe('Render', () => {
- it('should render component', () => {
- const { wrapper } = setup();
- expect(wrapper).toMatchSnapshot();
+ it('displays the correct curl path', () => {
+ render();
+ expect(
+ screen.getByText('curl -H "Authorization: Bearer myApiKey" test/path/api/dashboards/home')
+ ).toBeInTheDocument();
+ });
+
+ it('calls onDismiss when the modal is closed', () => {
+ render();
+ screen.getByRole('button', { name: 'Close dialogue' }).click();
+ expect(props.onDismiss).toHaveBeenCalled();
});
});
diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysAddedModal.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysAddedModal.test.tsx.snap
deleted file mode 100644
index 2a5670c70f1..00000000000
--- a/public/app/features/api-keys/__snapshots__/ApiKeysAddedModal.test.tsx.snap
+++ /dev/null
@@ -1,52 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Render should render component 1`] = `
-
-
-
-
- Copy
-
- }
- id="Key"
- readOnly={true}
- value="api key test"
- />
-
-
- It is not stored in this form, so be sure to copy it now.
-
-
- You can authenticate a request using the Authorization HTTP header, example:
-
-
- curl -H "Authorization: Bearer
- api key test
- "
- test/path
- /api/dashboards/home
-
-
-`;
diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx
index a694be7c3f6..c0a60e5bb55 100644
--- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx
+++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx
@@ -19,7 +19,7 @@ jest.mock('@grafana/runtime', () => ({
}));
setBackendSrv({
- get: jest.fn().mockResolvedValue({}),
+ get: jest.fn().mockResolvedValue([]),
} as any);
describe('DashboardSettings', () => {
diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx
index bbbaa0eda35..5d77b3db8bd 100644
--- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx
+++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.test.tsx
@@ -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) => {
const defaults: Props = {
dashboard: {
diff --git a/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx
index 11a62a02e3d..7a78192968a 100644
--- a/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx
+++ b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx
@@ -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 = ({ hideModal, dashboard }) => {
+const mapDispatchToProps = {
+ cleanUpDashboardAndVariables,
+};
+
+const connector = connect(null, mapDispatchToProps);
+
+type Props = DeleteDashboardModalProps & ConnectedProps;
+
+const DeleteDashboardModalUnconnected: React.FC = ({ 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
);
+
+export const DeleteDashboardModal = connector(DeleteDashboardModalUnconnected);
diff --git a/public/app/features/dashboard/components/DeleteDashboard/useDashboardDelete.tsx b/public/app/features/dashboard/components/DeleteDashboard/useDashboardDelete.tsx
index ed9429909c5..630ae39be28 100644
--- a/public/app/features/dashboard/components/DeleteDashboard/useDashboardDelete.tsx
+++ b/public/app/features/dashboard/components/DeleteDashboard/useDashboardDelete.tsx
@@ -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 };
};
diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts
index b4a64914493..817b9d76c5f 100644
--- a/public/app/features/dashboard/state/PanelModel.ts
+++ b/public/app/features/dashboard/state/PanelModel.ts
@@ -163,6 +163,8 @@ export class PanelModel implements DataConfigSource, IPanelModel {
libraryPanel?: { uid: undefined; name: string } | PanelModelLibraryPanel;
+ autoMigrateFrom?: string;
+
// non persisted
isViewing = false;
isEditing = false;
@@ -222,6 +224,12 @@ export class PanelModel implements DataConfigSource, IPanelModel {
(this as any)[property] = model[property];
}
+ // Special 'graph' migration logic
+ if (this.type === 'graph' && config?.featureToggles?.autoMigrateGraphPanels) {
+ this.autoMigrateFrom = this.type;
+ this.type = 'timeseries';
+ }
+
// defaults
defaultsDeep(this, cloneDeep(defaults));
@@ -368,6 +376,18 @@ export class PanelModel implements DataConfigSource, IPanelModel {
this.plugin = plugin;
const version = getPluginVersion(plugin);
+ if (this.autoMigrateFrom) {
+ const wasAngular = this.autoMigrateFrom === 'graph';
+ this.callPanelTypeChangeHandler(
+ plugin,
+ this.autoMigrateFrom,
+ this.getOptionsToRemember(), // old options
+ wasAngular
+ );
+
+ delete this.autoMigrateFrom;
+ }
+
if (plugin.onPanelMigration) {
if (version !== this.pluginVersion) {
this.options = plugin.onPanelMigration(this);
@@ -401,6 +421,19 @@ export class PanelModel implements DataConfigSource, IPanelModel {
};
}
+ // Let panel plugins inspect options from previous panel and keep any that it can use
+ private callPanelTypeChangeHandler(
+ newPlugin: PanelPlugin,
+ oldPluginId: string,
+ oldOptions: any,
+ wasAngular: boolean
+ ) {
+ if (newPlugin.onPanelTypeChanged) {
+ const prevOptions = wasAngular ? { angular: oldOptions } : oldOptions.options;
+ Object.assign(this.options, newPlugin.onPanelTypeChanged(this, oldPluginId, prevOptions, this.fieldConfig));
+ }
+ }
+
changePlugin(newPlugin: PanelPlugin) {
const pluginId = newPlugin.meta.id;
const oldOptions: any = this.getOptionsToRemember();
@@ -415,11 +448,8 @@ export class PanelModel implements DataConfigSource, IPanelModel {
this.clearPropertiesBeforePluginChange();
this.restorePanelOptions(pluginId);
- // Let panel plugins inspect options from previous panel and keep any that it can use
- if (newPlugin.onPanelTypeChanged) {
- const prevOptions = wasAngular ? { angular: oldOptions } : oldOptions.options;
- Object.assign(this.options, newPlugin.onPanelTypeChanged(this, oldPluginId, prevOptions, prevFieldConfig));
- }
+ // Potentially modify current options
+ this.callPanelTypeChangeHandler(newPlugin, oldPluginId, oldOptions, wasAngular);
// switch
this.type = pluginId;
diff --git a/public/app/features/data-connections/DataConnectionsPage.test.tsx b/public/app/features/data-connections/DataConnectionsPage.test.tsx
new file mode 100644
index 00000000000..3498905301f
--- /dev/null
+++ b/public/app/features/data-connections/DataConnectionsPage.test.tsx
@@ -0,0 +1,42 @@
+import { render, RenderResult, screen } from '@testing-library/react';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { Router } from 'react-router-dom';
+
+import { locationService } from '@grafana/runtime';
+import { configureStore } from 'app/store/configureStore';
+
+import DataConnectionsPage from './DataConnectionsPage';
+import navIndex from './__mocks__/store.navIndex.mock';
+import { ROUTE_BASE_ID } from './constants';
+
+const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {
+ // @ts-ignore
+ const store = configureStore({ navIndex });
+ locationService.push(path);
+
+ return render(
+
+
+
+
+
+ );
+};
+
+describe('Data Connections Page', () => {
+ test('shows all the four tabs', async () => {
+ renderPage();
+
+ expect(await screen.findByLabelText('Tab Data sources')).toBeVisible();
+ expect(await screen.findByLabelText('Tab Plugins')).toBeVisible();
+ expect(await screen.findByLabelText('Tab Cloud integrations')).toBeVisible();
+ expect(await screen.findByLabelText('Tab Recorded queries')).toBeVisible();
+ });
+
+ test('shows the "Data sources" tab by default', async () => {
+ renderPage();
+
+ expect(await screen.findByText('The list of data sources is under development.')).toBeVisible();
+ });
+});
diff --git a/public/app/features/data-connections/DataConnectionsPage.tsx b/public/app/features/data-connections/DataConnectionsPage.tsx
new file mode 100644
index 00000000000..0cf34e69871
--- /dev/null
+++ b/public/app/features/data-connections/DataConnectionsPage.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react';
+import { Route, Switch } from 'react-router-dom';
+
+import { Page } from 'app/core/components/Page/Page';
+
+import { ROUTES } from './constants';
+import { useNavModel } from './hooks/useNavModel';
+import { CloudIntegrations } from './tabs/CloudIntegrations';
+import { DataSources } from './tabs/DataSources';
+import { Plugins } from './tabs/Plugins';
+import { RecordedQueries } from './tabs/RecordedQueries';
+
+export default function DataConnectionsPage(): React.ReactElement | null {
+ const navModel = useNavModel();
+
+ return (
+
+
+
+
+
+
+
+ {/* Default page */}
+
+
+
+
+ );
+}
diff --git a/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts b/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts
new file mode 100644
index 00000000000..4546a29ec7c
--- /dev/null
+++ b/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts
@@ -0,0 +1,2519 @@
+export default {
+ dashboards: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ 'manage-dashboards': {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ parentItem: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ playlists: {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ parentItem: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ snapshots: {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ parentItem: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ 'library-panels': {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ parentItem: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ divider: {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ 'new-dashboard': {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ parentItem: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ 'new-folder': {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ parentItem: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ import: {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ parentItem: {
+ id: 'dashboards',
+ text: 'Dashboards',
+ section: 'core',
+ subTitle: 'Manage dashboards and folders',
+ icon: 'apps',
+ url: '/dashboards',
+ sortWeight: -1800,
+ children: [
+ {
+ id: 'manage-dashboards',
+ text: 'Browse',
+ icon: 'sitemap',
+ url: '/dashboards',
+ },
+ {
+ id: 'playlists',
+ text: 'Playlists',
+ icon: 'presentation-play',
+ url: '/playlists',
+ },
+ {
+ id: 'snapshots',
+ text: 'Snapshots',
+ icon: 'camera',
+ url: '/dashboard/snapshots',
+ },
+ {
+ id: 'library-panels',
+ text: 'Library panels',
+ icon: 'library-panel',
+ url: '/library-panels',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'new-dashboard',
+ text: 'New dashboard',
+ icon: 'plus',
+ url: '/dashboard/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'new-folder',
+ text: 'New folder',
+ subTitle: 'Create a new folder to organize your dashboards',
+ icon: 'plus',
+ url: '/dashboards/folder/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ {
+ id: 'import',
+ text: 'Import',
+ subTitle: 'Import dashboard from file or Grafana.com',
+ icon: 'plus',
+ url: '/dashboard/import',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ 'not-found': {
+ text: 'Page not found',
+ subTitle: '404 Error',
+ icon: 'exclamation-triangle',
+ },
+ explore: {
+ id: 'explore',
+ text: 'Explore',
+ section: 'core',
+ subTitle: 'Explore your data',
+ icon: 'compass',
+ url: '/explore',
+ sortWeight: -1700,
+ },
+ alerting: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ 'alert-list': {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ receivers: {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ 'am-routes': {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ silences: {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ groups: {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ 'alerting-admin': {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ alert: {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ parentItem: {
+ id: 'alerting',
+ text: 'Alerting',
+ section: 'core',
+ subTitle: 'Alert rules and notifications',
+ icon: 'bell',
+ url: '/alerting/list',
+ sortWeight: -1600,
+ children: [
+ {
+ id: 'alert-list',
+ text: 'Alert rules',
+ icon: 'list-ul',
+ url: '/alerting/list',
+ },
+ {
+ id: 'receivers',
+ text: 'Contact points',
+ icon: 'comment-alt-share',
+ url: '/alerting/notifications',
+ },
+ {
+ id: 'am-routes',
+ text: 'Notification policies',
+ icon: 'sitemap',
+ url: '/alerting/routes',
+ },
+ {
+ id: 'silences',
+ text: 'Silences',
+ icon: 'bell-slash',
+ url: '/alerting/silences',
+ },
+ {
+ id: 'groups',
+ text: 'Alert groups',
+ icon: 'layer-group',
+ url: '/alerting/groups',
+ },
+ {
+ id: 'alerting-admin',
+ text: 'Admin',
+ icon: 'cog',
+ url: '/alerting/admin',
+ },
+ {
+ id: 'divider',
+ text: 'Divider',
+ divider: true,
+ hideFromTabs: true,
+ },
+ {
+ id: 'alert',
+ text: 'New alert rule',
+ subTitle: 'Create an alert rule',
+ icon: 'plus',
+ url: '/alerting/new',
+ hideFromTabs: true,
+ showIconInNavbar: true,
+ },
+ ],
+ },
+ },
+ 'data-connections': {
+ id: 'data-connections',
+ text: 'Data Connections',
+ section: 'core',
+ icon: 'link',
+ url: '/data-connections',
+ sortWeight: -1500,
+ children: [
+ {
+ id: 'data-connections-datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/data-connections/datasources',
+ active: false,
+ },
+ {
+ id: 'data-connections-plugins',
+ text: 'Plugins',
+ description: 'Manage plugins',
+ icon: 'plug',
+ url: '/data-connections/plugins',
+ active: false,
+ },
+ {
+ id: 'data-connections-cloud-integrations',
+ text: 'Cloud integrations',
+ description: 'Manage your cloud integrations',
+ icon: 'bolt',
+ url: '/data-connections/cloud-integrations',
+ active: true,
+ },
+ {
+ id: 'data-connections-recorded-queries',
+ text: 'Recorded queries',
+ description: 'Manage your recorded queries',
+ icon: 'record-audio',
+ url: '/data-connections/recorded-queries',
+ active: false,
+ },
+ ],
+ },
+ 'data-connections-datasources': {
+ id: 'data-connections-datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/data-connections/datasources',
+ parentItem: {
+ id: 'data-connections',
+ text: 'Data Connections',
+ section: 'core',
+ icon: 'link',
+ url: '/data-connections',
+ sortWeight: -1500,
+ children: [
+ {
+ id: 'data-connections-datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/data-connections/datasources',
+ },
+ {
+ id: 'data-connections-plugins',
+ text: 'Plugins',
+ description: 'Manage plugins',
+ icon: 'plug',
+ url: '/data-connections/plugins',
+ },
+ {
+ id: 'data-connections-cloud-integrations',
+ text: 'Cloud integrations',
+ description: 'Manage your cloud integrations',
+ icon: 'bolt',
+ url: '/data-connections/cloud-integrations',
+ },
+ {
+ id: 'data-connections-recorded-queries',
+ text: 'Recorded queries',
+ description: 'Manage your recorded queries',
+ icon: 'record-audio',
+ url: '/data-connections/recorded-queries',
+ },
+ ],
+ },
+ },
+ 'data-connections-plugins': {
+ id: 'data-connections-plugins',
+ text: 'Plugins',
+ description: 'Manage plugins',
+ icon: 'plug',
+ url: '/data-connections/plugins',
+ parentItem: {
+ id: 'data-connections',
+ text: 'Data Connections',
+ section: 'core',
+ icon: 'link',
+ url: '/data-connections',
+ sortWeight: -1500,
+ children: [
+ {
+ id: 'data-connections-datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/data-connections/datasources',
+ },
+ {
+ id: 'data-connections-plugins',
+ text: 'Plugins',
+ description: 'Manage plugins',
+ icon: 'plug',
+ url: '/data-connections/plugins',
+ },
+ {
+ id: 'data-connections-cloud-integrations',
+ text: 'Cloud integrations',
+ description: 'Manage your cloud integrations',
+ icon: 'bolt',
+ url: '/data-connections/cloud-integrations',
+ },
+ {
+ id: 'data-connections-recorded-queries',
+ text: 'Recorded queries',
+ description: 'Manage your recorded queries',
+ icon: 'record-audio',
+ url: '/data-connections/recorded-queries',
+ },
+ ],
+ },
+ },
+ 'data-connections-cloud-integrations': {
+ id: 'data-connections-cloud-integrations',
+ text: 'Cloud integrations',
+ description: 'Manage your cloud integrations',
+ icon: 'bolt',
+ url: '/data-connections/cloud-integrations',
+ parentItem: {
+ id: 'data-connections',
+ text: 'Data Connections',
+ section: 'core',
+ icon: 'link',
+ url: '/data-connections',
+ sortWeight: -1500,
+ children: [
+ {
+ id: 'data-connections-datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/data-connections/datasources',
+ },
+ {
+ id: 'data-connections-plugins',
+ text: 'Plugins',
+ description: 'Manage plugins',
+ icon: 'plug',
+ url: '/data-connections/plugins',
+ },
+ {
+ id: 'data-connections-cloud-integrations',
+ text: 'Cloud integrations',
+ description: 'Manage your cloud integrations',
+ icon: 'bolt',
+ url: '/data-connections/cloud-integrations',
+ },
+ {
+ id: 'data-connections-recorded-queries',
+ text: 'Recorded queries',
+ description: 'Manage your recorded queries',
+ icon: 'record-audio',
+ url: '/data-connections/recorded-queries',
+ },
+ ],
+ },
+ },
+ 'data-connections-recorded-queries': {
+ id: 'data-connections-recorded-queries',
+ text: 'Recorded queries',
+ description: 'Manage your recorded queries',
+ icon: 'record-audio',
+ url: '/data-connections/recorded-queries',
+ parentItem: {
+ id: 'data-connections',
+ text: 'Data Connections',
+ section: 'core',
+ icon: 'link',
+ url: '/data-connections',
+ sortWeight: -1500,
+ children: [
+ {
+ id: 'data-connections-datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/data-connections/datasources',
+ },
+ {
+ id: 'data-connections-plugins',
+ text: 'Plugins',
+ description: 'Manage plugins',
+ icon: 'plug',
+ url: '/data-connections/plugins',
+ },
+ {
+ id: 'data-connections-cloud-integrations',
+ text: 'Cloud integrations',
+ description: 'Manage your cloud integrations',
+ icon: 'bolt',
+ url: '/data-connections/cloud-integrations',
+ },
+ {
+ id: 'data-connections-recorded-queries',
+ text: 'Recorded queries',
+ description: 'Manage your recorded queries',
+ icon: 'record-audio',
+ url: '/data-connections/recorded-queries',
+ },
+ ],
+ },
+ },
+ 'plugin-page-basic-app': {
+ id: 'plugin-page-basic-app',
+ text: 'Basic App',
+ section: 'plugin',
+ img: 'public/plugins/basic-app/img/logo.svg',
+ url: '/a/basic-app/one',
+ sortWeight: -1400,
+ children: [
+ {
+ text: 'Page One',
+ url: '/a/basic-app/one',
+ },
+ {
+ text: 'Page Two',
+ url: '/a/basic-app/two',
+ },
+ {
+ text: 'Page Three',
+ url: '/a/basic-app/three',
+ },
+ {
+ text: 'Page Four',
+ url: '/a/basic-app/four',
+ },
+ {
+ text: 'Configuration',
+ icon: 'fa fa-cog',
+ url: '/plugins/basic-app',
+ },
+ ],
+ },
+ undefined: {
+ text: 'Config',
+ url: '/plugins/grafana-synthetic-monitoring-app',
+ parentItem: {
+ id: 'plugin-page-grafana-synthetic-monitoring-app',
+ text: 'Synthetic Monitoring',
+ section: 'plugin',
+ img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg',
+ url: '/a/grafana-synthetic-monitoring-app/home',
+ sortWeight: -1400,
+ children: [
+ {
+ text: 'Home',
+ url: '/a/grafana-synthetic-monitoring-app/home',
+ },
+ {
+ text: 'Summary',
+ url: '/a/grafana-synthetic-monitoring-app/redirect?dashboard=summary',
+ },
+ {
+ text: 'Checks',
+ url: '/a/grafana-synthetic-monitoring-app/checks',
+ },
+ {
+ text: 'Probes',
+ url: '/a/grafana-synthetic-monitoring-app/probes',
+ },
+ {
+ text: 'Alerts',
+ url: '/a/grafana-synthetic-monitoring-app/alerts',
+ },
+ {
+ text: 'Config',
+ url: '/plugins/grafana-synthetic-monitoring-app',
+ },
+ ],
+ },
+ },
+ 'plugin-page-cloudflare-app': {
+ id: 'plugin-page-cloudflare-app',
+ text: 'Cloudflare Grafana App',
+ section: 'plugin',
+ img: 'public/plugins/cloudflare-app/img/cf_icon.png',
+ sortWeight: -1400,
+ children: [
+ {
+ text: 'Zones',
+ url: '/d/KAVdMAw9k',
+ },
+ {
+ text: 'DNS Firewall',
+ url: '/d/QrKttDVqu',
+ },
+ ],
+ },
+ 'plugin-page-grafana-easystart-app': {
+ id: 'plugin-page-grafana-easystart-app',
+ text: 'Integrations and Connections',
+ section: 'plugin',
+ img: 'public/plugins/grafana-easystart-app/img/logo.svg',
+ url: '/a/grafana-easystart-app',
+ sortWeight: -1400,
+ },
+ 'plugin-page-redis-explorer-app': {
+ id: 'plugin-page-redis-explorer-app',
+ text: 'Redis Explorer',
+ section: 'plugin',
+ img: 'public/plugins/redis-explorer-app/img/logo.svg',
+ url: '/a/redis-explorer-app/',
+ sortWeight: -1400,
+ children: [
+ {
+ text: 'Home',
+ icon: 'home-alt',
+ url: '/a/redis-explorer-app/',
+ },
+ {
+ text: 'Enterprise Clusters',
+ icon: 'apps',
+ url: '/d/1dKhTjtGk',
+ },
+ {
+ text: 'Cluster Overview',
+ icon: 'monitor',
+ url: '/d/viroIzSGz',
+ },
+ {
+ text: 'Cluster Nodes',
+ icon: 'sitemap',
+ url: '/d/hqze6rtGz',
+ },
+ {
+ text: 'Cluster Databases',
+ icon: 'database',
+ url: '/d/k_A8MjtMk',
+ },
+ {
+ text: 'Cluster Alerts',
+ icon: 'info-circle',
+ url: '/d/xESAiFcnk',
+ },
+ ],
+ },
+ 'plugin-page-grafana-synthetic-monitoring-app': {
+ id: 'plugin-page-grafana-synthetic-monitoring-app',
+ text: 'Synthetic Monitoring',
+ section: 'plugin',
+ img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg',
+ url: '/a/grafana-synthetic-monitoring-app/home',
+ sortWeight: -1400,
+ children: [
+ {
+ text: 'Home',
+ url: '/a/grafana-synthetic-monitoring-app/home',
+ },
+ {
+ text: 'Summary',
+ url: '/a/grafana-synthetic-monitoring-app/redirect?dashboard=summary',
+ },
+ {
+ text: 'Checks',
+ url: '/a/grafana-synthetic-monitoring-app/checks',
+ },
+ {
+ text: 'Probes',
+ url: '/a/grafana-synthetic-monitoring-app/probes',
+ },
+ {
+ text: 'Alerts',
+ url: '/a/grafana-synthetic-monitoring-app/alerts',
+ },
+ {
+ text: 'Config',
+ url: '/plugins/grafana-synthetic-monitoring-app',
+ },
+ ],
+ },
+ 'plugin-page-grafana-k6-app': {
+ id: 'plugin-page-grafana-k6-app',
+ text: 'k6 Cloud App',
+ section: 'plugin',
+ img: 'public/plugins/grafana-k6-app/img/logo.svg',
+ url: '/a/grafana-k6-app',
+ sortWeight: -1400,
+ },
+ cfg: {
+ id: 'cfg',
+ text: 'Configuration',
+ section: 'config',
+ subTitle: 'Organization: Main Org.',
+ icon: 'cog',
+ url: '/datasources',
+ sortWeight: -1300,
+ children: [
+ {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ },
+ {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ },
+ {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ },
+ {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ },
+ {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ },
+ {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ },
+ ],
+ },
+ datasources: {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ parentItem: {
+ id: 'cfg',
+ text: 'Configuration',
+ section: 'config',
+ subTitle: 'Organization: Main Org.',
+ icon: 'cog',
+ url: '/datasources',
+ sortWeight: -1300,
+ children: [
+ {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ },
+ {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ },
+ {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ },
+ {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ },
+ {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ },
+ {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ },
+ ],
+ },
+ },
+ users: {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ parentItem: {
+ id: 'cfg',
+ text: 'Configuration',
+ section: 'config',
+ subTitle: 'Organization: Main Org.',
+ icon: 'cog',
+ url: '/datasources',
+ sortWeight: -1300,
+ children: [
+ {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ },
+ {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ },
+ {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ },
+ {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ },
+ {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ },
+ {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ },
+ ],
+ },
+ },
+ teams: {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ parentItem: {
+ id: 'cfg',
+ text: 'Configuration',
+ section: 'config',
+ subTitle: 'Organization: Main Org.',
+ icon: 'cog',
+ url: '/datasources',
+ sortWeight: -1300,
+ children: [
+ {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ },
+ {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ },
+ {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ },
+ {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ },
+ {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ },
+ {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ },
+ ],
+ },
+ },
+ plugins: {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ parentItem: {
+ id: 'cfg',
+ text: 'Configuration',
+ section: 'config',
+ subTitle: 'Organization: Main Org.',
+ icon: 'cog',
+ url: '/datasources',
+ sortWeight: -1300,
+ children: [
+ {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ },
+ {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ },
+ {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ },
+ {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ },
+ {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ },
+ {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ },
+ ],
+ },
+ },
+ 'org-settings': {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ parentItem: {
+ id: 'cfg',
+ text: 'Configuration',
+ section: 'config',
+ subTitle: 'Organization: Main Org.',
+ icon: 'cog',
+ url: '/datasources',
+ sortWeight: -1300,
+ children: [
+ {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ },
+ {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ },
+ {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ },
+ {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ },
+ {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ },
+ {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ },
+ ],
+ },
+ },
+ apikeys: {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ parentItem: {
+ id: 'cfg',
+ text: 'Configuration',
+ section: 'config',
+ subTitle: 'Organization: Main Org.',
+ icon: 'cog',
+ url: '/datasources',
+ sortWeight: -1300,
+ children: [
+ {
+ id: 'datasources',
+ text: 'Data sources',
+ description: 'Add and configure data sources',
+ icon: 'database',
+ url: '/datasources',
+ },
+ {
+ id: 'users',
+ text: 'Users',
+ description: 'Manage org members',
+ icon: 'user',
+ url: '/org/users',
+ },
+ {
+ id: 'teams',
+ text: 'Teams',
+ description: 'Manage org groups',
+ icon: 'users-alt',
+ url: '/org/teams',
+ },
+ {
+ id: 'plugins',
+ text: 'Plugins',
+ description: 'View and configure plugins',
+ icon: 'plug',
+ url: '/plugins',
+ },
+ {
+ id: 'org-settings',
+ text: 'Preferences',
+ description: 'Organization preferences',
+ icon: 'sliders-v-alt',
+ url: '/org',
+ },
+ {
+ id: 'apikeys',
+ text: 'API keys',
+ description: 'Create & manage API keys',
+ icon: 'key-skeleton-alt',
+ url: '/org/apikeys',
+ },
+ ],
+ },
+ },
+ admin: {
+ id: 'admin',
+ text: 'Server Admin',
+ section: 'config',
+ subTitle: 'Manage all users and orgs',
+ icon: 'shield',
+ url: '/admin/users',
+ sortWeight: -1200,
+ hideFromTabs: true,
+ children: [
+ {
+ id: 'global-users',
+ text: 'Users',
+ icon: 'user',
+ url: '/admin/users',
+ },
+ {
+ id: 'global-orgs',
+ text: 'Orgs',
+ icon: 'building',
+ url: '/admin/orgs',
+ },
+ {
+ id: 'server-settings',
+ text: 'Settings',
+ icon: 'sliders-v-alt',
+ url: '/admin/settings',
+ },
+ {
+ id: 'admin-plugins',
+ text: 'Plugins',
+ icon: 'plug',
+ url: '/admin/plugins',
+ },
+ {
+ id: 'upgrading',
+ text: 'Stats and license',
+ icon: 'unlock',
+ url: '/admin/upgrading',
+ },
+ ],
+ },
+ 'global-users': {
+ id: 'global-users',
+ text: 'Users',
+ icon: 'user',
+ url: '/admin/users',
+ parentItem: {
+ id: 'admin',
+ text: 'Server Admin',
+ section: 'config',
+ subTitle: 'Manage all users and orgs',
+ icon: 'shield',
+ url: '/admin/users',
+ sortWeight: -1200,
+ hideFromTabs: true,
+ children: [
+ {
+ id: 'global-users',
+ text: 'Users',
+ icon: 'user',
+ url: '/admin/users',
+ },
+ {
+ id: 'global-orgs',
+ text: 'Orgs',
+ icon: 'building',
+ url: '/admin/orgs',
+ },
+ {
+ id: 'server-settings',
+ text: 'Settings',
+ icon: 'sliders-v-alt',
+ url: '/admin/settings',
+ },
+ {
+ id: 'admin-plugins',
+ text: 'Plugins',
+ icon: 'plug',
+ url: '/admin/plugins',
+ },
+ {
+ id: 'upgrading',
+ text: 'Stats and license',
+ icon: 'unlock',
+ url: '/admin/upgrading',
+ },
+ ],
+ },
+ },
+ 'global-orgs': {
+ id: 'global-orgs',
+ text: 'Orgs',
+ icon: 'building',
+ url: '/admin/orgs',
+ parentItem: {
+ id: 'admin',
+ text: 'Server Admin',
+ section: 'config',
+ subTitle: 'Manage all users and orgs',
+ icon: 'shield',
+ url: '/admin/users',
+ sortWeight: -1200,
+ hideFromTabs: true,
+ children: [
+ {
+ id: 'global-users',
+ text: 'Users',
+ icon: 'user',
+ url: '/admin/users',
+ },
+ {
+ id: 'global-orgs',
+ text: 'Orgs',
+ icon: 'building',
+ url: '/admin/orgs',
+ },
+ {
+ id: 'server-settings',
+ text: 'Settings',
+ icon: 'sliders-v-alt',
+ url: '/admin/settings',
+ },
+ {
+ id: 'admin-plugins',
+ text: 'Plugins',
+ icon: 'plug',
+ url: '/admin/plugins',
+ },
+ {
+ id: 'upgrading',
+ text: 'Stats and license',
+ icon: 'unlock',
+ url: '/admin/upgrading',
+ },
+ ],
+ },
+ },
+ 'server-settings': {
+ id: 'server-settings',
+ text: 'Settings',
+ icon: 'sliders-v-alt',
+ url: '/admin/settings',
+ parentItem: {
+ id: 'admin',
+ text: 'Server Admin',
+ section: 'config',
+ subTitle: 'Manage all users and orgs',
+ icon: 'shield',
+ url: '/admin/users',
+ sortWeight: -1200,
+ hideFromTabs: true,
+ children: [
+ {
+ id: 'global-users',
+ text: 'Users',
+ icon: 'user',
+ url: '/admin/users',
+ },
+ {
+ id: 'global-orgs',
+ text: 'Orgs',
+ icon: 'building',
+ url: '/admin/orgs',
+ },
+ {
+ id: 'server-settings',
+ text: 'Settings',
+ icon: 'sliders-v-alt',
+ url: '/admin/settings',
+ },
+ {
+ id: 'admin-plugins',
+ text: 'Plugins',
+ icon: 'plug',
+ url: '/admin/plugins',
+ },
+ {
+ id: 'upgrading',
+ text: 'Stats and license',
+ icon: 'unlock',
+ url: '/admin/upgrading',
+ },
+ ],
+ },
+ },
+ 'admin-plugins': {
+ id: 'admin-plugins',
+ text: 'Plugins',
+ icon: 'plug',
+ url: '/admin/plugins',
+ parentItem: {
+ id: 'admin',
+ text: 'Server Admin',
+ section: 'config',
+ subTitle: 'Manage all users and orgs',
+ icon: 'shield',
+ url: '/admin/users',
+ sortWeight: -1200,
+ hideFromTabs: true,
+ children: [
+ {
+ id: 'global-users',
+ text: 'Users',
+ icon: 'user',
+ url: '/admin/users',
+ },
+ {
+ id: 'global-orgs',
+ text: 'Orgs',
+ icon: 'building',
+ url: '/admin/orgs',
+ },
+ {
+ id: 'server-settings',
+ text: 'Settings',
+ icon: 'sliders-v-alt',
+ url: '/admin/settings',
+ },
+ {
+ id: 'admin-plugins',
+ text: 'Plugins',
+ icon: 'plug',
+ url: '/admin/plugins',
+ },
+ {
+ id: 'upgrading',
+ text: 'Stats and license',
+ icon: 'unlock',
+ url: '/admin/upgrading',
+ },
+ ],
+ },
+ },
+ upgrading: {
+ id: 'upgrading',
+ text: 'Stats and license',
+ icon: 'unlock',
+ url: '/admin/upgrading',
+ parentItem: {
+ id: 'admin',
+ text: 'Server Admin',
+ section: 'config',
+ subTitle: 'Manage all users and orgs',
+ icon: 'shield',
+ url: '/admin/users',
+ sortWeight: -1200,
+ hideFromTabs: true,
+ children: [
+ {
+ id: 'global-users',
+ text: 'Users',
+ icon: 'user',
+ url: '/admin/users',
+ },
+ {
+ id: 'global-orgs',
+ text: 'Orgs',
+ icon: 'building',
+ url: '/admin/orgs',
+ },
+ {
+ id: 'server-settings',
+ text: 'Settings',
+ icon: 'sliders-v-alt',
+ url: '/admin/settings',
+ },
+ {
+ id: 'admin-plugins',
+ text: 'Plugins',
+ icon: 'plug',
+ url: '/admin/plugins',
+ },
+ {
+ id: 'upgrading',
+ text: 'Stats and license',
+ icon: 'unlock',
+ url: '/admin/upgrading',
+ },
+ ],
+ },
+ },
+ profile: {
+ id: 'profile',
+ text: 'admin',
+ section: 'config',
+ img: '/avatar/46d229b033af06a191ff2267bca9ae56',
+ url: '/profile',
+ sortWeight: -1100,
+ children: [
+ {
+ id: 'profile-settings',
+ text: 'Preferences',
+ icon: 'sliders-v-alt',
+ url: '/profile',
+ },
+ {
+ id: 'notifications',
+ text: 'Notification history',
+ icon: 'bell',
+ url: '/notifications',
+ },
+ {
+ id: 'change-password',
+ text: 'Change password',
+ icon: 'lock',
+ url: '/profile/password',
+ },
+ {
+ id: 'sign-out',
+ text: 'Sign out',
+ icon: 'arrow-from-right',
+ url: '/logout',
+ target: '_self',
+ hideFromTabs: true,
+ },
+ ],
+ },
+ 'profile-settings': {
+ id: 'profile-settings',
+ text: 'Preferences',
+ icon: 'sliders-v-alt',
+ url: '/profile',
+ parentItem: {
+ id: 'profile',
+ text: 'admin',
+ section: 'config',
+ img: '/avatar/46d229b033af06a191ff2267bca9ae56',
+ url: '/profile',
+ sortWeight: -1100,
+ children: [
+ {
+ id: 'profile-settings',
+ text: 'Preferences',
+ icon: 'sliders-v-alt',
+ url: '/profile',
+ },
+ {
+ id: 'notifications',
+ text: 'Notification history',
+ icon: 'bell',
+ url: '/notifications',
+ },
+ {
+ id: 'change-password',
+ text: 'Change password',
+ icon: 'lock',
+ url: '/profile/password',
+ },
+ {
+ id: 'sign-out',
+ text: 'Sign out',
+ icon: 'arrow-from-right',
+ url: '/logout',
+ target: '_self',
+ hideFromTabs: true,
+ },
+ ],
+ },
+ },
+ notifications: {
+ id: 'notifications',
+ text: 'Notification history',
+ icon: 'bell',
+ url: '/notifications',
+ parentItem: {
+ id: 'profile',
+ text: 'admin',
+ section: 'config',
+ img: '/avatar/46d229b033af06a191ff2267bca9ae56',
+ url: '/profile',
+ sortWeight: -1100,
+ children: [
+ {
+ id: 'profile-settings',
+ text: 'Preferences',
+ icon: 'sliders-v-alt',
+ url: '/profile',
+ },
+ {
+ id: 'notifications',
+ text: 'Notification history',
+ icon: 'bell',
+ url: '/notifications',
+ },
+ {
+ id: 'change-password',
+ text: 'Change password',
+ icon: 'lock',
+ url: '/profile/password',
+ },
+ {
+ id: 'sign-out',
+ text: 'Sign out',
+ icon: 'arrow-from-right',
+ url: '/logout',
+ target: '_self',
+ hideFromTabs: true,
+ },
+ ],
+ },
+ },
+ 'change-password': {
+ id: 'change-password',
+ text: 'Change password',
+ icon: 'lock',
+ url: '/profile/password',
+ parentItem: {
+ id: 'profile',
+ text: 'admin',
+ section: 'config',
+ img: '/avatar/46d229b033af06a191ff2267bca9ae56',
+ url: '/profile',
+ sortWeight: -1100,
+ children: [
+ {
+ id: 'profile-settings',
+ text: 'Preferences',
+ icon: 'sliders-v-alt',
+ url: '/profile',
+ },
+ {
+ id: 'notifications',
+ text: 'Notification history',
+ icon: 'bell',
+ url: '/notifications',
+ },
+ {
+ id: 'change-password',
+ text: 'Change password',
+ icon: 'lock',
+ url: '/profile/password',
+ },
+ {
+ id: 'sign-out',
+ text: 'Sign out',
+ icon: 'arrow-from-right',
+ url: '/logout',
+ target: '_self',
+ hideFromTabs: true,
+ },
+ ],
+ },
+ },
+ 'sign-out': {
+ id: 'sign-out',
+ text: 'Sign out',
+ icon: 'arrow-from-right',
+ url: '/logout',
+ target: '_self',
+ hideFromTabs: true,
+ parentItem: {
+ id: 'profile',
+ text: 'admin',
+ section: 'config',
+ img: '/avatar/46d229b033af06a191ff2267bca9ae56',
+ url: '/profile',
+ sortWeight: -1100,
+ children: [
+ {
+ id: 'profile-settings',
+ text: 'Preferences',
+ icon: 'sliders-v-alt',
+ url: '/profile',
+ },
+ {
+ id: 'notifications',
+ text: 'Notification history',
+ icon: 'bell',
+ url: '/notifications',
+ },
+ {
+ id: 'change-password',
+ text: 'Change password',
+ icon: 'lock',
+ url: '/profile/password',
+ },
+ {
+ id: 'sign-out',
+ text: 'Sign out',
+ icon: 'arrow-from-right',
+ url: '/logout',
+ target: '_self',
+ hideFromTabs: true,
+ },
+ ],
+ },
+ },
+ help: {
+ id: 'help',
+ text: 'Help',
+ section: 'config',
+ subTitle: 'Grafana v9.0.0-pre (abb5c6109a)',
+ icon: 'question-circle',
+ url: '#',
+ sortWeight: -1000,
+ },
+};
diff --git a/public/app/features/data-connections/constants.ts b/public/app/features/data-connections/constants.ts
new file mode 100644
index 00000000000..c35a30aba1b
--- /dev/null
+++ b/public/app/features/data-connections/constants.ts
@@ -0,0 +1,12 @@
+// The ID of the app plugin that we render under that "Cloud Integrations" tab
+export const CLOUD_ONBOARDING_APP_ID = 'grafana-easystart-app';
+
+// The ID of the main nav-tree item (the main item in the NavIndex)
+export const ROUTE_BASE_ID = 'data-connections';
+
+export enum ROUTES {
+ DataSources = '/data-connections/data-sources',
+ Plugins = '/data-connections/plugins',
+ CloudIntegrations = '/data-connections/cloud-integrations',
+ RecordedQueries = '/data-connections/recorded-queries',
+}
diff --git a/public/app/features/data-connections/hooks/useNavModel.ts b/public/app/features/data-connections/hooks/useNavModel.ts
new file mode 100644
index 00000000000..b030c3e0b66
--- /dev/null
+++ b/public/app/features/data-connections/hooks/useNavModel.ts
@@ -0,0 +1,26 @@
+import { useSelector } from 'react-redux';
+import { useLocation } from 'react-router-dom';
+
+import { StoreState } from 'app/types/store';
+
+import { ROUTE_BASE_ID } from '../constants';
+
+// We need this utility logic to make sure that the tab with the current URL is marked as active.
+// (In case we were using `getNavModel()` from app/core/selectors/navModel, then we would need to set
+// the child nav-model-item's ID on the call-site.)
+export const useNavModel = () => {
+ const { pathname } = useLocation();
+ const navIndex = useSelector((state: StoreState) => state.navIndex);
+ const node = navIndex[ROUTE_BASE_ID];
+ const main = node;
+
+ main.children = main.children?.map((item) => ({
+ ...item,
+ active: item.url === pathname,
+ }));
+
+ return {
+ node,
+ main,
+ };
+};
diff --git a/public/app/features/data-connections/routes.tsx b/public/app/features/data-connections/routes.tsx
new file mode 100644
index 00000000000..5366ec53caa
--- /dev/null
+++ b/public/app/features/data-connections/routes.tsx
@@ -0,0 +1,21 @@
+import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
+import { config } from 'app/core/config';
+import { RouteDescriptor } from 'app/core/navigation/types';
+
+import { ROUTE_BASE_ID } from './constants';
+
+export function getRoutes(): RouteDescriptor[] {
+ if (config.featureToggles.dataConnectionsConsole) {
+ return [
+ {
+ path: `/${ROUTE_BASE_ID}`,
+ exact: false,
+ component: SafeDynamicImport(
+ () => import(/* webpackChunkName: "DataConnectionsPage"*/ 'app/features/data-connections/DataConnectionsPage')
+ ),
+ },
+ ];
+ }
+
+ return [];
+}
diff --git a/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx b/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx
new file mode 100644
index 00000000000..c912fb8d9b7
--- /dev/null
+++ b/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx
@@ -0,0 +1,25 @@
+import { css } from '@emotion/css';
+import React, { ReactElement } from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+import { AppPluginLoader } from 'app/features/plugins/components/AppPluginLoader';
+
+import { CLOUD_ONBOARDING_APP_ID } from '../../constants';
+
+export function CloudIntegrations(): ReactElement | null {
+ const s = useStyles2(getStyles);
+
+ return (
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ // We would like to force the app to stay inside the provided tab
+ container: css`
+ position: relative;
+ `,
+});
diff --git a/public/app/features/data-connections/tabs/CloudIntegrations/index.tsx b/public/app/features/data-connections/tabs/CloudIntegrations/index.tsx
new file mode 100644
index 00000000000..41c904956aa
--- /dev/null
+++ b/public/app/features/data-connections/tabs/CloudIntegrations/index.tsx
@@ -0,0 +1 @@
+export * from './CloudIntegrations';
diff --git a/public/app/features/data-connections/tabs/DataSources/DataSources.tsx b/public/app/features/data-connections/tabs/DataSources/DataSources.tsx
new file mode 100644
index 00000000000..a809c21511e
--- /dev/null
+++ b/public/app/features/data-connections/tabs/DataSources/DataSources.tsx
@@ -0,0 +1,5 @@
+import React, { ReactElement } from 'react';
+
+export function DataSources(): ReactElement | null {
+ return The list of data sources is under development.
;
+}
diff --git a/public/app/features/data-connections/tabs/DataSources/index.tsx b/public/app/features/data-connections/tabs/DataSources/index.tsx
new file mode 100644
index 00000000000..5d681837186
--- /dev/null
+++ b/public/app/features/data-connections/tabs/DataSources/index.tsx
@@ -0,0 +1 @@
+export * from './DataSources';
diff --git a/public/app/features/data-connections/tabs/Plugins/Plugins.tsx b/public/app/features/data-connections/tabs/Plugins/Plugins.tsx
new file mode 100644
index 00000000000..e1befaab13f
--- /dev/null
+++ b/public/app/features/data-connections/tabs/Plugins/Plugins.tsx
@@ -0,0 +1,5 @@
+import React, { ReactElement } from 'react';
+
+export function Plugins(): ReactElement | null {
+ return The list of plugins is under development
;
+}
diff --git a/public/app/features/data-connections/tabs/Plugins/index.tsx b/public/app/features/data-connections/tabs/Plugins/index.tsx
new file mode 100644
index 00000000000..20c2507f6d8
--- /dev/null
+++ b/public/app/features/data-connections/tabs/Plugins/index.tsx
@@ -0,0 +1 @@
+export * from './Plugins';
diff --git a/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx b/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx
new file mode 100644
index 00000000000..90ec2c24f76
--- /dev/null
+++ b/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx
@@ -0,0 +1,5 @@
+import React, { ReactElement } from 'react';
+
+export function RecordedQueries(): ReactElement | null {
+ return The recorded queries tab is under development.
;
+}
diff --git a/public/app/features/data-connections/tabs/RecordedQueries/index.tsx b/public/app/features/data-connections/tabs/RecordedQueries/index.tsx
new file mode 100644
index 00000000000..fc4ff37714f
--- /dev/null
+++ b/public/app/features/data-connections/tabs/RecordedQueries/index.tsx
@@ -0,0 +1 @@
+export * from './RecordedQueries';
diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx
index 97cf5f21bd4..84f610c2072 100644
--- a/public/app/features/explore/SecondaryActions.tsx
+++ b/public/app/features/explore/SecondaryActions.tsx
@@ -21,6 +21,11 @@ const getStyles = (theme: GrafanaTheme2) => {
containerMargin: css`
margin-top: ${theme.spacing(2)};
`,
+ exploreActiveButton: css`
+ box-shadow: ${theme.shadows.z1};
+ border: 1px solid ${theme.colors.warning.border};
+ color: ${theme.colors.warning.text};
+ `,
};
};
export function SecondaryActions(props: Props) {
@@ -44,7 +49,7 @@ export function SecondaryActions(props: Props) {