Merge branch 'main' of github.com:grafana/grafana into task/45304-integrate-alerts-with-folders
This commit is contained in:
+6
-21
@@ -8,21 +8,9 @@ exports[`no enzyme tests`] = {
|
||||
"packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx:2315765050": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/BigValue/BigValue.test.tsx:261212211": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.test.tsx:2771878691": [
|
||||
[1, 18, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx:1355456933": [
|
||||
[1, 31, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx:3194817479": [
|
||||
[2, 33, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx:3838344574": [
|
||||
[1, 17, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx:3475964456": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
@@ -116,7 +104,7 @@ exports[`no enzyme tests`] = {
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.test.js:2288177454": [
|
||||
[15, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.js:2545268168": [
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.js:2127169675": [
|
||||
[15, 17, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:2454947085": [
|
||||
@@ -197,9 +185,6 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/core/components/Select/MetricSelect.test.tsx:3409251428": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/core/specs/OrgSwitcher.test.tsx:848670248": [
|
||||
[2, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/alerting/AlertRuleList.test.tsx:1800339390": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
@@ -218,7 +203,7 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx:4134073823": [
|
||||
[1, 17, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:1141305288": [
|
||||
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:1044891955": [
|
||||
[1, 35, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx:1798654441": [
|
||||
@@ -290,6 +275,9 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/features/folders/FolderSettingsPage.test.tsx:1751147194": [
|
||||
[2, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/invites/InviteesTable.test.tsx:3077684439": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/org/OrgDetailsPage.test.tsx:2540662821": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
@@ -317,13 +305,10 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/features/teams/TeamSettings.test.tsx:2628968507": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/users/InviteesTable.test.tsx:2271264692": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/users/UsersActionBar.test.tsx:4031641375": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/users/UsersListPage.test.tsx:2523261097": [
|
||||
"public/app/features/users/UsersListPage.test.tsx:2626906707": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/users/UsersTable.test.tsx:3051231816": [
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@
|
||||
|
||||
load('scripts/drone/pipelines/pr.star', 'pr_pipelines')
|
||||
load('scripts/drone/pipelines/main.star', 'main_pipelines')
|
||||
load('scripts/drone/pipelines/docs.star', 'docs_pipelines')
|
||||
load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline')
|
||||
load('scripts/drone/version.star', 'version_branch_pipelines')
|
||||
load('scripts/drone/pipelines/cron.star', 'cronjobs')
|
||||
@@ -13,7 +14,7 @@ load('scripts/drone/vault.star', 'secrets')
|
||||
|
||||
def main(ctx):
|
||||
edition = 'oss'
|
||||
return pr_pipelines(edition=edition) + main_pipelines(edition=edition) + release_pipelines() + \
|
||||
return pr_pipelines(edition=edition) + docs_pipelines(edition=edition) + main_pipelines(edition=edition) + release_pipelines() + \
|
||||
publish_image_pipelines('public') + publish_image_pipelines('security') + \
|
||||
publish_artifacts_pipelines('security') + publish_artifacts_pipelines('public') + \
|
||||
publish_npm_pipelines('public') + publish_packages_pipeline() + \
|
||||
|
||||
+104
-47
@@ -11,7 +11,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -93,6 +93,9 @@ steps:
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
paths:
|
||||
exclude:
|
||||
- docs/**
|
||||
type: docker
|
||||
volumes:
|
||||
- host:
|
||||
@@ -111,7 +114,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -288,20 +291,6 @@ steps:
|
||||
failure: always
|
||||
image: grafana/docker-puppeteer:1.0.0
|
||||
name: test-a11y-frontend
|
||||
- commands:
|
||||
- ./scripts/ci-reference-docs-lint.sh ci
|
||||
depends_on:
|
||||
- build-frontend
|
||||
image: grafana/build-container:1.4.9
|
||||
name: build-frontend-docs
|
||||
- commands:
|
||||
- mkdir -p /hugo/content/docs/grafana
|
||||
- cp -r docs/sources/* /hugo/content/docs/grafana/latest/
|
||||
- cd /hugo && make prod
|
||||
depends_on:
|
||||
- build-frontend-docs
|
||||
image: grafana/docs-base:latest
|
||||
name: build-docs-website
|
||||
- commands:
|
||||
- ls dist/*.tar.gz*
|
||||
- cp dist/*.tar.gz* packaging/docker/
|
||||
@@ -324,6 +313,9 @@ steps:
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
paths:
|
||||
exclude:
|
||||
- docs/**
|
||||
type: docker
|
||||
volumes:
|
||||
- host:
|
||||
@@ -362,7 +354,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -399,6 +391,9 @@ steps:
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
paths:
|
||||
exclude:
|
||||
- docs/**
|
||||
type: docker
|
||||
volumes:
|
||||
- host:
|
||||
@@ -413,6 +408,68 @@ volumes:
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: pr-docs
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
arch: amd64
|
||||
os: linux
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
image: alpine:3.15
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- make gen-go
|
||||
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
|
||||
- yarn install --immutable
|
||||
image: grafana/build-container:1.4.9
|
||||
name: initialize
|
||||
- commands:
|
||||
- ./bin/grabpl build-frontend --jobs 8 --no-install-deps --edition oss --build-id
|
||||
${DRONE_BUILD_NUMBER} --no-pull-enterprise
|
||||
depends_on:
|
||||
- initialize
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
image: grafana/build-container:1.4.9
|
||||
name: build-frontend
|
||||
- commands:
|
||||
- ./scripts/ci-reference-docs-lint.sh ci
|
||||
depends_on:
|
||||
- build-frontend
|
||||
image: grafana/build-container:1.4.9
|
||||
name: build-frontend-docs
|
||||
- commands:
|
||||
- mkdir -p /hugo/content/docs/grafana
|
||||
- cp -r docs/sources/* /hugo/content/docs/grafana/latest/
|
||||
- cd /hugo && make prod
|
||||
depends_on:
|
||||
- build-frontend-docs
|
||||
image: grafana/docs-base:latest
|
||||
name: build-docs-website
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
paths:
|
||||
include:
|
||||
- docs/**
|
||||
- packages/**
|
||||
type: docker
|
||||
volumes:
|
||||
- host:
|
||||
path: /var/run/docker.sock
|
||||
name: docker
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: main-test
|
||||
node:
|
||||
type: no-parallel
|
||||
@@ -423,7 +480,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -524,7 +581,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -909,7 +966,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -977,7 +1034,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: initialize
|
||||
@@ -1060,7 +1117,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1142,7 +1199,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1446,7 +1503,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1567,7 +1624,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1651,7 +1708,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: initialize
|
||||
@@ -1710,7 +1767,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2050,7 +2107,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2223,7 +2280,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2351,7 +2408,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -2426,7 +2483,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2504,7 +2561,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2565,7 +2622,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2644,7 +2701,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2706,7 +2763,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2742,7 +2799,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2789,7 +2846,7 @@ steps:
|
||||
name: initialize
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2837,7 +2894,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2900,7 +2957,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3159,7 +3216,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3273,7 +3330,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3350,7 +3407,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: initialize
|
||||
@@ -3398,7 +3455,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3731,7 +3788,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3894,7 +3951,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4012,7 +4069,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.0/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.3/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -4209,6 +4266,6 @@ kind: secret
|
||||
name: gcp_upload_artifacts_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 559c02d03809e345c15ec6badc083947ac12d79dc851596a317a5bd83ad75941
|
||||
hmac: 703b9a44a073ab768a774029cee2c3fc7c5fe7b7e01495e18f048bc7d1f007b0
|
||||
|
||||
...
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ go.sum @grafana/backend-platform
|
||||
/public/app/features/live/ @grafana/grafana-edge-squad
|
||||
/public/app/features/explore/ @grafana/observability-experience-squad
|
||||
/public/app/features/plugins @grafana/plugins-platform-frontend
|
||||
/public/app/core/components/TransformersUI/spatial @grafana/grafana-edge-squad
|
||||
/public/app/features/transformers/spatial @grafana/grafana-edge-squad
|
||||
/public/app/plugins/panel/alertlist @grafana/alerting-squad
|
||||
/public/app/plugins/panel/barchart @grafana/grafana-bi-squad
|
||||
/public/app/plugins/panel/heatmap @grafana/grafana-bi-squad
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
|
||||
- name: Get link for the Github Action job
|
||||
id: job
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/workflows/scripts/pr-get-job-link.js')
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: 'Download artifact'
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
with:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
run: unzip "${ARTIFACT_FOLDER}/${ARTIFACT_NAME}.zip" -d "${ARTIFACT_FOLDER}"
|
||||
|
||||
- name: Parsing levitate result
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
id: levitate-run
|
||||
with:
|
||||
script: |
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Check if "breaking change" label exists
|
||||
id: does-label-exist
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
${{ steps.levitate-run.outputs.message }}
|
||||
|
||||
[Console output](${{ steps.levitate-run.outputs.job_link }})
|
||||
[Read our guideline](../../contribute/breaking-changes-guide.md)
|
||||
[Read our guideline](https://github.com/grafana/grafana/blob/main/contribute/breaking-changes-guide.md)
|
||||
|
||||
- name: Remove comment on PR
|
||||
if: ${{ steps.levitate-run.outputs.exit_code == 0 }}
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
|
||||
- name: Add "breaking change" label
|
||||
if: ${{ steps.levitate-run.outputs.exit_code == 1 && steps.does-label-exist.outputs.result == 0 }}
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
|
||||
with:
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Remove "breaking change" label
|
||||
if: ${{ steps.levitate-run.outputs.exit_code == 0 && steps.does-label-exist.outputs.result == 1 }}
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
|
||||
with:
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
# Related issue: https://github.com/renovatebot/renovate/issues/1908
|
||||
- name: Add "grafana/plugins-platform-frontend" as a reviewer
|
||||
if: ${{ steps.levitate-run.outputs.exit_code == 1 }}
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
|
||||
with:
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
|
||||
- name: Remove "grafana/plugins-platform-frontend" from the list of reviewers
|
||||
if: ${{ steps.levitate-run.outputs.exit_code == 0 }}
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }}
|
||||
with:
|
||||
|
||||
Vendored
+2
-1
@@ -15,7 +15,8 @@
|
||||
"name": "Debug Jest test",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand", "${file}"],
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeArgs": ["run", "jest", "--runInBand", "${file}"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"port": 9229
|
||||
|
||||
@@ -1,3 +1,73 @@
|
||||
<!-- 8.4.1 START -->
|
||||
|
||||
# 8.4.1 (2022-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Cloudwatch:** Add support for AWS/PrivateLink\* metrics and dimensions. [#45515](https://github.com/grafana/grafana/pull/45515), [@szymonpk](https://github.com/szymonpk)
|
||||
- **Configuration:** Add ability to customize okta login button name and icon. [#44079](https://github.com/grafana/grafana/pull/44079), [@DanCech](https://github.com/DanCech)
|
||||
- **Tempo:** Switch out Select with AsyncSelect component to get loading state in Tempo Search. [#45110](https://github.com/grafana/grafana/pull/45110), [@CatPerry](https://github.com/CatPerry)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Alerting:** Fix migrations by making send_alerts_to field nullable. [#45572](https://github.com/grafana/grafana/pull/45572), [@santihernandezc](https://github.com/santihernandezc)
|
||||
|
||||
<!-- 8.4.1 END -->
|
||||
<!-- 8.4.0 START -->
|
||||
|
||||
# 8.4.0 (2022-02-16)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **API:** Extract OpenAPI specification from source code using go-swagger. [#40528](https://github.com/grafana/grafana/pull/40528), [@papagian](https://github.com/papagian)
|
||||
- **AccessControl:** Disable user remove and user update roles when they do not have the permissions. [#43429](https://github.com/grafana/grafana/pull/43429), [@Jguer](https://github.com/Jguer)
|
||||
- **AccessControl:** Provisioning for teams. [#43767](https://github.com/grafana/grafana/pull/43767), [@gamab](https://github.com/gamab)
|
||||
- **API:** Add usage stats preview endpoint. [#43899](https://github.com/grafana/grafana/pull/43899), [@Jguer](https://github.com/Jguer)
|
||||
- **Alerting:** Move slow queries in the scheduler to another goroutine. [#44423](https://github.com/grafana/grafana/pull/44423), [@grobinson-grafana](https://github.com/grobinson-grafana)
|
||||
- **Alerting:** Use time.Ticker instead of alerting.Ticker in ngalert. [#44395](https://github.com/grafana/grafana/pull/44395), [@grobinson-grafana](https://github.com/grobinson-grafana)
|
||||
- **Alerting:** add custom grouping to Alert Panel. [#44559](https://github.com/grafana/grafana/pull/44559), [@gillesdemey](https://github.com/gillesdemey)
|
||||
- **Analytics:** Add user id tracking to google analytics. [#42763](https://github.com/grafana/grafana/pull/42763), [@autoric](https://github.com/autoric)
|
||||
- **Angular:** Add AngularJS plugin support deprecation plan to docs site. [#45149](https://github.com/grafana/grafana/pull/45149), [@torkelo](https://github.com/torkelo)
|
||||
- **Auth:** implement auto_sign_up for auth.jwt. [#43502](https://github.com/grafana/grafana/pull/43502), [@sakjur](https://github.com/sakjur)
|
||||
- **Azure Monitor Logs:** Order subscriptions in resource picker by name. [#45228](https://github.com/grafana/grafana/pull/45228), [@sunker](https://github.com/sunker)
|
||||
- **Azure monitor Logs:** Optimize data fetching in resource picker. [#44549](https://github.com/grafana/grafana/pull/44549), [@sunker](https://github.com/sunker)
|
||||
- **AzureMonitor:** Filter list of resources by resourceType. [#43522](https://github.com/grafana/grafana/pull/43522), [@andresmgot](https://github.com/andresmgot)
|
||||
- **BarChart:** color by field, x time field, bar radius, label skipping. [#43257](https://github.com/grafana/grafana/pull/43257), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Chore:** Implement OpenTelemetry in Grafana. [#42674](https://github.com/grafana/grafana/pull/42674), [@idafurjes](https://github.com/idafurjes)
|
||||
- **Cloud Monitoring:** Adds metric type to Metric drop down options. [#43268](https://github.com/grafana/grafana/pull/43268), [@tw1nk](https://github.com/tw1nk)
|
||||
- **CloudWatch:** Add Data Lifecycle Manager metrics and dimension. [#43310](https://github.com/grafana/grafana/pull/43310), [@ilyastoli](https://github.com/ilyastoli)
|
||||
- **CloudWatch:** Add Missing Elasticache Host-level metrics. [#43455](https://github.com/grafana/grafana/pull/43455), [@dhendo](https://github.com/dhendo)
|
||||
- **CloudWatch:** Add all ElastiCache Redis Metrics. [#43336](https://github.com/grafana/grafana/pull/43336), [@siavashs](https://github.com/siavashs)
|
||||
- **CloudWatch:** Add new AWS/ES metrics. [#43034](https://github.com/grafana/grafana/pull/43034), [@sunker](https://github.com/sunker)
|
||||
- **Cloudwatch:** Add syntax highlighting and autocomplete for "Metric Search". [#43985](https://github.com/grafana/grafana/pull/43985), [@sarahzinger](https://github.com/sarahzinger)
|
||||
- **Explore:** Support custom display label for exemplar links for Prometheus datasource. [#42732](https://github.com/grafana/grafana/pull/42732), [@JokerQueue](https://github.com/JokerQueue)
|
||||
- **Hotkeys:** Make time range absolute/permanent. [#43802](https://github.com/grafana/grafana/pull/43802), [@davkal](https://github.com/davkal)
|
||||
- **Playlists:** Enable sharing direct links to playlists. [#44161](https://github.com/grafana/grafana/pull/44161), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **SQLStore:** Prevent concurrent migrations. [#44101](https://github.com/grafana/grafana/pull/44101), [@papagian](https://github.com/papagian)
|
||||
- **SSE:** Add Mode to drop NaN/Inf/Null in Reduction operations. [#43583](https://github.com/grafana/grafana/pull/43583), [@kylebrandt](https://github.com/kylebrandt)
|
||||
- **Setting:** Support configuring feature toggles with bools instead of just passing an array. [#43326](https://github.com/grafana/grafana/pull/43326), [@bergquist](https://github.com/bergquist)
|
||||
- **TimeSeries:** Add support for negative Y and constant transform. [#44774](https://github.com/grafana/grafana/pull/44774), [@dprokop](https://github.com/dprokop)
|
||||
- **Transformations:** Add 'JSON' field type to ConvertFieldTypeTransformer. [#42624](https://github.com/grafana/grafana/pull/42624), [@sd2k](https://github.com/sd2k)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Auth:** Guarantee consistency of signed SigV4 headers. [#45054](https://github.com/grafana/grafana/pull/45054), [@wbrowne](https://github.com/wbrowne)
|
||||
- **CloudWatch:** Fix MetricName resetting on Namespace change. [#44165](https://github.com/grafana/grafana/pull/44165), [@yaelleC](https://github.com/yaelleC)
|
||||
- **Cloudwatch :** Fixed resetting metric name when changing namespace in Metric Query. [#44612](https://github.com/grafana/grafana/pull/44612), [@yaelleC](https://github.com/yaelleC)
|
||||
- **Explore:** Avoid locking timepicker when range is inverted. [#44790](https://github.com/grafana/grafana/pull/44790), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Instrumentation:** Fix HTTP request instrumentation of authentication failures. [#44234](https://github.com/grafana/grafana/pull/44234), [@marefr](https://github.com/marefr)
|
||||
- **LibraryPanels:** Prevent long descriptions and names from obscuring the delete button. [#45190](https://github.com/grafana/grafana/pull/45190), [@zuchka](https://github.com/zuchka)
|
||||
- **OAuth:** Fix parsing of ID token if header contains non-string value. [#44159](https://github.com/grafana/grafana/pull/44159), [@marefr](https://github.com/marefr)
|
||||
- **Panel Edit:** Visualization search now works correctly with special characters. [#45137](https://github.com/grafana/grafana/pull/45137), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Provisioning:** Fix duplicate validation when multiple organizations have been configured. [#44151](https://github.com/grafana/grafana/pull/44151), [@marefr](https://github.com/marefr)
|
||||
- **QueryField:** Fix issue with undo history when suggestion is inserted (#28656). [#39114](https://github.com/grafana/grafana/pull/39114), [@glintik](https://github.com/glintik)
|
||||
- **TablePanel:** Do not prefix columns with frame name if multiple frames and override active. [#45174](https://github.com/grafana/grafana/pull/45174), [@mdvictor](https://github.com/mdvictor)
|
||||
|
||||
### Deprecations
|
||||
|
||||
AngularJS plugin support is now in a deprecated state, meaning it will be removed in a future release. Currently, that is planned for version 10 (in 2023). The documentation site has an [article](https://grafana.com/docs/grafana/next/developers/angular_deprecation/) with more details on why, when, and how. Issue [#45149](https://github.com/grafana/grafana/issues/45149)
|
||||
|
||||
<!-- 8.4.0 END -->
|
||||
<!-- 8.4.0-beta1 START -->
|
||||
|
||||
# 8.4.0-beta1 (2022-02-02)
|
||||
|
||||
@@ -292,6 +292,9 @@ content_security_policy = false
|
||||
# $ROOT_PATH is server.root_url without the protocol.
|
||||
content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
|
||||
|
||||
# Controls if old angular plugins are supported or not. This will be disabled by default in Grafana v9.
|
||||
angular_support_enabled = true
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
@@ -497,6 +500,7 @@ allowed_groups =
|
||||
#################################### Okta OAuth #######################
|
||||
[auth.okta]
|
||||
name = Okta
|
||||
icon = okta
|
||||
enabled = false
|
||||
allow_sign_up = true
|
||||
client_id = some_id
|
||||
@@ -513,6 +517,7 @@ role_attribute_strict = false
|
||||
#################################### Generic OAuth #######################
|
||||
[auth.generic_oauth]
|
||||
name = OAuth
|
||||
icon = signin
|
||||
enabled = false
|
||||
allow_sign_up = true
|
||||
client_id = some_id
|
||||
|
||||
@@ -292,6 +292,9 @@
|
||||
# $ROOT_PATH is server.root_url without the protocol.
|
||||
;content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
|
||||
|
||||
# Controls if old angular plugins are supported or not. This will be disabled by default in Grafana v9.
|
||||
;angular_support_enabled = true
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/Users/mitchel/Workspace/grafana-enterprise/src/devenv/blocks/saml-enterprise
|
||||
+1
-1
@@ -10,7 +10,7 @@ Yarn >= 1.22.4
|
||||
## Build the doc site
|
||||
|
||||
1. On the command line, first change to the docs folder: `cd docs`.
|
||||
1. Run `make docs-quick`. This launches a preview of the website with the current grafana docs at `http://localhost:3002/docs/grafana/next/` which will refresh automatically when changes are made to content in the `sources` directory.
|
||||
1. Run `make docs`. This launches a preview of the website with the current grafana docs at `http://localhost:3002/docs/grafana/next/` which will refresh automatically when changes are made to content in the `sources` directory.
|
||||
|
||||
If you have the grafana/website repo checked out in the same directory as the grafana repo, then you can run `make docs-local-static` to use local assets (such as images).
|
||||
|
||||
|
||||
@@ -582,6 +582,21 @@ Set Content Security Policy template used when adding the Content-Security-Polic
|
||||
|
||||
<hr />
|
||||
|
||||
### angular_support_enabled
|
||||
|
||||
This currently defaults to `true` but will in Grafana v9 default to `false`. When set to false the angular framework and support components will not be loaded. This means that
|
||||
all plugins and core features that depend on angular support will stop working.
|
||||
|
||||
Current core features that will stop working:
|
||||
|
||||
- Heatmap panel
|
||||
- Old graph panel
|
||||
- Old table panel
|
||||
- Postgres, MySQL and MSSQL data source query editors
|
||||
- Legacy alerting edit rule UI
|
||||
|
||||
Before we disable angular support by default we plan to migrate these remaining areas to React.
|
||||
|
||||
## [snapshots]
|
||||
|
||||
### external_enabled
|
||||
|
||||
@@ -27,6 +27,8 @@ Example config:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = OAuth
|
||||
icon = signin
|
||||
enabled = true
|
||||
client_id = YOUR_APP_CLIENT_ID
|
||||
client_secret = YOUR_APP_CLIENT_SECRET
|
||||
|
||||
@@ -38,6 +38,7 @@ Before you can sign a user in, you need to create an Okta application from the O
|
||||
```ini
|
||||
[auth.okta]
|
||||
name = Okta
|
||||
icon = okta
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = some_id
|
||||
|
||||
@@ -55,11 +55,11 @@ By default, data source queries are not cached. To enable query caching for a si
|
||||
1. In the data source list, click the data source that you want to turn on caching for.
|
||||
1. Open the Cache tab.
|
||||
1. Press the Enable button.
|
||||
1. (Optional) Choose a custom TTL for that data source. If you skip this step, then Grafana uses the default TTL.
|
||||
1. (Optional) Choose custom TTLs for the data source's queries and resources caching. If you skip this step, then Grafana uses the default TTL.
|
||||
|
||||
> **Note:** If query caching is enabled and the Cache tab is not visible in a data source's settings, then query caching is not available for that data source.
|
||||
|
||||
To configure global settings for query caching, refer the the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}).
|
||||
To configure global settings for query caching, refer to the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}).
|
||||
|
||||
## Disable query caching
|
||||
|
||||
@@ -71,6 +71,19 @@ To disable query caching for a single data source:
|
||||
|
||||
To disable query caching for an entire Grafana instance, set the `enabled` flag to `false` in the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}). You will no longer see the Cache tab on any data sources, and no data source queries will be cached.
|
||||
|
||||
## Clear cache
|
||||
|
||||
If you experience performance issues or repeated queries become slower to execute, consider clearing your cache.
|
||||
|
||||
> **Note:** This action impacts all cache-enabled data sources. If you are using Memcached, the system clears all data from the Memcached instance.
|
||||
|
||||
**To clear the cache**:
|
||||
|
||||
1. Sign in to Grafana and click **Settings > Data Sources**.
|
||||
1. Select a data source.
|
||||
1. Click the **Cache** tab.
|
||||
1. Click **Clear cache**.
|
||||
|
||||
## Sending a request without cache
|
||||
|
||||
If a data source query request contains an `X-Cache-Skip` header, then Grafana skips the caching middleware, and does not search the cache for a response. This can be particularly useful when debugging data source queries using cURL.
|
||||
|
||||
@@ -29,7 +29,9 @@ If the data source does not support loading full range log volume histogram, the
|
||||
|
||||
#### Log level
|
||||
|
||||
For logs where a level label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to parse the log using logfmt and JSON parsers to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with an unknown log level.
|
||||
For logs where a level label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with an unknown log level.
|
||||
|
||||
> **Tip:** If you use Loki data source and the "level" is in you log content, try to use parsers (JSON, logfmt, regex,..) to extract level information into level label that is used to determine log level.
|
||||
|
||||
**Supported log levels and mapping of log level abbreviation and expressions:**
|
||||
|
||||
|
||||
-2
@@ -16,8 +16,6 @@ Expressions are primarily used by the new [Grafana 8 alerts]({{< relref "../../.
|
||||
|
||||
> **Note:** Expressions do not work with legacy dashboard alerts.
|
||||
|
||||
> **Note:** Expressions do not work with dashboard variables.
|
||||
|
||||
Expressions are meant to augment data sources by enabling queries from different data sources to be combined or by providing operations unavailable in a data source.
|
||||
|
||||
> **Note:** When possible, you should do data processing inside the data source. Copying data from storage to the Grafana server for processing is inefficient, so expressions are targeted at lightweight data processing.
|
||||
|
||||
@@ -8,6 +8,7 @@ weight = 10000
|
||||
Here you can find detailed release notes that list everything that is included in every release as well as notices
|
||||
about deprecations, breaking changes as well as changes that relate to plugin development.
|
||||
|
||||
- [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}})
|
||||
- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}})
|
||||
- [Release notes for 8.3.6]({{< relref "release-notes-8-3-6" >}})
|
||||
- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}})
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.4.0"
|
||||
hide_menu = true
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.4.0
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **API:** Extract OpenAPI specification from source code using go-swagger. [#40528](https://github.com/grafana/grafana/pull/40528), [@papagian](https://github.com/papagian)
|
||||
- **AccessControl:** Disable user remove and user update roles when they do not have the permissions. [#43429](https://github.com/grafana/grafana/pull/43429), [@Jguer](https://github.com/Jguer)
|
||||
- **AccessControl:** Provisioning for teams. [#43767](https://github.com/grafana/grafana/pull/43767), [@gamab](https://github.com/gamab)
|
||||
- **API:** Add usage stats preview endpoint. [#43899](https://github.com/grafana/grafana/pull/43899), [@Jguer](https://github.com/Jguer)
|
||||
- **Alerting:** Move slow queries in the scheduler to another goroutine. [#44423](https://github.com/grafana/grafana/pull/44423), [@grobinson-grafana](https://github.com/grobinson-grafana)
|
||||
- **Alerting:** Use time.Ticker instead of alerting.Ticker in ngalert. [#44395](https://github.com/grafana/grafana/pull/44395), [@grobinson-grafana](https://github.com/grobinson-grafana)
|
||||
- **Alerting:** add custom grouping to Alert Panel. [#44559](https://github.com/grafana/grafana/pull/44559), [@gillesdemey](https://github.com/gillesdemey)
|
||||
- **Analytics:** Add user id tracking to google analytics. [#42763](https://github.com/grafana/grafana/pull/42763), [@autoric](https://github.com/autoric)
|
||||
- **Angular:** Add AngularJS plugin support deprecation plan to docs site. [#45149](https://github.com/grafana/grafana/pull/45149), [@torkelo](https://github.com/torkelo)
|
||||
- **Auth:** implement auto_sign_up for auth.jwt. [#43502](https://github.com/grafana/grafana/pull/43502), [@sakjur](https://github.com/sakjur)
|
||||
- **Azure Monitor Logs:** Order subscriptions in resource picker by name. [#45228](https://github.com/grafana/grafana/pull/45228), [@sunker](https://github.com/sunker)
|
||||
- **Azure monitor Logs:** Optimize data fetching in resource picker. [#44549](https://github.com/grafana/grafana/pull/44549), [@sunker](https://github.com/sunker)
|
||||
- **AzureMonitor:** Filter list of resources by resourceType. [#43522](https://github.com/grafana/grafana/pull/43522), [@andresmgot](https://github.com/andresmgot)
|
||||
- **BarChart:** color by field, x time field, bar radius, label skipping. [#43257](https://github.com/grafana/grafana/pull/43257), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Chore:** Implement OpenTelemetry in Grafana. [#42674](https://github.com/grafana/grafana/pull/42674), [@idafurjes](https://github.com/idafurjes)
|
||||
- **Cloud Monitoring:** Adds metric type to Metric drop down options. [#43268](https://github.com/grafana/grafana/pull/43268), [@tw1nk](https://github.com/tw1nk)
|
||||
- **CloudWatch:** Add Data Lifecycle Manager metrics and dimension. [#43310](https://github.com/grafana/grafana/pull/43310), [@ilyastoli](https://github.com/ilyastoli)
|
||||
- **CloudWatch:** Add Missing Elasticache Host-level metrics. [#43455](https://github.com/grafana/grafana/pull/43455), [@dhendo](https://github.com/dhendo)
|
||||
- **CloudWatch:** Add all ElastiCache Redis Metrics. [#43336](https://github.com/grafana/grafana/pull/43336), [@siavashs](https://github.com/siavashs)
|
||||
- **CloudWatch:** Add new AWS/ES metrics. [#43034](https://github.com/grafana/grafana/pull/43034), [@sunker](https://github.com/sunker)
|
||||
- **Cloudwatch:** Add syntax highlighting and autocomplete for "Metric Search". [#43985](https://github.com/grafana/grafana/pull/43985), [@sarahzinger](https://github.com/sarahzinger)
|
||||
- **Explore:** Support custom display label for exemplar links for Prometheus datasource. [#42732](https://github.com/grafana/grafana/pull/42732), [@JokerQueue](https://github.com/JokerQueue)
|
||||
- **Hotkeys:** Make time range absolute/permanent. [#43802](https://github.com/grafana/grafana/pull/43802), [@davkal](https://github.com/davkal)
|
||||
- **Playlists:** Enable sharing direct links to playlists. [#44161](https://github.com/grafana/grafana/pull/44161), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **SQLStore:** Prevent concurrent migrations. [#44101](https://github.com/grafana/grafana/pull/44101), [@papagian](https://github.com/papagian)
|
||||
- **SSE:** Add Mode to drop NaN/Inf/Null in Reduction operations. [#43583](https://github.com/grafana/grafana/pull/43583), [@kylebrandt](https://github.com/kylebrandt)
|
||||
- **Setting:** Support configuring feature toggles with bools instead of just passing an array. [#43326](https://github.com/grafana/grafana/pull/43326), [@bergquist](https://github.com/bergquist)
|
||||
- **TimeSeries:** Add support for negative Y and constant transform. [#44774](https://github.com/grafana/grafana/pull/44774), [@dprokop](https://github.com/dprokop)
|
||||
- **Transformations:** Add 'JSON' field type to ConvertFieldTypeTransformer. [#42624](https://github.com/grafana/grafana/pull/42624), [@sd2k](https://github.com/sd2k)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Auth:** Guarantee consistency of signed SigV4 headers. [#45054](https://github.com/grafana/grafana/pull/45054), [@wbrowne](https://github.com/wbrowne)
|
||||
- **CloudWatch:** Fix MetricName resetting on Namespace change. [#44165](https://github.com/grafana/grafana/pull/44165), [@yaelleC](https://github.com/yaelleC)
|
||||
- **Cloudwatch :** Fixed resetting metric name when changing namespace in Metric Query. [#44612](https://github.com/grafana/grafana/pull/44612), [@yaelleC](https://github.com/yaelleC)
|
||||
- **Explore:** Avoid locking timepicker when range is inverted. [#44790](https://github.com/grafana/grafana/pull/44790), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Instrumentation:** Fix HTTP request instrumentation of authentication failures. [#44234](https://github.com/grafana/grafana/pull/44234), [@marefr](https://github.com/marefr)
|
||||
- **LibraryPanels:** Prevent long descriptions and names from obscuring the delete button. [#45190](https://github.com/grafana/grafana/pull/45190), [@zuchka](https://github.com/zuchka)
|
||||
- **OAuth:** Fix parsing of ID token if header contains non-string value. [#44159](https://github.com/grafana/grafana/pull/44159), [@marefr](https://github.com/marefr)
|
||||
- **Panel Edit:** Visualization search now works correctly with special characters. [#45137](https://github.com/grafana/grafana/pull/45137), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Provisioning:** Fix duplicate validation when multiple organizations have been configured. [#44151](https://github.com/grafana/grafana/pull/44151), [@marefr](https://github.com/marefr)
|
||||
- **QueryField:** Fix issue with undo history when suggestion is inserted (#28656). [#39114](https://github.com/grafana/grafana/pull/39114), [@glintik](https://github.com/glintik)
|
||||
- **TablePanel:** Do not prefix columns with frame name if multiple frames and override active. [#45174](https://github.com/grafana/grafana/pull/45174), [@mdvictor](https://github.com/mdvictor)
|
||||
|
||||
### Deprecations
|
||||
|
||||
AngularJS plugin support is now in a deprecated state, meaning it will be removed in a future release. Currently, that is planned for version 10 (in 2023). The documentation site has an [article](https://grafana.com/docs/grafana/next/developers/angular_deprecation/) with more details on why, when, and how. Issue [#45149](https://github.com/grafana/grafana/issues/45149)
|
||||
@@ -0,0 +1,18 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.4.1"
|
||||
hide_menu = true
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.4.1
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Cloudwatch:** Add support for AWS/PrivateLink\* metrics and dimensions. [#45515](https://github.com/grafana/grafana/pull/45515), [@szymonpk](https://github.com/szymonpk)
|
||||
- **Configuration:** Add ability to customize okta login button name and icon. [#44079](https://github.com/grafana/grafana/pull/44079), [@DanCech](https://github.com/DanCech)
|
||||
- **Tempo:** Switch out Select with AsyncSelect component to get loading state in Tempo Search. [#45110](https://github.com/grafana/grafana/pull/45110), [@CatPerry](https://github.com/CatPerry)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Alerting:** Fix migrations by making send_alerts_to field nullable. [#45572](https://github.com/grafana/grafana/pull/45572), [@santihernandezc](https://github.com/santihernandezc)
|
||||
@@ -17,7 +17,7 @@ To limit the number of lines rendered, you can use the **Max data points** setti
|
||||
|
||||
## Log level
|
||||
|
||||
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we parse the log to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions]({{< relref "../explore/_index.md#log-level" >}}).
|
||||
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions]({{< relref "../explore/_index.md#log-level" >}}).
|
||||
|
||||
## Log details
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ You can now share links to your playlists the same way as with dashboards, to ea
|
||||
|
||||
{{< figure src="/static/img/docs/dashboards/share-playlists-8-4.png" max-width="1200px" caption="Share playlist" >}}
|
||||
|
||||
### $**interval and $**interval_ms in panel title
|
||||
### `$__interval` and `$__interval_ms` in panel title
|
||||
|
||||
We’ve expanded the functionality of this existing and popular feature. You can now use $**interval and $**interval_ms in panel titles. This new function displays the interval that’s used in a specific panel without requiring edit mode.
|
||||
We’ve expanded the functionality of this existing and popular feature. You can now use `$__interval` and `$__interval_ms` in panel titles. This new function displays the interval that’s used in a specific panel without requiring edit mode.
|
||||
|
||||
{{< figure src="/static/img/docs/panels/interval-8-4.png" max-width="1200px" caption="Time intervals" >}}
|
||||
|
||||
|
||||
@@ -19,6 +19,28 @@ e2e.scenario({
|
||||
|
||||
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
|
||||
|
||||
cy.location().then((loc) => {
|
||||
const params = new URLSearchParams(loc.search);
|
||||
const leftJSON = JSON.parse(params.get('left'));
|
||||
expect(leftJSON.range.to).to.equal('now');
|
||||
expect(leftJSON.range.from).to.equal('now-1h');
|
||||
|
||||
cy.get('body').click();
|
||||
cy.get('body').type('t{leftarrow}');
|
||||
|
||||
cy.location().then((locPostKeypress) => {
|
||||
const params = new URLSearchParams(locPostKeypress.search);
|
||||
const leftJSON = JSON.parse(params.get('left'));
|
||||
// be sure the keypress affected the time window
|
||||
expect(leftJSON.range.to).to.not.equal('now');
|
||||
expect(leftJSON.range.from).to.not.equal('now-1h');
|
||||
// be sure the url does not contain dashboard range values
|
||||
// eslint wants this to be a function, so we use this instead of to.be.false
|
||||
expect(params.has('to')).to.equal(false);
|
||||
expect(params.has('from')).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
const canvases = e2e().get('canvas');
|
||||
canvases.should('have.length', 1);
|
||||
},
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ module.exports = {
|
||||
'node_modules/(?!(ol)/)', // <- exclude the open layers library
|
||||
],
|
||||
moduleDirectories: ['node_modules', 'public'],
|
||||
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/scripts'],
|
||||
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages'],
|
||||
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
||||
setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'],
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "8.3.6",
|
||||
"testing": "8.4.0-beta1"
|
||||
"stable": "8.4.1",
|
||||
"testing": "8.4.1"
|
||||
}
|
||||
|
||||
+13
-14
@@ -71,7 +71,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.17.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/plugin-transform-react-constant-elements": "7.16.7",
|
||||
@@ -93,9 +93,9 @@
|
||||
"@microsoft/api-extractor": "7.19.4",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.4",
|
||||
"@react-types/button": "3.4.1",
|
||||
"@react-types/menu": "3.4.1",
|
||||
"@react-types/menu": "3.5.1",
|
||||
"@react-types/overlays": "3.5.1",
|
||||
"@react-types/shared": "3.10.1",
|
||||
"@react-types/shared": "3.11.1",
|
||||
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
|
||||
"@swc/core": "1.2.136",
|
||||
"@swc/helpers": "0.3.2",
|
||||
@@ -126,7 +126,6 @@
|
||||
"@types/lingui__macro": "^3",
|
||||
"@types/lodash": "4.14.149",
|
||||
"@types/logfmt": "^1.2.1",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/mousetrap": "1.6.3",
|
||||
"@types/node": "16.11.22",
|
||||
"@types/papaparse": "5.3.2",
|
||||
@@ -158,7 +157,7 @@
|
||||
"@typescript-eslint/parser": "5.11.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.6.6",
|
||||
"autoprefixer": "10.4.2",
|
||||
"axios": "0.25.0",
|
||||
"axios": "0.26.0",
|
||||
"babel-jest": "27.5.0",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-angularjs-annotate": "0.10.0",
|
||||
@@ -169,11 +168,11 @@
|
||||
"cypress": "9.3.1",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-to-json": "3.6.2",
|
||||
"eslint": "8.8.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint": "8.9.0",
|
||||
"eslint-config-prettier": "8.4.0",
|
||||
"eslint-plugin-jest": "^26.1.0",
|
||||
"eslint-plugin-jsdoc": "37.8.0",
|
||||
"eslint-plugin-lodash": "^7.2.0",
|
||||
"eslint-plugin-jsdoc": "37.9.1",
|
||||
"eslint-plugin-lodash": "7.4.0",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"eslint-plugin-react": "7.28.0",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
@@ -213,7 +212,7 @@
|
||||
"redux-mock-store": "1.5.4",
|
||||
"rimraf": "3.0.2",
|
||||
"sass": "1.49.7",
|
||||
"sass-loader": "12.4.0",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "13.0.1",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.5.0",
|
||||
@@ -234,7 +233,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.7.1",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/react": "11.8.1",
|
||||
"@grafana/aws-sdk": "0.0.33",
|
||||
"@grafana/data": "workspace:*",
|
||||
"@grafana/e2e-selectors": "workspace:*",
|
||||
@@ -255,7 +254,7 @@
|
||||
"@react-aria/button": "3.3.4",
|
||||
"@react-aria/dialog": "3.1.4",
|
||||
"@react-aria/focus": "3.5.0",
|
||||
"@react-aria/interactions": "3.7.0",
|
||||
"@react-aria/interactions": "3.8.1",
|
||||
"@react-aria/menu": "3.3.0",
|
||||
"@react-aria/overlays": "3.7.3",
|
||||
"@react-aria/utils": "3.11.0",
|
||||
@@ -309,7 +308,7 @@
|
||||
"lezer-tree": "0.13.2",
|
||||
"lodash": "4.17.21",
|
||||
"logfmt": "^1.3.2",
|
||||
"lru-cache": "6.0.0",
|
||||
"lru-cache": "7.3.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"moment": "2.29.1",
|
||||
"moment-timezone": "0.5.34",
|
||||
@@ -335,7 +334,7 @@
|
||||
"react-draggable": "4.4.4",
|
||||
"react-grid-layout": "1.3.3",
|
||||
"react-highlight-words": "0.17.0",
|
||||
"react-hook-form": "7.5.3",
|
||||
"react-hook-form": "7.27.0",
|
||||
"react-inlinesvg": "2.3.0",
|
||||
"react-loadable": "5.5.0",
|
||||
"react-moveable": "0.30.3",
|
||||
|
||||
@@ -285,15 +285,24 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
|
||||
};
|
||||
}
|
||||
|
||||
// function incrRound(num: number, incr: number) {
|
||||
// return Math.round(num / incr) * incr;
|
||||
// }
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function incrRound(num: number, incr: number) {
|
||||
return Math.round(num / incr) * incr;
|
||||
}
|
||||
|
||||
// function incrRoundUp(num: number, incr: number) {
|
||||
// return Math.ceil(num / incr) * incr;
|
||||
// }
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function incrRoundUp(num: number, incr: number) {
|
||||
return Math.ceil(num / incr) * incr;
|
||||
}
|
||||
|
||||
function incrRoundDn(num: number, incr: number) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function incrRoundDn(num: number, incr: number) {
|
||||
return Math.floor(num / incr) * incr;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum DataTransformerID {
|
||||
prepareTimeSeries = 'prepareTimeSeries',
|
||||
convertFieldType = 'convertFieldType',
|
||||
fieldLookup = 'fieldLookup',
|
||||
heatmap = 'heatmap',
|
||||
spatial = 'spatial',
|
||||
extractFields = 'extractFields',
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export type OAuth =
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OAuthSettings = Partial<Record<OAuth, { name: string }>>;
|
||||
export type OAuthSettings = Partial<Record<OAuth, { name: string; icon?: string }>>;
|
||||
|
||||
/**
|
||||
* Describes all the different Grafana configuration values available for an instance.
|
||||
@@ -138,4 +138,5 @@ export interface GrafanaConfig {
|
||||
geomapDefaultBaseLayer?: MapLayerOptions;
|
||||
geomapDisableCustomBaseLayer?: boolean;
|
||||
unifiedAlertingEnabled: boolean;
|
||||
angularSupportEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -8,4 +8,17 @@ export enum DataFrameType {
|
||||
TimeSeriesWide = 'timeseries-wide',
|
||||
TimeSeriesLong = 'timeseries-long',
|
||||
TimeSeriesMany = 'timeseries-many',
|
||||
|
||||
/**
|
||||
* First field is X, the rest are bucket values
|
||||
*/
|
||||
HeatmapBuckets = 'heatmap-buckets',
|
||||
|
||||
/**
|
||||
* Explicit fields for:
|
||||
* xMin, yMin, count, ...
|
||||
*
|
||||
* All values in the grid exist and have regular spacing
|
||||
*/
|
||||
HeatmapScanlines = 'heatmap-scanlines',
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface FeatureToggles {
|
||||
database_metrics?: boolean;
|
||||
dashboardPreviews?: boolean;
|
||||
dashboardPreviewsScheduler?: boolean;
|
||||
dashboardPreviewsAdmin?: boolean;
|
||||
['live-config']?: boolean;
|
||||
['live-pipeline']?: boolean;
|
||||
['live-service-web-worker']?: boolean;
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface NavModelItem {
|
||||
onClick?: () => void;
|
||||
menuItemType?: NavMenuItemType;
|
||||
highlightText?: string;
|
||||
highlightId?: string;
|
||||
tabSuffix?: ComponentType<{ className?: string }>;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
alertingErrorOrTimeout = '';
|
||||
alertingNoDataOrNullValues = '';
|
||||
alertingMinInterval = 1;
|
||||
angularSupportEnabled = false;
|
||||
authProxyEnabled = false;
|
||||
exploreEnabled = false;
|
||||
ldapEnabled = false;
|
||||
@@ -65,6 +66,13 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
featureToggles: FeatureToggles = {};
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
rendererAvailable = false;
|
||||
dashboardPreviews: {
|
||||
systemRequirements: {
|
||||
met: boolean;
|
||||
requiredImageRendererPluginVersion: string;
|
||||
};
|
||||
thumbnailsExist: boolean;
|
||||
} = { systemRequirements: { met: false, requiredImageRendererPluginVersion: '' }, thumbnailsExist: false };
|
||||
rendererVersion = '';
|
||||
http2Enabled = false;
|
||||
dateFormats?: SystemDateFormatSettings;
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
ptable "github.com/grafana/grafana/public/app/plugins/panel/table:grafanaschema"
|
||||
ptext "github.com/grafana/grafana/public/app/plugins/panel/text:grafanaschema"
|
||||
ptimeseries "github.com/grafana/grafana/public/app/plugins/panel/timeseries:grafanaschema"
|
||||
pheatmap_new "github.com/grafana/grafana/public/app/plugins/panel/heatmap-new:grafanaschema"
|
||||
)
|
||||
|
||||
// Family composes the base dashboard scuemata family with all Grafana core plugins -
|
||||
@@ -40,5 +41,6 @@ Family: dashboard.Family & {
|
||||
text: ptext.Panel
|
||||
table: ptable.Panel
|
||||
timeseries: ptimeseries.Panel
|
||||
"heatmap-new": pheatmap_new.Panel
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.7.1",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/react": "11.8.1",
|
||||
"@grafana/aws-sdk": "0.0.33",
|
||||
"@grafana/data": "8.5.0-pre",
|
||||
"@grafana/e2e-selectors": "8.5.0-pre",
|
||||
@@ -74,7 +74,7 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "11.5.1",
|
||||
"react-highlight-words": "0.17.0",
|
||||
"react-hook-form": "7.5.3",
|
||||
"react-hook-form": "7.27.0",
|
||||
"react-inlinesvg": "2.3.0",
|
||||
"react-popper": "2.2.5",
|
||||
"react-popper-tooltip": "^4.3.1",
|
||||
@@ -169,7 +169,7 @@
|
||||
"rollup": "2.67.1",
|
||||
"rollup-plugin-sourcemaps": "0.6.3",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"sass-loader": "12.4.0",
|
||||
"sass-loader": "12.6.0",
|
||||
"storybook-dark-mode": "1.0.8",
|
||||
"style-loader": "3.3.1",
|
||||
"terser-webpack-plugin": "5.3.1",
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
elevated?: boolean;
|
||||
buttonContent?: React.ReactNode | string;
|
||||
bottomSpacing?: number;
|
||||
topSpacing?: number;
|
||||
}
|
||||
|
||||
function getIconFromSeverity(severity: AlertVariant): string {
|
||||
@@ -37,11 +38,22 @@ function getIconFromSeverity(severity: AlertVariant): string {
|
||||
|
||||
export const Alert = React.forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{ title, onRemove, children, buttonContent, elevated, bottomSpacing, className, severity = 'error', ...restProps },
|
||||
{
|
||||
title,
|
||||
onRemove,
|
||||
children,
|
||||
buttonContent,
|
||||
elevated,
|
||||
bottomSpacing,
|
||||
topSpacing,
|
||||
className,
|
||||
severity = 'error',
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, severity, elevated, bottomSpacing);
|
||||
const styles = getStyles(theme, severity, elevated, bottomSpacing, topSpacing);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -77,7 +89,13 @@ export const Alert = React.forwardRef<HTMLDivElement, Props>(
|
||||
|
||||
Alert.displayName = 'Alert';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, severity: AlertVariant, elevated?: boolean, bottomSpacing?: number) => {
|
||||
const getStyles = (
|
||||
theme: GrafanaTheme2,
|
||||
severity: AlertVariant,
|
||||
elevated?: boolean,
|
||||
bottomSpacing?: number,
|
||||
topSpacing?: number
|
||||
) => {
|
||||
const color = theme.colors[severity];
|
||||
const borderRadius = theme.shape.borderRadius();
|
||||
|
||||
@@ -92,6 +110,7 @@ const getStyles = (theme: GrafanaTheme2, severity: AlertVariant, elevated?: bool
|
||||
background: ${theme.colors.background.secondary};
|
||||
box-shadow: ${elevated ? theme.shadows.z3 : theme.shadows.z1};
|
||||
margin-bottom: ${theme.spacing(bottomSpacing ?? 2)};
|
||||
margin-top: ${theme.spacing(topSpacing ?? 0)};
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BigValue, Props, BigValueColorMode, BigValueGraphMode } from './BigValue';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BigValue, BigValueColorMode, BigValueGraphMode, Props } from './BigValue';
|
||||
import { createTheme } from '@grafana/data';
|
||||
|
||||
function getProps(propOverrides?: Partial<Props>): Props {
|
||||
@@ -21,22 +21,12 @@ function getProps(propOverrides?: Partial<Props>): Props {
|
||||
return props;
|
||||
}
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props = getProps(propOverrides);
|
||||
const wrapper = shallow(<BigValue {...props} />);
|
||||
const instance = wrapper.instance() as BigValue;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('BigValue', () => {
|
||||
describe('Render with basic options', () => {
|
||||
it('should render', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
render(<BigValue {...getProps()} />);
|
||||
|
||||
expect(screen.getByText('25')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Library
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DisplayValue, DisplayValueAlignmentFactors, FieldSparkline, TextDisplayOptions } from '@grafana/data';
|
||||
|
||||
// Types
|
||||
import { Themeable2 } from '../../types';
|
||||
import { buildLayout } from './BigValueLayout';
|
||||
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BigValue Render with basic options should render 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"background": "linear-gradient(120deg, rgb(179, 24, 0), rgb(230, 0, 31))",
|
||||
"borderRadius": "3px",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"height": "300px",
|
||||
"padding": "12px",
|
||||
"position": "relative",
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "column",
|
||||
"flexGrow": 1,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedDisplayValue
|
||||
style={
|
||||
Object {
|
||||
"color": "rgb(247, 248, 250)",
|
||||
"fontSize": 230,
|
||||
"fontWeight": 500,
|
||||
"lineHeight": 1.2,
|
||||
"position": "relative",
|
||||
"textAlign": "center",
|
||||
"zIndex": 1,
|
||||
}
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"color": "red",
|
||||
"numeric": 25,
|
||||
"text": "25",
|
||||
"titleToAlignTo": undefined,
|
||||
"valueToAlignTo": "25",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,33 +1,39 @@
|
||||
import React from 'react';
|
||||
import { render } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { CallToActionCard } from './CallToActionCard';
|
||||
import { expect } from '../../../../../public/test/lib/common';
|
||||
|
||||
describe('CallToActionCard', () => {
|
||||
describe('rendering', () => {
|
||||
it('when no message and footer provided', () => {
|
||||
const tree = render(<CallToActionCard callToActionElement={<a href="http://dummy.link">Click me</a>} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
it('should render callToActionElement', () => {
|
||||
render(<CallToActionCard callToActionElement={<a href="http://dummy.link">Click me</a>} />);
|
||||
expect(screen.getByRole('link', { name: 'Click me' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('when message and no footer provided', () => {
|
||||
const tree = render(
|
||||
<CallToActionCard
|
||||
message="Click button bellow"
|
||||
callToActionElement={<a href="http://dummy.link">Click me</a>}
|
||||
/>
|
||||
it('should render message when provided', () => {
|
||||
render(
|
||||
<CallToActionCard message="Click button below" callToActionElement={<a href="http://dummy.link">Click me</a>} />
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
expect(screen.getByText('Click button below')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('when message and footer provided', () => {
|
||||
const tree = render(
|
||||
it('should render footer when provided', () => {
|
||||
render(
|
||||
<CallToActionCard footer="footer content" callToActionElement={<a href="http://dummy.link">Click me</a>} />
|
||||
);
|
||||
expect(screen.getByText('footer content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render both message and footer when provided', () => {
|
||||
render(
|
||||
<CallToActionCard
|
||||
message="Click button bellow"
|
||||
message="Click button below"
|
||||
footer="footer content"
|
||||
callToActionElement={<a href="http://dummy.link">Click me</a>}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
expect(screen.getByText('Click button below')).toBeInTheDocument();
|
||||
expect(screen.getByText('footer content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallToActionCard rendering when message and footer provided 1`] = `
|
||||
<div
|
||||
class="css-xtd6ws-call-to-action-card"
|
||||
>
|
||||
<div
|
||||
class="css-m2iibx"
|
||||
>
|
||||
Click button bellow
|
||||
</div>
|
||||
<a
|
||||
href="http://dummy.link"
|
||||
>
|
||||
Click me
|
||||
</a>
|
||||
<div
|
||||
class="css-11lqfaj"
|
||||
>
|
||||
footer content
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CallToActionCard rendering when message and no footer provided 1`] = `
|
||||
<div
|
||||
class="css-xtd6ws-call-to-action-card"
|
||||
>
|
||||
<div
|
||||
class="css-m2iibx"
|
||||
>
|
||||
Click button bellow
|
||||
</div>
|
||||
<a
|
||||
href="http://dummy.link"
|
||||
>
|
||||
Click me
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CallToActionCard rendering when no message and footer provided 1`] = `
|
||||
<div
|
||||
class="css-xtd6ws-call-to-action-card"
|
||||
>
|
||||
<a
|
||||
href="http://dummy.link"
|
||||
>
|
||||
Click me
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,34 +1,72 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { ConfirmButton } from './ConfirmButton';
|
||||
import { mount, ShallowWrapper } from 'enzyme';
|
||||
import { Button } from '../Button';
|
||||
import { expect } from '../../../../../public/test/lib/common';
|
||||
|
||||
describe('ConfirmButton', () => {
|
||||
let wrapper: any;
|
||||
let deleted: any;
|
||||
|
||||
beforeAll(() => {
|
||||
deleted = false;
|
||||
|
||||
function deleteItem() {
|
||||
deleted = true;
|
||||
}
|
||||
|
||||
wrapper = mount(
|
||||
<ConfirmButton confirmText="Confirm delete" onConfirm={() => deleteItem()}>
|
||||
it('should show confirm delete when clicked', () => {
|
||||
const onConfirm = jest.fn();
|
||||
render(
|
||||
<ConfirmButton confirmText="Confirm delete" onConfirm={onConfirm}>
|
||||
Delete
|
||||
</ConfirmButton>
|
||||
);
|
||||
|
||||
// Confirm button should not be visible before clicking the Delete button
|
||||
expect(screen.queryByRole('button', { name: 'Confirm delete' })).not.toBeInTheDocument();
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
||||
// Confirm button should now be visible
|
||||
expect(screen.getByRole('button', { name: 'Confirm delete' })).toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('button', { name: 'Confirm delete' }));
|
||||
expect(onConfirm).toHaveBeenCalled();
|
||||
|
||||
// Confirm button should be visible if closeOnConfirm is false
|
||||
expect(screen.queryByRole('button', { name: 'Confirm delete' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show confirm delete when clicked', () => {
|
||||
expect(deleted).toBe(false);
|
||||
wrapper
|
||||
.find(Button)
|
||||
.findWhere((n: ShallowWrapper) => {
|
||||
return n.text() === 'Confirm delete' && n.type() === Button;
|
||||
})
|
||||
.simulate('click');
|
||||
expect(deleted).toBe(true);
|
||||
it('should hide confirm delete when closeOnConfirm is true', () => {
|
||||
render(
|
||||
<ConfirmButton confirmText="Confirm delete" onConfirm={() => {}} closeOnConfirm={true}>
|
||||
Delete
|
||||
</ConfirmButton>
|
||||
);
|
||||
|
||||
// Confirm button should not be visible before clicking the Delete button
|
||||
expect(screen.queryByRole('button', { name: 'Confirm delete' })).not.toBeInTheDocument();
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
||||
// Confirm button should now be visible
|
||||
expect(screen.getByRole('button', { name: 'Confirm delete' })).toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('button', { name: 'Confirm delete' }));
|
||||
|
||||
// Confirm button should not be visible if closeOnConfirm is true
|
||||
expect(screen.queryByRole('button', { name: 'Confirm delete' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show cancel when clicked', () => {
|
||||
const onCancel = jest.fn();
|
||||
render(
|
||||
<ConfirmButton confirmText="Confirm delete" onCancel={onCancel} onConfirm={() => {}}>
|
||||
Delete
|
||||
</ConfirmButton>
|
||||
);
|
||||
|
||||
// Cancel button should not be visible before clicking the Delete button
|
||||
expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
||||
// Cancel button should now be visible
|
||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||
expect(onCancel).toHaveBeenCalled();
|
||||
|
||||
// Cancel button should not be visible after click
|
||||
expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
|
||||
describe('ConfirmModal', () => {
|
||||
it('renders without error', () => {
|
||||
mount(
|
||||
it('should render correct title, body, dismiss-, alternative- and confirm-text', () => {
|
||||
render(
|
||||
<ConfirmModal
|
||||
title="Some Title"
|
||||
body="Some Body"
|
||||
confirmText="Confirm"
|
||||
confirmText="Please Confirm"
|
||||
alternativeText="Alternative Text"
|
||||
dismissText="Dismiss Text"
|
||||
isOpen={true}
|
||||
onConfirm={() => {}}
|
||||
onDismiss={() => {}}
|
||||
onAlternative={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Some Title' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Some Body')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Dismiss Text' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Alternative Text' })).toBeInTheDocument();
|
||||
const button = screen.getByRole('button', { name: 'Confirm Modal Danger Button' });
|
||||
expect(within(button).getByText('Please Confirm')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing by default or when isOpen is false', () => {
|
||||
const wrapper = mount(
|
||||
it('should render nothing when isOpen is false', () => {
|
||||
render(
|
||||
<ConfirmModal
|
||||
title="Some Title"
|
||||
body="Some Body"
|
||||
@@ -27,26 +38,11 @@ describe('ConfirmModal', () => {
|
||||
onDismiss={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.html()).toBe('');
|
||||
|
||||
wrapper.setProps({ ...wrapper.props(), isOpen: false });
|
||||
expect(wrapper.html()).toBe('');
|
||||
});
|
||||
|
||||
it('renders correct contents', () => {
|
||||
const wrapper = mount(
|
||||
<ConfirmModal
|
||||
title="Some Title"
|
||||
body="Content"
|
||||
confirmText="Confirm"
|
||||
isOpen={true}
|
||||
onConfirm={() => {}}
|
||||
onDismiss={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.contains('Some Title')).toBeTruthy();
|
||||
expect(wrapper.contains('Content')).toBeTruthy();
|
||||
expect(wrapper.contains('Confirm')).toBeTruthy();
|
||||
expect(screen.queryByRole('heading', { name: 'Some Title' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Some Body')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Dismiss Text' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Alternative Text' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Confirm Modal Danger Button' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { Modal } from '../Modal/Modal';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { Button } from '../Button';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { HorizontalGroup, Input } from '..';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
export interface ConfirmModalProps {
|
||||
/** Toggle modal's open/closed state */
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useStyles2 } from '../../../themes';
|
||||
|
||||
export interface RadioButtonGroupProps<T> {
|
||||
value?: T;
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
disabledOptions?: T[];
|
||||
options: Array<SelectableValue<T>>;
|
||||
@@ -28,6 +29,7 @@ export function RadioButtonGroup<T>({
|
||||
disabled,
|
||||
disabledOptions,
|
||||
size = 'md',
|
||||
id,
|
||||
className,
|
||||
fullWidth = false,
|
||||
autoFocus = false,
|
||||
@@ -52,8 +54,9 @@ export function RadioButtonGroup<T>({
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
const id = uniqueId('radiogroup-');
|
||||
const groupName = useRef(id);
|
||||
|
||||
const internalId = id ?? uniqueId('radiogroup-');
|
||||
const groupName = useRef(internalId);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const activeButtonRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -76,7 +79,7 @@ export function RadioButtonGroup<T>({
|
||||
aria-label={o.ariaLabel}
|
||||
onChange={handleOnChange(o)}
|
||||
onClick={handleOnClick(o)}
|
||||
id={`option-${o.value}-${id}`}
|
||||
id={`option-${o.value}-${internalId}`}
|
||||
name={groupName.current}
|
||||
description={o.description}
|
||||
fullWidth={fullWidth}
|
||||
|
||||
@@ -69,6 +69,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
transition: opacity 0.3s;
|
||||
z-index: ${theme.zIndex.tooltip};
|
||||
max-width: 400px;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&[data-popper-interactive='false'] {
|
||||
pointer-events: none;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
|
||||
position: { x: positionX, y: positionY },
|
||||
offset: { x: offsetX, y: offsetY },
|
||||
children,
|
||||
allowPointerEvents,
|
||||
allowPointerEvents = false,
|
||||
className,
|
||||
...otherProps
|
||||
}) => {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"@types/prop-types": "^15",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/react-icons": "2.2.7",
|
||||
"@types/recompose": "0.30.10",
|
||||
"@types/reselect": "2.2.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"enzyme": "3.11.0",
|
||||
@@ -46,7 +45,6 @@
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-icons": "2.2.7",
|
||||
"recompose": "0.30.0",
|
||||
"reselect": "4.1.5",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tslib": "2.3.1",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Popover } from '../common/Popover';
|
||||
|
||||
import SpanBar from './SpanBar';
|
||||
@@ -79,9 +80,13 @@ describe('<SpanBar>', () => {
|
||||
const { onMouseOver, onMouseLeave } = wrapper.find('[data-test-id="SpanBar--wrapper"]').props();
|
||||
const labelElm = wrapper.find('[data-test-id="SpanBar--label"]');
|
||||
expect(labelElm.text()).toBe(shortLabel);
|
||||
onMouseOver();
|
||||
act(() => {
|
||||
onMouseOver();
|
||||
});
|
||||
expect(labelElm.text()).toBe(longLabel);
|
||||
onMouseLeave();
|
||||
act(() => {
|
||||
onMouseLeave();
|
||||
});
|
||||
expect(labelElm.text()).toBe(shortLabel);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
import cx from 'classnames';
|
||||
import { css } from '@emotion/css';
|
||||
import { groupBy as _groupBy } from 'lodash';
|
||||
import React from 'react';
|
||||
import { compose, onlyUpdateForKeys, withProps, withState } from 'recompose';
|
||||
import React, { useState } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { autoColor } from '../Theme';
|
||||
@@ -90,7 +89,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
};
|
||||
};
|
||||
|
||||
type TCommonProps = {
|
||||
type Props = {
|
||||
color: string;
|
||||
onClick?: (evt: React.MouseEvent<any>) => void;
|
||||
viewEnd: number;
|
||||
@@ -107,39 +106,32 @@ type TCommonProps = {
|
||||
span: TraceSpan;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
};
|
||||
|
||||
type TInnerProps = {
|
||||
label: string;
|
||||
setLongLabel: () => void;
|
||||
setShortLabel: () => void;
|
||||
} & TCommonProps;
|
||||
|
||||
type TOuterProps = {
|
||||
longLabel: string;
|
||||
shortLabel: string;
|
||||
} & TCommonProps;
|
||||
};
|
||||
|
||||
function toPercent(value: number) {
|
||||
return `${(value * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function SpanBar(props: TInnerProps) {
|
||||
const {
|
||||
viewEnd,
|
||||
viewStart,
|
||||
getViewedBounds,
|
||||
color,
|
||||
label,
|
||||
onClick,
|
||||
setLongLabel,
|
||||
setShortLabel,
|
||||
rpc,
|
||||
traceStartTime,
|
||||
span,
|
||||
className,
|
||||
labelClassName,
|
||||
} = props;
|
||||
function SpanBar({
|
||||
viewEnd,
|
||||
viewStart,
|
||||
getViewedBounds,
|
||||
color,
|
||||
shortLabel,
|
||||
longLabel,
|
||||
onClick,
|
||||
rpc,
|
||||
traceStartTime,
|
||||
span,
|
||||
className,
|
||||
labelClassName,
|
||||
}: Props) {
|
||||
const [label, setLabel] = useState(shortLabel);
|
||||
const setShortLabel = () => setLabel(shortLabel);
|
||||
const setLongLabel = () => setLabel(longLabel);
|
||||
|
||||
// group logs based on timestamps
|
||||
const logGroups = _groupBy(span.logs, (log) => {
|
||||
const posPercent = getViewedBounds(log.timestamp, log.timestamp).start;
|
||||
@@ -196,21 +188,4 @@ function SpanBar(props: TInnerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default compose<TInnerProps, TOuterProps>(
|
||||
withState('label', 'setLabel', (props: { shortLabel: string }) => props.shortLabel),
|
||||
withProps(
|
||||
({
|
||||
setLabel,
|
||||
shortLabel,
|
||||
longLabel,
|
||||
}: {
|
||||
setLabel: (label: string) => void;
|
||||
shortLabel: string;
|
||||
longLabel: string;
|
||||
}) => ({
|
||||
setLongLabel: () => setLabel(longLabel),
|
||||
setShortLabel: () => setLabel(shortLabel),
|
||||
})
|
||||
),
|
||||
onlyUpdateForKeys(['label', 'rpc', 'viewStart', 'viewEnd'])
|
||||
)(SpanBar);
|
||||
export default React.memo(SpanBar);
|
||||
|
||||
@@ -6,9 +6,3 @@ export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailS
|
||||
export { default as transformTraceData } from './model/transform-trace-data';
|
||||
export { default as filterSpans } from './utils/filter-spans';
|
||||
export * from './Theme';
|
||||
|
||||
import { onlyUpdateForKeys } from 'recompose';
|
||||
|
||||
export default {
|
||||
onlyUpdateForKeys,
|
||||
} as any;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -29,49 +31,49 @@ const (
|
||||
|
||||
// API related scopes
|
||||
var (
|
||||
ScopeProvisionersAll = accesscontrol.Scope("provisioners", "*")
|
||||
ScopeProvisionersDashboards = accesscontrol.Scope("provisioners", "dashboards")
|
||||
ScopeProvisionersPlugins = accesscontrol.Scope("provisioners", "plugins")
|
||||
ScopeProvisionersDatasources = accesscontrol.Scope("provisioners", "datasources")
|
||||
ScopeProvisionersNotifications = accesscontrol.Scope("provisioners", "notifications")
|
||||
ScopeProvisionersAll = ac.Scope("provisioners", "*")
|
||||
ScopeProvisionersDashboards = ac.Scope("provisioners", "dashboards")
|
||||
ScopeProvisionersPlugins = ac.Scope("provisioners", "plugins")
|
||||
ScopeProvisionersDatasources = ac.Scope("provisioners", "datasources")
|
||||
ScopeProvisionersNotifications = ac.Scope("provisioners", "notifications")
|
||||
|
||||
ScopeDatasourcesAll = accesscontrol.Scope("datasources", "*")
|
||||
ScopeDatasourceID = accesscontrol.Scope("datasources", "id", accesscontrol.Parameter(":id"))
|
||||
ScopeDatasourceUID = accesscontrol.Scope("datasources", "uid", accesscontrol.Parameter(":uid"))
|
||||
ScopeDatasourceName = accesscontrol.Scope("datasources", "name", accesscontrol.Parameter(":name"))
|
||||
ScopeDatasourcesAll = ac.Scope("datasources", "*")
|
||||
ScopeDatasourceID = ac.Scope("datasources", "id", ac.Parameter(":id"))
|
||||
ScopeDatasourceUID = ac.Scope("datasources", "uid", ac.Parameter(":uid"))
|
||||
ScopeDatasourceName = ac.Scope("datasources", "name", ac.Parameter(":name"))
|
||||
)
|
||||
|
||||
// declareFixedRoles declares to the AccessControl service fixed roles and their
|
||||
// grants to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||
// that HTTPServer needs
|
||||
func (hs *HTTPServer) declareFixedRoles() error {
|
||||
provisioningWriterRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
provisioningWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 3,
|
||||
Name: "fixed:provisioning:writer",
|
||||
DisplayName: "Provisioning writer",
|
||||
Description: "Reload provisioning.",
|
||||
Group: "Provisioning",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
Permissions: []ac.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: ScopeProvisionersAll,
|
||||
},
|
||||
},
|
||||
},
|
||||
Grants: []string{accesscontrol.RoleGrafanaAdmin},
|
||||
Grants: []string{ac.RoleGrafanaAdmin},
|
||||
}
|
||||
|
||||
datasourcesExplorerRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
datasourcesExplorerRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 4,
|
||||
Name: "fixed:datasources:explorer",
|
||||
DisplayName: "Data source explorer",
|
||||
Description: "Enable the Explore feature. Data source permissions still apply; you can only query data sources for which you have query permissions.",
|
||||
Group: "Data sources",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
Permissions: []ac.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionDatasourcesExplore,
|
||||
Action: ac.ActionDatasourcesExplore,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -82,14 +84,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
datasourcesExplorerRole.Grants = append(datasourcesExplorerRole.Grants, string(models.ROLE_VIEWER))
|
||||
}
|
||||
|
||||
datasourcesReaderRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
datasourcesReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 3,
|
||||
Name: "fixed:datasources:reader",
|
||||
DisplayName: "Data source reader",
|
||||
Description: "Read and query all data sources.",
|
||||
Group: "Data sources",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
Permissions: []ac.Permission{
|
||||
{
|
||||
Action: ActionDatasourcesRead,
|
||||
Scope: ScopeDatasourcesAll,
|
||||
@@ -103,14 +105,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{string(models.ROLE_ADMIN)},
|
||||
}
|
||||
|
||||
datasourcesWriterRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
datasourcesWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 3,
|
||||
Name: "fixed:datasources:writer",
|
||||
DisplayName: "Data source writer",
|
||||
Description: "Create, update, delete, read, or query data sources.",
|
||||
Group: "Data sources",
|
||||
Permissions: accesscontrol.ConcatPermissions(datasourcesReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
Permissions: ac.ConcatPermissions(datasourcesReaderRole.Role.Permissions, []ac.Permission{
|
||||
{
|
||||
Action: ActionDatasourcesWrite,
|
||||
Scope: ScopeDatasourcesAll,
|
||||
@@ -127,14 +129,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{string(models.ROLE_ADMIN)},
|
||||
}
|
||||
|
||||
datasourcesIdReaderRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
datasourcesIdReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 4,
|
||||
Name: "fixed:datasources.id:reader",
|
||||
DisplayName: "Data source ID reader",
|
||||
Description: "Read the ID of a data source based on its name.",
|
||||
Group: "Infrequently used",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
Permissions: []ac.Permission{
|
||||
{
|
||||
Action: ActionDatasourcesIDRead,
|
||||
Scope: ScopeDatasourcesAll,
|
||||
@@ -144,14 +146,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{string(models.ROLE_VIEWER)},
|
||||
}
|
||||
|
||||
datasourcesCompatibilityReaderRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
datasourcesCompatibilityReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 3,
|
||||
Name: "fixed:datasources:compatibility:querier",
|
||||
DisplayName: "Data source compatibility querier",
|
||||
Description: "Only used for open source compatibility. Query data sources.",
|
||||
Group: "Infrequently used",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
Permissions: []ac.Permission{
|
||||
{Action: ActionDatasourcesQuery},
|
||||
{Action: ActionDatasourcesRead},
|
||||
},
|
||||
@@ -159,29 +161,29 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{string(models.ROLE_VIEWER)},
|
||||
}
|
||||
|
||||
orgReaderRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
orgReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 5,
|
||||
Name: "fixed:organization:reader",
|
||||
DisplayName: "Organization reader",
|
||||
Description: "Read an organization, such as its ID, name, address, or quotas.",
|
||||
Group: "Organizations",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
Permissions: []ac.Permission{
|
||||
{Action: ActionOrgsRead},
|
||||
{Action: ActionOrgsQuotasRead},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(models.ROLE_VIEWER), accesscontrol.RoleGrafanaAdmin},
|
||||
Grants: []string{string(models.ROLE_VIEWER), ac.RoleGrafanaAdmin},
|
||||
}
|
||||
|
||||
orgWriterRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
orgWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 5,
|
||||
Name: "fixed:organization:writer",
|
||||
DisplayName: "Organization writer",
|
||||
Description: "Read an organization, its quotas, or its preferences. Update organization properties, or its preferences.",
|
||||
Group: "Organizations",
|
||||
Permissions: accesscontrol.ConcatPermissions(orgReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{
|
||||
{Action: ActionOrgsPreferencesRead},
|
||||
{Action: ActionOrgsWrite},
|
||||
{Action: ActionOrgsPreferencesWrite},
|
||||
@@ -190,71 +192,71 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{string(models.ROLE_ADMIN)},
|
||||
}
|
||||
|
||||
orgMaintainerRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
orgMaintainerRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 5,
|
||||
Name: "fixed:organization:maintainer",
|
||||
DisplayName: "Organization maintainer",
|
||||
Description: "Create, read, write, or delete an organization. Read or write an organization's quotas. Needs to be assigned globally.",
|
||||
Group: "Organizations",
|
||||
Permissions: accesscontrol.ConcatPermissions(orgReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
Permissions: ac.ConcatPermissions(orgReaderRole.Role.Permissions, []ac.Permission{
|
||||
{Action: ActionOrgsCreate},
|
||||
{Action: ActionOrgsWrite},
|
||||
{Action: ActionOrgsDelete},
|
||||
{Action: ActionOrgsQuotasWrite},
|
||||
}),
|
||||
},
|
||||
Grants: []string{string(accesscontrol.RoleGrafanaAdmin)},
|
||||
Grants: []string{string(ac.RoleGrafanaAdmin)},
|
||||
}
|
||||
|
||||
teamCreatorGrants := []string{string(models.ROLE_ADMIN)}
|
||||
if hs.Cfg.EditorsCanAdmin {
|
||||
teamCreatorGrants = append(teamCreatorGrants, string(models.ROLE_EDITOR))
|
||||
}
|
||||
teamsCreatorRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
teamsCreatorRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:teams:creator",
|
||||
DisplayName: "Team creator",
|
||||
Description: "Create teams and read organisation users (required to manage the created teams).",
|
||||
Group: "Teams",
|
||||
Version: 2,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionTeamsCreate},
|
||||
{Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll},
|
||||
Permissions: []ac.Permission{
|
||||
{Action: ac.ActionTeamsCreate},
|
||||
{Action: ac.ActionOrgUsersRead, Scope: ac.ScopeUsersAll},
|
||||
},
|
||||
},
|
||||
Grants: teamCreatorGrants,
|
||||
}
|
||||
|
||||
teamsWriterRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
teamsWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:teams:writer",
|
||||
DisplayName: "Team writer",
|
||||
Description: "Create, read, write, or delete a team as well as controlling team memberships.",
|
||||
Group: "Teams",
|
||||
Version: 2,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionTeamsCreate},
|
||||
{Action: accesscontrol.ActionTeamsDelete, Scope: accesscontrol.ScopeTeamsAll},
|
||||
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: accesscontrol.ScopeTeamsAll},
|
||||
{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: accesscontrol.ScopeTeamsAll},
|
||||
{Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll},
|
||||
{Action: accesscontrol.ActionTeamsWrite, Scope: accesscontrol.ScopeTeamsAll},
|
||||
Permissions: []ac.Permission{
|
||||
{Action: ac.ActionTeamsCreate},
|
||||
{Action: ac.ActionTeamsDelete, Scope: ac.ScopeTeamsAll},
|
||||
{Action: ac.ActionTeamsPermissionsRead, Scope: ac.ScopeTeamsAll},
|
||||
{Action: ac.ActionTeamsPermissionsWrite, Scope: ac.ScopeTeamsAll},
|
||||
{Action: ac.ActionTeamsRead, Scope: ac.ScopeTeamsAll},
|
||||
{Action: ac.ActionTeamsWrite, Scope: ac.ScopeTeamsAll},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(models.ROLE_ADMIN)},
|
||||
}
|
||||
|
||||
annotationsReaderRole := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
annotationsReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:annotations:reader",
|
||||
DisplayName: "Annotation reader",
|
||||
Description: "Read annotations and tags",
|
||||
Group: "Annotations",
|
||||
Version: 1,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll},
|
||||
{Action: accesscontrol.ActionAnnotationsTagsRead, Scope: accesscontrol.ScopeAnnotationsTagsAll},
|
||||
Permissions: []ac.Permission{
|
||||
{Action: ac.ActionAnnotationsRead, Scope: ac.ScopeAnnotationsAll},
|
||||
{Action: ac.ActionAnnotationsTagsRead, Scope: ac.ScopeAnnotationsTagsAll},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(models.ROLE_VIEWER)},
|
||||
@@ -271,69 +273,96 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
// here is the list of complex evaluators we use in this package
|
||||
|
||||
// dataSourcesConfigurationAccessEvaluator is used to protect the "Configure > Data sources" tab access
|
||||
var dataSourcesConfigurationAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(ActionDatasourcesRead),
|
||||
accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(ActionDatasourcesCreate),
|
||||
accesscontrol.EvalPermission(ActionDatasourcesDelete),
|
||||
accesscontrol.EvalPermission(ActionDatasourcesWrite),
|
||||
var dataSourcesConfigurationAccessEvaluator = ac.EvalAll(
|
||||
ac.EvalPermission(ActionDatasourcesRead),
|
||||
ac.EvalAny(
|
||||
ac.EvalPermission(ActionDatasourcesCreate),
|
||||
ac.EvalPermission(ActionDatasourcesDelete),
|
||||
ac.EvalPermission(ActionDatasourcesWrite),
|
||||
),
|
||||
)
|
||||
|
||||
// dataSourcesNewAccessEvaluator is used to protect the "Configure > Data sources > New" page access
|
||||
var dataSourcesNewAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(ActionDatasourcesRead),
|
||||
accesscontrol.EvalPermission(ActionDatasourcesCreate),
|
||||
accesscontrol.EvalPermission(ActionDatasourcesWrite),
|
||||
var dataSourcesNewAccessEvaluator = ac.EvalAll(
|
||||
ac.EvalPermission(ActionDatasourcesRead),
|
||||
ac.EvalPermission(ActionDatasourcesCreate),
|
||||
ac.EvalPermission(ActionDatasourcesWrite),
|
||||
)
|
||||
|
||||
// dataSourcesEditAccessEvaluator is used to protect the "Configure > Data sources > Edit" page access
|
||||
var dataSourcesEditAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(ActionDatasourcesRead),
|
||||
accesscontrol.EvalPermission(ActionDatasourcesWrite),
|
||||
var dataSourcesEditAccessEvaluator = ac.EvalAll(
|
||||
ac.EvalPermission(ActionDatasourcesRead),
|
||||
ac.EvalPermission(ActionDatasourcesWrite),
|
||||
)
|
||||
|
||||
// orgPreferencesAccessEvaluator is used to protect the "Configure > Preferences" page access
|
||||
var orgPreferencesAccessEvaluator = accesscontrol.EvalAny(
|
||||
accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(ActionOrgsRead),
|
||||
accesscontrol.EvalPermission(ActionOrgsWrite),
|
||||
var orgPreferencesAccessEvaluator = ac.EvalAny(
|
||||
ac.EvalAll(
|
||||
ac.EvalPermission(ActionOrgsRead),
|
||||
ac.EvalPermission(ActionOrgsWrite),
|
||||
),
|
||||
accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(ActionOrgsPreferencesRead),
|
||||
accesscontrol.EvalPermission(ActionOrgsPreferencesWrite),
|
||||
ac.EvalAll(
|
||||
ac.EvalPermission(ActionOrgsPreferencesRead),
|
||||
ac.EvalPermission(ActionOrgsPreferencesWrite),
|
||||
),
|
||||
)
|
||||
|
||||
// orgsAccessEvaluator is used to protect the "Server Admin > Orgs" page access
|
||||
// (you need to have read access to update or delete orgs; read is the minimum)
|
||||
var orgsAccessEvaluator = accesscontrol.EvalPermission(ActionOrgsRead)
|
||||
var orgsAccessEvaluator = ac.EvalPermission(ActionOrgsRead)
|
||||
|
||||
// orgsCreateAccessEvaluator is used to protect the "Server Admin > Orgs > New Org" page access
|
||||
var orgsCreateAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(ActionOrgsRead),
|
||||
accesscontrol.EvalPermission(ActionOrgsCreate),
|
||||
var orgsCreateAccessEvaluator = ac.EvalAll(
|
||||
ac.EvalPermission(ActionOrgsRead),
|
||||
ac.EvalPermission(ActionOrgsCreate),
|
||||
)
|
||||
|
||||
// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access
|
||||
// grants access to a user when they can either create teams or can read and update a team
|
||||
var teamsAccessEvaluator = accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate),
|
||||
accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
|
||||
accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
|
||||
var teamsAccessEvaluator = ac.EvalAny(
|
||||
ac.EvalPermission(ac.ActionTeamsCreate),
|
||||
ac.EvalAll(
|
||||
ac.EvalPermission(ac.ActionTeamsRead),
|
||||
ac.EvalAny(
|
||||
ac.EvalPermission(ac.ActionTeamsWrite),
|
||||
ac.EvalPermission(ac.ActionTeamsPermissionsWrite),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access
|
||||
var teamsEditAccessEvaluator = accesscontrol.EvalAll(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
|
||||
accesscontrol.EvalAny(
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
|
||||
var teamsEditAccessEvaluator = ac.EvalAll(
|
||||
ac.EvalPermission(ac.ActionTeamsRead),
|
||||
ac.EvalAny(
|
||||
ac.EvalPermission(ac.ActionTeamsCreate),
|
||||
ac.EvalPermission(ac.ActionTeamsWrite),
|
||||
ac.EvalPermission(ac.ActionTeamsPermissionsWrite),
|
||||
),
|
||||
)
|
||||
|
||||
// Metadata helpers
|
||||
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
|
||||
func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, resource string, id int64) ac.Metadata {
|
||||
key := fmt.Sprintf("%d", id)
|
||||
ids := map[string]bool{key: true}
|
||||
|
||||
return hs.getMultiAccessControlMetadata(c, resource, ids)[key]
|
||||
}
|
||||
|
||||
// getMultiAccessControlMetadata returns the accesscontrol metadata associated with a given set of resources
|
||||
func (hs *HTTPServer) getMultiAccessControlMetadata(c *models.ReqContext, resource string, ids map[string]bool) map[string]ac.Metadata {
|
||||
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return map[string]ac.Metadata{}
|
||||
}
|
||||
|
||||
if c.SignedInUser.Permissions == nil {
|
||||
return map[string]ac.Metadata{}
|
||||
}
|
||||
|
||||
permissions, ok := c.SignedInUser.Permissions[c.OrgId]
|
||||
if !ok {
|
||||
return map[string]ac.Metadata{}
|
||||
}
|
||||
|
||||
return ac.GetResourcesMetadata(c.Req.Context(), permissions, resource, ids)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/dashboards"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// updateDashboardACL updates a dashboard's ACL items.
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var updateDashboardACL = func(ctx context.Context, s dashboards.Store, dashID int64, items []*models.DashboardAcl) error {
|
||||
return s.UpdateDashboardACLCtx(ctx, dashID, items)
|
||||
}
|
||||
+12
-12
@@ -239,7 +239,7 @@ func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Respo
|
||||
func (hs *HTTPServer) getAlertNotificationsInternal(c *models.ReqContext) ([]*models.AlertNotification, error) {
|
||||
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := hs.SQLStore.GetAllAlertNotifications(c.Req.Context(), query); err != nil {
|
||||
if err := hs.AlertNotificationService.GetAllAlertNotifications(c.Req.Context(), query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ func (hs *HTTPServer) GetAlertNotificationByID(c *models.ReqContext) response.Re
|
||||
return response.Error(404, "Alert notification not found", nil)
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.GetAlertNotifications(c.Req.Context(), query); err != nil {
|
||||
if err := hs.AlertNotificationService.GetAlertNotifications(c.Req.Context(), query); err != nil {
|
||||
return response.Error(500, "Failed to get alert notifications", err)
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.R
|
||||
return response.Error(404, "Alert notification not found", nil)
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.GetAlertNotificationsWithUid(c.Req.Context(), query); err != nil {
|
||||
if err := hs.AlertNotificationService.GetAlertNotificationsWithUid(c.Req.Context(), query); err != nil {
|
||||
return response.Error(500, "Failed to get alert notifications", err)
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Res
|
||||
}
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := hs.SQLStore.CreateAlertNotificationCommand(c.Req.Context(), &cmd); err != nil {
|
||||
if err := hs.AlertNotificationService.CreateAlertNotificationCommand(c.Req.Context(), &cmd); err != nil {
|
||||
if errors.Is(err, models.ErrAlertNotificationWithSameNameExists) || errors.Is(err, models.ErrAlertNotificationWithSameUIDExists) {
|
||||
return response.Error(409, "Failed to create alert notification", err)
|
||||
}
|
||||
@@ -325,7 +325,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Res
|
||||
return response.Error(500, "Failed to update alert notification", err)
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.UpdateAlertNotification(c.Req.Context(), &cmd); err != nil {
|
||||
if err := hs.AlertNotificationService.UpdateAlertNotification(c.Req.Context(), &cmd); err != nil {
|
||||
if errors.Is(err, models.ErrAlertNotificationNotFound) {
|
||||
return response.Error(404, err.Error(), err)
|
||||
}
|
||||
@@ -341,7 +341,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Res
|
||||
Id: cmd.Id,
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.GetAlertNotifications(c.Req.Context(), &query); err != nil {
|
||||
if err := hs.AlertNotificationService.GetAlertNotifications(c.Req.Context(), &query); err != nil {
|
||||
return response.Error(500, "Failed to get alert notification", err)
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) respons
|
||||
return response.Error(500, "Failed to update alert notification", err)
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.UpdateAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
|
||||
if err := hs.AlertNotificationService.UpdateAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
|
||||
if errors.Is(err, models.ErrAlertNotificationNotFound) {
|
||||
return response.Error(404, err.Error(), nil)
|
||||
}
|
||||
@@ -373,7 +373,7 @@ func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) respons
|
||||
Uid: cmd.Uid,
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.GetAlertNotificationsWithUid(c.Req.Context(), &query); err != nil {
|
||||
if err := hs.AlertNotificationService.GetAlertNotificationsWithUid(c.Req.Context(), &query); err != nil {
|
||||
return response.Error(500, "Failed to get alert notification", err)
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ func (hs *HTTPServer) fillWithSecureSettingsData(ctx context.Context, cmd *model
|
||||
Id: cmd.Id,
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.GetAlertNotifications(ctx, query); err != nil {
|
||||
if err := hs.AlertNotificationService.GetAlertNotifications(ctx, query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -418,7 +418,7 @@ func (hs *HTTPServer) fillWithSecureSettingsDataByUID(ctx context.Context, cmd *
|
||||
Uid: cmd.Uid,
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.GetAlertNotificationsWithUid(ctx, query); err != nil {
|
||||
if err := hs.AlertNotificationService.GetAlertNotificationsWithUid(ctx, query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ func (hs *HTTPServer) DeleteAlertNotification(c *models.ReqContext) response.Res
|
||||
Id: notificationId,
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.DeleteAlertNotification(c.Req.Context(), &cmd); err != nil {
|
||||
if err := hs.AlertNotificationService.DeleteAlertNotification(c.Req.Context(), &cmd); err != nil {
|
||||
if errors.Is(err, models.ErrAlertNotificationNotFound) {
|
||||
return response.Error(404, err.Error(), nil)
|
||||
}
|
||||
@@ -463,7 +463,7 @@ func (hs *HTTPServer) DeleteAlertNotificationByUID(c *models.ReqContext) respons
|
||||
Uid: web.Params(c.Req)[":uid"],
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.DeleteAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
|
||||
if err := hs.AlertNotificationService.DeleteAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil {
|
||||
if errors.Is(err, models.ErrAlertNotificationNotFound) {
|
||||
return response.Error(404, err.Error(), nil)
|
||||
}
|
||||
|
||||
+7
-4
@@ -26,7 +26,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
|
||||
reqEditorRole := middleware.ReqEditorRole
|
||||
reqOrgAdmin := middleware.ReqOrgAdmin
|
||||
reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin
|
||||
reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin(hs.SQLStore)
|
||||
reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin)
|
||||
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
|
||||
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg)
|
||||
@@ -336,8 +336,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
if hs.ThumbService != nil {
|
||||
dashboardRoute.Get("/uid/:uid/img/:kind/:theme", hs.ThumbService.GetImage)
|
||||
dashboardRoute.Post("/uid/:uid/img/:kind/:theme", hs.ThumbService.SetImage)
|
||||
dashboardRoute.Put("/uid/:uid/img/:kind/:theme", hs.ThumbService.UpdateThumbnailState)
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
|
||||
dashboardRoute.Post("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.SetImage)
|
||||
dashboardRoute.Put("/uid/:uid/img/:kind/:theme", reqGrafanaAdmin, hs.ThumbService.UpdateThumbnailState)
|
||||
}
|
||||
}
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", routing.Wrap(hs.CalculateDashboardDiff))
|
||||
@@ -470,7 +473,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
adminRoute.Get("/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), routing.Wrap(hs.AdminGetStats))
|
||||
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, routing.Wrap(hs.PauseAllAlerts))
|
||||
|
||||
if hs.ThumbService != nil {
|
||||
if hs.ThumbService != nil && hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) {
|
||||
adminRoute.Post("/crawler/start", reqGrafanaAdmin, routing.Wrap(hs.ThumbService.StartCrawler))
|
||||
adminRoute.Post("/crawler/stop", reqGrafanaAdmin, routing.Wrap(hs.ThumbService.StopCrawler))
|
||||
adminRoute.Get("/crawler/status", reqGrafanaAdmin, routing.Wrap(hs.ThumbService.CrawlerStatus))
|
||||
|
||||
+19
-11
@@ -25,9 +25,11 @@ import (
|
||||
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
@@ -276,6 +278,8 @@ type accessControlScenarioContext struct {
|
||||
|
||||
// cfg is the setting provider
|
||||
cfg *setting.Cfg
|
||||
|
||||
dashboardsStore dashboards.Store
|
||||
}
|
||||
|
||||
func setAccessControlPermissions(acmock *accesscontrolmock.Mock, perms []*accesscontrol.Permission, org int64) {
|
||||
@@ -347,6 +351,8 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
||||
|
||||
bus := bus.GetBus()
|
||||
|
||||
dashboardsStore := dashboardsstore.ProvideDashboardStore(db)
|
||||
|
||||
routeRegister := routing.NewRouteRegister()
|
||||
// Create minimal HTTP Server
|
||||
hs := &HTTPServer{
|
||||
@@ -358,6 +364,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
||||
RouteRegister: routeRegister,
|
||||
SQLStore: db,
|
||||
searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()),
|
||||
dashboardService: dashboardservice.ProvideDashboardService(dashboardsStore),
|
||||
}
|
||||
|
||||
// Defining the accesscontrol service has to be done before registering routes
|
||||
@@ -367,9 +374,9 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
||||
acmock = acmock.WithDisabled()
|
||||
}
|
||||
hs.AccessControl = acmock
|
||||
teamPermissionService, err := resourceservices.ProvideTeamPermissions(routeRegister, db, acmock, database.ProvideService(db))
|
||||
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(routeRegister, db, acmock, database.ProvideService(db))
|
||||
require.NoError(t, err)
|
||||
hs.TeamPermissionsService = teamPermissionService
|
||||
hs.teamPermissionsService = teamPermissionService
|
||||
} else {
|
||||
ac := ossaccesscontrol.ProvideService(hs.Features, &usagestats.UsageStatsMock{T: t},
|
||||
database.ProvideService(db), routing.NewRouteRegister())
|
||||
@@ -379,9 +386,9 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
||||
require.NoError(t, err)
|
||||
err = ac.RegisterFixedRoles()
|
||||
require.NoError(t, err)
|
||||
teamPermissionService, err := resourceservices.ProvideTeamPermissions(routeRegister, db, ac, database.ProvideService(db))
|
||||
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(routeRegister, db, ac, database.ProvideService(db))
|
||||
require.NoError(t, err)
|
||||
hs.TeamPermissionsService = teamPermissionService
|
||||
hs.teamPermissionsService = teamPermissionService
|
||||
}
|
||||
|
||||
// Instantiate a new Server
|
||||
@@ -402,12 +409,13 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
|
||||
hs.RouteRegister.Register(m.Router)
|
||||
|
||||
return accessControlScenarioContext{
|
||||
server: m,
|
||||
initCtx: initCtx,
|
||||
hs: hs,
|
||||
acmock: acmock,
|
||||
db: db,
|
||||
cfg: cfg,
|
||||
server: m,
|
||||
initCtx: initCtx,
|
||||
hs: hs,
|
||||
acmock: acmock,
|
||||
db: db,
|
||||
cfg: cfg,
|
||||
dashboardsStore: dashboardsStore,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-14
@@ -148,8 +148,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
|
||||
meta.FolderUrl = query.Result.GetUrl()
|
||||
}
|
||||
|
||||
svc := dashboards.NewProvisioningService(hs.SQLStore)
|
||||
provisioningData, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id)
|
||||
provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id)
|
||||
if err != nil {
|
||||
return response.Error(500, "Error while checking if dashboard is provisioned", err)
|
||||
}
|
||||
@@ -233,8 +232,8 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
|
||||
if err != nil {
|
||||
hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
|
||||
}
|
||||
svc := dashboards.NewService(hs.SQLStore)
|
||||
err = svc.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId)
|
||||
|
||||
err = hs.dashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId)
|
||||
if err != nil {
|
||||
var dashboardErr models.DashboardErr
|
||||
if ok := errors.As(err, &dashboardErr); ok {
|
||||
@@ -271,8 +270,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.UserId = c.UserId
|
||||
if cmd.FolderUid != "" {
|
||||
folders := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
folder, err := folders.GetFolderByUID(ctx, cmd.FolderUid)
|
||||
folder, err := hs.folderService.GetFolderByUID(ctx, c.SignedInUser, c.OrgId, cmd.FolderUid)
|
||||
if err != nil {
|
||||
if errors.Is(err, models.ErrFolderNotFound) {
|
||||
return response.Error(400, "Folder not found", err)
|
||||
@@ -294,18 +292,17 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
}
|
||||
}
|
||||
|
||||
svc := dashboards.NewProvisioningService(hs.SQLStore)
|
||||
var provisioningData *models.DashboardProvisioning
|
||||
if dash.Id != 0 {
|
||||
data, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id)
|
||||
data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id)
|
||||
if err != nil {
|
||||
return response.Error(500, "Error while checking if dashboard is provisioned using ID", err)
|
||||
}
|
||||
provisioningData = data
|
||||
} else if dash.Uid != "" {
|
||||
data, err := svc.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid)
|
||||
if err != nil && (!errors.Is(err, models.ErrProvisionedDashboardNotFound) && !errors.Is(err, models.ErrDashboardNotFound)) {
|
||||
return response.Error(500, "Error while checking if dashboard is provisioned using UID", err)
|
||||
data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid)
|
||||
if err != nil && !errors.Is(err, models.ErrProvisionedDashboardNotFound) && !errors.Is(err, models.ErrDashboardNotFound) {
|
||||
return response.Error(500, "Error while checking if dashboard is provisioned", err)
|
||||
}
|
||||
provisioningData = data
|
||||
}
|
||||
@@ -329,8 +326,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
Overwrite: cmd.Overwrite,
|
||||
}
|
||||
|
||||
dashSvc := dashboards.NewService(hs.SQLStore)
|
||||
dashboard, err := dashSvc.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate)
|
||||
dashboard, err := hs.dashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate)
|
||||
|
||||
if hs.Live != nil {
|
||||
// Tell everyone listening that the dashboard changed
|
||||
@@ -362,7 +358,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
|
||||
if hs.Cfg.EditorsCanAdmin && newDashboard {
|
||||
inFolder := cmd.FolderId > 0
|
||||
err := dashSvc.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
|
||||
err := hs.dashboardService.MakeUserAdmin(ctx, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
|
||||
if err != nil {
|
||||
hs.log.Error("Could not make user admin", "dashboard", dashboard.Title, "user", c.SignedInUser.UserId, "error", err)
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.
|
||||
return response.Error(403, "Cannot remove own admin permission for a folder", nil)
|
||||
}
|
||||
|
||||
if err := updateDashboardACL(c.Req.Context(), hs.SQLStore, dashID, items); err != nil {
|
||||
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil {
|
||||
if errors.Is(err, models.ErrDashboardAclInfoMissing) ||
|
||||
errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) {
|
||||
return response.Error(409, err.Error(), err)
|
||||
|
||||
@@ -1,59 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/dashboards"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDashboardPermissionAPIEndpoint(t *testing.T) {
|
||||
t.Run("Dashboard permissions test", func(t *testing.T) {
|
||||
settings := setting.NewCfg()
|
||||
dashboardStore := &database.FakeDashboardStore{}
|
||||
defer dashboardStore.AssertExpectations(t)
|
||||
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: settings,
|
||||
SQLStore: mockSQLStore,
|
||||
Cfg: settings,
|
||||
dashboardService: dashboardservice.ProvideDashboardService(dashboardStore),
|
||||
SQLStore: mockSQLStore,
|
||||
}
|
||||
|
||||
t.Run("Given dashboard not exists", func(t *testing.T) {
|
||||
mockSQLStore.ExpectedError = models.ErrDashboardNotFound
|
||||
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions",
|
||||
"/api/dashboards/id/:dashboardId/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
callGetDashboardPermissions(sc, hs)
|
||||
assert.Equal(t, 404, sc.resp.Code)
|
||||
}, mockSQLStore)
|
||||
|
||||
cmd := dtos.UpdateDashboardAclCommand{
|
||||
Items: []dtos.DashboardAclUpdateItem{
|
||||
{UserID: 1000, Permission: models.PERMISSION_ADMIN},
|
||||
},
|
||||
}
|
||||
|
||||
updateDashboardPermissionScenario(t, updatePermissionContext{
|
||||
desc: "When calling POST on",
|
||||
url: "/api/dashboards/id/1/permissions",
|
||||
routePattern: "/api/dashboards/id/:dashboardId/permissions",
|
||||
cmd: cmd,
|
||||
fn: func(sc *scenarioContext) {
|
||||
callUpdateDashboardPermissions(t, sc)
|
||||
assert.Equal(t, 404, sc.resp.Code)
|
||||
},
|
||||
}, hs)
|
||||
})
|
||||
|
||||
t.Run("Given user has no admin permissions", func(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
t.Cleanup(func() {
|
||||
@@ -63,7 +42,6 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
|
||||
|
||||
getDashboardQueryResult := models.NewDashboard("Dash")
|
||||
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
mockSQLStore.ExpectedDashboard = getDashboardQueryResult
|
||||
mockSQLStore.ExpectedError = nil
|
||||
@@ -80,6 +58,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
updateDashboardPermissionScenario(t, updatePermissionContext{
|
||||
desc: "When calling POST on",
|
||||
url: "/api/dashboards/id/1/permissions",
|
||||
@@ -324,26 +303,20 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
assert.Len(t, cmd.Items, 3)
|
||||
|
||||
var numOfItems []*models.DashboardAcl
|
||||
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
items := args.Get(2).([]*models.DashboardAcl)
|
||||
numOfItems = items
|
||||
}).Return(nil).Once()
|
||||
updateDashboardPermissionScenario(t, updatePermissionContext{
|
||||
desc: "When calling POST on",
|
||||
url: "/api/dashboards/id/1/permissions",
|
||||
routePattern: "/api/dashboards/id/:dashboardId/permissions",
|
||||
cmd: cmd,
|
||||
fn: func(sc *scenarioContext) {
|
||||
// TODO: Replace this fake with a fake SQLStore instead (once we can use an interface in its stead)
|
||||
origUpdateDashboardACL := updateDashboardACL
|
||||
t.Cleanup(func() {
|
||||
updateDashboardACL = origUpdateDashboardACL
|
||||
})
|
||||
var gotItems []*models.DashboardAcl
|
||||
updateDashboardACL = func(_ context.Context, _ dashboards.Store, folderID int64, items []*models.DashboardAcl) error {
|
||||
gotItems = items
|
||||
return nil
|
||||
}
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Len(t, gotItems, 4)
|
||||
assert.Len(t, numOfItems, 4)
|
||||
},
|
||||
}, hs)
|
||||
})
|
||||
@@ -357,15 +330,6 @@ func callGetDashboardPermissions(sc *scenarioContext, hs *HTTPServer) {
|
||||
|
||||
func callUpdateDashboardPermissions(t *testing.T, sc *scenarioContext) {
|
||||
t.Helper()
|
||||
|
||||
origUpdateDashboardACL := updateDashboardACL
|
||||
t.Cleanup(func() {
|
||||
updateDashboardACL = origUpdateDashboardACL
|
||||
})
|
||||
updateDashboardACL = func(_ context.Context, _ dashboards.Store, dashID int64, items []*models.DashboardAcl) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
metrics.MApiDashboardSnapshotCreate.Inc()
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.CreateDashboardSnapshot(c.Req.Context(), &cmd); err != nil {
|
||||
if err := hs.DashboardsnapshotsService.CreateDashboardSnapshot(c.Req.Context(), &cmd); err != nil {
|
||||
c.JsonApiErr(500, "Failed to create snapshot", err)
|
||||
return nil
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Respon
|
||||
|
||||
query := &models.GetDashboardSnapshotQuery{Key: key}
|
||||
|
||||
err := hs.SQLStore.GetDashboardSnapshot(query)
|
||||
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to get dashboard snapshot", err)
|
||||
}
|
||||
@@ -224,8 +224,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r
|
||||
}
|
||||
|
||||
query := &models.GetDashboardSnapshotQuery{DeleteKey: key}
|
||||
|
||||
err := hs.SQLStore.GetDashboardSnapshot(query)
|
||||
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to get dashboard snapshot", err)
|
||||
}
|
||||
@@ -239,7 +238,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r
|
||||
|
||||
cmd := &models.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
|
||||
|
||||
if err := hs.SQLStore.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
|
||||
if err := hs.DashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
|
||||
return response.Error(500, "Failed to delete dashboard snapshot", err)
|
||||
}
|
||||
|
||||
@@ -258,7 +257,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
|
||||
query := &models.GetDashboardSnapshotQuery{Key: key}
|
||||
|
||||
err := hs.SQLStore.GetDashboardSnapshot(query)
|
||||
err := hs.DashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to get dashboard snapshot", err)
|
||||
}
|
||||
@@ -287,7 +286,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
|
||||
cmd := &models.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
|
||||
|
||||
if err := hs.SQLStore.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
|
||||
if err := hs.DashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
|
||||
return response.Error(500, "Failed to delete dashboard snapshot", err)
|
||||
}
|
||||
|
||||
@@ -313,7 +312,7 @@ func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Re
|
||||
SignedInUser: c.SignedInUser,
|
||||
}
|
||||
|
||||
err := hs.SQLStore.SearchDashboardSnapshots(&searchQuery)
|
||||
err := hs.DashboardsnapshotsService.SearchDashboardSnapshots(c.Req.Context(), &searchQuery)
|
||||
if err != nil {
|
||||
return response.Error(500, "Search failed", err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -33,7 +34,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
|
||||
editorRole := models.ROLE_EDITOR
|
||||
sqlmock := mockstore.NewSQLStoreMock()
|
||||
aclMockResp := []*models.DashboardAclInfoDTO{}
|
||||
hs := &HTTPServer{SQLStore: sqlmock}
|
||||
hs := &HTTPServer{DashboardsnapshotsService: &dashboardsnapshots.Service{SQLStore: sqlmock}}
|
||||
|
||||
setUpSnapshotTest := func(t *testing.T) *models.DashboardSnapshot {
|
||||
t.Helper()
|
||||
|
||||
+61
-84
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@@ -15,11 +14,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
dboards "github.com/grafana/grafana/pkg/dashboards"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -145,7 +146,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
setUp()
|
||||
sc.sqlStore = mockSQLStore
|
||||
dash := getDashboardShouldReturn200(sc)
|
||||
dash := getDashboardShouldReturn200(t, sc)
|
||||
|
||||
assert.False(t, dash.Meta.CanEdit)
|
||||
assert.False(t, dash.Meta.CanSave)
|
||||
@@ -177,7 +178,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
setUp()
|
||||
sc.sqlStore = mockSQLStore
|
||||
dash := getDashboardShouldReturn200(sc)
|
||||
dash := getDashboardShouldReturn200(t, sc)
|
||||
|
||||
assert.True(t, dash.Meta.CanEdit)
|
||||
assert.True(t, dash.Meta.CanSave)
|
||||
@@ -212,11 +213,13 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
mockSQLStore.ExpectedDashboard = fakeDash
|
||||
|
||||
dashboardStore := database.ProvideDashboardStore(sqlstore.InitTestDB(t))
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
Live: newTestLive(t),
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
dashboardService: service.ProvideDashboardService(dashboardStore),
|
||||
SQLStore: mockSQLStore,
|
||||
}
|
||||
hs.SQLStore = mockSQLStore
|
||||
@@ -345,7 +348,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
setUpInner()
|
||||
sc.sqlStore = mockSQLStore
|
||||
dash := getDashboardShouldReturn200(sc)
|
||||
dash := getDashboardShouldReturn200(t, sc)
|
||||
|
||||
assert.True(t, dash.Meta.CanEdit)
|
||||
assert.True(t, dash.Meta.CanSave)
|
||||
@@ -412,7 +415,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
|
||||
require.True(t, setting.ViewersCanEdit)
|
||||
sc.sqlStore = mockSQLStore
|
||||
dash := getDashboardShouldReturn200(sc)
|
||||
dash := getDashboardShouldReturn200(t, sc)
|
||||
|
||||
assert.True(t, dash.Meta.CanEdit)
|
||||
assert.False(t, dash.Meta.CanSave)
|
||||
@@ -445,7 +448,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
setUpInner()
|
||||
sc.sqlStore = mockSQLStore
|
||||
dash := getDashboardShouldReturn200(sc)
|
||||
dash := getDashboardShouldReturn200(t, sc)
|
||||
|
||||
assert.True(t, dash.Meta.CanEdit)
|
||||
assert.True(t, dash.Meta.CanSave)
|
||||
@@ -493,7 +496,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
setUpInner()
|
||||
sc.sqlStore = mockSQLStore
|
||||
dash := getDashboardShouldReturn200(sc)
|
||||
dash := getDashboardShouldReturn200(t, sc)
|
||||
|
||||
assert.False(t, dash.Meta.CanEdit)
|
||||
assert.False(t, dash.Meta.CanSave)
|
||||
@@ -535,6 +538,8 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Post dashboard response tests", func(t *testing.T) {
|
||||
dashboardStore := &database.FakeDashboardStore{}
|
||||
defer dashboardStore.AssertExpectations(t)
|
||||
// This tests that a valid request returns correct response
|
||||
t.Run("Given a correct request for creating a dashboard", func(t *testing.T) {
|
||||
const folderID int64 = 3
|
||||
@@ -562,7 +567,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) {
|
||||
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) {
|
||||
callPostDashboardShouldReturnSuccess(sc)
|
||||
|
||||
dto := mock.SavedDashboards[0]
|
||||
@@ -612,7 +617,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
GetFolderByUIDResult: &models.Folder{Id: 1, Uid: "folderUID", Title: "Folder"},
|
||||
}
|
||||
|
||||
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) {
|
||||
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) {
|
||||
callPostDashboardShouldReturnSuccess(sc)
|
||||
|
||||
dto := mock.SavedDashboards[0]
|
||||
@@ -661,7 +666,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
GetFolderByUIDError: errors.New("Error while searching Folder ID"),
|
||||
}
|
||||
|
||||
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, mockFolder, cmd, func(sc *scenarioContext) {
|
||||
postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, mock, mockFolder, func(sc *scenarioContext) {
|
||||
callPostDashboard(sc)
|
||||
assert.Equal(t, 500, sc.resp.Code)
|
||||
})
|
||||
@@ -706,7 +711,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
postDashboardScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()),
|
||||
"/api/dashboards", "/api/dashboards", mock, nil, cmd, func(sc *scenarioContext) {
|
||||
"/api/dashboards", "/api/dashboards", cmd, mock, nil, func(sc *scenarioContext) {
|
||||
callPostDashboard(sc)
|
||||
assert.Equal(t, tc.ExpectedStatusCode, sc.resp.Code)
|
||||
})
|
||||
@@ -852,14 +857,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
|
||||
t.Run("Given provisioned dashboard", func(t *testing.T) {
|
||||
setUp := func() {
|
||||
origGetProvisionedData := dashboards.GetProvisionedData
|
||||
t.Cleanup(func() {
|
||||
dashboards.GetProvisionedData = origGetProvisionedData
|
||||
})
|
||||
dashboards.GetProvisionedData = func(dboards.Store, int64) (*models.DashboardProvisioning, error) {
|
||||
return &models.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"}, nil
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = []*models.DashboardAclInfoDTO{
|
||||
{OrgId: testOrgID, DashboardId: 1, UserId: testUserID, Permission: models.PERMISSION_EDIT},
|
||||
@@ -867,6 +864,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`))
|
||||
require.NoError(t, err)
|
||||
@@ -874,37 +872,39 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
|
||||
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
setUp()
|
||||
dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`))
|
||||
require.NoError(t, err)
|
||||
mockSQLStore.ExpectedDashboard = &models.Dashboard{Id: 1, Data: dataValue}
|
||||
sc.sqlStore = mockSQLStore
|
||||
mock := provisioning.NewProvisioningServiceMock(context.Background())
|
||||
mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
|
||||
fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background())
|
||||
fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
|
||||
return "/tmp/grafana/dashboards"
|
||||
}
|
||||
|
||||
dash := getDashboardShouldReturn200WithConfig(sc, mock)
|
||||
dashboardStore := &database.FakeDashboardStore{}
|
||||
defer dashboardStore.AssertExpectations(t)
|
||||
|
||||
assert.Equal(t, filepath.Join("test", "dashboard1.json"), dash.Meta.ProvisionedExternalId)
|
||||
dashboardStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{ExternalId: "/dashboard1.json"}, nil).Once()
|
||||
|
||||
dash := getDashboardShouldReturn200WithConfig(t, sc, fakeProvisioningService, dashboardStore)
|
||||
|
||||
assert.Equal(t, "../../../dashboard1.json", dash.Meta.ProvisionedExternalId, mockSQLStore)
|
||||
}, mockSQLStore)
|
||||
|
||||
loggedInUserScenarioWithRole(t, "When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
setUp()
|
||||
|
||||
mock := provisioning.NewProvisioningServiceMock(context.Background())
|
||||
mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
|
||||
fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background())
|
||||
fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
|
||||
return "/tmp/grafana/dashboards"
|
||||
}
|
||||
mock.GetAllowUIUpdatesFromConfigFunc = func(name string) bool {
|
||||
|
||||
fakeProvisioningService.GetAllowUIUpdatesFromConfigFunc = func(name string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
ProvisioningService: mock,
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
SQLStore: mockSQLStore,
|
||||
Cfg: setting.NewCfg(),
|
||||
ProvisioningService: fakeProvisioningService,
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
dashboardProvisioningService: mockDashboardProvisioningService{},
|
||||
SQLStore: mockSQLStore,
|
||||
}
|
||||
hs.callGetDashboard(sc)
|
||||
|
||||
@@ -919,21 +919,28 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService provisioning.ProvisioningService) dtos.
|
||||
DashboardFullWithMeta {
|
||||
func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, provisioningService provisioning.ProvisioningService, dashboardStore dashboards.Store) dtos.DashboardFullWithMeta {
|
||||
t.Helper()
|
||||
|
||||
if provisioningService == nil {
|
||||
provisioningService = provisioning.NewProvisioningServiceMock(context.Background())
|
||||
}
|
||||
|
||||
if dashboardStore == nil {
|
||||
sql := sqlstore.InitTestDB(t)
|
||||
dashboardStore = database.ProvideDashboardStore(sql)
|
||||
}
|
||||
|
||||
libraryPanelsService := mockLibraryPanelService{}
|
||||
libraryElementsService := mockLibraryElementService{}
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
LibraryPanelService: &libraryPanelsService,
|
||||
LibraryElementService: &libraryElementsService,
|
||||
ProvisioningService: provisioningService,
|
||||
SQLStore: sc.sqlStore,
|
||||
Cfg: setting.NewCfg(),
|
||||
LibraryPanelService: &libraryPanelsService,
|
||||
LibraryElementService: &libraryElementsService,
|
||||
ProvisioningService: provisioningService,
|
||||
dashboardProvisioningService: service.ProvideDashboardService(dashboardStore),
|
||||
SQLStore: sc.sqlStore,
|
||||
}
|
||||
|
||||
hs.callGetDashboard(sc)
|
||||
@@ -947,8 +954,8 @@ func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningServ
|
||||
return dash
|
||||
}
|
||||
|
||||
func getDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||
return getDashboardShouldReturn200WithConfig(sc, nil)
|
||||
func getDashboardShouldReturn200(t *testing.T, sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||
return getDashboardShouldReturn200WithConfig(t, sc, nil, nil)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) {
|
||||
@@ -976,17 +983,13 @@ func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T, sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
|
||||
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T,
|
||||
sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
origNewDashboardService := dashboards.NewService
|
||||
t.Cleanup(func() {
|
||||
dashboards.NewService = origNewDashboardService
|
||||
})
|
||||
dashboards.MockDashboardService(mockDashboard)
|
||||
|
||||
hs.dashboardService = mockDashboard
|
||||
sc.handlerFunc = hs.DeleteDashboardByUID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
@@ -1005,9 +1008,7 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) {
|
||||
assert.Equal(sc.t, 200, sc.resp.Code)
|
||||
}
|
||||
|
||||
func postDashboardScenario(t *testing.T, desc string, url string, routePattern string,
|
||||
mock *dashboards.FakeDashboardService, mockFolder *fakeFolderService, cmd models.SaveDashboardCommand,
|
||||
fn scenarioFunc) {
|
||||
func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, cmd models.SaveDashboardCommand, dashboardService dashboards.DashboardService, folderService dashboards.FolderService, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
@@ -1023,6 +1024,8 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
pluginStore: &fakePluginStore{},
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
dashboardService: dashboardService,
|
||||
folderService: folderService,
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
@@ -1035,20 +1038,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
return hs.PostDashboard(c)
|
||||
})
|
||||
|
||||
origNewDashboardService := dashboards.NewService
|
||||
origProvisioningService := dashboards.NewProvisioningService
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
t.Cleanup(func() {
|
||||
dashboards.NewService = origNewDashboardService
|
||||
dashboards.NewProvisioningService = origProvisioningService
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
})
|
||||
dashboards.MockDashboardService(mock)
|
||||
dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService {
|
||||
return mockDashboardProvisioningService{}
|
||||
}
|
||||
mockFolderService(mockFolder)
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
@@ -1091,9 +1080,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
|
||||
})
|
||||
}
|
||||
|
||||
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string,
|
||||
mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc,
|
||||
sqlStore sqlstore.Store) {
|
||||
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
@@ -1107,6 +1094,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
dashboardService: mock,
|
||||
SQLStore: sqlStore,
|
||||
}
|
||||
|
||||
@@ -1125,17 +1113,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
|
||||
return hs.RestoreDashboardVersion(c)
|
||||
})
|
||||
|
||||
origProvisioningService := dashboards.NewProvisioningService
|
||||
origNewDashboardService := dashboards.NewService
|
||||
t.Cleanup(func() {
|
||||
dashboards.NewService = origNewDashboardService
|
||||
dashboards.NewProvisioningService = origProvisioningService
|
||||
})
|
||||
dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService {
|
||||
return mockDashboardProvisioningService{}
|
||||
}
|
||||
dashboards.MockDashboardService(mock)
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
|
||||
+3
-28
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/adapters"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -71,23 +70,6 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response {
|
||||
return response.JSON(200, &result)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getDataSourceAccessControlMetadata(c *models.ReqContext, dsID int64) (accesscontrol.Metadata, error) {
|
||||
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser,
|
||||
accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil || len(userPermissions) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d", dsID)
|
||||
dsIDs := map[string]bool{key: true}
|
||||
|
||||
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "datasources", dsIDs)[key], nil
|
||||
}
|
||||
|
||||
// GET /api/datasources/:id
|
||||
func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response {
|
||||
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
|
||||
@@ -115,12 +97,9 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response
|
||||
}
|
||||
|
||||
dto := convertModelToDtos(filtered[0])
|
||||
|
||||
// Add accesscontrol metadata
|
||||
metadata, err := hs.getDataSourceAccessControlMetadata(c, dto.Id)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to query metadata", err)
|
||||
}
|
||||
dto.AccessControl = metadata
|
||||
dto.AccessControl = hs.getAccessControlMetadata(c, "datasources", dto.Id)
|
||||
|
||||
return response.JSON(200, &dto)
|
||||
}
|
||||
@@ -179,11 +158,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response
|
||||
dto := convertModelToDtos(filtered[0])
|
||||
|
||||
// Add accesscontrol metadata
|
||||
metadata, err := hs.getDataSourceAccessControlMetadata(c, dto.Id)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to query metadata", err)
|
||||
}
|
||||
dto.AccessControl = metadata
|
||||
dto.AccessControl = hs.getAccessControlMetadata(c, "datasources", dto.Id)
|
||||
|
||||
return response.JSON(200, &dto)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ type NavLink struct {
|
||||
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||
Children []*NavLink `json:"children,omitempty"`
|
||||
HighlightText string `json:"highlightText,omitempty"`
|
||||
HighlightID string `json:"highlightId,omitempty"`
|
||||
}
|
||||
|
||||
// NavIDCfg is the id for org configuration navigation node
|
||||
|
||||
+7
-16
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -19,8 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
folders, err := s.GetFolders(c.Req.Context(), c.QueryInt64("limit"), c.QueryInt64("page"))
|
||||
folders, err := hs.folderService.GetFolders(c.Req.Context(), c.SignedInUser, c.OrgId, c.QueryInt64("limit"), c.QueryInt64("page"))
|
||||
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
@@ -40,8 +38,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"])
|
||||
folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
@@ -51,14 +48,11 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
|
||||
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "id is invalid", err)
|
||||
}
|
||||
|
||||
folder, err := s.GetFolderByID(c.Req.Context(), id)
|
||||
folder, err := hs.folderService.GetFolderByID(c.Req.Context(), c.SignedInUser, c.OrgId, id)
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
@@ -72,14 +66,13 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
folder, err := s.CreateFolder(c.Req.Context(), cmd.Title, cmd.Uid)
|
||||
folder, err := hs.folderService.CreateFolder(c.Req.Context(), c.SignedInUser, c.OrgId, cmd.Title, cmd.Uid)
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
if hs.Cfg.EditorsCanAdmin {
|
||||
if err := s.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil {
|
||||
if err := hs.folderService.MakeUserAdmin(c.Req.Context(), c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil {
|
||||
hs.log.Error("Could not make user admin", "folder", folder.Title, "user",
|
||||
c.SignedInUser.UserId, "error", err)
|
||||
}
|
||||
@@ -94,8 +87,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
err := s.UpdateFolder(c.Req.Context(), web.Params(c.Req)[":uid"], &cmd)
|
||||
err := hs.folderService.UpdateFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], &cmd)
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
@@ -105,7 +97,6 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) {
|
||||
@@ -114,7 +105,7 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { //
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
f, err := s.DeleteFolder(c.Req.Context(), web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules"))
|
||||
f, err := hs.folderService.DeleteFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules"))
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
@@ -9,15 +9,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"])
|
||||
folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"])
|
||||
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
@@ -68,8 +66,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
|
||||
return response.Error(400, err.Error(), err)
|
||||
}
|
||||
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
|
||||
folder, err := s.GetFolderByUID(c.Req.Context(), web.Params(c.Req)[":uid"])
|
||||
folder, err := hs.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
@@ -117,7 +114,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res
|
||||
return response.Error(403, "Cannot remove own admin permission for a folder", nil)
|
||||
}
|
||||
|
||||
if err := updateDashboardACL(c.Req.Context(), hs.SQLStore, folder.Id, items); err != nil {
|
||||
if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil {
|
||||
if errors.Is(err, models.ErrDashboardAclInfoMissing) {
|
||||
err = models.ErrFolderAclInfoMissing
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
@@ -13,28 +12,28 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
dashboardifaces "github.com/grafana/grafana/pkg/dashboards"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
settings := setting.NewCfg()
|
||||
hs := &HTTPServer{Cfg: settings}
|
||||
folderService := &dashboards.FakeFolderService{}
|
||||
defer folderService.AssertExpectations(t)
|
||||
|
||||
dashboardStore := &database.FakeDashboardStore{}
|
||||
defer dashboardStore.AssertExpectations(t)
|
||||
|
||||
hs := &HTTPServer{Cfg: settings, folderService: folderService, dashboardService: service.ProvideDashboardService(dashboardStore)}
|
||||
|
||||
t.Run("Given folder not exists", func(t *testing.T) {
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUIDError: models.ErrFolderNotFound,
|
||||
}
|
||||
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
t.Cleanup(func() {
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
})
|
||||
mockFolderService(mock)
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderNotFound).Twice()
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
callGetFolderPermissions(sc, hs)
|
||||
@@ -61,24 +60,14 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
|
||||
t.Run("Given user has no admin permissions", func(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
})
|
||||
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUIDResult: &models.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
},
|
||||
}
|
||||
|
||||
mockFolderService(mock)
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrFolderAccessDenied).Twice()
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
|
||||
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
callGetFolderPermissions(sc, hs)
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
@@ -104,10 +93,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
|
||||
t.Run("Given user has admin permissions and permissions to update", func(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
})
|
||||
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||
@@ -122,16 +109,11 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUIDResult: &models.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
},
|
||||
}
|
||||
|
||||
mockFolderService(mock)
|
||||
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Twice()
|
||||
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
|
||||
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||
callGetFolderPermissions(sc, hs)
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
@@ -175,10 +157,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
|
||||
t.Run("When trying to update permissions with duplicate permissions", func(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
})
|
||||
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||
@@ -187,15 +167,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists,
|
||||
})
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUIDResult: &models.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
},
|
||||
}
|
||||
|
||||
mockFolderService(mock)
|
||||
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Once()
|
||||
|
||||
cmd := dtos.UpdateDashboardAclCommand{
|
||||
Items: []dtos.DashboardAclUpdateItem{
|
||||
@@ -249,10 +222,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
|
||||
t.Run("When trying to override inherited permissions with lower precedence", func(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
})
|
||||
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||
@@ -261,15 +232,8 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride},
|
||||
)
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUIDResult: &models.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
},
|
||||
}
|
||||
|
||||
mockFolderService(mock)
|
||||
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Once()
|
||||
|
||||
cmd := dtos.UpdateDashboardAclCommand{
|
||||
Items: []dtos.DashboardAclUpdateItem{
|
||||
@@ -291,14 +255,12 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
|
||||
t.Run("Getting and updating folder permissions with hidden users", func(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
settings.HiddenUsers = map[string]struct{}{
|
||||
"hiddenUser": {},
|
||||
testUserLogin: {},
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
settings.HiddenUsers = make(map[string]struct{})
|
||||
})
|
||||
|
||||
@@ -315,15 +277,13 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUIDResult: &models.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
},
|
||||
}
|
||||
var gotItems []*models.DashboardAcl
|
||||
|
||||
mockFolderService(mock)
|
||||
folderResponse := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResponse, nil).Twice()
|
||||
dashboardStore.On("UpdateDashboardACL", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
gotItems = args.Get(2).([]*models.DashboardAcl)
|
||||
}).Return(nil).Once()
|
||||
|
||||
var resp []*models.DashboardAclInfoDTO
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
@@ -360,16 +320,6 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) {
|
||||
routePattern: "/api/folders/:uid/permissions",
|
||||
cmd: cmd,
|
||||
fn: func(sc *scenarioContext) {
|
||||
origUpdateDashboardACL := updateDashboardACL
|
||||
t.Cleanup(func() {
|
||||
updateDashboardACL = origUpdateDashboardACL
|
||||
})
|
||||
var gotItems []*models.DashboardAcl
|
||||
updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, _ int64, items []*models.DashboardAcl) error {
|
||||
gotItems = items
|
||||
return nil
|
||||
}
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
assert.Len(t, gotItems, 4)
|
||||
@@ -385,21 +335,12 @@ func callGetFolderPermissions(sc *scenarioContext, hs *HTTPServer) {
|
||||
|
||||
func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) {
|
||||
t.Helper()
|
||||
|
||||
origUpdateDashboardACL := updateDashboardACL
|
||||
t.Cleanup(func() {
|
||||
updateDashboardACL = origUpdateDashboardACL
|
||||
})
|
||||
updateDashboardACL = func(_ context.Context, _ dashboardifaces.Store, dashID int64, items []*models.DashboardAcl) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) {
|
||||
t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
sc := setupScenarioContext(t, ctx.url)
|
||||
|
||||
|
||||
+29
-49
@@ -10,26 +10,28 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
dboards "github.com/grafana/grafana/pkg/dashboards"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFoldersAPIEndpoint(t *testing.T) {
|
||||
folderService := &dashboards.FakeFolderService{}
|
||||
defer folderService.AssertExpectations(t)
|
||||
|
||||
t.Run("Given a correct request for creating a folder", func(t *testing.T) {
|
||||
cmd := models.CreateFolderCommand{
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
}
|
||||
|
||||
mock := &fakeFolderService{
|
||||
CreateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder"},
|
||||
}
|
||||
folderResult := &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}
|
||||
folderService.On("CreateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(folderResult, nil).Once()
|
||||
|
||||
createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", mock, cmd,
|
||||
createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", folderService, cmd,
|
||||
func(sc *scenarioContext) {
|
||||
callCreateFolder(sc)
|
||||
|
||||
@@ -64,12 +66,10 @@ func TestFoldersAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
mock := &fakeFolderService{
|
||||
CreateFolderError: tc.Error,
|
||||
}
|
||||
folderService.On("CreateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, tc.Error).Once()
|
||||
|
||||
createFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()),
|
||||
"/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) {
|
||||
"/api/folders", "/api/folders", folderService, cmd, func(sc *scenarioContext) {
|
||||
callCreateFolder(sc)
|
||||
assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for error %s", tc.Error)
|
||||
})
|
||||
@@ -81,11 +81,12 @@ func TestFoldersAPIEndpoint(t *testing.T) {
|
||||
Title: "Folder upd",
|
||||
}
|
||||
|
||||
mock := &fakeFolderService{
|
||||
UpdateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"},
|
||||
}
|
||||
folderService.On("UpdateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
cmd := args.Get(4).(*models.UpdateFolderCommand)
|
||||
cmd.Result = &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"}
|
||||
}).Return(nil).Once()
|
||||
|
||||
updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd,
|
||||
updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", folderService, cmd,
|
||||
func(sc *scenarioContext) {
|
||||
callUpdateFolder(sc)
|
||||
|
||||
@@ -119,12 +120,9 @@ func TestFoldersAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
mock := &fakeFolderService{
|
||||
UpdateFolderError: tc.Error,
|
||||
}
|
||||
|
||||
folderService.On("UpdateFolder", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.Error).Once()
|
||||
updateFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()),
|
||||
"/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) {
|
||||
"/api/folders/uid", "/api/folders/:uid", folderService, cmd, func(sc *scenarioContext) {
|
||||
callUpdateFolder(sc)
|
||||
assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for %s", tc.Error)
|
||||
})
|
||||
@@ -136,14 +134,15 @@ func callCreateFolder(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService,
|
||||
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
|
||||
cmd models.CreateFolderCommand, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: setting.NewCfg(),
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: setting.NewCfg(),
|
||||
folderService: folderService,
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
@@ -156,15 +155,8 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st
|
||||
return hs.CreateFolder(c)
|
||||
})
|
||||
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
mockFolderService(mock)
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
defer func() {
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
}()
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
@@ -173,13 +165,14 @@ func callUpdateFolder(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService,
|
||||
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
|
||||
cmd models.UpdateFolderCommand, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
hs := HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
Cfg: setting.NewCfg(),
|
||||
folderService: folderService,
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
@@ -192,12 +185,6 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st
|
||||
return hs.UpdateFolder(c)
|
||||
})
|
||||
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
t.Cleanup(func() {
|
||||
dashboards.NewFolderService = origNewFolderService
|
||||
})
|
||||
mockFolderService(mock)
|
||||
|
||||
sc.m.Put(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
@@ -222,35 +209,28 @@ type fakeFolderService struct {
|
||||
DeletedFolderUids []string
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) GetFolders(ctx context.Context, limit int64, page int64) ([]*models.Folder, error) {
|
||||
func (s *fakeFolderService) GetFolders(ctx context.Context, user *models.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) {
|
||||
return s.GetFoldersResult, s.GetFoldersError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) GetFolderByID(ctx context.Context, id int64) (*models.Folder, error) {
|
||||
func (s *fakeFolderService) GetFolderByID(ctx context.Context, user *models.SignedInUser, id int64, orgID int64) (*models.Folder, error) {
|
||||
return s.GetFolderByIDResult, s.GetFolderByIDError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) GetFolderByUID(ctx context.Context, uid string) (*models.Folder, error) {
|
||||
func (s *fakeFolderService) GetFolderByUID(ctx context.Context, user *models.SignedInUser, orgID int64, uid string) (*models.Folder, error) {
|
||||
return s.GetFolderByUIDResult, s.GetFolderByUIDError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) CreateFolder(ctx context.Context, title, uid string) (*models.Folder, error) {
|
||||
func (s *fakeFolderService) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) {
|
||||
return s.CreateFolderResult, s.CreateFolderError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) UpdateFolder(ctx context.Context, existingUID string, cmd *models.UpdateFolderCommand) error {
|
||||
func (s *fakeFolderService) UpdateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error {
|
||||
cmd.Result = s.UpdateFolderResult
|
||||
return s.UpdateFolderError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) DeleteFolder(ctx context.Context, uid string, forceDeleteRules bool) (*models.Folder, error) {
|
||||
func (s *fakeFolderService) DeleteFolder(ctx context.Context, user *models.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) {
|
||||
s.DeletedFolderUids = append(s.DeletedFolderUids, uid)
|
||||
return s.DeleteFolderResult, s.DeleteFolderError
|
||||
}
|
||||
|
||||
func mockFolderService(mock *fakeFolderService) {
|
||||
dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser,
|
||||
dashboardStore dboards.Store) dashboards.FolderService {
|
||||
return mock
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
||||
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
||||
"viewersCanEdit": setting.ViewersCanEdit,
|
||||
"angularSupportEnabled": hs.Cfg.AngularSupportEnabled,
|
||||
"editorsCanAdmin": hs.Cfg.EditorsCanAdmin,
|
||||
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
|
||||
"pluginsToPreload": pluginsToPreload,
|
||||
@@ -274,6 +275,10 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled,
|
||||
}
|
||||
|
||||
if hs.ThumbService != nil {
|
||||
jsonObj["dashboardPreviews"] = hs.ThumbService.GetDashboardPreviewsSetupSettings(c)
|
||||
}
|
||||
|
||||
if hs.Cfg.GeomapDefaultBaseLayerConfig != nil {
|
||||
jsonObj["geomapDefaultBaseLayerConfig"] = hs.Cfg.GeomapDefaultBaseLayerConfig
|
||||
}
|
||||
|
||||
+20
-6
@@ -28,11 +28,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/encryption"
|
||||
@@ -128,9 +128,14 @@ type HTTPServer struct {
|
||||
queryDataService *query.Service
|
||||
serviceAccountsService serviceaccounts.Service
|
||||
authInfoService login.AuthInfoService
|
||||
TeamPermissionsService *resourcepermissions.Service
|
||||
teamPermissionsService accesscontrol.PermissionsService
|
||||
NotificationService *notifications.NotificationService
|
||||
dashboardService dashboards.DashboardService
|
||||
dashboardProvisioningService dashboards.DashboardProvisioningService
|
||||
folderService dashboards.FolderService
|
||||
DatasourcePermissionsService DatasourcePermissionsService
|
||||
AlertNotificationService *alerting.AlertNotificationService
|
||||
DashboardsnapshotsService *dashboardsnapshots.Service
|
||||
}
|
||||
|
||||
type ServerOptions struct {
|
||||
@@ -157,8 +162,12 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
||||
dataSourcesService datasources.DataSourceService, secretsService secrets.Service, queryDataService *query.Service,
|
||||
ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service,
|
||||
authInfoService login.AuthInfoService, resourcePermissionServices *resourceservices.ResourceServices,
|
||||
notificationService *notifications.NotificationService, datasourcePermissionsService DatasourcePermissionsService) (*HTTPServer, error) {
|
||||
authInfoService login.AuthInfoService, permissionsServices accesscontrol.PermissionsServices,
|
||||
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
|
||||
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService,
|
||||
datasourcePermissionsService DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
|
||||
dashboardsnapshotsService *dashboardsnapshots.Service,
|
||||
) (*HTTPServer, error) {
|
||||
web.Env = cfg.Env
|
||||
m := web.New()
|
||||
|
||||
@@ -217,9 +226,14 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
queryDataService: queryDataService,
|
||||
serviceAccountsService: serviceaccountsService,
|
||||
authInfoService: authInfoService,
|
||||
TeamPermissionsService: resourcePermissionServices.GetTeamService(),
|
||||
NotificationService: notificationService,
|
||||
dashboardService: dashboardService,
|
||||
dashboardProvisioningService: dashboardProvisioningService,
|
||||
folderService: folderService,
|
||||
DatasourcePermissionsService: datasourcePermissionsService,
|
||||
teamPermissionsService: permissionsServices.GetTeamService(),
|
||||
AlertNotificationService: alertNotificationService,
|
||||
DashboardsnapshotsService: dashboardsnapshotsService,
|
||||
}
|
||||
if hs.Listener != nil {
|
||||
hs.log.Debug("Using provided listener")
|
||||
|
||||
+4
-1
@@ -92,7 +92,10 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
|
||||
enabledOAuths := make(map[string]interface{})
|
||||
providers := hs.SocialService.GetOAuthInfoProviders()
|
||||
for key, oauth := range providers {
|
||||
enabledOAuths[key] = map[string]string{"name": oauth.Name}
|
||||
enabledOAuths[key] = map[string]string{
|
||||
"name": oauth.Name,
|
||||
"icon": oauth.Icon,
|
||||
}
|
||||
}
|
||||
|
||||
viewData.Settings["oauth"] = enabledOAuths
|
||||
|
||||
+5
-25
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -113,19 +112,6 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo
|
||||
return response.JSON(200, result)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getUserAccessControlMetadata(c *models.ReqContext, resourceIDs map[string]bool) (map[string]accesscontrol.Metadata, error) {
|
||||
if hs.AccessControl == nil || hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil || len(userPermissions) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "users", resourceIDs), nil
|
||||
}
|
||||
|
||||
// GET /api/orgs/:orgId/users
|
||||
func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response {
|
||||
orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
|
||||
@@ -164,17 +150,11 @@ func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *models.GetO
|
||||
filteredUsers = append(filteredUsers, user)
|
||||
}
|
||||
|
||||
accessControlMetadata, errAC := hs.getUserAccessControlMetadata(c, userIDs)
|
||||
if errAC != nil {
|
||||
hs.log.Error("Failed to get access control metadata", "error", errAC)
|
||||
|
||||
return filteredUsers, nil
|
||||
} else if accessControlMetadata == nil {
|
||||
return filteredUsers, nil
|
||||
}
|
||||
|
||||
for i := range filteredUsers {
|
||||
filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserId)]
|
||||
accessControlMetadata := hs.getMultiAccessControlMetadata(c, "users", userIDs)
|
||||
if len(accessControlMetadata) > 0 {
|
||||
for i := range filteredUsers {
|
||||
filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserId)]
|
||||
}
|
||||
}
|
||||
|
||||
return filteredUsers, nil
|
||||
|
||||
@@ -150,7 +150,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) {
|
||||
"tags": "prod",
|
||||
}),
|
||||
}
|
||||
folder, err := sc.db.SaveDashboard(cmd)
|
||||
folder, err := sc.dashboardsStore.SaveDashboard(cmd)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, folder)
|
||||
|
||||
@@ -165,7 +165,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) {
|
||||
Updated: time.Now(),
|
||||
},
|
||||
}
|
||||
err = sc.db.UpdateDashboardACL(context.Background(), folder.Id, acls)
|
||||
err = sc.dashboardsStore.UpdateDashboardACL(context.Background(), folder.Id, acls)
|
||||
require.NoError(t, err)
|
||||
|
||||
response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
@@ -129,7 +130,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
|
||||
t.Run("When matching route path", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider,
|
||||
&oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
@@ -142,7 +143,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
|
||||
t.Run("When matching route path and has dynamic url", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
proxy.matchedRoute = routes[3]
|
||||
@@ -154,7 +155,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
|
||||
t.Run("When matching route path with no url", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
proxy.matchedRoute = routes[4]
|
||||
@@ -165,7 +166,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
|
||||
t.Run("When matching route path and has dynamic body", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
proxy.matchedRoute = routes[5]
|
||||
@@ -179,7 +180,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("Validating request", func(t *testing.T) {
|
||||
t.Run("plugin route with valid role", func(t *testing.T) {
|
||||
ctx, _ := setUp()
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
err = proxy.validateRequest()
|
||||
@@ -188,7 +189,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
|
||||
t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
|
||||
ctx, _ := setUp()
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
err = proxy.validateRequest()
|
||||
@@ -198,7 +199,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("plugin route with admin role and user is admin", func(t *testing.T) {
|
||||
ctx, _ := setUp()
|
||||
ctx.SignedInUser.OrgRole = models.ROLE_ADMIN
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
err = proxy.validateRequest()
|
||||
@@ -289,7 +290,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
|
||||
@@ -305,7 +306,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
|
||||
require.NoError(t, err)
|
||||
client = newFakeHTTPClient(t, json2)
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
|
||||
@@ -322,7 +323,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
client = newFakeHTTPClient(t, []byte{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
|
||||
@@ -344,7 +345,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
ctx := &models.ReqContext{}
|
||||
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
@@ -370,7 +371,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
ctx := &models.ReqContext{}
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -394,7 +395,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
ctx := &models.ReqContext{}
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -422,7 +423,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
ctx := &models.ReqContext{}
|
||||
var pluginRoutes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -445,7 +446,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
ctx := &models.ReqContext{}
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
@@ -509,7 +510,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
@@ -642,7 +643,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
ctx, ds := setUp(t)
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -660,7 +661,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
})
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -674,7 +675,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
ctx, ds := setUp(t)
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -696,7 +697,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
})
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -721,7 +722,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -745,7 +746,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -770,7 +771,7 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.Error(t, err)
|
||||
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`))
|
||||
@@ -791,7 +792,7 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
|
||||
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -834,7 +835,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
|
||||
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
if tc.err == nil {
|
||||
require.NoError(t, err)
|
||||
@@ -861,7 +862,7 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett
|
||||
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
@@ -985,7 +986,7 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, test *t
|
||||
require.NoError(t, err)
|
||||
|
||||
var routes []*plugins.Route
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1027,7 +1028,7 @@ func Test_PathCheck(t *testing.T) {
|
||||
}
|
||||
ctx, _ := setUp()
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
|
||||
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), &acmock.Mock{}, acmock.NewPermissionsServicesMock())
|
||||
proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
+5
-38
@@ -2,14 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -39,7 +37,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
|
||||
// the SignedInUser is an empty struct therefore
|
||||
// an additional check whether it is an actual user is required
|
||||
if c.SignedInUser.IsRealUser() {
|
||||
if err := addOrUpdateTeamMember(c.Req.Context(), hs.TeamPermissionsService, c.SignedInUser.UserId, c.OrgId, team.Id, models.PERMISSION_ADMIN.String()); err != nil {
|
||||
if err := addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, c.SignedInUser.UserId, c.OrgId, team.Id, models.PERMISSION_ADMIN.String()); err != nil {
|
||||
c.Logger.Error("Could not add creator to team", "error", err)
|
||||
}
|
||||
} else {
|
||||
@@ -105,20 +103,6 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response {
|
||||
return response.Success("Team deleted")
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamIDs map[string]bool) (map[string]accesscontrol.Metadata, error) {
|
||||
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil || len(userPermissions) == 0 {
|
||||
hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs), nil
|
||||
}
|
||||
|
||||
// GET /api/teams/search
|
||||
func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
perPage := c.QueryInt("perpage")
|
||||
@@ -157,8 +141,8 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
teamIDs[strconv.FormatInt(team.Id, 10)] = true
|
||||
}
|
||||
|
||||
metadata, err := hs.getTeamsAccessControlMetadata(c, teamIDs)
|
||||
if err == nil && len(metadata) != 0 {
|
||||
metadata := hs.getMultiAccessControlMetadata(c, "teams", teamIDs)
|
||||
if len(metadata) > 0 {
|
||||
for _, team := range query.Result.Teams {
|
||||
team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)]
|
||||
}
|
||||
@@ -170,23 +154,6 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
return response.JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID int64) (accesscontrol.Metadata, error) {
|
||||
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil || len(userPermissions) == 0 {
|
||||
hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d", teamID)
|
||||
teamIDs := map[string]bool{key: true}
|
||||
|
||||
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil
|
||||
}
|
||||
|
||||
// UserFilter returns the user ID used in a filter when querying a team
|
||||
// 1. If the user is a viewer or editor, this will return the user's ID.
|
||||
// 2. If the user is an admin, this will return models.FilterIgnoreUser (0)
|
||||
@@ -227,8 +194,8 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
|
||||
return response.Error(500, "Failed to get Team", err)
|
||||
}
|
||||
|
||||
metadata, _ := hs.getTeamAccessControlMetadata(c, query.Result.Id)
|
||||
query.Result.AccessControl = metadata
|
||||
// Add accesscontrol metadata
|
||||
query.Result.AccessControl = hs.getAccessControlMetadata(c, "teams", query.Result.Id)
|
||||
|
||||
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
|
||||
return response.JSON(200, &query.Result)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -85,7 +84,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response {
|
||||
return response.Error(400, "User is already added to this team", nil)
|
||||
}
|
||||
|
||||
err = addOrUpdateTeamMember(c.Req.Context(), hs.TeamPermissionsService, cmd.UserId, cmd.OrgId, cmd.TeamId, getPermissionName(cmd.Permission))
|
||||
err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, cmd.UserId, cmd.OrgId, cmd.TeamId, getPermissionName(cmd.Permission))
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to add Member to Team", err)
|
||||
}
|
||||
@@ -125,7 +124,7 @@ func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response {
|
||||
return response.Error(404, "Team member not found.", nil)
|
||||
}
|
||||
|
||||
err = addOrUpdateTeamMember(c.Req.Context(), hs.TeamPermissionsService, userId, orgId, teamId, getPermissionName(cmd.Permission))
|
||||
err = addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, userId, orgId, teamId, getPermissionName(cmd.Permission))
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to update team member.", err)
|
||||
}
|
||||
@@ -161,7 +160,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
teamIDString := strconv.FormatInt(teamId, 10)
|
||||
if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
|
||||
if _, err := hs.teamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
|
||||
if errors.Is(err, models.ErrTeamNotFound) {
|
||||
return response.Error(404, "Team not found", nil)
|
||||
}
|
||||
@@ -178,7 +177,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
|
||||
// addOrUpdateTeamMember adds or updates a team member.
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64, permission string) error {
|
||||
var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.PermissionsService, userID, orgID, teamID int64, permission string) error {
|
||||
teamIDString := strconv.FormatInt(teamID, 10)
|
||||
if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil {
|
||||
return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -112,7 +111,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
|
||||
teamName := "team foo"
|
||||
|
||||
addTeamMemberCalled := 0
|
||||
addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64,
|
||||
addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.PermissionsService, userID, orgID, teamID int64,
|
||||
permission string) error {
|
||||
addTeamMemberCalled++
|
||||
return nil
|
||||
|
||||
+1
-24
@@ -3,14 +3,12 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -48,33 +46,12 @@ func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) res
|
||||
query.Result.IsExternal = true
|
||||
}
|
||||
|
||||
accessControlMetadata, errAC := hs.getGlobalUserAccessControlMetadata(c, userID)
|
||||
if errAC != nil {
|
||||
hs.log.Error("Failed to get access control metadata", "error", errAC)
|
||||
}
|
||||
|
||||
query.Result.AccessControl = accessControlMetadata
|
||||
query.Result.AccessControl = hs.getAccessControlMetadata(c, "global:users", userID)
|
||||
query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email)
|
||||
|
||||
return response.JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getGlobalUserAccessControlMetadata(c *models.ReqContext, userID int64) (accesscontrol.Metadata, error) {
|
||||
if hs.AccessControl == nil || hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil || len(userPermissions) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d", userID)
|
||||
userIDs := map[string]bool{key: true}
|
||||
|
||||
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "global:users", userIDs)[key], nil
|
||||
}
|
||||
|
||||
// GET /api/users/lookup
|
||||
func (hs *HTTPServer) GetUserByLoginOrEmail(c *models.ReqContext) response.Response {
|
||||
query := models.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
|
||||
|
||||
+12
-11
@@ -8,32 +8,33 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
||||
authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/database"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||
settings := setting.NewCfg()
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
hs := &HTTPServer{
|
||||
Cfg: settings,
|
||||
SQLStore: sqlStore,
|
||||
Cfg: settings,
|
||||
SQLStore: sqlStore,
|
||||
AccessControl: &acmock.Mock{},
|
||||
}
|
||||
|
||||
mockResult := models.SearchUserQueryResult{
|
||||
|
||||
@@ -12,9 +12,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -59,14 +56,7 @@ func runDbCommand(command func(commandLine utils.CommandLine, sqlStore *sqlstore
|
||||
return errutil.Wrap("failed to initialize tracer service", err)
|
||||
}
|
||||
|
||||
hooksService := hooks.ProvideService()
|
||||
ossLicensingService := licensing.ProvideService(cfg, hooksService)
|
||||
featureManager, err := featuremgmt.ProvideManagerService(cfg, ossLicensingService)
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to initialize feature manager service", err)
|
||||
}
|
||||
|
||||
sqlStore, err := sqlstore.ProvideService(cfg, nil, bus.GetBus(), &migrations.OSSMigrations{}, tracer, featureManager)
|
||||
sqlStore, err := sqlstore.ProvideService(cfg, nil, bus.GetBus(), &migrations.OSSMigrations{}, tracer)
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to initialize SQL store", err)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ var skipPaths = []string{
|
||||
"public/app/plugins/panel/dashlist/models.cue",
|
||||
"public/app/plugins/panel/gauge/models.cue",
|
||||
"public/app/plugins/panel/histogram/models.cue",
|
||||
"public/app/plugins/panel/heatmap-new/models.cue",
|
||||
"public/app/plugins/panel/stat/models.cue",
|
||||
"public/app/plugins/panel/candlestick/models.cue",
|
||||
"public/app/plugins/panel/state-timeline/models.cue",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// Store is a dashboard store.
|
||||
type Store interface {
|
||||
// ValidateDashboardBeforeSave validates a dashboard before save.
|
||||
ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error)
|
||||
// GetFolderByTitle retrieves a dashboard by its title and is used by unified alerting
|
||||
GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error)
|
||||
GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
|
||||
GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
|
||||
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
||||
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
||||
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)
|
||||
UpdateDashboardACLCtx(ctx context.Context, uid int64, items []*models.DashboardAcl) error
|
||||
// SaveAlerts saves dashboard alerts.
|
||||
SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error
|
||||
}
|
||||
@@ -45,6 +45,7 @@ type OAuthInfo struct {
|
||||
TeamsUrl string
|
||||
AllowSignup bool
|
||||
Name string
|
||||
Icon string
|
||||
TlsClientCert string
|
||||
TlsClientKey string
|
||||
TlsClientCa string
|
||||
@@ -81,6 +82,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
HostedDomain: sec.Key("hosted_domain").String(),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
Icon: sec.Key("icon").String(),
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
|
||||
+21
-19
@@ -191,28 +191,30 @@ func shouldForceLogin(c *models.ReqContext) bool {
|
||||
return forceLogin
|
||||
}
|
||||
|
||||
func OrgAdminFolderAdminOrTeamAdmin(c *models.ReqContext) {
|
||||
if c.OrgRole == models.ROLE_ADMIN {
|
||||
return
|
||||
}
|
||||
func OrgAdminFolderAdminOrTeamAdmin(ss sqlstore.Store) func(c *models.ReqContext) {
|
||||
return func(c *models.ReqContext) {
|
||||
if c.OrgRole == models.ROLE_ADMIN {
|
||||
return
|
||||
}
|
||||
|
||||
hasAdminPermissionInFoldersQuery := models.HasAdminPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
|
||||
if err := sqlstore.HasAdminPermissionInFolders(c.Req.Context(), &hasAdminPermissionInFoldersQuery); err != nil {
|
||||
c.JsonApiErr(500, "Failed to check if user is a folder admin", err)
|
||||
}
|
||||
hasAdminPermissionInFoldersQuery := models.HasAdminPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
|
||||
if err := ss.HasAdminPermissionInFolders(c.Req.Context(), &hasAdminPermissionInFoldersQuery); err != nil {
|
||||
c.JsonApiErr(500, "Failed to check if user is a folder admin", err)
|
||||
}
|
||||
|
||||
if hasAdminPermissionInFoldersQuery.Result {
|
||||
return
|
||||
}
|
||||
if hasAdminPermissionInFoldersQuery.Result {
|
||||
return
|
||||
}
|
||||
|
||||
isAdminOfTeamsQuery := models.IsAdminOfTeamsQuery{SignedInUser: c.SignedInUser}
|
||||
if err := sqlstore.IsAdminOfTeams(c.Req.Context(), &isAdminOfTeamsQuery); err != nil {
|
||||
c.JsonApiErr(500, "Failed to check if user is a team admin", err)
|
||||
}
|
||||
isAdminOfTeamsQuery := models.IsAdminOfTeamsQuery{SignedInUser: c.SignedInUser}
|
||||
if err := sqlstore.IsAdminOfTeams(c.Req.Context(), &isAdminOfTeamsQuery); err != nil {
|
||||
c.JsonApiErr(500, "Failed to check if user is a team admin", err)
|
||||
}
|
||||
|
||||
if isAdminOfTeamsQuery.Result {
|
||||
return
|
||||
}
|
||||
if isAdminOfTeamsQuery.Result {
|
||||
return
|
||||
}
|
||||
|
||||
accessForbidden(c)
|
||||
accessForbidden(c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,10 @@ type DashboardWithStaleThumbnail struct {
|
||||
Slug string
|
||||
}
|
||||
|
||||
type FindDashboardThumbnailCountCommand struct {
|
||||
Result int64
|
||||
}
|
||||
|
||||
type FindDashboardsWithStaleThumbnailsCommand struct {
|
||||
IncludeManuallyUploadedThumbnails bool
|
||||
Theme Theme
|
||||
|
||||
@@ -456,7 +456,3 @@ type GetDashboardRefByIdQuery struct {
|
||||
Id int64
|
||||
Result *DashboardRef
|
||||
}
|
||||
|
||||
type UnprovisionDashboardCommand struct {
|
||||
Id int64
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -24,8 +27,9 @@ func TestGetPluginDashboards(t *testing.T) {
|
||||
},
|
||||
}
|
||||
pmCfg := plugins.FromGrafanaCfg(cfg)
|
||||
dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{}))
|
||||
pm, err := ProvideService(cfg, loader.New(pmCfg, nil,
|
||||
signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{}))
|
||||
signature.NewUnsignedAuthorizer(pmCfg), &provider.Service{}), dashboardService)
|
||||
require.NoError(t, err)
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -240,7 +241,7 @@ func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDi
|
||||
Class: class,
|
||||
}
|
||||
|
||||
plugin.SetLogger(logger.New("pluginID", plugin.ID))
|
||||
plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID)))
|
||||
setImages(plugin)
|
||||
|
||||
return plugin
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/installer"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
@@ -30,35 +30,38 @@ var _ plugins.StaticRouteResolver = (*PluginManager)(nil)
|
||||
var _ plugins.RendererManager = (*PluginManager)(nil)
|
||||
|
||||
type PluginManager struct {
|
||||
cfg *plugins.Cfg
|
||||
store map[string]*plugins.Plugin
|
||||
pluginInstaller plugins.Installer
|
||||
pluginLoader plugins.Loader
|
||||
pluginsMu sync.RWMutex
|
||||
pluginPaths map[plugins.Class][]string
|
||||
log log.Logger
|
||||
cfg *plugins.Cfg
|
||||
store map[string]*plugins.Plugin
|
||||
pluginInstaller plugins.Installer
|
||||
pluginLoader plugins.Loader
|
||||
pluginsMu sync.RWMutex
|
||||
pluginPaths map[plugins.Class][]string
|
||||
dashboardService dashboards.DashboardService
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader) (*PluginManager, error) {
|
||||
func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader, dashboardService dashboards.DashboardService) (*PluginManager, error) {
|
||||
pm := New(plugins.FromGrafanaCfg(grafanaCfg), map[plugins.Class][]string{
|
||||
plugins.Core: corePluginPaths(grafanaCfg),
|
||||
plugins.Bundled: {grafanaCfg.BundledPluginsPath},
|
||||
plugins.External: append([]string{grafanaCfg.PluginsPath}, pluginSettingPaths(grafanaCfg)...),
|
||||
}, pluginLoader)
|
||||
}, pluginLoader, dashboardService)
|
||||
if err := pm.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pm, nil
|
||||
}
|
||||
|
||||
func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader) *PluginManager {
|
||||
func New(cfg *plugins.Cfg, pluginPaths map[plugins.Class][]string, pluginLoader plugins.Loader,
|
||||
dashboardService dashboards.DashboardService) *PluginManager {
|
||||
return &PluginManager{
|
||||
cfg: cfg,
|
||||
pluginLoader: pluginLoader,
|
||||
pluginPaths: pluginPaths,
|
||||
store: make(map[string]*plugins.Plugin),
|
||||
log: log.New("plugin.manager"),
|
||||
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
|
||||
cfg: cfg,
|
||||
pluginLoader: pluginLoader,
|
||||
pluginPaths: pluginPaths,
|
||||
store: make(map[string]*plugins.Plugin),
|
||||
log: log.New("plugin.manager"),
|
||||
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
|
||||
dashboardService: dashboardService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
@@ -37,6 +35,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/tsdb/prometheus"
|
||||
"github.com/grafana/grafana/pkg/tsdb/tempo"
|
||||
"github.com/grafana/grafana/pkg/tsdb/testdatasource"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -95,7 +94,7 @@ func TestPluginManager_int_init(t *testing.T) {
|
||||
|
||||
pmCfg := plugins.FromGrafanaCfg(cfg)
|
||||
pm, err := ProvideService(cfg, loader.New(pmCfg, license, signature.NewUnsignedAuthorizer(pmCfg),
|
||||
provider.ProvideService(coreRegistry)))
|
||||
provider.ProvideService(coreRegistry)), &service.DashboardServiceImpl{})
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyCorePluginCatalogue(t, pm)
|
||||
@@ -120,6 +119,7 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
|
||||
"gettingstarted": {},
|
||||
"graph": {},
|
||||
"heatmap": {},
|
||||
"heatmap-new": {},
|
||||
"histogram": {},
|
||||
"icon": {},
|
||||
"live": {},
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -466,7 +469,8 @@ func TestPluginManager_lifecycle_unmanaged(t *testing.T) {
|
||||
func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager {
|
||||
t.Helper()
|
||||
|
||||
pm := New(&plugins.Cfg{}, nil, &fakeLoader{})
|
||||
dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{}))
|
||||
pm := New(&plugins.Cfg{}, nil, &fakeLoader{}, dashboardService)
|
||||
|
||||
for _, cb := range cbs {
|
||||
cb(pm)
|
||||
@@ -520,7 +524,8 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS
|
||||
cfg.Azure.ManagedIdentityClientId = "client-id"
|
||||
|
||||
loader := &fakeLoader{}
|
||||
manager := New(cfg, nil, loader)
|
||||
dashboardService := service.ProvideDashboardService(database.ProvideDashboardStore(&sqlstore.SQLStore{}))
|
||||
manager := New(cfg, nil, loader, dashboardService)
|
||||
manager.pluginLoader = loader
|
||||
ctx := &managerScenarioCtx{
|
||||
manager: manager,
|
||||
|
||||
+10
-2
@@ -27,13 +27,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/auth/jwt"
|
||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||
dashboardimportservice "github.com/grafana/grafana/pkg/services/dashboardimport/service"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
@@ -199,7 +201,13 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
|
||||
featuremgmt.ProvideManagerService,
|
||||
featuremgmt.ProvideToggles,
|
||||
resourceservices.ProvideResourceServices,
|
||||
dashboardservice.ProvideDashboardService,
|
||||
dashboardservice.ProvideFolderService,
|
||||
dashboardstore.ProvideDashboardStore,
|
||||
wire.Bind(new(dashboards.DashboardService), new(*dashboardservice.DashboardServiceImpl)),
|
||||
wire.Bind(new(dashboards.DashboardProvisioningService), new(*dashboardservice.DashboardServiceImpl)),
|
||||
wire.Bind(new(dashboards.FolderService), new(*dashboardservice.FolderServiceImpl)),
|
||||
wire.Bind(new(dashboards.Store), new(*dashboardstore.DashboardStore)),
|
||||
dashboardimportservice.ProvideService,
|
||||
wire.Bind(new(dashboardimport.Service), new(*dashboardimportservice.ImportDashboardService)),
|
||||
plugindashboards.ProvideService,
|
||||
|
||||
@@ -77,6 +77,8 @@ var wireExtsBasicSet = wire.NewSet(
|
||||
wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)),
|
||||
api.ProvideDatasourcePermissionsService,
|
||||
wire.Bind(new(api.DatasourcePermissionsService), new(*api.OSSDatasourcePermissionsService)),
|
||||
ossaccesscontrol.ProvidePermissionsServices,
|
||||
wire.Bind(new(accesscontrol.PermissionsServices), new(*ossaccesscontrol.PermissionsService)),
|
||||
)
|
||||
|
||||
var wireExtsSet = wire.NewSet(
|
||||
|
||||
@@ -37,7 +37,12 @@ type PermissionsProvider interface {
|
||||
GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]*Permission, error)
|
||||
}
|
||||
|
||||
type ResourcePermissionsService interface {
|
||||
type PermissionsServices interface {
|
||||
GetTeamService() PermissionsService
|
||||
GetDataSourceService() PermissionsService
|
||||
}
|
||||
|
||||
type PermissionsService interface {
|
||||
// GetPermissions returns all permissions for given resourceID
|
||||
GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error)
|
||||
// SetUserPermission sets permission on resource for a user
|
||||
@@ -146,7 +151,7 @@ func addActionToMetadata(allMetadata map[string]Metadata, action, id string) map
|
||||
}
|
||||
|
||||
// GetResourcesMetadata returns a map of accesscontrol metadata, listing for each resource, users available actions
|
||||
func GetResourcesMetadata(ctx context.Context, permissions []*Permission, resource string, resourceIDs map[string]bool) map[string]Metadata {
|
||||
func GetResourcesMetadata(ctx context.Context, permissions map[string][]string, resource string, ids map[string]bool) map[string]Metadata {
|
||||
allScope := GetResourceAllScope(resource)
|
||||
allIDScope := GetResourceAllIDScope(resource)
|
||||
|
||||
@@ -157,16 +162,18 @@ func GetResourcesMetadata(ctx context.Context, permissions []*Permission, resour
|
||||
|
||||
// Loop through permissions once
|
||||
result := map[string]Metadata{}
|
||||
for _, p := range permissions {
|
||||
if p.Scope == "*" || p.Scope == allScope || p.Scope == allIDScope {
|
||||
// Add global action to all resources
|
||||
for id := range resourceIDs {
|
||||
result = addActionToMetadata(result, p.Action, id)
|
||||
}
|
||||
} else {
|
||||
if len(p.Scope) > idIndex && strings.HasPrefix(p.Scope, idPrefix) && resourceIDs[p.Scope[idIndex:]] {
|
||||
// Add action to a specific resource
|
||||
result = addActionToMetadata(result, p.Action, p.Scope[idIndex:])
|
||||
for action, scopes := range permissions {
|
||||
for _, scope := range scopes {
|
||||
if scope == "*" || scope == allScope || scope == allIDScope {
|
||||
// Add global action to all resources
|
||||
for id := range ids {
|
||||
result = addActionToMetadata(result, action, id)
|
||||
}
|
||||
} else {
|
||||
if len(scope) > idIndex && strings.HasPrefix(scope, idPrefix) && ids[scope[idIndex:]] {
|
||||
// Add action to a specific resource
|
||||
result = addActionToMetadata(result, action, scope[idIndex:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,17 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setupTestEnv(b *testing.B, resourceCount, permissionPerResource int) ([]*Permission, map[string]bool) {
|
||||
res := make([]*Permission, resourceCount*permissionPerResource)
|
||||
func setupTestEnv(b *testing.B, resourceCount, permissionPerResource int) (map[string][]string, map[string]bool) {
|
||||
res := map[string][]string{}
|
||||
ids := make(map[string]bool, resourceCount)
|
||||
|
||||
for r := 0; r < resourceCount; r++ {
|
||||
for p := 0; p < permissionPerResource; p++ {
|
||||
perm := Permission{Action: fmt.Sprintf("resources:action%v", p), Scope: fmt.Sprintf("resources:id:%v", r)}
|
||||
id := r*permissionPerResource + p
|
||||
res[id] = &perm
|
||||
for p := 0; p < permissionPerResource; p++ {
|
||||
action := fmt.Sprintf("resources:action%v", p)
|
||||
for r := 0; r < resourceCount; r++ {
|
||||
scope := fmt.Sprintf("resources:id:%v", r)
|
||||
res[action] = append(res[action], scope)
|
||||
ids[fmt.Sprintf("%d", r)] = true
|
||||
}
|
||||
ids[fmt.Sprintf("%d", r)] = true
|
||||
}
|
||||
|
||||
return res, ids
|
||||
@@ -40,23 +40,23 @@ func benchGetMetadata(b *testing.B, resourceCount, permissionPerResource int) {
|
||||
}
|
||||
|
||||
// Lots of permissions
|
||||
func BenchmarkGetResourcesMetadata_10_1000(b *testing.B) { benchGetMetadata(b, 10, 1000) } // ~0.0017s/op
|
||||
func BenchmarkGetResourcesMetadata_10_10000(b *testing.B) { benchGetMetadata(b, 10, 10000) } // ~0.016s/op
|
||||
func BenchmarkGetResourcesMetadata_10_100000(b *testing.B) { benchGetMetadata(b, 10, 100000) } // ~0.17s/op
|
||||
func BenchmarkGetResourcesMetadata_10_1000(b *testing.B) { benchGetMetadata(b, 10, 1000) } // ~0.0022s/op
|
||||
func BenchmarkGetResourcesMetadata_10_10000(b *testing.B) { benchGetMetadata(b, 10, 10000) } // ~0.019s/op
|
||||
func BenchmarkGetResourcesMetadata_10_100000(b *testing.B) { benchGetMetadata(b, 10, 100000) } // ~0.25s/op
|
||||
func BenchmarkGetResourcesMetadata_10_1000000(b *testing.B) {
|
||||
if testing.Short() {
|
||||
b.Skip("Skipping benchmark in short mode")
|
||||
}
|
||||
benchGetMetadata(b, 10, 1000000)
|
||||
} // ~3.89s/op
|
||||
} // ~5.8s/op
|
||||
|
||||
// Lots of resources
|
||||
func BenchmarkGetResourcesMetadata_1000_10(b *testing.B) { benchGetMetadata(b, 1000, 10) } // ~0,0023s/op
|
||||
func BenchmarkGetResourcesMetadata_10000_10(b *testing.B) { benchGetMetadata(b, 10000, 10) } // ~0.021s/op
|
||||
func BenchmarkGetResourcesMetadata_100000_10(b *testing.B) { benchGetMetadata(b, 100000, 10) } // ~0.22s/op
|
||||
func BenchmarkGetResourcesMetadata_10000_10(b *testing.B) { benchGetMetadata(b, 10000, 10) } // ~0.022s/op
|
||||
func BenchmarkGetResourcesMetadata_100000_10(b *testing.B) { benchGetMetadata(b, 100000, 10) } // ~0.26s/op
|
||||
func BenchmarkGetResourcesMetadata_1000000_10(b *testing.B) {
|
||||
if testing.Short() {
|
||||
b.Skip("Skipping benchmark in short mode")
|
||||
}
|
||||
benchGetMetadata(b, 1000000, 10)
|
||||
} // ~2.8s/op
|
||||
} // ~4.1s/op
|
||||
|
||||
@@ -12,7 +12,7 @@ func TestGetResourcesMetadata(t *testing.T) {
|
||||
desc string
|
||||
resource string
|
||||
resourcesIDs map[string]bool
|
||||
permissions []*Permission
|
||||
permissions map[string][]string
|
||||
expected map[string]Metadata
|
||||
}{
|
||||
{
|
||||
@@ -24,10 +24,10 @@ func TestGetResourcesMetadata(t *testing.T) {
|
||||
{
|
||||
desc: "Should return no permission for resources 1,2,3 given the user has permissions for 4 only",
|
||||
resource: "resources",
|
||||
permissions: []*Permission{
|
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "4")},
|
||||
{Action: "resources:action2", Scope: Scope("resources", "id", "4")},
|
||||
{Action: "resources:action3", Scope: Scope("resources", "id", "4")},
|
||||
permissions: map[string][]string{
|
||||
"resources:action1": {Scope("resources", "id", "4")},
|
||||
"resources:action2": {Scope("resources", "id", "4")},
|
||||
"resources:action3": {Scope("resources", "id", "4")},
|
||||
},
|
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
|
||||
expected: map[string]Metadata{},
|
||||
@@ -35,10 +35,10 @@ func TestGetResourcesMetadata(t *testing.T) {
|
||||
{
|
||||
desc: "Should only return permissions for resources 1 and 2, given the user has no permissions for 3",
|
||||
resource: "resources",
|
||||
permissions: []*Permission{
|
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "1")},
|
||||
{Action: "resources:action2", Scope: Scope("resources", "id", "2")},
|
||||
{Action: "resources:action3", Scope: Scope("resources", "id", "2")},
|
||||
permissions: map[string][]string{
|
||||
"resources:action1": {Scope("resources", "id", "1")},
|
||||
"resources:action2": {Scope("resources", "id", "2")},
|
||||
"resources:action3": {Scope("resources", "id", "2")},
|
||||
},
|
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
|
||||
expected: map[string]Metadata{
|
||||
@@ -49,13 +49,13 @@ func TestGetResourcesMetadata(t *testing.T) {
|
||||
{
|
||||
desc: "Should return permissions with global scopes for resources 1,2,3",
|
||||
resource: "resources",
|
||||
permissions: []*Permission{
|
||||
{Action: "resources:action4", Scope: Scope("resources", "id", "*")},
|
||||
{Action: "resources:action5", Scope: Scope("resources", "*")},
|
||||
{Action: "resources:action6", Scope: "*"},
|
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "1")},
|
||||
{Action: "resources:action2", Scope: Scope("resources", "id", "2")},
|
||||
{Action: "resources:action3", Scope: Scope("resources", "id", "2")},
|
||||
permissions: map[string][]string{
|
||||
"resources:action1": {Scope("resources", "id", "1")},
|
||||
"resources:action2": {Scope("resources", "id", "2")},
|
||||
"resources:action3": {Scope("resources", "id", "2")},
|
||||
"resources:action4": {Scope("resources", "id", "*")},
|
||||
"resources:action5": {Scope("resources", "*")},
|
||||
"resources:action6": {"*"},
|
||||
},
|
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
|
||||
expected: map[string]Metadata{
|
||||
@@ -67,11 +67,10 @@ func TestGetResourcesMetadata(t *testing.T) {
|
||||
{
|
||||
desc: "Should correctly filter out irrelevant permissions for resources 1,2,3",
|
||||
resource: "resources",
|
||||
permissions: []*Permission{
|
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "1")},
|
||||
{Action: "otherresources:action1", Scope: Scope("resources", "id", "1")},
|
||||
{Action: "resources:action2", Scope: Scope("otherresources", "id", "*")},
|
||||
{Action: "otherresources:action1", Scope: Scope("otherresources", "id", "*")},
|
||||
permissions: map[string][]string{
|
||||
"resources:action1": {Scope("resources", "id", "1")},
|
||||
"resources:action2": {Scope("otherresources", "id", "*")},
|
||||
"otherresources:action1": {Scope("resources", "id", "1"), Scope("otherresources", "id", "*")},
|
||||
},
|
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true},
|
||||
expected: map[string]Metadata{
|
||||
@@ -81,9 +80,8 @@ func TestGetResourcesMetadata(t *testing.T) {
|
||||
{
|
||||
desc: "Should correctly handle permissions with multilayer scope",
|
||||
resource: "resources:sub",
|
||||
permissions: []*Permission{
|
||||
{Action: "resources:action1", Scope: Scope("resources", "sub", "id", "1")},
|
||||
{Action: "resources:action1", Scope: Scope("resources", "sub", "id", "123")},
|
||||
permissions: map[string][]string{
|
||||
"resources:action1": {Scope("resources", "sub", "id", "1"), Scope("resources", "sub", "id", "123")},
|
||||
},
|
||||
resourcesIDs: map[string]bool{"1": true, "123": true},
|
||||
expected: map[string]Metadata{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
var _ accesscontrol.PermissionsServices = new(PermissionsServicesMock)
|
||||
|
||||
func NewPermissionsServicesMock() *PermissionsServicesMock {
|
||||
return &PermissionsServicesMock{
|
||||
teams: &MockPermissionsService{},
|
||||
datasources: &MockPermissionsService{},
|
||||
}
|
||||
}
|
||||
|
||||
type PermissionsServicesMock struct {
|
||||
teams *MockPermissionsService
|
||||
datasources *MockPermissionsService
|
||||
}
|
||||
|
||||
func (p PermissionsServicesMock) GetTeamService() accesscontrol.PermissionsService {
|
||||
return p.teams
|
||||
}
|
||||
|
||||
func (p PermissionsServicesMock) GetDataSourceService() accesscontrol.PermissionsService {
|
||||
return p.datasources
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
var _ accesscontrol.PermissionsService = new(MockPermissionsService)
|
||||
|
||||
type MockPermissionsService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockPermissionsService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, resourceID)
|
||||
return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockPermissionsService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, user, resourceID, permission)
|
||||
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockPermissionsService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, teamID, resourceID, permission)
|
||||
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockPermissionsService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, builtInRole, resourceID, permission)
|
||||
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockPermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, resourceID, commands)
|
||||
return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user