diff --git a/.betterer.results b/.betterer.results index 17a6ea61e05..f5efc89e878 100644 --- a/.betterer.results +++ b/.betterer.results @@ -8,15 +8,9 @@ exports[`no enzyme tests`] = { "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:2976628669": [ [0, 26, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx:793800575": [ - [14, 35, 13, "RegExp match", "2409514259"] - ], "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx:3266788928": [ [14, 56, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx:3450948735": [ - [15, 31, 13, "RegExp match", "2409514259"] - ], "packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.tsx:3891071965": [ [13, 42, 13, "RegExp match", "2409514259"] ] @@ -1494,7 +1488,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"] + [0, 0, 0, "Unexpected any. Specify a different type.", "14"], + [0, 0, 0, "Do not use any type assertions.", "15"] ], "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -5063,9 +5058,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -5725,19 +5717,19 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/plugins/datasource/influxdb/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], + [0, 0, 0, "Do not use any type assertions.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], [0, 0, 0, "Unexpected any. Specify a different type.", "15"], @@ -5747,18 +5739,13 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "19"], [0, 0, 0, "Unexpected any. Specify a different type.", "20"], [0, 0, 0, "Unexpected any. Specify a different type.", "21"], - [0, 0, 0, "Do not use any type assertions.", "22"], + [0, 0, 0, "Unexpected any. Specify a different type.", "22"], [0, 0, 0, "Unexpected any. Specify a different type.", "23"], [0, 0, 0, "Unexpected any. Specify a different type.", "24"], [0, 0, 0, "Unexpected any. Specify a different type.", "25"], [0, 0, 0, "Unexpected any. Specify a different type.", "26"], [0, 0, 0, "Unexpected any. Specify a different type.", "27"], - [0, 0, 0, "Unexpected any. Specify a different type.", "28"], - [0, 0, 0, "Unexpected any. Specify a different type.", "29"], - [0, 0, 0, "Unexpected any. Specify a different type.", "30"], - [0, 0, 0, "Unexpected any. Specify a different type.", "31"], - [0, 0, 0, "Unexpected any. Specify a different type.", "32"], - [0, 0, 0, "Unexpected any. Specify a different type.", "33"] + [0, 0, 0, "Unexpected any. Specify a different type.", "28"] ], "public/app/plugins/datasource/influxdb/influx_query_model.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -5783,27 +5770,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "19"], [0, 0, 0, "Unexpected any. Specify a different type.", "20"] ], - "public/app/plugins/datasource/influxdb/influx_series.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"] - ], "public/app/plugins/datasource/influxdb/migrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -6355,8 +6321,7 @@ exports[`better eslint`] = { ], "public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilterItem.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c6c16aa07f6..d87feb8b0a3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -167,7 +167,7 @@ WORKFLOW.md @torkelo /devenv/dev-dashboards-without-uid/ @grafana/dashboards-squad /devenv/dev-dashboards/ @grafana/dashboards-squad /devenv/docker/blocks/alert_webhook_listener/ @grafana/alerting-squad-backend -/devenv/docker/blocks/clickhouse/ @grafana/enterprise-plugins +/devenv/docker/blocks/clickhouse/ @grafana/partner-plugins /devenv/docker/blocks/collectd/ @grafana/observability-metrics /devenv/docker/blocks/grafana/ @grafana/grafana-as-code /devenv/docker/blocks/graphite/ @grafana/observability-metrics @@ -228,10 +228,10 @@ WORKFLOW.md @torkelo /Makefile @grafana/grafana-release-eng /scripts/build/ @grafana/grafana-release-eng -# Cloud Datasources backend code +# OSS Plugin Partnerships backend code /pkg/tsdb/cloudwatch/ @grafana/aws-plugins -/pkg/tsdb/azuremonitor/ @grafana/cloud-provider-plugins -/pkg/tsdb/cloudmonitoring/ @grafana/cloud-provider-plugins +/pkg/tsdb/azuremonitor/ @grafana/partner-plugins +/pkg/tsdb/cloudmonitoring/ @grafana/partner-plugins # Observability backend code /pkg/tsdb/prometheus/ @grafana/observability-metrics @@ -293,7 +293,7 @@ WORKFLOW.md @torkelo /public/locales/ @grafana/user-essentials /public/app/core/internationalization/ @grafana/user-essentials /e2e/ @grafana/user-essentials -/e2e/cloud-plugins-suite/ @grafana/cloud-provider-plugins +/e2e/cloud-plugins-suite/ @grafana/partner-plugins /packages/ @grafana/user-essentials @grafana/plugins-platform-frontend @grafana/grafana-bi-squad /packages/grafana-e2e-selectors/ @grafana/user-essentials /packages/grafana-e2e/ @grafana/user-essentials @@ -487,7 +487,7 @@ lerna.json @grafana/frontend-ops /public/app/plugins/datasource/elasticsearch/ @grafana/observability-logs /public/app/plugins/datasource/grafana/ @grafana/user-essentials /public/app/plugins/datasource/testdata/ @grafana/backend-platform -/public/app/plugins/datasource/grafana-azure-monitor-datasource/ @grafana/cloud-provider-plugins +/public/app/plugins/datasource/grafana-azure-monitor-datasource/ @grafana/partner-plugins /public/app/plugins/datasource/graphite/ @grafana/observability-metrics /public/app/plugins/datasource/influxdb/ @grafana/observability-metrics /public/app/plugins/datasource/jaeger/ @grafana/observability-traces-and-profiling @@ -498,7 +498,7 @@ lerna.json @grafana/frontend-ops /public/app/plugins/datasource/opentsdb/ @grafana/backend-platform /public/app/plugins/datasource/postgres/ @grafana/grafana-bi-squad /public/app/plugins/datasource/prometheus/ @grafana/observability-metrics -/public/app/plugins/datasource/cloud-monitoring/ @grafana/cloud-provider-plugins +/public/app/plugins/datasource/cloud-monitoring/ @grafana/partner-plugins /public/app/plugins/datasource/zipkin/ @grafana/observability-traces-and-profiling /public/app/plugins/datasource/tempo/ @grafana/observability-traces-and-profiling /public/app/plugins/datasource/phlare/ @grafana/observability-traces-and-profiling @@ -565,7 +565,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/backport.yml @grafana/grafana-release-eng /.github/workflows/bump-version.yml @grafana/grafana-release-eng /.github/workflows/close-milestone.yml @grafana/grafana-release-eng -/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/cloud-provider-plugins +/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/partner-plugins /.github/workflows/codeowners-validator.yml @tolzhabayev /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo diff --git a/.prettierignore b/.prettierignore index cc9c39938ad..62700e93ea4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -33,3 +33,6 @@ public/openapi3.json # Generated Kinds report kinds/report.json + +# Generated schema docs +docs/sources/developers/kinds/ diff --git a/conf/sample.ini b/conf/sample.ini index 9fef1b9c5a4..284c8304f1c 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -582,6 +582,8 @@ ;allowed_groups = ;role_attribute_strict = false ;allow_assign_grafana_admin = false +# prevent synchronizing users organization roles +;skip_org_role_sync = false #################################### Okta OAuth ####################### [auth.okta] diff --git a/docs/Makefile b/docs/Makefile index 4f553a5e878..96dc60c463f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,24 +1,28 @@ .PHONY: pull docs docs-quick docs-no-pull docs-test docs-local-static +PODMAN = $(shell if command -v podman &>/dev/null; then echo podman; else echo docker; fi) IMAGE = grafana/docs-base:latest CONTENT_PATH = /hugo/content/docs/grafana/next LOCAL_STATIC_PATH = ../../website/static PORT = 3002:3002 pull: - docker pull $(IMAGE) + $(PODMAN) pull $(IMAGE) docs: pull - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" - + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" + +docs-preview: pull + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server BUILD_DRAFTS=true" + docs-no-pull: - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" docs-test: pull - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod' + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod' # expects that you have grafana/website checked out in same path as the grafana repo. docs-local-static: pull if [ ! -d "$(LOCAL_STATIC_PATH)" ]; then echo "local path (website project) $(LOCAL_STATIC_PATH) not found"]; exit 1; fi - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \ + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \ -v $(shell pwd)/$(LOCAL_STATIC_PATH):/hugo/static:Z -p $(PORT) --rm -it $(IMAGE) diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index c8c148d3adc..33c22c8a913 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -95,6 +95,8 @@ The following list contains role-based access control actions. | `orgs:read` | `orgs:*`
`orgs:id:*` | Read one or more organizations. | | `orgs:write` | `orgs:*`
`orgs:id:*` | Update one or more organizations. | | `plugins.app:access` | `plugins:*`
`plugins:id:*` | Access one or more application plugins (still enforcing the organization role) | +| `plugins:install` | n/a | Install and uninstall plugins. | +| `plugins:write` | `plugins:*`
`plugins:id:*` | Edit settings for one or more plugins. | | `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "#scope-definitions" >}}). | | `reports:create` | n/a | Create reports. | | `reports:write` | `reports:*`
`reports:id:*` | Update reports. | @@ -196,6 +198,7 @@ The following list contains role-based access control scopes. | `orgs:*`
`orgs:id:*` | Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. | | `permissions:type:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. | | `permissions:type:escalate` | The scope is required to trigger the reset of basic roles permissions. It indicates that users might acquire additional permissions they did not previously have. | +| `plugins:*`
`plugins:id:*` | Restrict an action to a set of plugins. For example, `plugins:id:grafana-oncall-app` matches Grafana OnCall plugin, and `plugins:*` matches all plugins. | | `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the role-based access control [provisioner]({{< relref "./rbac-grafana-provisioning/" >}}). | | `reports:*`
`reports:id:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. | | `roles:*`
`roles:uid:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. | diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 675d13a5853..907ac6a92f8 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -17,12 +17,12 @@ The following tables list permissions associated with basic and fixed roles. ## Basic role assignments -| Basic role | Associated fixed roles | Description | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. | -| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | -| Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. | -| Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Basic role | Associated fixed roles | Description | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:maintainer` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. | +| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:writer` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. | ## Fixed role definitions @@ -72,6 +72,8 @@ The following tables list permissions associated with basic and fixed roles. | `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | | `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | | `fixed:plugins.app:reader` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | +| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. | +| `fixed:plugins:writer` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | | `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | | `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | | `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index b3046262c9b..4719728582f 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -524,9 +524,9 @@ policies: - severity =~ "warning|critical" # a list of grafana-like matchers that an alert rule has to fulfill to match the node object_matchers: - - alertname = CPUUsage - - service_id-X = serviceX - - severity =~ "warning|critical" + - ['alertname', '=', 'CPUUsage'] + - ['service_id-X', '=', 'serviceX'] + - ['severity', '=~', 'warning|critical'] # Times when the route should be muted. These must match the name of a # mute time interval. # Additionally, the root node cannot have any mute times. diff --git a/docs/sources/dashboards/dashboard-public/index.md b/docs/sources/dashboards/dashboard-public/index.md index 45e2080d6a6..bd40a262b55 100644 --- a/docs/sources/dashboards/dashboard-public/index.md +++ b/docs/sources/dashboards/dashboard-public/index.md @@ -32,6 +32,12 @@ Add the `publicDashboards` feature toggle to your `custom.ini` file. publicDashboards = true ``` +If you are using Docker, use an environment variable to enable public dashboards: + +``` +--env GF_FEATURE_TOGGLES_ENABLE=publicDashboards +``` + > **Note:** For Grafana Cloud, you will need to contact support to have the feature enabled. #### Make a dashboard public diff --git a/docs/sources/datasources/elasticsearch/_index.md b/docs/sources/datasources/elasticsearch/_index.md index 4ad5848e5b2..16b3a051472 100644 --- a/docs/sources/datasources/elasticsearch/_index.md +++ b/docs/sources/datasources/elasticsearch/_index.md @@ -31,7 +31,7 @@ Once you've added the Elasticsearch data source, you can [configure it]({{< relr This data source supports these versions of Elasticsearch: - v7.10+ -- v8.0+ +- v8.x ## Configure the data source diff --git a/docs/sources/developers/kinds/_index.md b/docs/sources/developers/kinds/_index.md new file mode 100644 index 00000000000..cdb07012a0c --- /dev/null +++ b/docs/sources/developers/kinds/_index.md @@ -0,0 +1,10 @@ +--- +title: Grafana schema +weight: 200 +_build: + list: false +--- + +# Grafana schema + +{{< section >}} diff --git a/docs/sources/developers/kinds/core/_index.md b/docs/sources/developers/kinds/core/_index.md new file mode 100644 index 00000000000..9e1eedc72aa --- /dev/null +++ b/docs/sources/developers/kinds/core/_index.md @@ -0,0 +1,10 @@ +--- +title: Core kinds +weight: 200 +--- + +# Grafana core kinds + +Kinds that define Grafana’s core schematized object types - dashboards, datasources, users, etc. + +{{< section >}} diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md new file mode 100644 index 00000000000..5fc5f9be873 --- /dev/null +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -0,0 +1,219 @@ +--- +keywords: + - grafana + - schema +title: Dashboard kind +--- + +# Dashboard kind + +### Maturity: merged +### Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|------------------------|-----------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `editable` | boolean | **Yes** | Whether a dashboard is editable or not. Default: `true`. | +| `graphTooltip` | integer | **Yes** | 0 for no shared crosshair or tooltip (default).
1 for shared crosshair.
2 for shared crosshair AND shared tooltip. Possible values are: `0`, `1`, `2`. Default: `0`. | +| `revision` | integer | **Yes** | Version of the current dashboard data Default: `-1`. | +| `schemaVersion` | integer | **Yes** | Version of the JSON schema, incremented each time a Grafana update brings
changes to said schema.
TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion Default: `36`. | +| `style` | string | **Yes** | Theme of dashboard. Possible values are: `dark`, `light`. Default: `dark`. | +| `annotations` | [object](#annotations) | No | TODO docs | +| `description` | string | No | Description of dashboard. | +| `fiscalYearStartMonth` | integer | No | TODO docs | +| `gnetId` | string | No | | +| `id` | integer | No | Unique numeric identifier for the dashboard.
TODO must isolate or remove identifiers local to a Grafana instance...? | +| `links` | [DashboardLink](#dashboardlink)[] | No | TODO docs | +| `liveNow` | boolean | No | TODO docs | +| `panels` | [object](#panels)[] | No | | +| `refresh` | | No | TODO docs | +| `snapshot` | [Snapshot](#snapshot) | No | TODO docs | +| `tags` | string[] | No | Tags associated with dashboard. | +| `templating` | [object](#templating) | No | TODO docs | +| `time` | [object](#time) | No | Time range for dashboard, e.g. last 6 hours, last 7 days, etc | +| `timepicker` | [object](#timepicker) | No | TODO docs
TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes | +| `timezone` | string | No | Timezone of dashboard, Possible values are: `browser`, `utc`, ``. Default: `browser`. | +| `title` | string | No | Title of dashboard. | +| `uid` | string | No | Unique dashboard identifier that can be generated by anyone. string (8-40) | +| `version` | integer | No | Version of the dashboard, incremented each time the dashboard is updated. | +| `weekStart` | string | No | TODO docs | + +## DashboardLink + +FROM public/app/features/dashboard/state/DashboardModels.ts - ish +TODO docs + +### Properties + +| Property | Type | Required | Description | +|---------------|----------|----------|------------------------------------------------------| +| `asDropdown` | boolean | **Yes** | Default: `false`. | +| `icon` | string | **Yes** | | +| `includeVars` | boolean | **Yes** | Default: `false`. | +| `keepTime` | boolean | **Yes** | Default: `false`. | +| `tags` | string[] | **Yes** | | +| `targetBlank` | boolean | **Yes** | Default: `false`. | +| `title` | string | **Yes** | | +| `tooltip` | string | **Yes** | | +| `type` | string | **Yes** | TODO docs Possible values are: `link`, `dashboards`. | +| `url` | string | **Yes** | | + +## Snapshot + +TODO docs + +### Properties + +| Property | Type | Required | Description | +|---------------|---------|----------|-------------| +| `created` | string | **Yes** | TODO docs | +| `expires` | string | **Yes** | TODO docs | +| `externalUrl` | string | **Yes** | TODO docs | +| `external` | boolean | **Yes** | TODO docs | +| `id` | integer | **Yes** | TODO docs | +| `key` | string | **Yes** | TODO docs | +| `name` | string | **Yes** | TODO docs | +| `orgId` | integer | **Yes** | TODO docs | +| `updated` | string | **Yes** | TODO docs | +| `userId` | integer | **Yes** | TODO docs | +| `url` | string | No | TODO docs | + +## annotations + +TODO docs + +### Properties + +| Property | Type | Required | Description | +|----------|---------------------------------------|----------|-------------| +| `list` | [AnnotationQuery](#annotationquery)[] | No | | + +### AnnotationQuery + +TODO docs +FROM: AnnotationQuery in grafana-data/src/types/annotations.ts + +#### Properties + +| Property | Type | Required | Description | +|--------------|---------------------------------------|----------|-------------------------------------------------| +| `builtIn` | integer | **Yes** | Default: `0`. | +| `datasource` | [object](#datasource) | **Yes** | Datasource to use for annotation. | +| `enable` | boolean | **Yes** | Whether annotation is enabled. Default: `true`. | +| `showIn` | integer | **Yes** | Default: `0`. | +| `type` | string | **Yes** | Default: `dashboard`. | +| `hide` | boolean | No | Whether to hide annotation. Default: `false`. | +| `iconColor` | string | No | Annotation icon color. | +| `name` | string | No | Name of annotation. | +| `rawQuery` | string | No | Query for annotation data. | +| `target` | [AnnotationTarget](#annotationtarget) | No | TODO docs | + +#### AnnotationTarget + +TODO docs + +##### Properties + +| Property | Type | Required | Description | +|------------|----------|----------|-------------| +| `limit` | integer | **Yes** | | +| `matchAny` | boolean | **Yes** | | +| `tags` | string[] | **Yes** | | +| `type` | string | **Yes** | | + +#### datasource + +Datasource to use for annotation. + +##### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|-------------| +| `type` | string | No | | +| `uid` | string | No | | + +## panels + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + +## templating + +TODO docs + +### Properties + +| Property | Type | Required | Description | +|----------|-----------------------------------|----------|-------------| +| `list` | [VariableModel](#variablemodel)[] | No | | + +### VariableModel + +FROM: packages/grafana-data/src/types/templateVars.ts +TODO docs +TODO what about what's in public/app/features/types.ts? +TODO there appear to be a lot of different kinds of [template] vars here? if so need a disjunction + +#### Properties + +| Property | Type | Required | Description | +|----------------|---------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `global` | boolean | **Yes** | Default: `false`. | +| `hide` | integer | **Yes** | Possible values are: `0`, `1`, `2`. | +| `id` | string | **Yes** | Default: `00000000-0000-0000-0000-000000000000`. | +| `index` | integer | **Yes** | Default: `-1`. | +| `name` | string | **Yes** | | +| `skipUrlSync` | boolean | **Yes** | Default: `false`. | +| `state` | string | **Yes** | Possible values are: `NotStarted`, `Loading`, `Streaming`, `Done`, `Error`. | +| `type` | string | **Yes** | FROM: packages/grafana-data/src/types/templateVars.ts
TODO docs
TODO this implies some wider pattern/discriminated union, probably? Possible values are: `query`, `adhoc`, `constant`, `datasource`, `interval`, `textbox`, `custom`, `system`. | +| `datasource` | [DataSourceRef](#datasourceref) | No | Ref to a DataSource instance | +| `description` | string | No | | +| `error` | [object](#error) | No | | +| `label` | string | No | | +| `query` | | No | TODO: Move this into a separated QueryVariableModel type | +| `rootStateKey` | string | No | | + +#### DataSourceRef + +Ref to a DataSource instance + +##### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|------------------------------| +| `type` | string | No | The plugin type-id | +| `uid` | string | No | Specific datasource instance | + +#### error + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + +## time + +Time range for dashboard, e.g. last 6 hours, last 7 days, etc + +### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|--------------------| +| `from` | string | **Yes** | Default: `now-6h`. | +| `to` | string | **Yes** | Default: `now`. | + +## timepicker + +TODO docs +TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes + +### Properties + +| Property | Type | Required | Description | +|---------------------|----------|----------|----------------------------------------------------------------------------------------| +| `collapse` | boolean | **Yes** | Whether timepicker is collapsed or not. Default: `false`. | +| `enable` | boolean | **Yes** | Whether timepicker is enabled or not. Default: `true`. | +| `hidden` | boolean | **Yes** | Whether timepicker is visible or not. Default: `false`. | +| `refresh_intervals` | string[] | **Yes** | Selectable intervals for auto-refresh. Default: `[5s 10s 30s 1m 5m 15m 30m 1h 2h 1d]`. | +| `time_options` | string[] | **Yes** | TODO docs Default: `[5m 15m 1h 6h 12h 24h 2d 7d 30d]`. | + + diff --git a/docs/sources/developers/kinds/core/playlist/schema-reference.md b/docs/sources/developers/kinds/core/playlist/schema-reference.md new file mode 100644 index 00000000000..918c9cb05ca --- /dev/null +++ b/docs/sources/developers/kinds/core/playlist/schema-reference.md @@ -0,0 +1,32 @@ +--- +keywords: + - grafana + - schema +title: Playlist kind +--- + +# Playlist kind + +### Maturity: merged +### Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|------------|---------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `interval` | string | **Yes** | Interval sets the time between switching views in a playlist.
FIXME: Is this based on a standardized format or what options are available? Can datemath be used? Default: `5m`. | +| `name` | string | **Yes** | Name of the playlist. | +| `uid` | string | **Yes** | Unique playlist identifier. Generated on creation, either by the
creator of the playlist of by the application. | +| `items` | [PlaylistItem](#playlistitem)[] | No | The ordered list of items that the playlist will iterate over.
FIXME! This should not be optional, but changing it makes the godegen awkward | + +## PlaylistItem + +### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | Type of the item. Possible values are: `dashboard_by_uid`, `dashboard_by_id`, `dashboard_by_tag`. | +| `value` | string | **Yes** | Value depends on type and describes the playlist item.

- dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
is not portable as the numerical identifier is non-deterministic between different instances.
Will be replaced by dashboard_by_uid in the future. (deprecated)
- dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
dashboards behind the tag will be added to the playlist.
- dashboard_by_uid: The value is the dashboard UID | +| `title` | string | No | Title is an unused property -- it will be removed in the future | + + diff --git a/docs/sources/developers/kinds/core/team/schema-reference.md b/docs/sources/developers/kinds/core/team/schema-reference.md new file mode 100644 index 00000000000..6da29527078 --- /dev/null +++ b/docs/sources/developers/kinds/core/team/schema-reference.md @@ -0,0 +1,34 @@ +--- +keywords: + - grafana + - schema +title: Team kind +--- + +# Team kind + +### Maturity: merged +### Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|-----------------|--------------------------|----------|----------------------------------------------------------| +| `created` | integer | **Yes** | Created indicates when the team was created. | +| `memberCount` | integer | **Yes** | MemberCount is the number of the team members. | +| `name` | string | **Yes** | Name of the team. | +| `orgId` | integer | **Yes** | OrgId is the ID of an organisation the team belongs to. | +| `permission` | integer | **Yes** | Possible values are: `0`, `1`, `2`, `4`. | +| `updated` | integer | **Yes** | Updated indicates when the team was updated. | +| `accessControl` | [object](#accesscontrol) | No | AccessControl metadata associated with a given resource. | +| `avatarUrl` | string | No | AvatarUrl is the team's avatar URL. | +| `email` | string | No | Email of the team. | + +## accessControl + +AccessControl metadata associated with a given resource. + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + + diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 7c8883d7c9d..696c0a112f1 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -849,7 +849,7 @@ Administrators can increase this if they experience OAuth login state mismatch e ### oauth_skip_org_role_update_sync -> **Note**: This option will soon be a legacy option in favor of OAuth provider specific `skip_org_role_sync` settings. +> **Note**: This option will soon be a legacy option in favor of OAuth provider specific `skip_org_role_sync` settings. The following sections explain settings for each provider. Skip forced assignment of OrgID `1` or `auto_assign_org_id` for external logins. Default is `false`. Use this setting to allow users with external login to be manually assigned to multiple organizations. @@ -860,13 +860,13 @@ By default, the users' organization and role is reset on every new login. > With Grafana 10, if `oauth_skip_org_role_update_sync` option is set to `false`, users with no mapping will be > reset to the default organization role on every login. [See `auto_assign_org_role` option]({{< relref ".#auto_assign_org_role" >}}). -### [auth.grafana_com] skip_org_role_update_sync +### [auth.grafana_com] skip_org_role_sync To prevent synchronization of organization roles for a specific OAuth integration, you can set the `skip_org_role_sync` option to `true`. Please note that there is also a separate setting called `oauth_skip_org_role_update_sync` which has a different scope. While `skip_org_role_sync` only applies to the specific OAuth provider, `oauth_skip_org_role_update_sync` is a generic setting that affects all configured OAuth providers. The setting `oauth_skip_org_role_update_sync` will be deprecated in favor of provider-specific settings. -The table below shows the available OAuth providers and their setting with the default value and the skip org role sync setting. +The table below show the OAuth provider and their setting with the default value and the skip org role sync setting. | OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior | | --- | --- | --- | --- | | Grafana.com | false | false | will sync with Grafana.com roles | @@ -874,6 +874,20 @@ The table below shows the available OAuth providers and their setting with the d | Grafana.com | false | true | skip org role sync for grafana.com users | | Grafana.com | true | true | skip org role sync for Grafana.com users and all other OAuth providers | +### [auth.azuread] skip_org_role_sync + +To prevent synchronization of organization roles for a specific OAuth integration, you can set the `skip_org_role_sync` option to `true`. Please note that there is also a separate setting called `oauth_skip_org_role_update_sync` which has a different scope. While `skip_org_role_sync` only applies to the specific OAuth provider, `oauth_skip_org_role_update_sync` is a generic setting that affects all configured OAuth providers. + +The setting `oauth_skip_org_role_update_sync` will be deprecated in favor of provider-specific settings. + +The following table shows the OAuth providers, the default value setting, and the skip org role sync setting. +| OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior | +| --- | --- | --- | --- | +| AzureAD | false | false | will sync with AzureAD roles | +| AzureAD | true | false | skip org role sync for OAuth providers including AzureAD users | +| AzureAD | false | true | skip org role sync for AzureAD users | +| AzureAD | true | true | skip org role sync for AzureAD users and all other OAuth providers | + ### api_key_max_seconds_to_live Limit of API key seconds to live before expiration. Default is -1 (unlimited). diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index e204c799f47..9ba9c758972 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -62,7 +62,6 @@ Alpha features might be changed or removed without prior notice. | `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | | `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | | `tempoApmTable` | Show APM table | -| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | | `publicDashboards` | Enables public access to dashboards | | `lokiLive` | Support WebSocket streaming for loki (early prototype) | | `lokiDataframeApi` | Use experimental loki api for WebSocket streaming (early prototype) | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index 321056dc7e4..c3243a11409 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -99,22 +99,6 @@ To enable the Azure AD OAuth2, register your application with Azure AD. 1. Click on **Users and Groups** and add Users/Groups to the Grafana roles by using **Add User**. -### Map roles - -By default, Azure AD authentication will map users to organization roles based on the most privileged application role assigned to the user in AzureAD. - -If no application role is found, the user is assigned the role specified by -[the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). -You can disable this default role assignment by setting `role_attribute_strict = true`. -It denies user access if no role or an invalid role is returned. - -**On every login** the user organization role will be reset to match AzureAD's application role and -their organization membership will be reset to the default organization. - -If Azure AD authentication is not intended to sync user roles and organization membership, -`oauth_skip_org_role_update_sync` should be enabled. -See [configure-grafana]({{< relref "../../../configure-grafana#oauth_skip_org_role_update_sync" >}}) for more details. - ### Assign server administrator privileges > Available in Grafana v9.2 and later versions. @@ -157,6 +141,7 @@ allowed_domains = allowed_groups = role_attribute_strict = false allow_assign_grafana_admin = false +skip_org_role_sync = false ``` You can also use these environment variables to configure **client_id** and **client_secret**: @@ -244,3 +229,30 @@ To force fetching groups from Microsoft Graph API instead of the `id_token`. You ``` force_use_graph_api = true ``` + +### Map roles + +By default, Azure AD authentication will map users to organization roles based on the most privileged application role assigned to the user in AzureAD. + +If no application role is found, the user is assigned the role specified by +[the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). +You can disable this default role assignment by setting `role_attribute_strict = true`. +It denies user access if no role or an invalid role is returned. + +**On every login** the user organization role will be reset to match AzureAD's application role and +their organization membership will be reset to the default organization. + +## Skip organization role sync + +If Azure AD authentication is not intended to sync user roles and organization membership, +`oauth_skip_org_role_update_sync` should be enabled, this is not recommended to use in favor of setting provider specific `skip_org_role_sync` option. +See [configure-grafana]({{< relref "../../../configure-grafana#oauth_skip_org_role_update_sync" >}}) for more details. + +To prevent the sync of org roles from Grafana.com, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana. + +```ini +[auth.azuread] +# .. +# prevents the sync of org roles from Grafana.com +skip_org_role_sync = true +``` diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 63f38d3cd4d..c15151b064f 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -110,6 +110,12 @@ the group `foo`, set allowed_groups = example, foo/bar ``` +To put values containing spaces in the list, use the following JSON syntax: + +```ini +allowed_groups = ["Admins", "Software Engineers"] +``` + Note that in GitLab, the group or subgroup name doesn't always match its display name, especially if the display name contains spaces or special characters. Make sure you always use the group or subgroup name as it appears diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index ae587b56cb4..4b3834cf72e 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -81,6 +81,12 @@ The `allowed_domains` option limits access to the users belonging to the specifi allowed_domains = mycompany.com mycompany.org ``` +To put values containing spaces in the list, use the following JSON syntax: + +```ini +allowed_groups = ["Admins", "Software Engineers"] +``` + ### Map roles Grafana can attempt to do role mapping through Okta OAuth. In order to achieve this, Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index df1e6340556..82aab5f70cd 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -390,6 +390,12 @@ You can use `*` as the Grafana organization in the mapping if you want all users With the [`allowed_organizations`]({{< relref "../../../configure-grafana/enterprise-configuration/#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. +To put values containing spaces in the list, use the following JSON syntax: + +```ini +allowed_organizations = ["org 1", "second org"] +``` + ### Example SAML configuration ```bash diff --git a/go.mod b/go.mod index d420673b13d..da974b8e47b 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,6 @@ require ( github.com/grafana/grafana-aws-sdk v0.11.0 github.com/grafana/grafana-azure-sdk-go v1.5.1 github.com/grafana/grafana-plugin-sdk-go v0.147.0 - github.com/grafana/thema v0.0.0-20221222001256-d3a8fa941a6b github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v1.2.0 github.com/hashicorp/go-plugin v1.4.3 @@ -78,7 +77,7 @@ require ( github.com/linkedin/goavro/v2 v2.10.0 github.com/m3db/prometheus_remote_client_golang v0.4.4 github.com/magefile/mage v1.14.0 - github.com/mattn/go-isatty v0.0.14 + github.com/mattn/go-isatty v0.0.16 github.com/mattn/go-sqlite3 v1.14.16 github.com/matttproud/golang_protobuf_extensions v1.0.4 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f @@ -127,7 +126,7 @@ require ( gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 - xorm.io/builder v0.3.6 // indirect + xorm.io/builder v0.3.6 xorm.io/core v0.7.3 xorm.io/xorm v0.8.2 ) @@ -253,7 +252,7 @@ require ( github.com/bufbuild/connect-go v1.4.1 github.com/dlmiddlecote/sqlstats v1.0.2 github.com/drone/drone-cli v1.6.1 - github.com/getkin/kin-openapi v0.103.0 + github.com/getkin/kin-openapi v0.107.0 github.com/golang-migrate/migrate/v4 v4.7.0 github.com/google/go-github/v45 v45.2.0 github.com/grafana/codejen v0.0.3 @@ -274,7 +273,9 @@ require ( require ( github.com/dave/dst v0.27.2 + github.com/grafana/thema v0.0.0-20230113164405-e0e8893756cf github.com/parca-dev/parca v0.12.1 + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f k8s.io/apimachinery v0.25.3 ) @@ -308,11 +309,12 @@ require ( github.com/hashicorp/memberlist v0.5.0 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-ieproxy v0.0.3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect @@ -377,13 +379,13 @@ require ( github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/klauspost/compress v1.15.13 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/labstack/echo/v4 v4.9.0 // indirect - github.com/labstack/gommon v0.3.1 // indirect + github.com/labstack/echo/v4 v4.9.1 // indirect + github.com/labstack/gommon v0.4.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect - github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wk8/go-ordered-map v1.0.0 github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xlab/treeprint v1.1.0 // indirect diff --git a/go.sum b/go.sum index b921e920314..fd311161d03 100644 --- a/go.sum +++ b/go.sum @@ -874,8 +874,8 @@ github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= -github.com/getkin/kin-openapi v0.103.0 h1:F5wAtaQvPWxKCAYZ69LgHAThgu16p4u41VQtbn1U8LA= -github.com/getkin/kin-openapi v0.103.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= +github.com/getkin/kin-openapi v0.107.0 h1:bxhL6QArW7BXQj8NjXfIJQy680NsMKd25nwhvpCXchg= +github.com/getkin/kin-openapi v0.107.0/go.mod h1:9Dhr+FasATJZjS4iOLvB0hkaxgYdulrNYm2e9epLWOo= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= @@ -1383,10 +1383,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20230112102204-5545cdf0edc9 h1:rMfagCACaYtBvj6bbJq5zVuNR6ASeKUTUOgr/DM+QrU= -github.com/grafana/alerting v0.0.0-20230112102204-5545cdf0edc9/go.mod h1:w5jcmHYPAfju/QcI69SwFs0+qMcm8jvW6fsqSc2tJtc= -github.com/grafana/alerting v0.0.0-20230113002715-e287d43f4b57 h1:VyzefGsa17SXPzqCvnI8cczbPfuVNUMXiPJNVNy/PdA= -github.com/grafana/alerting v0.0.0-20230113002715-e287d43f4b57/go.mod h1:w5jcmHYPAfju/QcI69SwFs0+qMcm8jvW6fsqSc2tJtc= github.com/grafana/alerting v0.0.0-20230113220341-b9b3de89bca2 h1:XkzN0dBir+oso3XjGOV2HLGSCjyyYHLL5AuUqI/LIXY= github.com/grafana/alerting v0.0.0-20230113220341-b9b3de89bca2/go.mod h1:w5jcmHYPAfju/QcI69SwFs0+qMcm8jvW6fsqSc2tJtc= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= @@ -1412,8 +1408,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20230109182643-002d8ba820f6 github.com/grafana/prometheus-alertmanager v0.25.1-0.20230109182643-002d8ba820f6/go.mod h1:MnBfDPXJqXmmfPwQlCLvVUdqfnvrAw+hSPtDeaaFwj4= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4= -github.com/grafana/thema v0.0.0-20221222001256-d3a8fa941a6b h1:LBjw2NaMbTdx/etBbyZO0n0L2Lqj5PCDJAGwKk9TCII= -github.com/grafana/thema v0.0.0-20221222001256-d3a8fa941a6b/go.mod h1:8wmENx19kzNI+5De7889AVRJ3T4OnOb5I1bSJLcj1t4= +github.com/grafana/thema v0.0.0-20230113164405-e0e8893756cf h1:jl1iWUlJt5GJWaafOqtaEp46MDFBCni7bmv75VGFkq0= +github.com/grafana/thema v0.0.0-20230113164405-e0e8893756cf/go.mod h1:5j2nf4xmWhKr+1vyGouML8eJ8xERS5Jw/lhjs0eyz78= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6 h1:I9dh1MXGX0wGyxdV/Sl7+ugnki4Dfsy8lv2s5Yf887o= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -1765,11 +1761,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= -github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY= -github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= +github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= @@ -1849,8 +1846,9 @@ github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20191113090002-7c0f6868bffe/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= @@ -1866,8 +1864,9 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -1971,6 +1970,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -2518,8 +2519,9 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy3uBj1F4O3AaFh7hUGBrJjAssJhKyAbwxtkslxqo= github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= @@ -2560,9 +2562,12 @@ github.com/xdg/scram v1.0.3/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49 github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= @@ -3191,6 +3196,7 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= diff --git a/kinds/gen.go b/kinds/gen.go index 9a06c592c39..3c437c7049d 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -40,6 +40,7 @@ func main() { codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), kindsys.GoCoreKindParentPath), codegen.LatestMajorsOrXJenny(kindsys.TSCoreKindParentPath, codegen.TSTypesJenny{}), codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")), + codegen.DocsJenny(filepath.Join("docs", "sources", "developers", "kinds", "core")), ) coreKindsGen.AddPostprocessors(codegen.SlashHeaderMapper("kinds/gen.go")) diff --git a/package.json b/package.json index 1f25f7c4400..0938ff5d28a 100644 --- a/package.json +++ b/package.json @@ -253,7 +253,7 @@ "@grafana/lezer-logql": "0.1.1", "@grafana/monaco-logql": "^0.0.6", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "0.0.4", + "@grafana/scenes": "latest", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@jaegertracing/jaeger-ui-components": "workspace:*", @@ -261,7 +261,7 @@ "@leeoniya/ufuzzy": "0.9.1", "@lezer/common": "1.0.1", "@lezer/highlight": "1.1.2", - "@lezer/lr": "1.2.3", + "@lezer/lr": "1.3.1", "@opentelemetry/api": "1.3.0", "@opentelemetry/exporter-collector": "0.25.0", "@opentelemetry/semantic-conventions": "1.8.0", @@ -429,10 +429,5 @@ "engines": { "node": ">= 16" }, - "packageManager": "yarn@3.3.0", - "dependenciesMeta": { - "@grafana/scenes@0.0.3": { - "unplugged": true - } - } + "packageManager": "yarn@3.3.0" } diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 739bde84869..d3919b65ce2 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -224,5 +224,6 @@ export interface AuthSettings { SAMLSkipOrgRoleSync?: boolean; LDAPSkipOrgRoleSync?: boolean; GrafanaComSkipOrgRoleSync?: boolean; + AzureADSkipOrgRoleSync?: boolean; DisableSyncLock?: boolean; } diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 829f798f5b9..99bc06b2590 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -30,7 +30,6 @@ export interface FeatureToggles { panelTitleSearch?: boolean; tempoApmTable?: boolean; prometheusAzureOverrideAudience?: boolean; - influxdbBackendMigration?: boolean; showFeatureFlagsInUI?: boolean; publicDashboards?: boolean; publicDashboardsEmailSharing?: boolean; diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index c58967fa1dc..74f9c258a33 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -44,6 +44,7 @@ export const availableIconsIndex = { 'check-circle': true, 'check-square': true, circle: true, + 'circle-mono': true, 'clipboard-alt': true, 'clock-nine': true, cloud: true, diff --git a/packages/grafana-schema/src/schema/mudball.cue b/packages/grafana-schema/src/schema/mudball.cue index ee7aaff82cd..c1098d0204e 100644 --- a/packages/grafana-schema/src/schema/mudball.cue +++ b/packages/grafana-schema/src/schema/mudball.cue @@ -206,7 +206,6 @@ FieldTextAlignment: "auto" | "left" | "right" | "center" @cuetsy(kind="type") // The color-background-solid, gradient-gauge, and lcd-gauge // modes are deprecated in favor of new cell subOptions TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge") -// : TableCellDisplayMode @cuetsy(kind="enum") // Display mode to the "Colored Background" display // mode for table cells. Either displays a solid color (basic mode) @@ -260,26 +259,41 @@ VizLegendOptions: { // for the bar gauge component of Grafana UI BarGaugeDisplayMode: "basic" | "lcd" | "gradient" @cuetsy(kind="enum") -// Interface for table cell types that have no additional options. +// Auto mode table cell options TableAutoCellOptions: { - type: TableCellDisplayMode + type: TableCellDisplayMode & "auto" } @cuetsy(kind="interface") -// Allows for the table cell gauge display type to set the gauge mode. +// Colored text cell options +TableColorTextCellOptions: { + type: TableCellDisplayMode & "color-text" +} @cuetsy(kind="interface") + +// Json view cell options +TableJsonViewCellOptions: { + type: TableCellDisplayMode & "json-view" +} @cuetsy(kind="interface") + +// Json view cell options +TableImageCellOptions: { + type: TableCellDisplayMode & "image" +} @cuetsy(kind="interface") + +// Gauge cell options TableBarGaugeCellOptions: { type: TableCellDisplayMode & "gauge" - mode: BarGaugeDisplayMode + mode?: BarGaugeDisplayMode } @cuetsy(kind="interface") -// Allows for the background display mode to be set for the color background cell. +// Colored background cell options TableColoredBackgroundCellOptions: { type: TableCellDisplayMode & "color-background" - mode: TableCellBackgroundDisplayMode + mode?: TableCellBackgroundDisplayMode } @cuetsy(kind="interface") // Table cell options. Each cell has a display mode // and other potential options for that display. -TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions @cuetsy(kind="type") +TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions @cuetsy(kind="type") // Field options for each field within a table (e.g 10, "The String", 64.20, etc.) // Generally defines alignment, filtering capabilties, display options, etc. diff --git a/packages/grafana-schema/src/schema/mudball.gen.ts b/packages/grafana-schema/src/schema/mudball.gen.ts index a279dae6967..3c2b02ed86b 100644 --- a/packages/grafana-schema/src/schema/mudball.gen.ts +++ b/packages/grafana-schema/src/schema/mudball.gen.ts @@ -489,25 +489,46 @@ export enum BarGaugeDisplayMode { } /** - * Interface for table cell types that have no additional options. + * Auto mode table cell options */ export interface TableAutoCellOptions { - type: TableCellDisplayMode; + type: TableCellDisplayMode.Auto; } /** - * Allows for the table cell gauge display type to set the gauge mode. + * Colored text cell options + */ +export interface TableColorTextCellOptions { + type: TableCellDisplayMode.ColorText; +} + +/** + * Json view cell options + */ +export interface TableJsonViewCellOptions { + type: TableCellDisplayMode.JSONView; +} + +/** + * Json view cell options + */ +export interface TableImageCellOptions { + type: TableCellDisplayMode.Image; +} + +/** + * Gauge cell options */ export interface TableBarGaugeCellOptions { - mode: BarGaugeDisplayMode; + mode?: BarGaugeDisplayMode; type: TableCellDisplayMode.Gauge; } /** - * Allows for the background display mode to be set for the color background cell. + * Colored background cell options */ export interface TableColoredBackgroundCellOptions { - mode: TableCellBackgroundDisplayMode; + mode?: TableCellBackgroundDisplayMode; type: TableCellDisplayMode.ColorBackground; } @@ -515,7 +536,7 @@ export interface TableColoredBackgroundCellOptions { * Table cell options. Each cell has a display mode * and other potential options for that display. */ -export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions); +export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions); /** * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index 992ba6de058..f692a2ff568 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -62,7 +62,7 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex onClick={linkModel.onClick} target={linkModel.target} title={linkModel.title} - style={{ ...style, overflow: 'hidden', display: 'flex', flexGrow: 1 }} + style={{ ...style, overflow: 'hidden', display: 'flex' }} aria-label={selectors.components.DataLinksContextMenu.singleLink} > {children({})} diff --git a/packages/grafana-ui/src/components/Icon/utils.ts b/packages/grafana-ui/src/components/Icon/utils.ts index f7ead12d92c..d31311a54d5 100644 --- a/packages/grafana-ui/src/components/Icon/utils.ts +++ b/packages/grafana-ui/src/components/Icon/utils.ts @@ -1,6 +1,14 @@ import { IconName, IconSize } from '../../types/icon'; -const alwaysMonoIcons: IconName[] = ['grafana', 'favorite', 'heart-break', 'heart', 'panel-add', 'library-panel']; +const alwaysMonoIcons: IconName[] = [ + 'grafana', + 'favorite', + 'heart-break', + 'heart', + 'panel-add', + 'library-panel', + 'circle-mono', +]; export function getIconSubDir(name: IconName, type: string): string { if (name?.startsWith('gf-')) { diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx index ab52fc5b442..994756175e4 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx @@ -12,8 +12,6 @@ import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { HorizontalGroup, VerticalGroup } from '../Layout/Layout'; import { Menu } from '../Menu/Menu'; -import { PanelChromeInfoState } from './PanelChrome'; - const meta: ComponentMeta = { title: 'Visualizations/PanelChrome', component: PanelChrome, @@ -95,34 +93,33 @@ export const Examples = () => { - {renderPanel('Error status', { + {renderPanel('Has statusMessage', { title: 'Default title', - status: { - message: 'Error text', - onClick: action('ErrorIndicator: onClick fired'), - }, + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No padding, error loadingState', { + {renderPanel('No padding, has statusMessage', { padding: 'none', title: 'Default title', + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), + })} + {renderPanel('No title, loadingState is Error, no statusMessage', { loadingState: LoadingState.Error, })} - {renderPanel('No title, error loadingState', { - loadingState: LoadingState.Error, - })} - {renderPanel('Streaming loadingState', { + {renderPanel('loadingState is Streaming', { title: 'Default title', loadingState: LoadingState.Streaming, })} - {renderPanel('Loading loadingState', { + {renderPanel('loadingState is Loading', { title: 'Default title', loadingState: LoadingState.Loading, })} {renderPanel('Default panel: no non-required props')} - {renderPanel('No padding, no title', { + {renderPanel('No padding', { padding: 'none', })} {renderPanel('Very long title', { @@ -139,28 +136,27 @@ export const Examples = () => { {renderPanel('Error status, menu', { title: 'Default title', menu, - status: { - message: 'Error text', - onClick: action('ErrorIndicator: onClick fired'), - }, + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No padding, error loadingState, menu', { + {renderPanel('No padding; has statusMessage, menu', { padding: 'none', title: 'Default title', menu, - loadingState: LoadingState.Error, + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No title, error loadingState, menu', { + {renderPanel('No title, loadingState is Error, no statusMessage, menu', { menu, loadingState: LoadingState.Error, })} - {renderPanel('Streaming loadingState, menu', { + {renderPanel('loadingState is Streaming, menu', { title: 'Default title', menu, loadingState: LoadingState.Streaming, })} - {renderPanel('Loading loadingState, menu', { + {renderPanel('loadingState is Loading, menu', { title: 'Default title', menu, loadingState: LoadingState.Loading, @@ -235,29 +231,11 @@ const ErrorIcon = [ const leftItems = { LoadingIcon, ErrorIcon, Default }; -const titleItems: PanelChromeInfoState[] = [ - { - icon: 'info', - tooltip: - 'Description text with very long descriptive words that describe what is going on in the panel and not beyond. Or maybe beyond, not up to us.', - }, - { - icon: 'external-link-alt', - tooltip: 'wearegoingonanadventure.openanewtab.maybe', - onClick: () => {}, - }, - { - icon: 'clock-nine', - tooltip: 'Time range: 2021-09-01 00:00:00 to 2021-09-01 00:00:00', - onClick: () => {}, - }, - { - icon: 'heart', - tooltip: 'Health of the panel', - }, -]; +const description = + 'Description text with very long descriptive words that describe what is going on in the panel and not beyond. Or maybe beyond, not up to us.'; Basic.argTypes = { + description: { control: { type: 'text' } }, leftItems: { options: Object.keys(leftItems), mapping: leftItems, @@ -276,9 +254,8 @@ Basic.args = { width: 400, height: 200, title: 'Very long title that should get ellipsis when there is no more space', - titleItems, + description, menu, - loadingState: LoadingState.Loading, }; export default meta; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx index 6c4d000b151..2d2169e4812 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx @@ -49,13 +49,7 @@ it('renders panel with a header with title in place if prop title', () => { it('renders panel with a header if prop titleItems', () => { setup({ - titleItems: [ - { - icon: 'info-circle', - tooltip: 'This is the panel description', - onClick: () => {}, - }, - ], + titleItems: [
This should be a self-contained node
], }); expect(screen.getByTestId('header-container')).toBeInTheDocument(); @@ -63,13 +57,7 @@ it('renders panel with a header if prop titleItems', () => { it('renders panel with a header with icons in place if prop titleItems', () => { setup({ - titleItems: [ - { - icon: 'info-circle', - tooltip: 'This is the panel description', - onClick: () => {}, - }, - ], + titleItems: [
This should be a self-contained node
], }); expect(screen.getByTestId('title-items-container')).toBeInTheDocument(); diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 1b94df2f5f6..e91d3297633 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -1,36 +1,19 @@ import { css, cx } from '@emotion/css'; -import { isEmpty } from 'lodash'; import React, { CSSProperties, ReactElement, ReactNode } from 'react'; -import { GrafanaTheme2, isIconName, LoadingState } from '@grafana/data'; +import { GrafanaTheme2, LoadingState } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { useStyles2, useTheme2 } from '../../themes'; -import { IconName } from '../../types/icon'; import { Dropdown } from '../Dropdown/Dropdown'; import { Icon } from '../Icon/Icon'; -import { IconButton, IconButtonVariant } from '../IconButton/IconButton'; import { LoadingBar } from '../LoadingBar/LoadingBar'; import { ToolbarButton } from '../ToolbarButton'; -import { PopoverContent, Tooltip } from '../Tooltip'; +import { Tooltip } from '../Tooltip'; +import { PanelDescription } from './PanelDescription'; import { PanelStatus } from './PanelStatus'; -interface Status { - message?: string; - onClick?: (e: React.SyntheticEvent) => void; -} - -/** - * @internal - */ -export interface PanelChromeInfoState { - icon: IconName; - label?: string | ReactNode; - tooltip?: PopoverContent; - variant?: IconButtonVariant; - onClick?: () => void; -} - /** * @internal */ @@ -40,13 +23,21 @@ export interface PanelChromeProps { children: (innerWidth: number, innerHeight: number) => ReactNode; padding?: PanelPadding; title?: string; - titleItems?: PanelChromeInfoState[]; + description?: string | (() => string); + titleItems?: ReactNode[]; menu?: ReactElement | (() => ReactElement); /** dragClass, hoverHeader not yet implemented */ // dragClass?: string; hoverHeader?: boolean; loadingState?: LoadingState; - status?: Status; + /** + * Used to display status message (used for panel errors currently) + */ + statusMessage?: string; + /** + * Handle opening error details view (like inspect / error tab) + */ + statusMessageOnClick?: (e: React.SyntheticEvent) => void; /** @deprecated in favor of props * status for errors and loadingState for loading and streaming * which will serve the same purpose @@ -69,20 +60,31 @@ export function PanelChrome({ children, padding = 'md', title = '', + description = '', titleItems = [], menu, // dragClass, hoverHeader = false, loadingState, - status, - leftItems = [], + statusMessage, + statusMessageOnClick, + leftItems, }: PanelChromeProps) { const theme = useTheme2(); const styles = useStyles2(getStyles); // To Do rely on hoverHeader prop for header, not separate props // once hoverHeader is implemented - const hasHeader = title.length > 0 || leftItems.length > 0; + // + // Backwards compatibility for having a designated space for the header + + const hasHeader = + hoverHeader === false && + (title.length > 0 || + titleItems.length > 0 || + description !== '' || + loadingState === LoadingState.Streaming || + (leftItems?.length ?? 0) > 0); const headerHeight = getHeaderHeight(theme, hasHeader); const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height); @@ -97,27 +99,12 @@ export function PanelChrome({ }; const containerStyles: CSSProperties = { width, height }; + const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel'; - const isUsingDeprecatedLeftItems = isEmpty(status) && !loadingState; - const showLoading = loadingState === LoadingState.Loading && !isUsingDeprecatedLeftItems; - const showStreaming = loadingState === LoadingState.Streaming && !isUsingDeprecatedLeftItems; - - const renderStatus = () => { - const showError = loadingState === LoadingState.Error || status?.message; - if (!isUsingDeprecatedLeftItems && showError) { - return ( -
- -
- ); - } else { - return null; - } - }; return ( -
+
- {showLoading ? : null} + {loadingState === LoadingState.Loading ? : null}
@@ -127,29 +114,19 @@ export function PanelChrome({ )} - {showStreaming && ( -
- - - + + + {titleItems.length > 0 && ( +
+ {titleItems.map((item) => item)}
)} - {titleItems.length > 0 && ( -
- {titleItems - .filter((item) => isIconName(item.icon)) - .map((item, i) => ( -
- {item.onClick ? ( - - ) : ( - - - - )} -
- ))} + {loadingState === LoadingState.Streaming && ( +
+ + +
)} @@ -167,10 +144,14 @@ export function PanelChrome({ )} - {isUsingDeprecatedLeftItems &&
{itemsRenderer(leftItems, (item) => item)}
} + {leftItems &&
{itemsRenderer(leftItems, (item) => item)}
}
- {renderStatus()} + {statusMessage && ( +
+ +
+ )}
@@ -189,6 +170,7 @@ const getHeaderHeight = (theme: GrafanaTheme2, hasHeader: boolean) => { if (hasHeader) { return theme.spacing.gridSize * theme.components.panel.headerHeight; } + return 0; }; @@ -299,5 +281,11 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'flex', alignItems: 'center', }), + titleItems: css({ + display: 'flex', + alignItems: 'center', + overflow: 'hidden', + padding: theme.spacing(1), + }), }; }; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx new file mode 100644 index 00000000000..be82080da43 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useTheme2 } from '../../themes'; +import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; +import { Icon } from '../Icon/Icon'; +import { Tooltip } from '../Tooltip'; + +interface Props { + description: string | (() => string); +} + +export function PanelDescription({ description }: Props) { + const theme = useTheme2(); + const styles = getStyles(theme); + + const getDescriptionContent = (): JSX.Element => { + // description + const panelDescription = typeof description === 'function' ? description() : description; + + return ( +
+
+
+ ); + }; + + return description !== '' ? ( + + + + + + ) : null; +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + description: css({ + color: `${theme.colors.text.secondary}`, + backgroundColor: `${theme.colors.background.primary}`, + cursor: 'auto', + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + padding: `${theme.spacing(0, 1)}`, + height: ` ${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + color: `${theme.colors.text.primary}`, + background: `${theme.colors.background.secondary}`, + }, + + code: { + whiteSpace: 'normal', + wordWrap: 'break-word', + }, + + 'pre > code': { + display: 'block', + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/PanelChrome/index.ts b/packages/grafana-ui/src/components/PanelChrome/index.ts index 89f7e995c40..22cf206bfbd 100644 --- a/packages/grafana-ui/src/components/PanelChrome/index.ts +++ b/packages/grafana-ui/src/components/PanelChrome/index.ts @@ -38,6 +38,8 @@ export { type ErrorIndicatorProps as PanelChromeErrorIndicatorProps, } from './ErrorIndicator'; +export { PanelDescription } from './PanelDescription'; + export { usePanelContext, PanelContextProvider, type PanelContext, PanelContextRoot } from './PanelContext'; export * from './types'; diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index cdb831d81ab..3bbb3b2761a 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -8,6 +8,7 @@ import { BarGauge } from '../BarGauge/BarGauge'; import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; import { TableCellProps, TableCellDisplayMode } from './types'; +import { getCellOptions } from './utils'; const defaultScale: ThresholdsConfig = { mode: ThresholdsMode.Absolute, @@ -39,28 +40,9 @@ export const BarGaugeCell: FC = (props) => { // Set default display mode let barGaugeMode: BarGaugeDisplayMode = BarGaugeDisplayMode.Gradient; - // Support deprecated settings - const usingDeprecatedSettings = field.config.custom.displayMode !== undefined; - - // If we're using the old settings format we read the displayMode directly from - // the cell options - if (usingDeprecatedSettings) { - if ( - (field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) || - (field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Lcd) - ) { - barGaugeMode = BarGaugeDisplayMode.Lcd; - } else if ( - (field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) || - (field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Basic) - ) { - barGaugeMode = BarGaugeDisplayMode.Basic; - } - } - // Otherwise in the case of sub-options we read specifically from the sub-options - // object in order to get the display mode - else { - barGaugeMode = field.config.custom.cellOptions.mode; + const cellOptions = getCellOptions(field); + if (cellOptions.type === TableCellDisplayMode.Gauge) { + barGaugeMode = cellOptions.mode ?? BarGaugeDisplayMode.Gradient; } const getLinks = () => { diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 1e2c0bcb953..026cfb16e8a 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -2,8 +2,8 @@ import { cx } from '@emotion/css'; import React, { FC, ReactElement } from 'react'; import tinycolor from 'tinycolor2'; -import { DisplayValue, Field, formattedValueToString } from '@grafana/data'; -import { TableCellBackgroundDisplayMode } from '@grafana/schema'; +import { DisplayValue, formattedValueToString } from '@grafana/data'; +import { TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; import { getCellLinks, getTextColorForAlphaBackground } from '../../utils'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; @@ -11,6 +11,7 @@ import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; import { TableStyles } from './styles'; import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types'; +import { getCellOptions } from './utils'; export const DefaultCell: FC = (props) => { const { field, cell, tableStyles, row, cellProps } = props; @@ -27,7 +28,8 @@ export const DefaultCell: FC = (props) => { const showFilters = field.config.filterable; const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; - const cellStyle = getCellStyle(tableStyles, field, displayValue, inspectEnabled); + const cellOptions = getCellOptions(field); + const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled); const hasLinks = Boolean(getCellLinks(field, row)?.length); return ( @@ -38,7 +40,7 @@ export const DefaultCell: FC = (props) => { getCellLinks(field, row) || []}> {(api) => { return ( -
+
{value}
); @@ -53,49 +55,24 @@ export const DefaultCell: FC = (props) => { function getCellStyle( tableStyles: TableStyles, - field: Field, + cellOptions: TableCellOptions, displayValue: DisplayValue, disableOverflowOnHover = false ) { // How much to darken elements depends upon if we're in dark mode const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7; - // See if we're using deprecated settings - const usingDeprecatedSettings = field.config.custom?.displayMode !== undefined; - // Setup color variables let textColor: string | undefined = undefined; let bgColor: string | undefined = undefined; - // Set colors using deprecated settings format - if (usingDeprecatedSettings) { - if (field.config.custom?.displayMode === TableCellDisplayMode.ColorText) { - textColor = displayValue.color; - } else if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground) { + if (cellOptions.type === TableCellDisplayMode.ColorText) { + textColor = displayValue.color; + } else if (cellOptions.type === TableCellDisplayMode.ColorBackground) { + if (cellOptions.mode === TableCellBackgroundDisplayMode.Basic) { textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); bgColor = tinycolor(displayValue.color).toRgbString(); - } else if ( - field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground && - field.config.custom?.backgroundDisplayMode === TableCellBackgroundDisplayMode.Gradient - ) { - const bgColor2 = tinycolor(displayValue.color) - .darken(10 * darkeningFactor) - .spin(5); - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; - } - } - // Set colors using updated sub-options format - else { - const cellDisplayMode = field.config.custom?.cellOptions?.mode; - const cellDisplayType = field.config.custom?.cellOptions?.type; - - if (cellDisplayType === TableCellDisplayMode.ColorText) { - textColor = displayValue.color; - } else if (cellDisplayMode === TableCellBackgroundDisplayMode.Basic) { - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = tinycolor(displayValue.color).toRgbString(); - } else if (cellDisplayMode === TableCellBackgroundDisplayMode.Gradient) { + } else if (cellOptions.mode === TableCellBackgroundDisplayMode.Gradient) { const bgColor2 = tinycolor(displayValue.color) .darken(10 * darkeningFactor) .spin(5); @@ -113,8 +90,8 @@ function getCellStyle( return disableOverflowOnHover ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer; } -function getLinkStyle(tableStyles: TableStyles, field: Field, targetClassName: string | undefined) { - if (field.config.custom?.displayMode === TableCellDisplayMode.Auto) { +function getLinkStyle(tableStyles: TableStyles, cellOptions: TableCellOptions, targetClassName: string | undefined) { + if (cellOptions.type === TableCellDisplayMode.Auto) { return cx(tableStyles.cellLink, targetClassName); } diff --git a/packages/grafana-ui/src/components/Table/ImageCell.tsx b/packages/grafana-ui/src/components/Table/ImageCell.tsx index 6961be2fe22..c1e8b2ee2f1 100644 --- a/packages/grafana-ui/src/components/Table/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/ImageCell.tsx @@ -17,7 +17,7 @@ export const ImageCell: FC = (props) => {
{!hasLinks && } {hasLinks && ( - getCellLinks(field, row) || []}> + getCellLinks(field, row) || []}> {(api) => { return (
diff --git a/packages/grafana-ui/src/components/Table/utils.tsx b/packages/grafana-ui/src/components/Table/utils.tsx index edaa526e0b1..77b68af4b1b 100644 --- a/packages/grafana-ui/src/components/Table/utils.tsx +++ b/packages/grafana-ui/src/components/Table/utils.tsx @@ -16,6 +16,12 @@ import { GrafanaTheme2, ArrayVector, } from '@grafana/data'; +import { + BarGaugeDisplayMode, + TableAutoCellOptions, + TableCellBackgroundDisplayMode, + TableCellOptions, +} from '@grafana/schema'; import { BarGaugeCell } from './BarGaugeCell'; import { DefaultCell } from './DefaultCell'; @@ -344,3 +350,65 @@ export function createFooterCalculationValues(rows: Row[]): any[number] { return values; } + +const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto }; + +export function getCellOptions(field: Field): TableCellOptions { + if (field.config.custom?.displayMode) { + return migrateTableDisplayModeToCellOptions(field.config.custom?.displayMode); + } + + if (!field.config.custom?.cellOptions) { + return defaultCellOptions; + } + + return (field.config.custom as TableFieldOptions).cellOptions; +} + +/** + * Migrates table cell display mode to new object format. + * + * @param displayMode The display mode of the cell + * @returns TableCellOptions object in the correct format + * relative to the old display mode. + */ +export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDisplayMode): TableCellOptions { + switch (displayMode) { + // In the case of the gauge we move to a different option + case 'basic': + case 'gradient-gauge': + case 'lcd-gauge': + let gaugeMode = BarGaugeDisplayMode.Basic; + + if (displayMode === 'gradient-gauge') { + gaugeMode = BarGaugeDisplayMode.Gradient; + } else if (displayMode === 'lcd-gauge') { + gaugeMode = BarGaugeDisplayMode.Lcd; + } + + return { + type: TableCellDisplayMode.Gauge, + mode: gaugeMode, + }; + // Also true in the case of the color background + case 'color-background': + case 'color-background-solid': + let mode = TableCellBackgroundDisplayMode.Basic; + + // Set the new mode field, somewhat confusingly the + // color-background mode is for gradient display + if (displayMode === 'color-background') { + mode = TableCellBackgroundDisplayMode.Gradient; + } + + return { + type: TableCellDisplayMode.ColorBackground, + mode: mode, + }; + default: + return { + // @ts-ignore + type: displayMode, + }; + } +} diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index 239002b718b..b5e3a78cab7 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -58,6 +58,7 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme <> {React.cloneElement(children, { ref: setTriggerRef, + tabIndex: 0, // tooltip should be keyboard focusable })} {visible && ( diff --git a/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx b/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx index 12dbefd7d04..5e498152762 100644 --- a/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx +++ b/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx @@ -56,7 +56,7 @@ export const PlotLegend: React.FC = React.memo( fieldIndex, color: seriesColor, label, - yAxis: axisPlacement === AxisPlacement.Left ? 1 : 2, + yAxis: axisPlacement === AxisPlacement.Left || axisPlacement === AxisPlacement.Bottom ? 1 : 2, getDisplayValues: () => { if (!calcs?.length) { return []; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx index e83e65f778c..bbe8f896ffb 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx @@ -12,18 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; -import { createTheme } from '@grafana/data'; - -import { ViewRangeTime } from '../../TraceTimelineViewer/types'; -import DraggableManager, { DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager'; import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame'; -import GraphTicks from './GraphTicks'; -import Scrubber, { ScrubberProps } from './Scrubber'; -import ViewingLayer, { dragTypes, getStyles, ViewingLayerProps, UnthemedViewingLayer } from './ViewingLayer'; +import ViewingLayer, { ViewingLayerProps } from './ViewingLayer'; function getViewRange(viewStart: number, viewEnd: number) { return { @@ -33,11 +28,10 @@ function getViewRange(viewStart: number, viewEnd: number) { }; } -describe('', () => { +describe('', () => { polyfillAnimationFrame(window); let props: ViewingLayerProps; - let wrapper: ShallowWrapper; beforeEach(() => { props = { @@ -47,302 +41,74 @@ describe('', () => { updateViewRangeTime: jest.fn(), viewRange: getViewRange(0, 1), } as unknown as ViewingLayerProps; - - wrapper = shallow() - .dive() - .dive(); }); - describe('_getDraggingBounds()', () => { - beforeEach(() => { - props = { ...props, viewRange: getViewRange(0.1, 0.9) }; - wrapper = shallow() - .dive() - .dive(); - - wrapper.instance()._setRoot({ - getBoundingClientRect() { - return { left: 10, width: 100 }; - }, - } as SVGElement); - }); - - it('throws if _root is not set', () => { - const instance = wrapper.instance(); - instance._root = null; - expect(() => instance._getDraggingBounds(dragTypes.REFRAME)).toThrow(); - }); - - it('returns the correct bounds for reframe', () => { - const bounds = wrapper.instance()._getDraggingBounds(dragTypes.REFRAME); - expect(bounds).toEqual({ - clientXLeft: 10, - width: 100, - maxValue: 1, - minValue: 0, - }); - }); - - it('returns the correct bounds for shiftStart', () => { - const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_START); - expect(bounds).toEqual({ - clientXLeft: 10, - width: 100, - maxValue: 0.9, - minValue: 0, - }); - }); - - it('returns the correct bounds for shiftEnd', () => { - const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_END); - expect(bounds).toEqual({ - clientXLeft: 10, - width: 100, - maxValue: 1, - minValue: 0.1, - }); - }); + it('does not render ViewingLayerCursorGuide if the cursor position is not defined', () => { + render(); + expect(screen.queryByTestId('ViewingLayerCursorGuide')).toBeNull(); }); - describe('DraggableManager callbacks', () => { - describe('reframe', () => { - it('handles mousemove', () => { - const value = 0.5; - wrapper.instance()._handleReframeMouseMove({ value } as DraggingUpdate); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ cursor: value }]]); - }); - - it('handles mouseleave', () => { - wrapper.instance()._handleReframeMouseLeave(); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ cursor: null }]]); - }); - - describe('drag update', () => { - it('handles sans anchor', () => { - const value = 0.5; - wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ reframe: { anchor: value, shift: value } }]]); - }); - - it('handles the existing anchor', () => { - const value = 0.5; - const anchor = 0.1; - const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; - props = { ...props, viewRange: { time } }; - wrapper = shallow() - .dive() - .dive(); - wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]); - }); - }); - - describe('drag end', () => { - let manager: DraggableManager; - - beforeEach(() => { - manager = { resetBounds: jest.fn() } as unknown as DraggableManager; - }); - - it('handles sans anchor', () => { - const value = 0.5; - wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; - expect(calls).toEqual([[value, value, 'minimap']]); - }); - - it('handles dragged left (anchor is greater)', () => { - const value = 0.5; - const anchor = 0.6; - const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; - props = { ...props, viewRange: { time } }; - wrapper = shallow() - .dive() - .dive(); - wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; - expect(calls).toEqual([[value, anchor, 'minimap']]); - }); - - it('handles dragged right (anchor is less)', () => { - const value = 0.5; - const anchor = 0.4; - const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; - props = { ...props, viewRange: { time } }; - wrapper = shallow() - .dive() - .dive(); - wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; - expect(calls).toEqual([[anchor, value, 'minimap']]); - }); - }); - }); - - describe('scrubber', () => { - it('prevents the cursor from being drawn on scrubber mouseover', () => { - wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter } as DraggingUpdate); - expect(wrapper.state('preventCursorLine')).toBe(true); - }); - - it('prevents the cursor from being drawn on scrubber mouseleave', () => { - wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave } as DraggingUpdate); - expect(wrapper.state('preventCursorLine')).toBe(false); - }); - - describe('drag start and update', () => { - it('stops propagation on drag start', () => { - const stopPropagation = jest.fn(); - const update = { - event: { stopPropagation }, - type: EUpdateTypes.DragStart, - }; - wrapper.instance()._handleScrubberDragUpdate(update as unknown as DraggingUpdate); - expect(stopPropagation.mock.calls).toEqual([[]]); - }); - - it('updates the viewRange for shiftStart and shiftEnd', () => { - const instance = wrapper.instance(); - const value = 0.5; - const cases = [ - { - dragUpdate: { - value, - tag: dragTypes.SHIFT_START, - type: EUpdateTypes.DragMove, - }, - viewRangeUpdate: { shiftStart: value }, - }, - { - dragUpdate: { - value, - tag: dragTypes.SHIFT_END, - type: EUpdateTypes.DragMove, - }, - viewRangeUpdate: { shiftEnd: value }, - }, - ]; - cases.forEach((_case) => { - instance._handleScrubberDragUpdate(_case.dragUpdate as DraggingUpdate); - expect(props.updateNextViewRangeTime).lastCalledWith(_case.viewRangeUpdate); - }); - }); - }); - - it('updates the view on drag end', () => { - const instance = wrapper.instance(); - const [viewStart, viewEnd] = props.viewRange.time.current; - const value = 0.5; - const cases = [ - { - dragUpdate: { - value, - manager: { resetBounds: jest.fn() }, - tag: dragTypes.SHIFT_START, - }, - viewRangeUpdate: [value, viewEnd], - }, - { - dragUpdate: { - value, - manager: { resetBounds: jest.fn() }, - tag: dragTypes.SHIFT_END, - }, - viewRangeUpdate: [viewStart, value], - }, - ]; - cases.forEach((_case) => { - const { manager } = _case.dragUpdate; - wrapper.setState({ preventCursorLine: true }); - expect(wrapper.state('preventCursorLine')).toBe(true); - instance._handleScrubberDragEnd(_case.dragUpdate as unknown as DraggingUpdate); - expect(wrapper.state('preventCursorLine')).toBe(false); - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate, 'minimap'); - }); - }); - }); - - describe('.ViewingLayer--resetZoom', () => { - it('should not render .ViewingLayer--resetZoom if props.viewRange.time.current = [0,1]', () => { - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - wrapper.setProps({ viewRange: { time: { current: [0, 1] } } }); - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - }); - - it('should render ViewingLayer--resetZoom if props.viewRange.time.current[0] !== 0', () => { - // If the test fails on the following expect statement, this may be a false negative - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - wrapper.setProps({ viewRange: { time: { current: [0.1, 1] } } }); - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(1); - }); - - it('should render ViewingLayer--resetZoom if props.viewRange.time.current[1] !== 1', () => { - // If the test fails on the following expect statement, this may be a false negative - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - wrapper.setProps({ viewRange: { time: { current: [0, 0.9] } } }); - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(1); - }); - - it('should call props.updateViewRangeTime when clicked', () => { - wrapper.setProps({ viewRange: { time: { current: [0.1, 0.9] } } }); - const resetZoomButton = wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`); - // If the test fails on the following expect statement, this may be a false negative caused - // by a regression to rendering. - expect(resetZoomButton.length).toBe(1); - - resetZoomButton.simulate('click'); - expect(props.updateViewRangeTime).lastCalledWith(0, 1); - }); - }); + it('renders ViewingLayerCursorGuide when the cursor position is defined', () => { + props = { ...props, viewRange: { time: { current: [0.1, 1], cursor: 0.5 } } }; + render(); + expect(screen.getByTestId('ViewingLayerCursorGuide')).toBeTruthy(); }); - it('renders a ', () => { - expect(wrapper.find(GraphTicks).length).toBe(1); + it('renders ', () => { + render(); + expect(screen.getByTestId('ticks')).toBeTruthy(); + }); + + it('renders the scrubber component lines in the correct locations when an area of the minimap is selected', () => { + props = { ...props, viewRange: { time: { current: [0.3, 0.7] } } }; + render(); + expect(screen.getAllByTestId('scrubber-component-line')[0]).toHaveAttribute('x1', '30%'); + expect(screen.getAllByTestId('scrubber-component-line')[1]).toHaveAttribute('x1', '70%'); + }); + + it('renders the scrubbers', () => { + render(); + expect(screen.getAllByTestId('scrubber-component')).toBeTruthy(); }); it('renders a filtering box if leftBound exists', () => { - const _props = { ...props, viewRange: getViewRange(0.2, 1) }; - wrapper = shallow() - .dive() - .dive(); - - const leftBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`); - expect(leftBox.length).toBe(1); - const width = Number(leftBox.prop('width')?.toString().slice(0, -1)); - const x = leftBox.prop('x'); - expect(Math.round(width)).toBe(20); - expect(x).toBe(0); + props = { ...props, viewRange: { time: { current: [0.1, 0.9] } } }; + render(); + expect(screen.getByTestId('left-ViewingLayerInactive')).toHaveAttribute('width', '10%'); + expect(screen.getByTestId('left-ViewingLayerInactive')).toHaveAttribute('x', '0'); }); it('renders a filtering box if rightBound exists', () => { - const _props = { ...props, viewRange: getViewRange(0, 0.8) }; - wrapper = shallow() - .dive() - .dive(); - - const rightBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`); - expect(rightBox.length).toBe(1); - const width = Number(rightBox.prop('width')?.toString().slice(0, -1)); - const x = Number(rightBox.prop('x')?.toString().slice(0, -1)); - expect(Math.round(width)).toBe(20); - expect(x).toBe(80); + props = { ...props, viewRange: { time: { current: [0, 0.8] } } }; + render(); + expect(screen.getByTestId('right-ViewingLayerInactive')).toHaveAttribute('width', '20%'); + expect(screen.getByTestId('right-ViewingLayerInactive')).toHaveAttribute('x', '80%'); }); - it('renders handles for the timeRangeFilter', () => { - const [viewStart, viewEnd] = props.viewRange.time.current; - let scrubber = ; - expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); - scrubber = ; - expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); + describe('reset selection button', () => { + it('should not render the reset selection button if props.viewRange.time.current = [0,1]', () => { + render(); + expect(screen.queryByRole('button', { hidden: true })).toBeNull(); + }); + + it('should render the reset selection button if props.viewRange.time.current[0] !== 0', () => { + props = { ...props, viewRange: { time: { current: [0.1, 1] } } }; + render(); + expect(screen.queryByRole('button', { hidden: true })).toBeInTheDocument(); + }); + + it('should render the reset selection button if props.viewRange.time.current[1] !== 1', () => { + props = { ...props, viewRange: { time: { current: [0, 0.9] } } }; + render(); + expect(screen.queryByRole('button', { hidden: true })).toBeInTheDocument(); + }); + + it('should call props.updateViewRangeTime when clicked', async () => { + props = { ...props, viewRange: { time: { current: [0.1, 0.9] } } }; + render(); + const button = screen.queryByRole('button', { hidden: true })!; + await userEvent.click(button); + expect(props.updateViewRangeTime).toHaveBeenCalledWith(0, 1); + }); }); }); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx index 17025ba4a65..eb2277c1b3d 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx @@ -364,7 +364,14 @@ export class UnthemedViewingLayer extends React.PureComponent {leftInactive > 0 && ( - + )} {rightInactive > 0 && ( )} @@ -384,6 +392,7 @@ export class UnthemedViewingLayer extends React.PureComponent )} {shiftStart != null && this._getMarkers(viewStart, shiftStart)} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx index 84a4a3c6c82..186e0d2b074 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx @@ -17,6 +17,7 @@ import cx from 'classnames'; import React, { memo, Dispatch, SetStateAction } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { Button, useStyles2 } from '@grafana/ui'; import UiFindInput from '../common/UiFindInput'; @@ -29,12 +30,11 @@ export const getStyles = (theme: GrafanaTheme2) => { TracePageSearchBar: css` label: TracePageSearchBar; float: right; - position: sticky; - top: 8px; + position: absolute; + top: 0; right: 0; z-index: ${theme.zIndex.navbarFixed}; background: ${theme.colors.background.primary}; - margin-top: 8px; margin-bottom: -48px; padding: 8px; margin-right: 2px; @@ -78,6 +78,7 @@ export type TracePageSearchBarProps = { focusedSpanIdForSearch: string; setSearchBarSuffix: Dispatch>; setFocusedSpanIdForSearch: Dispatch>; + datasourceType: string; }; export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) { @@ -90,6 +91,7 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) focusedSpanIdForSearch, setSearchBarSuffix, setFocusedSpanIdForSearch, + datasourceType, } = props; const styles = useStyles2(getStyles); @@ -113,6 +115,11 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) }; const nextResult = () => { + reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { + datasourceType: datasourceType, + direction: 'next', + }); + const spanMatches = Array.from(spanFindMatches!); const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) ? spanMatches.indexOf(focusedSpanIdForSearch) @@ -131,6 +138,11 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) }; const prevResult = () => { + reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { + datasourceType: datasourceType, + direction: 'prev', + }); + const spanMatches = Array.from(spanFindMatches!); const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) ? spanMatches.indexOf(focusedSpanIdForSearch) diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx index f90c2c1fd52..654ac191b18 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx @@ -12,34 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { cx } from '@emotion/css'; -import { mount, ReactWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import React from 'react'; -import DraggableManager, { DraggingUpdate } from 'src/utils/DraggableManager'; import { ViewRangeTime } from '../types'; -import TimelineViewingLayer, { getStyles, TimelineViewingLayerProps } from './TimelineViewingLayer'; - -function mapFromSubRange(viewStart: number, viewEnd: number, value: number) { - return viewStart + value * (viewEnd - viewStart); -} - -// Remove the prefix generated by cx (css-), i.e. change the string from something like: -// 'css-7g92us-TimelineViewingLayerDragged-Timeline...' -// to: -// 'TimelineViewingLayerDragged-Timeline...' -function removeCxPrefix(cxString: string) { - return cxString.split('-').slice(2).join('-'); -} +import TimelineViewingLayer, { TimelineViewingLayerProps } from './TimelineViewingLayer'; describe('', () => { - let wrapper: ReactWrapper; - let instance: TimelineViewingLayer; - const viewStart = 0.25; const viewEnd = 0.9; - const props = { + + let props: TimelineViewingLayerProps = { boundsInvalidator: Math.random(), updateNextViewRangeTime: jest.fn(), updateViewRangeTime: jest.fn(), @@ -48,184 +32,49 @@ describe('', () => { }, }; - beforeEach(() => { - props.updateNextViewRangeTime.mockReset(); - props.updateViewRangeTime.mockReset(); - wrapper = mount(); - instance = wrapper.instance(); - }); - it('renders without exploding', () => { - expect(wrapper).toBeDefined(); - expect(wrapper.find('[data-testid="TimelineViewingLayer"]').length).toBe(1); - }); - - it('sets _root to the root DOM node', () => { - expect(instance._root).toBeDefined(); - expect(wrapper.find('[data-testid="TimelineViewingLayer"]').getDOMNode()).toBe(instance._root); - }); - - describe('uses DraggableManager', () => { - it('initializes the DraggableManager', () => { - const dm = instance._draggerReframe; - expect(dm).toBeDefined(); - expect(dm._onMouseMove).toBe(instance._handleReframeMouseMove); - expect(dm._onMouseLeave).toBe(instance._handleReframeMouseLeave); - expect(dm._onDragStart).toBe(instance._handleReframeDragUpdate); - expect(dm._onDragMove).toBe(instance._handleReframeDragUpdate); - expect(dm._onDragEnd).toBe(instance._handleReframeDragEnd); - }); - - it('provides the DraggableManager handlers as callbacks', () => { - const { handleMouseDown, handleMouseLeave, handleMouseMove } = instance._draggerReframe; - const rootWrapper = wrapper.find('[data-testid="TimelineViewingLayer"]'); - expect(rootWrapper.prop('onMouseDown')).toBe(handleMouseDown); - expect(rootWrapper.prop('onMouseLeave')).toBe(handleMouseLeave); - expect(rootWrapper.prop('onMouseMove')).toBe(handleMouseMove); - }); - - it('returns the dragging bounds from _getDraggingBounds()', () => { - const left = 10; - const width = 100; - instance._root!.getBoundingClientRect = () => ({ left, width } as DOMRect); - expect(instance._getDraggingBounds()).toEqual({ width, clientXLeft: left }); - }); - - it('updates viewRange.time.cursor via _draggerReframe._onMouseMove', () => { - const value = 0.5; - const cursor = mapFromSubRange(viewStart, viewEnd, value); - instance._draggerReframe._onMouseMove!({ value } as DraggingUpdate); - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[{ cursor }]]); - }); - - it('resets viewRange.time.cursor via _draggerReframe._onMouseLeave', () => { - instance._draggerReframe._onMouseLeave!(null as unknown as DraggingUpdate); - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[{ cursor: undefined }]]); - }); - - it('handles drag start via _draggerReframe._onDragStart', () => { - const value = 0.5; - const shift = mapFromSubRange(viewStart, viewEnd, value); - const update = { reframe: { shift, anchor: shift } }; - instance._draggerReframe._onDragStart!({ value } as DraggingUpdate); - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[update]]); - }); - - it('handles drag move via _draggerReframe._onDragMove', () => { - const anchor = 0.25; - const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } } as ViewRangeTime; - const value = 0.5; - const shift = mapFromSubRange(viewStart, viewEnd, value); - // make sure `anchor` is already present on the props - wrapper.setProps({ viewRangeTime }); - expect(wrapper.prop('viewRangeTime').reframe?.anchor).toBe(anchor); - // the next update should integrate `value` and use the existing anchor - instance._draggerReframe._onDragStart!({ value } as DraggingUpdate); - const update = { reframe: { anchor, shift } }; - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[update]]); - }); - - it('handles drag end via _draggerReframe._onDragEnd', () => { - const manager = { resetBounds: jest.fn() } as unknown as DraggableManager; - const value = 0.5; - const shift = mapFromSubRange(viewStart, viewEnd, value); - const anchor = 0.25; - const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } } as ViewRangeTime; - wrapper.setProps({ viewRangeTime }); - instance._draggerReframe._onDragEnd!({ manager, value } as DraggingUpdate); - expect(jest.mocked(manager.resetBounds).mock.calls).toEqual([[]]); - expect(props.updateViewRangeTime.mock.calls).toEqual([[anchor, shift, 'timeline-header']]); - }); + render(); + expect(screen.getByTestId('TimelineViewingLayer')).toBeTruthy(); }); describe('render()', () => { it('renders nothing without a nextViewRangeTime', () => { - expect(wrapper.find('div').length).toBe(1); - }); - - it('renders the cursor when it is the only non-current value set', () => { - const cursor = viewStart + 0.5 * (viewEnd - viewStart); - const baseViewRangeTime = { ...props.viewRangeTime, cursor }; - wrapper.setProps({ viewRangeTime: baseViewRangeTime }); - // cursor is rendered when solo - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(1); - // cursor is skipped when shiftStart, shiftEnd, or reframe are present - let viewRangeTime: ViewRangeTime = { - ...baseViewRangeTime, - shiftStart: cursor, - shiftEnd: cursor, - reframe: { anchor: cursor, shift: cursor }, - }; - wrapper.setProps({ viewRangeTime }); - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(0); - viewRangeTime = { ...baseViewRangeTime, shiftEnd: cursor }; - wrapper.setProps({ viewRangeTime }); - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(0); - viewRangeTime = { ...baseViewRangeTime, reframe: { anchor: cursor, shift: cursor } }; - wrapper.setProps({ viewRangeTime }); - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(0); - }); - - it('renders the reframe dragging', () => { - const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor: viewStart, shift: viewEnd } }; - wrapper.setProps({ viewRangeTime }); - const styles = getStyles(); - expect( - wrapper - .find('[data-testid="Dragged"]') - .prop('className')! - .indexOf( - removeCxPrefix( - cx( - styles.TimelineViewingLayerDragged, - styles.TimelineViewingLayerDraggedDraggingLeft, - styles.TimelineViewingLayerDraggedDraggingRight, - styles.TimelineViewingLayerDraggedReframeDrag - ) - ) - ) >= 0 - ).toBe(true); - }); - - it('renders the shiftStart dragging', () => { - const viewRangeTime = { ...props.viewRangeTime, shiftStart: viewEnd }; - wrapper.setProps({ viewRangeTime }); - const styles = getStyles(); - expect( - wrapper - .find('[data-testid="Dragged"]') - .prop('className')! - .indexOf( - removeCxPrefix( - cx( - styles.TimelineViewingLayerDragged, - styles.TimelineViewingLayerDraggedDraggingLeft, - styles.TimelineViewingLayerDraggedDraggingRight, - styles.TimelineViewingLayerDraggedShiftDrag - ) - ) - ) >= 0 - ).toBe(true); - }); - - it('renders the shiftEnd dragging', () => { - const viewRangeTime = { ...props.viewRangeTime, shiftEnd: viewStart }; - wrapper.setProps({ viewRangeTime }); - const styles = getStyles(); - expect( - wrapper - .find('[data-testid="Dragged"]') - .prop('className')! - .indexOf( - removeCxPrefix( - cx( - styles.TimelineViewingLayerDragged, - styles.TimelineViewingLayerDraggedDraggingLeft, - styles.TimelineViewingLayerDraggedShiftDrag - ) - ) - ) >= 0 - ).toBe(true); + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); }); }); + + it('renders the cursor when it is the only non-current value set', () => { + const cursor = viewStart + 0.5 * (viewEnd - viewStart); + const baseViewRangeTime = { ...props.viewRangeTime, cursor }; + props = { ...props, viewRangeTime: baseViewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).toBeInTheDocument(); + }); + + it('does not render the cursor when shiftStart, shiftEnd, or reframe are present', () => { + const cursor = viewStart + 0.5 * (viewEnd - viewStart); + const baseViewRangeTime = { ...props.viewRangeTime, cursor }; + + let viewRangeTime: ViewRangeTime = { + ...baseViewRangeTime, + shiftStart: cursor, + shiftEnd: cursor, + reframe: { anchor: cursor, shift: cursor }, + }; + + props = { ...props, viewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); + + viewRangeTime = { ...baseViewRangeTime, shiftEnd: cursor }; + props = { ...props, viewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); + + viewRangeTime = { ...baseViewRangeTime, reframe: { anchor: cursor, shift: cursor } }; + props = { ...props, viewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); + }); }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx index f36bcbc03dc..b789298dae8 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -19,6 +19,7 @@ import * as React from 'react'; import { createRef, RefObject } from 'react'; import { GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui'; import { Accessors } from '../ScrollManager'; @@ -536,8 +537,13 @@ export class UnthemedVirtualizedTraceView extends React.Component { - const { topOfViewRef } = this.props; + const { topOfViewRef, datasourceType, trace } = this.props; topOfViewRef?.current?.scrollIntoView({ behavior: 'smooth' }); + reportInteraction('grafana_traces_trace_view_scroll_to_top_clicked', { + datasourceType: datasourceType, + numServices: trace.services.length, + numSpans: trace.spans.length, + }); }; render() { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx index fcae2626799..c27231564e4 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx @@ -216,6 +216,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent
); diff --git a/pkg/api/admin.go b/pkg/api/admin.go index 21603822519..347aa45c20d 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -45,7 +46,7 @@ func (hs *HTTPServer) AdminGetSettings(c *models.ReqContext) response.Response { // 403: forbiddenError // 500: internalServerError func (hs *HTTPServer) AdminGetStats(c *models.ReqContext) response.Response { - statsQuery := models.GetAdminStatsQuery{} + statsQuery := stats.GetAdminStatsQuery{} if err := hs.statsService.GetAdminStats(c.Req.Context(), &statsQuery); err != nil { return response.Error(500, "Failed to get admin stats from database", err) @@ -108,5 +109,5 @@ type GetSettingsResponse struct { // swagger:response adminGetStatsResponse type GetStatsResponse struct { // in:body - Body models.AdminStats `json:"body"` + Body stats.AdminStats `json:"body"` } diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 285d2005a9c..de7a4955591 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -645,7 +646,7 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response { } if err := hs.AlertNotificationService.HandleNotificationTestCommand(c.Req.Context(), cmd); err != nil { - if errors.Is(err, models.ErrSmtpNotEnabled) { + if errors.Is(err, notifications.ErrSmtpNotEnabled) { return response.Error(412, err.Error(), err) } var alertingErr alerting.ValidationError diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index b86508acdc6..d48132b769b 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -50,7 +50,7 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { // When dashboard UID present in the request, we ignore dashboard ID if query.DashboardUid != "" { - dq := models.GetDashboardQuery{Uid: query.DashboardUid, OrgId: c.OrgID} + dq := dashboards.GetDashboardQuery{UID: query.DashboardUid, OrgID: c.OrgID} err := hs.DashboardService.GetDashboard(c.Req.Context(), &dq) if err != nil { if hs.Features.IsEnabled(featuremgmt.FlagDashboardsFromStorage) { @@ -59,7 +59,7 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "Invalid dashboard UID in annotation request", err) } } else { - query.DashboardId = dq.Result.Id + query.DashboardId = dq.Result.ID } } @@ -79,11 +79,11 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { if val, ok := dashboardCache[item.DashboardId]; ok { item.DashboardUID = val } else { - query := models.GetDashboardQuery{Id: item.DashboardId, OrgId: c.OrgID} + query := dashboards.GetDashboardQuery{ID: item.DashboardId, OrgID: c.OrgID} err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil && query.Result != nil { - item.DashboardUID = &query.Result.Uid - dashboardCache[item.DashboardId] = &query.Result.Uid + item.DashboardUID = &query.Result.UID + dashboardCache[item.DashboardId] = &query.Result.UID } } } @@ -122,10 +122,10 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { // overwrite dashboardId when dashboardUID is not empty if cmd.DashboardUID != "" { - query := models.GetDashboardQuery{OrgId: c.OrgID, Uid: cmd.DashboardUID} + query := dashboards.GetDashboardQuery{OrgID: c.OrgID, UID: cmd.DashboardUID} err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { - cmd.DashboardId = query.Result.Id + cmd.DashboardId = query.Result.ID } } @@ -379,10 +379,10 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo } if cmd.DashboardUID != "" { - query := models.GetDashboardQuery{OrgId: c.OrgID, Uid: cmd.DashboardUID} + query := dashboards.GetDashboardQuery{OrgID: c.OrgID, UID: cmd.DashboardUID} err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { - cmd.DashboardId = query.Result.Id + cmd.DashboardId = query.Result.ID } } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 7de7e945eba..b85bab9418e 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -227,11 +227,11 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { t.Run("Should be able to do anything", func(t *testing.T) { dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil) postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, dashSvc, func(sc *scenarioContext) { @@ -244,7 +244,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) - dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")) + dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")) }) putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { @@ -267,11 +267,11 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { }) dashSvc = dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: 1, - Uid: deleteWithDashboardUIDCmd.DashboardUID, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: 1, + UID: deleteWithDashboardUIDCmd.DashboardUID, } }).Return(nil) deleteAnnotationsScenario(t, "When calling POST with dashboardUID on", "/api/annotations/mass-delete", @@ -279,7 +279,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) - dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")) + dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")) }) }) }) @@ -687,11 +687,11 @@ func setUpACL() { {Role: &editorRole, Permission: models.PERMISSION_EDIT}, } }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 1dd8e021ece..addac788e9e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -109,7 +109,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { // If public dashboards is enabled and we have a public dashboard, update meta // values if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { - publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, dash.Uid) + publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, dash.UID) if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) { return response.Error(500, "Error while retrieving public dashboards", err) } @@ -146,7 +146,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { canAdmin, _ := guardian.CanAdmin() canDelete, _ := guardian.CanDelete() - isStarred, err := hs.isDashboardStarredByUser(c, dash.Id) + isStarred, err := hs.isDashboardStarredByUser(c, dash.ID) if err != nil { return response.Error(500, "Error while checking if dashboard was starred by user", err) } @@ -182,8 +182,8 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { Version: dash.Version, HasACL: dash.HasACL, IsFolder: dash.IsFolder, - FolderId: dash.FolderId, - Url: dash.GetUrl(), + FolderId: dash.FolderID, + Url: dash.GetURL(), FolderTitle: "General", AnnotationsPermissions: annotationPermissions, PublicDashboardEnabled: publicDashboardEnabled, @@ -191,20 +191,20 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { } // lookup folder title - if dash.FolderId > 0 { - query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgID} + if dash.FolderID > 0 { + query := dashboards.GetDashboardQuery{ID: dash.FolderID, OrgID: c.OrgID} if err := hs.DashboardService.GetDashboard(c.Req.Context(), &query); err != nil { if errors.Is(err, dashboards.ErrFolderNotFound) { return response.Error(404, "Folder not found", err) } return response.Error(500, "Dashboard folder could not be read", err) } - meta.FolderUid = query.Result.Uid + meta.FolderUid = query.Result.UID meta.FolderTitle = query.Result.Title - meta.FolderUrl = query.Result.GetUrl() + meta.FolderUrl = query.Result.GetURL() } - provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.Id) + provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.ID) if err != nil { return response.Error(500, "Error while checking if dashboard is provisioned", err) } @@ -275,13 +275,13 @@ func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string { return user.Login } -func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*models.Dashboard, response.Response) { - var query models.GetDashboardQuery +func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*dashboards.Dashboard, response.Response) { + var query dashboards.GetDashboardQuery if len(uid) > 0 { - query = models.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID} + query = dashboards.GetDashboardQuery{UID: uid, ID: id, OrgID: orgID} } else { - query = models.GetDashboardQuery{Id: id, OrgId: orgID} + query = dashboards.GetDashboardQuery{ID: id, OrgID: orgID} } if err := hs.DashboardService.GetDashboard(ctx, &query); err != nil { @@ -322,12 +322,12 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { } // disconnect all library elements for this dashboard - err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.Id) + err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID) if err != nil { - hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserID, "error", err) + hs.log.Error("Failed to disconnect library elements", "dashboard", dash.ID, "user", c.SignedInUser.UserID, "error", err) } - err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgID) + err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, c.OrgID) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok { @@ -339,15 +339,15 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { } if hs.Live != nil { - err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.OrgID, c.ToUserDisplayDTO(), dash.Uid) + err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.OrgID, c.ToUserDisplayDTO(), dash.UID) if err != nil { - hs.log.Error("Failed to broadcast delete info", "dashboard", dash.Uid, "error", err) + hs.log.Error("Failed to broadcast delete info", "dashboard", dash.UID, "error", err) } } return response.JSON(http.StatusOK, util.DynMap{ "title": dash.Title, "message": fmt.Sprintf("Dashboard %s deleted", dash.Title), - "id": dash.Id, + "id": dash.ID, }) } @@ -367,7 +367,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { // 422: unprocessableEntityError // 500: internalServerError func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { - cmd := models.SaveDashboardCommand{} + cmd := dashboards.SaveDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } @@ -397,15 +397,15 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { return hs.postDashboard(c, cmd) } -func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboardCommand) response.Response { +func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response { ctx := c.Req.Context() var err error - cmd.OrgId = c.OrgID - cmd.UserId = c.UserID - if cmd.FolderUid != "" { + cmd.OrgID = c.OrgID + cmd.UserID = c.UserID + if cmd.FolderUID != "" { folder, err := hs.folderService.Get(ctx, &folder.GetFolderQuery{ OrgID: c.OrgID, - UID: &cmd.FolderUid, + UID: &cmd.FolderUID, SignedInUser: c.SignedInUser, }) if err != nil { @@ -414,11 +414,11 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } return response.Error(500, "Error while checking folder ID", err) } - cmd.FolderId = folder.ID + cmd.FolderID = folder.ID } dash := cmd.GetDashboardModel() - newDashboard := dash.Id == 0 + newDashboard := dash.ID == 0 if newDashboard { limitReached, err := hs.QuotaService.QuotaReached(c, dashboards.QuotaTargetSrv) if err != nil { @@ -430,14 +430,14 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } var provisioningData *models.DashboardProvisioning - if dash.Id != 0 { - data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.Id) + if dash.ID != 0 { + data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), 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 := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(c.Req.Context(), dash.OrgId, dash.Uid) + } else if dash.UID != "" { + data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(c.Req.Context(), dash.OrgID, dash.UID) if err != nil && !errors.Is(err, dashboards.ErrProvisionedDashboardNotFound) && !errors.Is(err, dashboards.ErrDashboardNotFound) { return response.Error(500, "Error while checking if dashboard is provisioned", err) } @@ -452,7 +452,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: cmd.Message, - OrgId: c.OrgID, + OrgID: c.OrgID, User: c.SignedInUser, Overwrite: cmd.Overwrite, } @@ -479,7 +479,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } if liveerr != nil { - hs.log.Warn("unable to broadcast save event", "uid", dashboard.Uid, "error", liveerr) + hs.log.Warn("unable to broadcast save event", "uid", dashboard.UID, "error", liveerr) } } @@ -504,9 +504,9 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa "status": "success", "slug": dashboard.Slug, "version": dashboard.Version, - "id": dashboard.Id, - "uid": dashboard.Uid, - "url": dashboard.GetUrl(), + "id": dashboard.ID, + "uid": dashboard.UID, + "url": dashboard.GetURL(), }) } @@ -533,10 +533,10 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { } if preference.HomeDashboardID != 0 { - slugQuery := models.GetDashboardRefByIdQuery{Id: preference.HomeDashboardID} - err := hs.DashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery) + slugQuery := dashboards.GetDashboardRefByIDQuery{ID: preference.HomeDashboardID} + err := hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), &slugQuery) if err == nil { - url := models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) + url := models.GetDashboardUrl(slugQuery.Result.UID, slugQuery.Result.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return response.JSON(http.StatusOK, &dashRedirect) } @@ -655,15 +655,15 @@ func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Respon query := dashver.ListDashboardVersionsQuery{ OrgID: c.OrgID, - DashboardID: dash.Id, - DashboardUID: dash.Uid, + DashboardID: dash.ID, + DashboardUID: dash.UID, Limit: c.QueryInt("limit"), Start: c.QueryInt("start"), } res, err := hs.dashboardVersionService.List(c.Req.Context(), &query) if err != nil { - return response.Error(404, fmt.Sprintf("No versions found for dashboardId %d", dash.Id), err) + return response.Error(404, fmt.Sprintf("No versions found for dashboardId %d", dash.ID), err) } for _, version := range res { @@ -716,7 +716,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons var err error dashUID := web.Params(c.Req)[":uid"] - var dash *models.Dashboard + var dash *dashboards.Dashboard if dashUID == "" { dashID, err = strconv.ParseInt(web.Params(c.Req)[":dashboardId"], 10, 64) if err != nil { @@ -741,13 +741,13 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons version, _ := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 32) query := dashver.GetDashboardVersionQuery{ OrgID: c.OrgID, - DashboardID: dash.Id, + DashboardID: dash.ID, Version: int(version), } res, err := hs.dashboardVersionService.Get(c.Req.Context(), &query) if err != nil { - return response.Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dash.Id), err) + return response.Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dash.ID), err) } creator := anonString @@ -996,15 +996,15 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Res return response.Error(404, "Dashboard version not found", nil) } - saveCmd := models.SaveDashboardCommand{} + saveCmd := dashboards.SaveDashboardCommand{} saveCmd.RestoredFrom = version.Version - saveCmd.OrgId = c.OrgID - saveCmd.UserId = c.UserID + saveCmd.OrgID = c.OrgID + saveCmd.UserID = c.UserID saveCmd.Dashboard = version.Data saveCmd.Dashboard.Set("version", dash.Version) - saveCmd.Dashboard.Set("uid", dash.Uid) + saveCmd.Dashboard.Set("uid", dash.UID) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) - saveCmd.FolderId = dash.FolderId + saveCmd.FolderID = dash.FolderID return hs.postDashboard(c, saveCmd) } @@ -1033,18 +1033,18 @@ func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) { ids := strings.Split(web.Params(c.Req)[":ids"], ",") uids := make([]string, 0, len(ids)) - q := &models.GetDashboardRefByIdQuery{} + q := &dashboards.GetDashboardRefByIDQuery{} for _, idstr := range ids { id, err := strconv.ParseInt(idstr, 10, 64) if err != nil { continue } - q.Id = id - err = hs.DashboardService.GetDashboardUIDById(c.Req.Context(), q) + q.ID = id + err = hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), q) if err != nil { continue } - uids = append(uids, q.Result.Uid) + uids = append(uids, q.Result.UID) } c.JSON(http.StatusOK, uids) } diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 7307bc940aa..34a0ef066f3 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -192,7 +192,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response. if err != nil { return response.Error(500, "Error while checking dashboard permissions", err) } - if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgId, dash.Uid, false, items, old); err != nil { + if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgID, dash.UID, false, items, old); err != nil { return response.Error(500, "Failed to update permissions", err) } return response.Success("Dashboard permissions updated") diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 3b0ec28cb8a..ac250c18bfa 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -27,11 +27,11 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { t.Run("Dashboard permissions test", func(t *testing.T) { settings := setting.NewCfg() dashboardStore := &dashboards.FakeDashboardStore{} - dashboardStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashboardStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil, nil) defer dashboardStore.AssertExpectations(t) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index a5c115401e8..72fb6199834 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -74,11 +74,11 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { teamSvc := &teamtest.FakeService{} dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil).Maybe() dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardACLInfoListQuery")).Return(nil).Maybe() @@ -136,11 +136,11 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { externalRequest = req }) dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - OrgId: q.OrgId, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + OrgID: q.OrgID, } }).Return(nil).Maybe() dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index baeaa8b222d..d3e29ae8c5f 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -63,7 +64,7 @@ func TestGetHomeDashboard(t *testing.T) { hs := &HTTPServer{ Cfg: cfg, pluginStore: &plugins.FakePluginStore{}, - SQLStore: mockstore.NewSQLStoreMock(), + SQLStore: dbtest.NewFakeDB(), preferenceService: prefService, dashboardVersionService: dashboardVersionService, Kinds: corekind.NewBase(nil), @@ -128,19 +129,19 @@ func newTestLive(t *testing.T, store db.DB) *live.GrafanaLive { func TestDashboardAPIEndpoint(t *testing.T) { t.Run("Given a dashboard with a parent folder which does not have an ACL", func(t *testing.T) { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 1 + fakeDash.FolderID = 1 fakeDash.HasACL = false fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake() fakeDashboardVersionService.ExpectedDashboardVersion = &dashver.DashboardVersionDTO{} teamService := &teamtest.FakeService{} dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) q.Result = fakeDash }).Return(nil) - mockSQLStore := mockstore.NewSQLStoreMock() + mockSQLStore := dbtest.NewFakeDB() hs := &HTTPServer{ Cfg: setting.NewCfg(), @@ -236,16 +237,16 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) t.Run("Given a dashboard with a parent folder which has an ACL", func(t *testing.T) { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 1 + fakeDash.FolderID = 1 fakeDash.HasACL = true fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake() fakeDashboardVersionService.ExpectedDashboardVersion = &dashver.DashboardVersionDTO{} teamService := &teamtest.FakeService{} dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) q.Result = fakeDash }).Return(nil) dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { @@ -259,7 +260,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { } }).Return(nil) - mockSQLStore := mockstore.NewSQLStoreMock() + mockSQLStore := dbtest.NewFakeDB() cfg := setting.NewCfg() sql := db.InitTestDB(t) @@ -403,9 +404,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = models.NewDashboard("test") + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = dashboards.NewDashboard("test") }).Return(nil) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) @@ -505,9 +506,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUpInner() sc.sqlStore = mockSQLStore dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = models.NewDashboard("test") + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = dashboards.NewDashboard("test") }).Return(nil) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) hs.callDeleteDashboardByUID(t, sc, dashboardService) @@ -610,7 +611,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")). - Return(&models.Dashboard{Id: dashID, Uid: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) + Return(&dashboards.Dashboard{ID: dashID, UID: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, dashboardService, nil, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) @@ -642,7 +643,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")). - Return(&models.Dashboard{Id: dashID, Uid: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) + Return(&dashboards.Dashboard{ID: dashID, UID: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) mockFolder := &foldertest.FakeService{ ExpectedFolder: &folder.Folder{ID: 1, UID: "folderUID", Title: "Folder"}, @@ -803,19 +804,19 @@ func TestDashboardAPIEndpoint(t *testing.T) { }), }, } - sqlmock := mockstore.SQLStoreMock{} + sqlmock := dbtest.NewFakeDB() setUp := func() { teamSvc := &teamtest.FakeService{} dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardACLInfoListQuery")).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - OrgId: q.OrgId, - Id: q.Id, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + OrgID: q.OrgID, + ID: q.ID, } }).Return(nil) - guardian.InitLegacyGuardian(&sqlmock, dashSvc, teamSvc) + guardian.InitLegacyGuardian(sqlmock, dashSvc, teamSvc) } cmd := dtos.CalculateDiffOptions{ @@ -837,7 +838,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { callPostDashboard(sc) assert.Equal(t, 403, sc.resp.Code) - }, &sqlmock, fakeDashboardVersionService) + }, sqlmock, fakeDashboardVersionService) }) t.Run("when user does have permission", func(t *testing.T) { @@ -847,26 +848,26 @@ func TestDashboardAPIEndpoint(t *testing.T) { sc.dashboardVersionService = fakeDashboardVersionService callPostDashboard(sc) assert.Equal(t, 200, sc.resp.Code) - }, &sqlmock, fakeDashboardVersionService) + }, sqlmock, fakeDashboardVersionService) }) }) t.Run("Given dashboard in folder being restored should restore to folder", func(t *testing.T) { const folderID int64 = 1 - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 2 - fakeDash.FolderId = folderID + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 2 + fakeDash.FolderID = folderID fakeDash.HasACL = false dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) q.Result = fakeDash }).Return(nil) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { cmd := args.Get(1).(*dashboards.SaveDashboardDTO) - cmd.Dashboard = &models.Dashboard{ - Id: 2, Uid: "uid", Title: "Dash", Slug: "dash", Version: 1, + cmd.Dashboard = &dashboards.Dashboard{ + ID: 2, UID: "uid", Title: "Dash", Slug: "dash", Version: 1, } }).Return(nil, nil) @@ -891,19 +892,19 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) t.Run("Given dashboard in general folder being restored should restore to general folder", func(t *testing.T) { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 2 + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 2 fakeDash.HasACL = false dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) q.Result = fakeDash }).Return(nil) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { cmd := args.Get(1).(*dashboards.SaveDashboardDTO) - cmd.Dashboard = &models.Dashboard{ - Id: 2, Uid: "uid", Title: "Dash", Slug: "dash", Version: 1, + cmd.Dashboard = &dashboards.Dashboard{ + ID: 2, UID: "uid", Title: "Dash", Slug: "dash", Version: 1, } }).Return(nil, nil) @@ -936,9 +937,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`)) require.NoError(t, err) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Id: 1, Data: dataValue} + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ID: 1, Data: dataValue} }).Return(nil) dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*models.GetDashboardACLInfoListQuery) @@ -1256,7 +1257,7 @@ func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboard type mockLibraryPanelService struct { } -func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { +func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { return nil } diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 7ff1decbd26..2e66c071b4d 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -242,11 +242,11 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st q := args.Get(1).(*models.GetDashboardACLInfoListQuery) q.Result = aclMockResp }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil) store := mockstore.NewSQLStoreMock() diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 6e49a583422..23f355753fd 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -149,6 +149,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false), "LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync, "GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync, + "AzureADSkipOrgRoleSync": hs.Cfg.AzureADSkipOrgRoleSync, "DisableSyncLock": hs.Cfg.DisableSyncLock, }, "buildInfo": map[string]interface{}{ diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f218a72d893..15a75846c23 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/querylibrary" @@ -211,6 +212,7 @@ type HTTPServer struct { tagService tag.Service oauthTokenService oauthtoken.OAuthTokenService statsService stats.Service + authnService authn.Service } type ServerOptions struct { @@ -253,7 +255,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, - statsService stats.Service, + statsService stats.Service, authnService authn.Service, k8saccess k8saccess.K8SAccess, // required so that the router is registered ) (*HTTPServer, error) { web.Env = cfg.Env @@ -360,6 +362,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi QueryLibraryService: queryLibraryService, oauthTokenService: oauthTokenService, statsService: statsService, + authnService: authnService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/api/login.go b/pkg/api/login.go index 1ed59213fbe..74d76373081 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -17,6 +17,8 @@ import ( "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/featuremgmt" loginService "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" @@ -167,6 +169,34 @@ func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { } func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { + if hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientForm, &authn.Request{HTTPRequest: c.Req, Resp: c.Resp}) + if err != nil { + tokenErr := &auth.CreateTokenErr{} + if errors.As(err, &tokenErr) { + return response.Error(tokenErr.StatusCode, tokenErr.ExternalErr, tokenErr.InternalErr) + } + return response.Err(err) + } + + cookies.WriteSessionCookie(c, hs.Cfg, identity.SessionToken.UnhashedToken, hs.Cfg.LoginMaxLifetime) + result := map[string]interface{}{ + "message": "Logged in", + } + + if redirectTo := c.GetCookie("redirect_to"); len(redirectTo) > 0 { + if err := hs.ValidateRedirectTo(redirectTo); err == nil { + result["redirectUrl"] = redirectTo + } else { + c.Logger.Info("Ignored invalid redirect_to cookie value.", "url", redirectTo) + } + cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg) + } + + metrics.MApiLoginPost.Inc() + return response.JSON(http.StatusOK, result) + } + cmd := dtos.LoginCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad login data", err) diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index ea30addb451..9e09ba3deaa 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -12,9 +12,6 @@ import ( "strings" "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" @@ -24,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" loginservice "github.com/grafana/grafana/pkg/services/login" @@ -33,6 +31,8 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func fakeSetIndexViewData(t *testing.T) { @@ -324,6 +324,7 @@ func TestLoginPostRedirect(t *testing.T) { HooksService: &hooks.HooksService{}, License: &licensing.OSSLicensingService{}, AuthTokenService: authtest.NewFakeUserAuthTokenService(), + Features: featuremgmt.WithFeatures(), } hs.Cfg.CookieSecure = true @@ -603,6 +604,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, AuthTokenService: authtest.NewFakeUserAuthTokenService(), + Features: featuremgmt.WithFeatures(), HooksService: hookService, } diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 5cf7b7f0706..30271ed2f55 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/user" @@ -113,7 +114,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { // send invite email if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) { - emailCmd := models.SendEmailCommand{ + emailCmd := notifications.SendEmailCommand{ To: []string{inviteDto.LoginOrEmail}, Template: "new_user_invite", Data: map[string]interface{}{ @@ -126,7 +127,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { } if err := hs.AlertNG.NotificationService.SendEmailCommandHandler(c.Req.Context(), &emailCmd); err != nil { - if errors.Is(err, models.ErrSmtpNotEnabled) { + if errors.Is(err, notifications.ErrSmtpNotEnabled) { return response.Error(412, err.Error(), err) } @@ -155,7 +156,7 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *models.ReqContext, user *user.U } if inviteDto.SendEmail && util.IsEmail(user.Email) { - emailCmd := models.SendEmailCommand{ + emailCmd := notifications.SendEmailCommand{ To: []string{user.Email}, Template: "invited_to_org", Data: map[string]interface{}{ diff --git a/pkg/api/org_test.go b/pkg/api/org_test.go index 84ae9726370..26ecdbfcea9 100644 --- a/pkg/api/org_test.go +++ b/pkg/api/org_test.go @@ -1,8 +1,6 @@ package api import ( - "context" - "fmt" "net/http" "strings" "testing" @@ -14,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" @@ -622,18 +619,3 @@ func TestAPIEndpoint_GetOrg_RBAC(t *testing.T) { }) } } - -// setupOrgsDBForAccessControlTests creates orgs up until orgID and fake user as member of org -func setupOrgsDBForAccessControlTests(t *testing.T, db *sqlstore.SQLStore, c accessControlScenarioContext, orgID int64) { - t.Helper() - setInitCtxSignedInViewer(c.initCtx) - u := *c.initCtx.SignedInUser - u.OrgID = orgID - c.userService.(*usertest.FakeUserService).ExpectedSignedInUser = &u - - // Create `orgsCount` orgs - for i := 1; i <= int(orgID); i++ { - _, err := c.hs.orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("TestOrg%v", i), UserID: 0}) - require.NoError(t, err) - } -} diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index d1097a57eeb..4b4b1a83081 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -203,9 +204,9 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create a dashboard folder - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, - FolderId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + FolderID: 1, IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -220,7 +221,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { // Grant our test Viewer with permission to admin the folder acls := []*models.DashboardACL{ { - DashboardID: folder.Id, + DashboardID: folder.ID, OrgID: testOrgID, UserID: testUserID, Permission: models.PERMISSION_ADMIN, @@ -228,7 +229,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { Updated: time.Now(), }, } - err = sc.dashboardsStore.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) diff --git a/pkg/api/password.go b/pkg/api/password.go index c21c30f0ae2..5d60de08254 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -45,7 +46,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Resp } } - emailCmd := models.SendResetPasswordEmailCommand{User: usr} + emailCmd := notifications.SendResetPasswordEmailCommand{User: usr} if err := hs.NotificationService.SendResetPasswordEmail(c.Req.Context(), &emailCmd); err != nil { return response.Error(500, "Failed to send email", err) } @@ -58,7 +59,7 @@ func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - query := models.ValidateResetPasswordCodeQuery{Code: form.Code} + query := notifications.ValidateResetPasswordCodeQuery{Code: form.Code} // For now the only way to know the username to clear login attempts for is // to set it in the function provided to NotificationService @@ -71,7 +72,7 @@ func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { } if err := hs.NotificationService.ValidateResetPasswordCode(c.Req.Context(), &query, getUserByLogin); err != nil { - if errors.Is(err, models.ErrInvalidEmailCode) { + if errors.Is(err, notifications.ErrInvalidEmailCode) { return response.Error(400, "Invalid or expired reset password code", nil) } return response.Error(500, "Unknown error validating email code", err) diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 36259a458a8..4a17235aa91 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" _ "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/user" @@ -18,19 +19,19 @@ func (hs *HTTPServer) populateDashboardsByID(ctx context.Context, dashboardByIDs result := make(dtos.PlaylistDashboardsSlice, 0) if len(dashboardByIDs) > 0 { - dashboardQuery := models.GetDashboardsQuery{DashboardIds: dashboardByIDs} + dashboardQuery := dashboards.GetDashboardsQuery{DashboardIDs: dashboardByIDs} if err := hs.DashboardService.GetDashboards(ctx, &dashboardQuery); err != nil { return result, err } for _, item := range dashboardQuery.Result { result = append(result, dtos.PlaylistDashboard{ - Id: item.Id, + Id: item.ID, Slug: item.Slug, Title: item.Title, Uri: "db/" + item.Slug, - Url: models.GetDashboardUrl(item.Uid, item.Slug), - Order: dashboardIDOrder[item.Id], + Url: models.GetDashboardUrl(item.UID, item.Slug), + Order: dashboardIDOrder[item.ID], }) } } diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index e5e2a40a009..a005b9cdbc9 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -7,6 +7,7 @@ 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" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/web" ) @@ -30,15 +31,15 @@ func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { // UID is used in preference to identify dashboard dashboardID := cmd.HomeDashboardID if cmd.HomeDashboardUID != nil { - query := models.GetDashboardQuery{Uid: *cmd.HomeDashboardUID} - if query.Uid == "" { + query := dashboards.GetDashboardQuery{UID: *cmd.HomeDashboardUID} + if query.UID == "" { dashboardID = 0 // clear the value } else { err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = query.Result.Id + dashboardID = query.Result.ID } } @@ -75,10 +76,10 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team // when homedashboardID is 0, that means it is the default home dashboard, no UID would be returned in the response if preference.HomeDashboardID != 0 { - query := models.GetDashboardQuery{Id: preference.HomeDashboardID, OrgId: orgID} + query := dashboards.GetDashboardQuery{ID: preference.HomeDashboardID, OrgID: orgID} err = hs.DashboardService.GetDashboard(ctx, &query) if err == nil { - dashboardUID = query.Result.Uid + dashboardUID = query.Result.UID } } @@ -130,8 +131,8 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t dashboardID := dtoCmd.HomeDashboardID if dtoCmd.HomeDashboardUID != nil { - query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID} - if query.Uid == "" { + query := dashboards.GetDashboardQuery{UID: *dtoCmd.HomeDashboardUID, OrgID: orgID} + if query.UID == "" { // clear the value dashboardID = 0 } else { @@ -139,7 +140,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = query.Result.Id + dashboardID = query.Result.ID } } dtoCmd.HomeDashboardID = dashboardID @@ -189,8 +190,8 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te // convert dashboard UID to ID in order to store internally if it exists in the query, otherwise take the id from query dashboardID := dtoCmd.HomeDashboardID if dtoCmd.HomeDashboardUID != nil { - query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID} - if query.Uid == "" { + query := dashboards.GetDashboardQuery{UID: *dtoCmd.HomeDashboardUID, OrgID: orgID} + if query.UID == "" { // clear the value defaultDash := int64(0) dashboardID = &defaultDash @@ -199,7 +200,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = &query.Result.Id + dashboardID = &query.Result.ID } } dtoCmd.HomeDashboardID = dashboardID diff --git a/pkg/api/preferences_test.go b/pkg/api/preferences_test.go index 751f23f9a5a..ffeb42d8818 100644 --- a/pkg/api/preferences_test.go +++ b/pkg/api/preferences_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" @@ -41,9 +40,9 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) cfg.RBACEnabled = false sc := setupHTTPServerWithCfg(t, true, cfg) dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Uid: "home", Id: 1} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{UID: "home", ID: 1} }).Return(nil) sc.hs.DashboardService = dashSvc @@ -175,9 +174,9 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) { }) input = strings.NewReader(testUpdateOrgPreferencesWithHomeDashboardUIDCmd) dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Uid: "home", Id: 1} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{UID: "home", ID: 1} }).Return(nil) sc.hs.DashboardService = dashSvc t.Run("Returns 200 on success", func(t *testing.T) { diff --git a/pkg/api/quota_test.go b/pkg/api/quota_test.go index 36e128f9124..16564c90f0f 100644 --- a/pkg/api/quota_test.go +++ b/pkg/api/quota_test.go @@ -7,9 +7,14 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web/webtest" ) var ( @@ -28,57 +33,60 @@ var testOrgQuota = setting.OrgQuota{ AlertRule: 10, } -// setupDBAndSettingsForAccessControlQuotaTests stores users and create two orgs -func setupDBAndSettingsForAccessControlQuotaTests(t *testing.T, sc accessControlScenarioContext) { - t.Helper() - - // Create two orgs with the context user - setupOrgsDBForAccessControlTests(t, sc.db, sc, 2) -} - func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true cfg.RBACEnabled = false - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) - - setupDBAndSettingsForAccessControlQuotaTests(t, sc) - - t.Run("Viewer can view CurrentOrgQuotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusOK, response.Code) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg }) - sc.initCtx.IsSignedIn = false t.Run("Unsigned user cannot view CurrentOrgQuotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusUnauthorized, response.Code) + req := server.NewGetRequest(getCurrentOrgQuotasURL) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + t.Run("Viewer can view CurrentOrgQuotas", func(t *testing.T) { + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer}) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } func TestAPIEndpoint_GetCurrentOrgQuotas_AccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) - - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + }) t.Run("AccessControl allows viewing CurrentOrgQuotas with correct permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, sc.initCtx.OrgID) - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), userWithPermissions(1, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing CurrentOrgQuotas with correct permissions in another org", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 2) - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + // Set permissions in org 2, but set current org to org 1 + user := userWithPermissions(2, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}) + user.OrgID = 1 + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), user) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing CurrentOrgQuotas with incorrect permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: "orgs:invalid"}}, sc.initCtx.OrgID) - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), userWithPermissions(1, []accesscontrol.Permission{{Action: "orgs:invalid"}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } @@ -86,46 +94,60 @@ func TestAPIEndpoint_GetOrgQuotas_LegacyAccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true cfg.RBACEnabled = false - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + }) - setupDBAndSettingsForAccessControlQuotaTests(t, sc) - t.Run("Viewer cannot view another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer}) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) - sc.initCtx.SignedInUser.IsGrafanaAdmin = true t.Run("Grafana admin viewer can view another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, IsGrafanaAdmin: true}) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } func TestAPIEndpoint_GetOrgQuotas_AccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true - sc := setupHTTPServerWithCfg(t, true, cfg) - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.userService = &usertest.FakeUserService{ + ExpectedSignedInUser: &user.SignedInUser{OrgID: 2}, + } + }) t.Run("AccessControl allows viewing another org quotas with correct permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 2) - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), userWithPermissions(2, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing another org quotas with correct permissions in another org", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 1) - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + // Set correct permissions in org 1 and empty permissions in org 2 + user := userWithPermissions(1, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}) + user.Permissions[2] = map[string][]string{} + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), user) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing another org quotas with incorrect permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), userWithPermissions(2, []accesscontrol.Permission{{Action: "orgs:invalid"}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } @@ -133,22 +155,33 @@ func TestAPIEndpoint_PutOrgQuotas_LegacyAccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true cfg.RBACEnabled = false - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) - - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + }) input := strings.NewReader(testUpdateOrgQuotaCmd) t.Run("Viewer cannot update another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), &user.SignedInUser{ + OrgID: 1, + OrgRole: org.RoleViewer, + }) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + require.NoError(t, response.Body.Close()) }) - sc.initCtx.SignedInUser.IsGrafanaAdmin = true input = strings.NewReader(testUpdateOrgQuotaCmd) t.Run("Grafana admin viewer can update another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), &user.SignedInUser{ + OrgID: 1, + OrgRole: org.RoleViewer, + IsGrafanaAdmin: true, + }) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + require.NoError(t, response.Body.Close()) }) } @@ -166,30 +199,42 @@ func TestAPIEndpoint_PutOrgQuotas_AccessControl(t *testing.T) { Org: 5, }, } - sc := setupHTTPServerWithCfg(t, true, cfg) - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.userService = &usertest.FakeUserService{ + ExpectedSignedInUser: &user.SignedInUser{OrgID: 2}, + } + }) input := strings.NewReader(testUpdateOrgQuotaCmd) t.Run("AccessControl allows updating another org quotas with correct permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}, 2) - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusOK, response.Code) + user := userWithPermissions(2, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}) + user.OrgID = 1 + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), user) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + require.NoError(t, response.Body.Close()) }) input = strings.NewReader(testUpdateOrgQuotaCmd) t.Run("AccessControl prevents updating another org quotas with correct permissions in another org", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}, 1) - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusForbidden, response.Code) + user := userWithPermissions(1, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}) + user.Permissions[2] = map[string][]string{} + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), user) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + require.NoError(t, response.Body.Close()) }) input = strings.NewReader(testUpdateOrgQuotaCmd) t.Run("AccessControl prevents updating another org quotas with incorrect permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusForbidden, response.Code) + user := userWithPermissions(2, []accesscontrol.Permission{{Action: "orgs:invalid"}}) + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), user) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + require.NoError(t, response.Body.Close()) }) } diff --git a/pkg/api/stars.go b/pkg/api/stars.go index c1a98c239aa..47bd698b969 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -6,6 +6,7 @@ import ( "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/star" "github.com/grafana/grafana/pkg/web" ) @@ -22,15 +23,15 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { uids := []string{} for dashboardId := range iuserstars.UserStars { - query := &models.GetDashboardQuery{ - Id: dashboardId, - OrgId: c.OrgID, + query := &dashboards.GetDashboardQuery{ + ID: dashboardId, + OrgID: c.OrgID, } err := hs.DashboardService.GetDashboard(c.Req.Context(), query) // Grafana admin users may have starred dashboards in multiple orgs. This will avoid returning errors when the dashboard is in another org if err == nil { - uids = append(uids, query.Result.Uid) + uids = append(uids, query.Result.UID) } } return response.JSON(200, uids) @@ -91,7 +92,7 @@ func (hs *HTTPServer) StarDashboardByUID(c *models.ReqContext) response.Response return rsp } - cmd := star.StarDashboardCommand{UserID: c.UserID, DashboardID: dash.Id} + cmd := star.StarDashboardCommand{UserID: c.UserID, DashboardID: dash.ID} if err := hs.starService.Add(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to star dashboard", err) @@ -156,7 +157,7 @@ func (hs *HTTPServer) UnstarDashboardByUID(c *models.ReqContext) response.Respon return rsp } - cmd := star.UnstarDashboardCommand{UserID: c.UserID, DashboardID: dash.Id} + cmd := star.UnstarDashboardCommand{UserID: c.UserID, DashboardID: dash.ID} if err := hs.starService.Delete(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to unstar dashboard", err) diff --git a/pkg/codegen/astmanip_test.go b/pkg/codegen/astmanip_test.go index e09e1071b16..bdfd0a4eef2 100644 --- a/pkg/codegen/astmanip_test.go +++ b/pkg/codegen/astmanip_test.go @@ -2,13 +2,13 @@ package codegen import ( "bytes" - "go/format" "go/parser" "go/token" "testing" + "github.com/dave/dst/decorator" + "github.com/dave/dst/dstutil" "github.com/matryer/is" - "golang.org/x/tools/go/ast/astutil" ) func TestPrefixDropper(t *testing.T) { @@ -276,15 +276,15 @@ type Thing struct { } is := is.New(t) fset := token.NewFileSet() - inf, err := parser.ParseFile(fset, "input.go", item.in, parser.ParseComments) + inf, err := decorator.ParseFile(fset, "input.go", item.in, parser.ParseComments) if err != nil { t.Fatal(err) } drop := PrefixDropper("Foo") - astutil.Apply(inf, drop, nil) + dstutil.Apply(inf, drop, nil) buf := new(bytes.Buffer) - err = format.Node(buf, fset, inf) + err = decorator.Fprint(buf, inf) if err != nil { t.Fatal(err) } diff --git a/pkg/codegen/generators.go b/pkg/codegen/generators.go index 037766d13d7..1c6d0743f9f 100644 --- a/pkg/codegen/generators.go +++ b/pkg/codegen/generators.go @@ -60,7 +60,7 @@ func SlashHeaderMapper(maingen string) codejen.FileMapper { return func(f codejen.File) (codejen.File, error) { // Never inject on certain filetypes, it's never valid switch filepath.Ext(f.RelativePath) { - case ".json", ".yml", ".yaml": + case ".json", ".yml", ".yaml", ".md": return f, nil default: buf := new(bytes.Buffer) diff --git a/pkg/codegen/jenny_docs.go b/pkg/codegen/jenny_docs.go new file mode 100644 index 00000000000..a2cb7667ad9 --- /dev/null +++ b/pkg/codegen/jenny_docs.go @@ -0,0 +1,490 @@ +package codegen + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "path" + "path/filepath" + "sort" + "strings" + "text/template" + + "cuelang.org/go/cue/cuecontext" + "github.com/grafana/codejen" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/thema/encoding/jsonschema" + "github.com/olekukonko/tablewriter" + "github.com/xeipuuv/gojsonpointer" +) + +func DocsJenny(docsPath string) OneToOne { + return docsJenny{ + docsPath: docsPath, + } +} + +type docsJenny struct { + docsPath string +} + +func (j docsJenny) JennyName() string { + return "DocsJenny" +} + +func (j docsJenny) Generate(decl *DeclForGen) (*codejen.File, error) { + f, err := jsonschema.GenerateSchema(decl.Lineage().Latest()) + if err != nil { + return nil, fmt.Errorf("failed to generate json representation for the schema: %v", err) + } + b, err := cuecontext.New().BuildFile(f).MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal schema value to json: %v", err) + } + + // We don't need entire json obj, only the value of components.schemas path + var obj struct { + Components struct { + Schemas json.RawMessage + } + } + err = json.Unmarshal(b, &obj) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal schema json: %v", err) + } + + // fixes the references between the types within a json after making components.schema. the root of the json + kindJsonStr := strings.Replace(string(obj.Components.Schemas), "#/components/schemas/", "#/", -1) + + kindProps := decl.Properties.Common() + kindName := strings.ToLower(kindProps.Name) + data := templateData{ + KindName: kindProps.Name, + KindVersion: decl.Lineage().Latest().Version().String(), + KindMaturity: string(kindProps.Maturity), + Markdown: "{{ .Markdown 1 }}", + } + + tmpl, err := makeTemplate(data, "docs.tmpl") + if err != nil { + return nil, err + } + + doc, err := jsonToMarkdown([]byte(kindJsonStr), string(tmpl), kindName) + if err != nil { + return nil, fmt.Errorf("failed to build markdown for kind %s: %v", kindName, err) + } + + return codejen.NewFile(filepath.Join(j.docsPath, kindName, "schema-reference.md"), doc, j), nil +} + +// makeTemplate pre-populates the template with the kind metadata +func makeTemplate(data templateData, tmpl string) ([]byte, error) { + buf := new(bytes.Buffer) + if err := tmpls.Lookup(tmpl).Execute(buf, data); err != nil { + return []byte{}, fmt.Errorf("failed to populate docs template with the kind metadata") + } + return buf.Bytes(), nil +} + +type templateData struct { + KindName string + KindVersion string + KindMaturity string + Markdown string +} + +// -------------------- JSON to Markdown conversion -------------------- +// Copied from https://github.com/marcusolsson/json-schema-docs and slightly changed to fit the DocsJenny + +type schema struct { + ID string `json:"$id,omitempty"` + Ref string `json:"$ref,omitempty"` + Schema string `json:"$schema,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Required []string `json:"required,omitempty"` + Type PropertyTypes `json:"type,omitempty"` + Properties map[string]*schema `json:"properties,omitempty"` + Items *schema `json:"items,omitempty"` + Definitions map[string]*schema `json:"definitions,omitempty"` + Enum []Any `json:"enum"` + Default any `json:"default"` +} + +func jsonToMarkdown(jsonData []byte, tpl string, kindName string) ([]byte, error) { + sch, err := newSchema(jsonData, kindName) + if err != nil { + return []byte{}, err + } + + t, err := template.New("markdown").Parse(tpl) + if err != nil { + return []byte{}, err + } + + buf := new(bytes.Buffer) + err = t.Execute(buf, sch) + if err != nil { + return []byte{}, err + } + + return buf.Bytes(), nil +} + +func newSchema(b []byte, kindName string) (*schema, error) { + var data map[string]*schema + if err := json.Unmarshal(b, &data); err != nil { + return nil, err + } + + // Needed for resolving in-schema references. + root, err := simplejson.NewJson(b) + if err != nil { + return nil, err + } + + return resolveSchema(data[kindName], root) +} + +// resolveSchema recursively resolves schemas. +func resolveSchema(schem *schema, root *simplejson.Json) (*schema, error) { + for _, prop := range schem.Properties { + if prop.Ref != "" { + tmp, err := resolveReference(prop.Ref, root) + if err != nil { + return nil, err + } + *prop = *tmp + } + foo, err := resolveSchema(prop, root) + if err != nil { + return nil, err + } + *prop = *foo + } + + if schem.Items != nil { + if schem.Items.Ref != "" { + tmp, err := resolveReference(schem.Items.Ref, root) + if err != nil { + return nil, err + } + *schem.Items = *tmp + } + foo, err := resolveSchema(schem.Items, root) + if err != nil { + return nil, err + } + *schem.Items = *foo + } + + return schem, nil +} + +// resolveReference loads a schema from a $ref. +// If ref contains a hashtag (#), the part after represents a in-schema reference. +func resolveReference(ref string, root *simplejson.Json) (*schema, error) { + i := strings.Index(ref, "#") + + if i != 0 { + return nil, fmt.Errorf("not in-schema reference: %s", ref) + } + return resolveInSchemaReference(ref[i+1:], root) +} + +func resolveInSchemaReference(ref string, root *simplejson.Json) (*schema, error) { + // in-schema reference + pointer, err := gojsonpointer.NewJsonPointer(ref) + if err != nil { + return nil, err + } + + v, _, err := pointer.Get(root.MustMap()) + if err != nil { + return nil, err + } + + var sch schema + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(b, &sch); err != nil { + return nil, err + } + + // Set the ref name as title + sch.Title = path.Base(ref) + + return &sch, nil +} + +// Markdown returns the Markdown representation of the schema. +// +// The level argument can be used to offset the heading levels. This can be +// useful if you want to add the schema under a subheading. +func (s schema) Markdown(level int) string { + if level < 1 { + level = 1 + } + + var buf bytes.Buffer + + if s.Title != "" { + fmt.Fprintln(&buf, makeHeading(s.Title, level)) + fmt.Fprintln(&buf) + } + + if s.Description != "" { + fmt.Fprintln(&buf, s.Description) + if s.Default != nil { + fmt.Fprintf(&buf, "The default value is: `%v`.", s.Default) + } + fmt.Fprintln(&buf) + } + + if len(s.Properties) > 0 { + fmt.Fprintln(&buf, makeHeading("Properties", level+1)) + fmt.Fprintln(&buf) + } + + printProperties(&buf, &s) + + // Add padding. + fmt.Fprintln(&buf) + + for _, obj := range findDefinitions(&s) { + fmt.Fprint(&buf, obj.Markdown(level+1)) + } + + return buf.String() +} + +func makeHeading(heading string, level int) string { + if level < 0 { + return heading + } + + if level <= 6 { + return strings.Repeat("#", level) + " " + heading + } + + return fmt.Sprintf("**%s**", heading) +} + +func findDefinitions(s *schema) []*schema { + // Gather all properties of object type so that we can generate the + // properties for them recursively. + var objs []*schema + + for k, p := range s.Properties { + // Use the identifier as the title. + if p.Type.HasType(PropertyTypeObject) { + if len(p.Title) == 0 { + p.Title = k + } + objs = append(objs, p) + } + + // If the property is an array of objects, use the name of the array + // property as the title. + if p.Type.HasType(PropertyTypeArray) { + if p.Items != nil { + if p.Items.Type.HasType(PropertyTypeObject) { + if len(p.Items.Title) == 0 { + p.Items.Title = k + } + objs = append(objs, p.Items) + } + } + } + } + + // Sort the object schemas. + sort.Slice(objs, func(i, j int) bool { + return objs[i].Title < objs[j].Title + }) + + return objs +} + +func printProperties(w io.Writer, s *schema) { + table := tablewriter.NewWriter(w) + table.SetHeader([]string{"Property", "Type", "Required", "Description"}) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.SetAutoFormatHeaders(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAutoWrapText(false) + + // Buffer all property rows so that we can sort them before printing them. + var rows [][]string + + for k, p := range s.Properties { + // Generate relative links for objects and arrays of objects. + var propType []string + for _, pt := range p.Type { + switch pt { + case PropertyTypeObject: + name, anchor := propNameAndAnchor(k, p.Title) + propType = append(propType, fmt.Sprintf("[%s](#%s)", name, anchor)) + case PropertyTypeArray: + if p.Items != nil { + for _, pi := range p.Items.Type { + if pi == PropertyTypeObject { + name, anchor := propNameAndAnchor(k, p.Items.Title) + propType = append(propType, fmt.Sprintf("[%s](#%s)[]", name, anchor)) + } else { + propType = append(propType, fmt.Sprintf("%s[]", pi)) + } + } + } else { + propType = append(propType, string(pt)) + } + default: + propType = append(propType, string(pt)) + } + } + + var propTypeStr string + if len(propType) == 1 { + propTypeStr = propType[0] + } else if len(propType) == 2 { + propTypeStr = strings.Join(propType, " or ") + } else if len(propType) > 2 { + propTypeStr = fmt.Sprintf("%s, or %s", strings.Join(propType[:len(propType)-1], ", "), propType[len(propType)-1]) + } + + // Emphasize required properties. + var required string + if in(s.Required, k) { + required = "**Yes**" + } else { + required = "No" + } + + desc := p.Description + + if len(p.Enum) > 0 { + var vals []string + for _, e := range p.Enum { + vals = append(vals, e.String()) + } + desc += " Possible values are: `" + strings.Join(vals, "`, `") + "`." + } + + if p.Default != nil { + desc += fmt.Sprintf(" Default: `%v`.", p.Default) + } + + rows = append(rows, []string{fmt.Sprintf("`%s`", k), propTypeStr, required, formatForTable(desc)}) + } + + // Sort by the required column, then by the name column. + sort.Slice(rows, func(i, j int) bool { + if rows[i][2] < rows[j][2] { + return true + } + if rows[i][2] > rows[j][2] { + return false + } + return rows[i][0] < rows[j][0] + }) + + table.AppendBulk(rows) + table.Render() +} + +func propNameAndAnchor(prop, title string) (string, string) { + if len(title) > 0 { + return title, strings.ToLower(title) + } + return string(PropertyTypeObject), strings.ToLower(prop) +} + +// in returns true if a string slice contains a specific string. +func in(strs []string, str string) bool { + for _, s := range strs { + if s == str { + return true + } + } + return false +} + +// formatForTable returns string usable in a Markdown table. +// It trims white spaces, replaces new lines and pipe characters. +func formatForTable(in string) string { + s := strings.TrimSpace(in) + s = strings.ReplaceAll(s, "\n", "
") + s = strings.ReplaceAll(s, "|", "|") + return s +} + +type PropertyTypes []PropertyType + +func (pts *PropertyTypes) HasType(pt PropertyType) bool { + for _, t := range *pts { + if t == pt { + return true + } + } + return false +} + +func (pts *PropertyTypes) UnmarshalJSON(data []byte) error { + var value interface{} + if err := json.Unmarshal(data, &value); err != nil { + return err + } + + switch val := value.(type) { + case string: + *pts = []PropertyType{PropertyType(val)} + return nil + case []interface{}: + var pt []PropertyType + for _, t := range val { + s, ok := t.(string) + if !ok { + return errors.New("unsupported property type") + } + pt = append(pt, PropertyType(s)) + } + *pts = pt + default: + return errors.New("unsupported property type") + } + + return nil +} + +type PropertyType string + +const ( + PropertyTypeString PropertyType = "string" + PropertyTypeNumber PropertyType = "number" + PropertyTypeBoolean PropertyType = "boolean" + PropertyTypeObject PropertyType = "object" + PropertyTypeArray PropertyType = "array" + PropertyTypeNull PropertyType = "null" +) + +type Any struct { + value interface{} +} + +func (u *Any) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &u.value); err != nil { + return err + } + return nil +} + +func (u *Any) String() string { + return fmt.Sprintf("%v", u.value) +} diff --git a/pkg/codegen/jenny_gotypes.go b/pkg/codegen/jenny_gotypes.go index 7f4ad8ecae6..972dd0eb818 100644 --- a/pkg/codegen/jenny_gotypes.go +++ b/pkg/codegen/jenny_gotypes.go @@ -1,19 +1,15 @@ package codegen import ( - "bytes" - - "github.com/dave/dst/decorator" "github.com/dave/dst/dstutil" "github.com/grafana/codejen" "github.com/grafana/thema/encoding/gocode" - "golang.org/x/tools/go/ast/astutil" ) // GoTypesJenny creates a [OneToOne] that produces Go types for the provided // [thema.Schema]. type GoTypesJenny struct { - ApplyFuncs []astutil.ApplyFunc + ApplyFuncs []dstutil.ApplyFunc } func (j GoTypesJenny) JennyName() string { @@ -25,32 +21,12 @@ func (j GoTypesJenny) Generate(sfg SchemaForGen) (*codejen.File, error) { b, err := gocode.GenerateTypesOpenAPI(sfg.Schema, &gocode.TypeConfigOpenAPI{ // TODO will need to account for sanitizing e.g. dashes here at some point PackageName: sfg.Schema.Lineage().Name(), - ApplyFuncs: append(j.ApplyFuncs, PrefixDropper(sfg.Name)), + ApplyFuncs: append(j.ApplyFuncs, PrefixDropper(sfg.Name), DecoderCompactor()), }) if err != nil { return nil, err } - // TODO switch to dst completely in thema so this can be made an ApplyFuncs element - fb, err := decorator.Parse(b) - if err != nil { - return nil, err - } - dstutil.Apply(fb, DecoderCompactor(), nil) - buf := new(bytes.Buffer) - err = decorator.Fprint(buf, fb) - if err != nil { - return nil, err - } - b, err = postprocessGoFile(genGoFile{ - path: "", - walker: nil, - in: buf.Bytes(), - }) - if err != nil { - return nil, err - } - return codejen.NewFile(sfg.Schema.Lineage().Name()+"_types_gen.go", b, j), nil } diff --git a/pkg/codegen/tmpl/docs.tmpl b/pkg/codegen/tmpl/docs.tmpl new file mode 100644 index 00000000000..7f135c97cea --- /dev/null +++ b/pkg/codegen/tmpl/docs.tmpl @@ -0,0 +1,13 @@ +--- +keywords: + - grafana + - schema +title: {{ .KindName }} kind +--- + +# {{ .KindName }} kind + +### Maturity: {{ .KindMaturity }} +### Version: {{ .KindVersion }} + +{{ .Markdown }} diff --git a/pkg/codegen/util_go.go b/pkg/codegen/util_go.go index cdd464e2914..70604f035d5 100644 --- a/pkg/codegen/util_go.go +++ b/pkg/codegen/util_go.go @@ -3,7 +3,6 @@ package codegen import ( "bytes" "fmt" - "go/ast" "go/format" "go/parser" "go/token" @@ -13,14 +12,14 @@ import ( "strings" "github.com/dave/dst" + "github.com/dave/dst/decorator" "github.com/dave/dst/dstutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/imports" ) type genGoFile struct { path string - walker astutil.ApplyFunc + walker dstutil.ApplyFunc in []byte } @@ -28,13 +27,13 @@ func postprocessGoFile(cfg genGoFile) ([]byte, error) { fname := filepath.Base(cfg.path) buf := new(bytes.Buffer) fset := token.NewFileSet() - gf, err := parser.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) + gf, err := decorator.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) if err != nil { return nil, fmt.Errorf("error parsing generated file: %w", err) } if cfg.walker != nil { - astutil.Apply(gf, cfg.walker, nil) + dstutil.Apply(gf, cfg.walker, nil) err = format.Node(buf, fset, gf) if err != nil { @@ -77,10 +76,10 @@ type prefixmod struct { rxpsuff *regexp.Regexp } -// PrefixDropper returns an astutil.ApplyFunc that removes the provided prefix +// PrefixDropper returns a dstutil.ApplyFunc that removes the provided prefix // string when it appears as a leading sequence in type names, var names, and // comments in a generated Go file. -func PrefixDropper(prefix string) astutil.ApplyFunc { +func PrefixDropper(prefix string) dstutil.ApplyFunc { return (&prefixmod{ prefix: prefix, rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]+)`, prefix)), @@ -88,13 +87,13 @@ func PrefixDropper(prefix string) astutil.ApplyFunc { }).applyfunc } -// PrefixReplacer returns an astutil.ApplyFunc that removes the provided prefix +// PrefixReplacer returns a dstutil.ApplyFunc that removes the provided prefix // string when it appears as a leading sequence in type names, var names, and // comments in a generated Go file. // // When an exact match for prefix is found, the provided replace string // is substituted. -func PrefixReplacer(prefix, replace string) astutil.ApplyFunc { +func PrefixReplacer(prefix, replace string) dstutil.ApplyFunc { return (&prefixmod{ prefix: prefix, replace: replace, @@ -103,62 +102,70 @@ func PrefixReplacer(prefix, replace string) astutil.ApplyFunc { }).applyfunc } -func depoint(e ast.Expr) ast.Expr { - if star, is := e.(*ast.StarExpr); is { +func depoint(e dst.Expr) dst.Expr { + if star, is := e.(*dst.StarExpr); is { return star.X } return e } -func (d prefixmod) applyfunc(c *astutil.Cursor) bool { +func (d prefixmod) applyfunc(c *dstutil.Cursor) bool { n := c.Node() switch x := n.(type) { - case *ast.ValueSpec: + case *dst.ValueSpec: d.handleExpr(x.Type) for _, id := range x.Names { d.do(id) } - case *ast.TypeSpec: + case *dst.TypeSpec: // Always do typespecs d.do(x.Name) - case *ast.Field: + case *dst.Field: // Don't rename struct fields. We just want to rename type declarations, and // field value specifications that reference those types. d.handleExpr(x.Type) - - case *ast.CommentGroup: - for _, c := range x.List { - c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1") - if d.replace != "" { - c.Text = d.rxp.ReplaceAllString(c.Text, d.replace+"$1") + case *dst.File: + for _, decl := range x.Decls { + comments := decl.Decorations().Start.All() + decl.Decorations().Start.Clear() + // For any reason, sometimes it retrieves the comment duplicated 🤷 + commentMap := make(map[string]bool) + for _, c := range comments { + if _, ok := commentMap[c]; !ok { + commentMap[c] = true + decl.Decorations().Start.Append(d.rxpsuff.ReplaceAllString(c, "$1")) + if d.replace != "" { + decl.Decorations().Start.Append(d.rxp.ReplaceAllString(c, d.replace+"$1")) + } + } } } } return true } -func (d prefixmod) handleExpr(e ast.Expr) { +func (d prefixmod) handleExpr(e dst.Expr) { // Deref a StarExpr, if there is one expr := depoint(e) switch x := expr.(type) { - case *ast.Ident: + case *dst.Ident: d.do(x) - case *ast.ArrayType: - if id, is := depoint(x.Elt).(*ast.Ident); is { + case *dst.ArrayType: + if id, is := depoint(x.Elt).(*dst.Ident); is { d.do(id) } - case *ast.MapType: - if id, is := depoint(x.Key).(*ast.Ident); is { + case *dst.MapType: + if id, is := depoint(x.Key).(*dst.Ident); is { d.do(id) } - if id, is := depoint(x.Value).(*ast.Ident); is { + if id, is := depoint(x.Value).(*dst.Ident); is { d.do(id) } } } -func (d prefixmod) do(n *ast.Ident) { +func (d prefixmod) do(n *dst.Ident) { if n.Name != d.prefix { n.Name = strings.TrimPrefix(n.Name, d.prefix) } else if d.replace != "" { diff --git a/pkg/infra/usagestats/service/api_test.go b/pkg/infra/usagestats/service/api_test.go index 2e5c5d054fe..ed4c5a898f9 100644 --- a/pkg/infra/usagestats/service/api_test.go +++ b/pkg/infra/usagestats/service/api_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -49,10 +50,10 @@ func TestApi_getUsageStats(t *testing.T) { uss := createService(t, setting.Cfg{}, sqlStore, false) uss.registerAPIEndpoints() - sqlStore.ExpectedSystemStats = &models.SystemStats{} - sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{} - sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{} - sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{} + sqlStore.ExpectedSystemStats = &stats.SystemStats{} + sqlStore.ExpectedDataSourceStats = []*stats.DataSourceStats{} + sqlStore.ExpectedDataSourcesAccessStats = []*stats.DataSourceAccessStats{} + sqlStore.ExpectedNotifierUsageStats = []*stats.NotifierUsageStats{} for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { @@ -69,13 +70,13 @@ func TestApi_getUsageStats(t *testing.T) { } } -func getUsageStats(t *testing.T, server *web.Mux) (*models.SystemStats, *httptest.ResponseRecorder) { +func getUsageStats(t *testing.T, server *web.Mux) (*stats.SystemStats, *httptest.ResponseRecorder) { req, err := http.NewRequest(http.MethodGet, "/api/admin/usage-report-preview", http.NoBody) require.NoError(t, err) recorder := httptest.NewRecorder() server.ServeHTTP(recorder, req) - var usageStats models.SystemStats + var usageStats stats.SystemStats if recorder.Code == http.StatusOK { require.NoError(t, json.NewDecoder(recorder.Body).Decode(&usageStats)) } diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index 74c3fed99bd..a2b814ac6b1 100644 --- a/pkg/infra/usagestats/statscollector/service.go +++ b/pkg/infra/usagestats/statscollector/service.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/datasources" @@ -109,7 +108,7 @@ func (s *Service) Run(ctx context.Context) error { func (s *Service) collectSystemStats(ctx context.Context) (map[string]interface{}, error) { m := map[string]interface{}{} - statsQuery := models.GetSystemStatsQuery{} + statsQuery := stats.GetSystemStatsQuery{} if err := s.statsService.GetSystemStats(ctx, &statsQuery); err != nil { s.log.Error("Failed to get system stats", "error", err) return nil, err @@ -222,7 +221,7 @@ func (s *Service) collectAdditionalMetrics(ctx context.Context) (map[string]inte func (s *Service) collectAlertNotifierStats(ctx context.Context) (map[string]interface{}, error) { m := map[string]interface{}{} // get stats about alert notifier usage - anStats := models.GetAlertNotifierUsageStatsQuery{} + anStats := stats.GetAlertNotifierUsageStatsQuery{} if err := s.statsService.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil { s.log.Error("Failed to get alert notification stats", "error", err) return nil, err @@ -236,7 +235,7 @@ func (s *Service) collectAlertNotifierStats(ctx context.Context) (map[string]int func (s *Service) collectDatasourceStats(ctx context.Context) (map[string]interface{}, error) { m := map[string]interface{}{} - dsStats := models.GetDataSourceStatsQuery{} + dsStats := stats.GetDataSourceStatsQuery{} if err := s.statsService.GetDataSourceStats(ctx, &dsStats); err != nil { s.log.Error("Failed to get datasource stats", "error", err) return nil, err @@ -283,7 +282,7 @@ func (s *Service) collectDatasourceAccess(ctx context.Context) (map[string]inter m := map[string]interface{}{} // fetch datasource access stats - dsAccessStats := models.GetDataSourceAccessStatsQuery{} + dsAccessStats := stats.GetDataSourceAccessStatsQuery{} if err := s.statsService.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil { s.log.Error("Failed to get datasource access stats", "error", err) return nil, err @@ -319,7 +318,7 @@ func (s *Service) updateTotalStats(ctx context.Context) bool { return false } - statsQuery := models.GetSystemStatsQuery{} + statsQuery := stats.GetSystemStatsQuery{} if err := s.statsService.GetSystemStats(ctx, &statsQuery); err != nil { s.log.Error("Failed to get system stats", "error", err) return false @@ -354,7 +353,7 @@ func (s *Service) updateTotalStats(ctx context.Context) bool { metrics.MStatTotalPublicDashboards.Set(float64(statsQuery.Result.PublicDashboards)) - dsStats := models.GetDataSourceStatsQuery{} + dsStats := stats.GetDataSourceStatsQuery{} if err := s.statsService.GetDataSourceStats(ctx, &dsStats); err != nil { s.log.Error("Failed to get datasource stats", "error", err) return true diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 0d9bbdd0c2e..265031cb4d5 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/datasources" @@ -36,7 +35,7 @@ func TestTotalStatsUpdate(t *testing.T) { s.cfg.MetricsEndpointEnabled = true s.cfg.MetricsEndpointDisableTotalStats = false - statsService.ExpectedSystemStats = &models.SystemStats{} + statsService.ExpectedSystemStats = &stats.SystemStats{} tests := []struct { MetricsEndpointEnabled bool @@ -254,7 +253,7 @@ func TestDatasourceStats(t *testing.T) { setupSomeDataSourcePlugins(t, s) - statsService.ExpectedDataSourceStats = []*models.DataSourceStats{ + statsService.ExpectedDataSourceStats = []*stats.DataSourceStats{ { Type: datasources.DS_ES, Count: 9, @@ -291,7 +290,7 @@ func TestDatasourceStats(t *testing.T) { }, } - statsService.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{ + statsService.ExpectedDataSourcesAccessStats = []*stats.DataSourceAccessStats{ { Type: datasources.DS_ES, Access: "direct", @@ -360,7 +359,7 @@ func TestAlertNotifiersStats(t *testing.T) { statsService := statstest.NewFakeService() s := createService(t, &setting.Cfg{}, sqlStore, statsService) - statsService.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{ + statsService.ExpectedNotifierUsageStats = []*stats.NotifierUsageStats{ { Type: "slack", Count: 1, @@ -379,7 +378,7 @@ func TestAlertNotifiersStats(t *testing.T) { } func mockSystemStats(statsService *statstest.FakeService) { - statsService.ExpectedSystemStats = &models.SystemStats{ + statsService.ExpectedSystemStats = &stats.SystemStats{ Dashboards: 1, Datasources: 2, Users: 3, diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index df6c94ca4eb..8f10f8e812c 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/org" "golang.org/x/oauth2" @@ -18,6 +19,7 @@ type SocialAzureAD struct { *SocialBase allowedGroups []string forceUseGraphAPI bool + skipOrgRoleSync bool } type azureClaims struct { @@ -70,18 +72,21 @@ func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*Bas return nil, ErrEmailNotFound } - role, grafanaAdmin := s.extractRoleAndAdmin(&claims) + // setting the role, grafanaAdmin to empty to reflect that we are not syncronizing with the external provider + var role roletype.RoleType + var grafanaAdmin bool + if !s.skipOrgRoleSync { + role, grafanaAdmin = s.extractRoleAndAdmin(&claims) + } if s.roleAttributeStrict && !role.IsValid() { return nil, &InvalidBasicRoleError{idP: "Azure", assignedRole: string(role)} } - logger.Debug("AzureAD OAuth: extracted role", "email", email, "role", role) groups, err := s.extractGroups(client, claims, token) if err != nil { return nil, fmt.Errorf("failed to extract groups: %w", err) } - logger.Debug("AzureAD OAuth: extracted groups", "email", email, "groups", fmt.Sprintf("%v", groups)) if !s.IsGroupMember(groups) { return nil, errMissingGroupMembership diff --git a/pkg/login/social/azuread_oauth_test.go b/pkg/login/social/azuread_oauth_test.go index 4a03dc12002..26a005ddb22 100644 --- a/pkg/login/social/azuread_oauth_test.go +++ b/pkg/login/social/azuread_oauth_test.go @@ -483,3 +483,147 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { }) } } + +func TestSocialAzureAD_SkipOrgRole(t *testing.T) { + type fields struct { + SocialBase *SocialBase + allowedGroups []string + forceUseGraphAPI bool + skipOrgRoleSync bool + } + type args struct { + client *http.Client + } + + tests := []struct { + name string + fields fields + claims *azureClaims + args args + settingAutoAssignOrgRole string + want *BasicUserInfo + wantErr bool + }{ + { + name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles, skipOrgRoleSyncBase disabled", + fields: fields{ + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()), + skipOrgRoleSync: false, + }, + claims: &azureClaims{ + Email: "me@example.com", + PreferredUsername: "", + Roles: []string{"GrafanaAdmin", "Editor"}, + Name: "My Name", + ID: "1234", + }, + want: &BasicUserInfo{ + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + IsGrafanaAdmin: trueBoolPtr(), + Groups: []string{}, + }, + }, + { + name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should not get roles", + fields: fields{ + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()), + skipOrgRoleSync: false, + }, + claims: &azureClaims{ + Email: "me@example.com", + PreferredUsername: "", + Roles: []string{"GrafanaAdmin", "Editor"}, + Name: "My Name", + ID: "1234", + }, + want: &BasicUserInfo{ + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + IsGrafanaAdmin: trueBoolPtr(), + Groups: []string{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SocialAzureAD{ + SocialBase: tt.fields.SocialBase, + allowedGroups: tt.fields.allowedGroups, + forceUseGraphAPI: tt.fields.forceUseGraphAPI, + skipOrgRoleSync: tt.fields.skipOrgRoleSync, + } + + if tt.fields.SocialBase == nil { + s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()) + } + + key := []byte("secret") + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) + if err != nil { + panic(err) + } + + cl := jwt.Claims{ + Subject: "subject", + Issuer: "issuer", + NotBefore: jwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)), + Audience: jwt.Audience{"leela", "fry"}, + } + + var raw string + if tt.claims != nil { + if tt.claims.ClaimNames.Groups != "" { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + tokenParts := strings.Split(request.Header.Get("Authorization"), " ") + require.Len(t, tokenParts, 2) + require.Equal(t, "fake_token", tokenParts[1]) + + writer.WriteHeader(http.StatusOK) + + type response struct { + Value []string + } + res := response{Value: []string{"from_server"}} + require.NoError(t, json.NewEncoder(writer).Encode(&res)) + })) + // need to set the fake servers url as endpoint to capture request + tt.claims.ClaimSources = map[string]claimSource{ + tt.claims.ClaimNames.Groups: {Endpoint: server.URL}, + } + } + raw, err = jwt.Signed(sig).Claims(cl).Claims(tt.claims).CompactSerialize() + require.NoError(t, err) + } else { + raw, err = jwt.Signed(sig).Claims(cl).CompactSerialize() + require.NoError(t, err) + } + + token := &oauth2.Token{ + AccessToken: "fake_token", + } + if tt.claims != nil { + token = token.WithExtra(map[string]interface{}{"id_token": raw}) + } + + if tt.fields.SocialBase != nil { + tt.args.client = s.Client(context.Background(), token) + } + + got, err := s.UserInfo(tt.args.client, token) + if (err != nil) != tt.wantErr { + t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr) + return + } + + require.EqualValues(t, tt.want, got) + }) + } +} diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 19ac4b311de..056457a8238 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -171,6 +171,7 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *Soc SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false), + skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync, } } diff --git a/pkg/middleware/middleware_jwt_auth_test.go b/pkg/middleware/middleware_jwt_auth_test.go index abb35ae7469..ab41f4d4abe 100644 --- a/pkg/middleware/middleware_jwt_auth_test.go +++ b/pkg/middleware/middleware_jwt_auth_test.go @@ -55,7 +55,8 @@ func TestMiddlewareJWTAuth(t *testing.T) { cfg.JWTAuthAllowAssignGrafanaAdmin = true } - token := "some-token" + // #nosec G101 -- This is dummy/test token + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2bGFkaW1pckBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwiZm9vLXVzZXJuYW1lIjoidmxhZGltaXIiLCJuYW1lIjoiVmxhZGltaXIgRXhhbXBsZSIsImZvby1lbWFpbCI6InZsYWRpbWlyQGV4YW1wbGUuY29tIn0.MeNU1pCzRHGdQuu5ppeftxT31_2Le2kM1wd1GK2jExs" middlewareScenario(t, "Valid token with valid login claim", func(t *testing.T, sc *scenarioContext) { myUsername := "vladimir" @@ -85,7 +86,7 @@ func TestMiddlewareJWTAuth(t *testing.T) { myUsername := "vladimir" // We can ignore gosec G101 since this does not contain any credentials. // nolint:gosec - myToken := "some.jwt.token" + myToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2bGFkaW1pckBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwiZm9vLXVzZXJuYW1lIjoidmxhZGltaXIiLCJuYW1lIjoiVmxhZGltaXIgRXhhbXBsZSIsImZvby1lbWFpbCI6InZsYWRpbWlyQGV4YW1wbGUuY29tIn0.MeNU1pCzRHGdQuu5ppeftxT31_2Le2kM1wd1GK2jExs" var verifiedToken string sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) { verifiedToken = myToken diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 173c295b45d..60d59fa12a1 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -234,6 +234,50 @@ func TestMiddlewareContext(t *testing.T) { assert.Equal(t, org.RoleEditor, sc.context.OrgRole) }, configureJWTAuthHeader) + middlewareScenario(t, "Valid Basic Auth header with JWT enabled and empty 'sub' claim", func(t *testing.T, sc *scenarioContext) { + const password = "MyPass" + const orgID int64 = 2 + const userID int64 = 12 + // #nosec G101 -- This is dummy/test token + const emptySubToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIiLCJpYXQiOjE1MTYyMzkwMjJ9.tnwtOHK58d47dO4DHW4b9MzeToxa1kGiko5Oo887Rqc" + + sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: orgID, UserID: userID} + authHeader := util.GetBasicAuthHeader("myuser", password) + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).withJWTAuthHeader(emptySubToken).exec() + + require.Equal(t, 200, sc.resp.Code) + + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, orgID, sc.context.OrgID) + assert.Equal(t, userID, sc.context.UserID) + }, func(cfg *setting.Cfg) { + cfg.JWTAuthEnabled = true + cfg.JWTAuthHeaderName = "X-JWT-Token" + cfg.BasicAuthEnabled = true + }) + + middlewareScenario(t, "Valid Basic Auth header with JWT enabled and missing 'sub' claim", func(t *testing.T, sc *scenarioContext) { + const password = "MyPass" + const orgID int64 = 2 + const userID int64 = 12 + // #nosec G101 -- This is dummy/test token + const missingSubToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.8nYFUX869Y1mnDDDU4yL11aANgVRuifoxrE8BHZY1iE" + + sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: orgID, UserID: userID} + authHeader := util.GetBasicAuthHeader("myuser", password) + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).withJWTAuthHeader(missingSubToken).exec() + + require.Equal(t, 200, sc.resp.Code) + + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, orgID, sc.context.OrgID) + assert.Equal(t, userID, sc.context.UserID) + }, func(cfg *setting.Cfg) { + cfg.JWTAuthEnabled = true + cfg.JWTAuthHeaderName = "X-JWT-Token" + cfg.BasicAuthEnabled = true + }) + middlewareScenario(t, "Valid API key, but does not match DB hash", func(t *testing.T, sc *scenarioContext) { const keyhash = "Something_not_matching" sc.apiKeyService.ExpectedAPIKey = &apikey.APIKey{OrgId: 12, Role: org.RoleEditor, Key: keyhash} @@ -696,7 +740,7 @@ func TestMiddlewareContext(t *testing.T) { }) middlewareScenario(t, "Request body should not be read in default context handler, but query should be altered - jwt", func(t *testing.T, sc *scenarioContext) { - sc.fakeReq("POST", "/?targetOrgId=123&auth_token=token") + sc.fakeReq("POST", "/?targetOrgId=123&auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsImlhdCI6MTUxNjIzOTAyMn0.1E9qmtctlHAeJzNLPgGFfxdA8WfbEl_vwYO91ffQGxs") body := "key=value" sc.req.Body = io.NopCloser(strings.NewReader(body)) diff --git a/pkg/models/address.go b/pkg/models/address.go deleted file mode 100644 index 0cf8aaf7ddb..00000000000 --- a/pkg/models/address.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -type Address struct { - Address1 string `json:"address1"` - Address2 string `json:"address2"` - City string `json:"city"` - ZipCode string `json:"zipCode"` - State string `json:"state"` - Country string `json:"country"` -} diff --git a/pkg/models/context.go b/pkg/models/context.go index 432dee40e99..eaf1bbf2871 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -1,6 +1,7 @@ package models import ( + "net/http" "strings" "github.com/grafana/grafana/pkg/infra/log" @@ -60,16 +61,21 @@ func (ctx *ReqContext) JsonApiErr(status int, message string, err error) { if err != nil { resp["traceID"] = traceID - ctx.Logger.Error(message, "error", err, "traceID", traceID) + if status == http.StatusInternalServerError { + ctx.Logger.Error(message, "error", err, "traceID", traceID) + } else { + ctx.Logger.Warn(message, "error", err, "traceID", traceID) + } + if setting.Env != setting.Prod { resp["error"] = err.Error() } } switch status { - case 404: + case http.StatusNotFound: resp["message"] = "Not Found" - case 500: + case http.StatusInternalServerError: resp["message"] = "Internal Server Error" } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index d4aabc68e6c..85e628d0fd0 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -172,10 +172,6 @@ func GetFolderUrl(folderUid string, slug string) string { return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug) } -type ValidateDashboardBeforeSaveResult struct { - IsParentFolderChanged bool -} - // // COMMANDS // @@ -261,18 +257,6 @@ type GetDashboardsByPluginIdQuery struct { Result []*Dashboard } -type GetDashboardSlugByIdQuery struct { - Id int64 - Result string -} - -type GetDashboardsBySlugQuery struct { - OrgId int64 - Slug string - - Result []*Dashboard -} - type DashboardRef struct { Uid string Slug string diff --git a/pkg/models/helpflags.go b/pkg/models/helpflags.go deleted file mode 100644 index 48b35954aa0..00000000000 --- a/pkg/models/helpflags.go +++ /dev/null @@ -1,8 +0,0 @@ -package models - -import "github.com/grafana/grafana/pkg/services/user" - -type SetUserHelpFlagCommand struct { - HelpFlags1 user.HelpFlags1 - UserId int64 -} diff --git a/pkg/models/plugin_settings.go b/pkg/models/plugin_settings.go index 2052976917c..e9e46191f03 100644 --- a/pkg/models/plugin_settings.go +++ b/pkg/models/plugin_settings.go @@ -57,14 +57,6 @@ type UpdatePluginSettingVersionCmd struct { // --------------------- // QUERIES -type PluginSettingInfoDTO struct { - OrgId int64 - PluginId string - Enabled bool - Pinned bool - PluginVersion string -} - type GetPluginSettingByIdQuery struct { PluginId string OrgId int64 diff --git a/pkg/plugins/codegen/util_go.go b/pkg/plugins/codegen/util_go.go index 9033e29b968..a9caf75ea2a 100644 --- a/pkg/plugins/codegen/util_go.go +++ b/pkg/plugins/codegen/util_go.go @@ -10,13 +10,14 @@ import ( "path/filepath" "strings" - "golang.org/x/tools/go/ast/astutil" + "github.com/dave/dst/decorator" + "github.com/dave/dst/dstutil" "golang.org/x/tools/imports" ) type genGoFile struct { path string - walker astutil.ApplyFunc + walker dstutil.ApplyFunc in []byte } @@ -24,13 +25,13 @@ func postprocessGoFile(cfg genGoFile) ([]byte, error) { fname := filepath.Base(cfg.path) buf := new(bytes.Buffer) fset := token.NewFileSet() - gf, err := parser.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) + gf, err := decorator.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) if err != nil { return nil, fmt.Errorf("error parsing generated file: %w", err) } if cfg.walker != nil { - astutil.Apply(gf, cfg.walker, nil) + dstutil.Apply(gf, cfg.walker, nil) err = format.Node(buf, fset, gf) if err != nil { diff --git a/pkg/plugins/plugindef/gen.go b/pkg/plugins/plugindef/gen.go index 1c1c8e4595a..d3a7333282b 100644 --- a/pkg/plugins/plugindef/gen.go +++ b/pkg/plugins/plugindef/gen.go @@ -8,19 +8,19 @@ import ( "context" "encoding/json" "fmt" - "go/ast" "os" "path/filepath" "strings" "cuelang.org/go/cue/cuecontext" + "github.com/dave/dst" + "github.com/dave/dst/dstutil" "github.com/grafana/codejen" "github.com/grafana/grafana/pkg/codegen" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/thema" "github.com/grafana/thema/encoding/gocode" "github.com/grafana/thema/encoding/jsonschema" - "golang.org/x/tools/go/ast/astutil" ) var dirPlugindef = filepath.Join("pkg", "plugins", "plugindef") @@ -62,7 +62,7 @@ func (j *jennytypego) JennyName() string { } func (j *jennytypego) Generate(lin thema.Lineage) (*codejen.File, error) { - f, err := codegen.GoTypesJenny{ApplyFuncs: []astutil.ApplyFunc{ + f, err := codegen.GoTypesJenny{ApplyFuncs: []dstutil.ApplyFunc{ codegen.PrefixReplacer("Plugindef", "PluginDef"), }}.Generate(codegen.SchemaForGen{ Name: "PluginDef", @@ -85,7 +85,7 @@ func (j *jennybindgo) JennyName() string { func (j *jennybindgo) Generate(lin thema.Lineage) (*codejen.File, error) { b, err := gocode.GenerateLineageBinding(lin, &gocode.BindingConfig{ TitleName: "PluginDef", - Assignee: ast.NewIdent("*PluginDef"), + Assignee: dst.NewIdent("*PluginDef"), PrivateFactory: true, }) if err != nil { diff --git a/pkg/plugins/plugindef/plugindef_bindings_gen.go b/pkg/plugins/plugindef/plugindef_bindings_gen.go index 1a9aae120a6..3438f6896a3 100644 --- a/pkg/plugins/plugindef/plugindef_bindings_gen.go +++ b/pkg/plugins/plugindef/plugindef_bindings_gen.go @@ -46,7 +46,6 @@ func doLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLin } return tsch.ConvergentLineage(), nil } - func baseLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { // First, we must get the bytes of the .cue file(s) in which the "plugindef" lineage // is declared, and load them into a diff --git a/pkg/plugins/plugindef/plugindef_types_gen.go b/pkg/plugins/plugindef/plugindef_types_gen.go index 471dcaae99b..2c26d1d9510 100644 --- a/pkg/plugins/plugindef/plugindef_types_gen.go +++ b/pkg/plugins/plugindef/plugindef_types_gen.go @@ -67,6 +67,7 @@ const ( ) // Defines values for Category. +// Defines values for PlugindefCategory. const ( CategoryCloud Category = "cloud" @@ -86,6 +87,7 @@ const ( ) // Defines values for Type. +// Defines values for PlugindefType. const ( TypeApp Type = "app" @@ -358,6 +360,7 @@ type URLParam struct { Name string `json:"name"` } +// Plugindef defines model for plugindef. // PluginDef defines model for plugindef. type PluginDef struct { // For data source plugins, if the plugin supports alerting. diff --git a/pkg/services/accesscontrol/accesscontrol_test.go b/pkg/services/accesscontrol/accesscontrol_test.go index 9d20738441a..6d8afd673da 100644 --- a/pkg/services/accesscontrol/accesscontrol_test.go +++ b/pkg/services/accesscontrol/accesscontrol_test.go @@ -42,6 +42,17 @@ func TestReduce(t *testing.T) { "teams:write": {"teams:id:1"}, }, }, + { + name: "specific permissions with repeated scope", + ps: []Permission{ + {Action: "teams:read", Scope: "teams:id:1"}, + {Action: "teams:read", Scope: "teams:id:2"}, + {Action: "teams:read", Scope: "teams:id:1"}, + }, + want: map[string][]string{ + "teams:read": {"teams:id:1", "teams:id:2"}, + }, + }, { name: "wildcard permission", ps: []Permission{ @@ -88,6 +99,16 @@ func TestReduce(t *testing.T) { "dashboards:read": {"*"}, }, }, + { + name: "non-wilcard scopes with * in them", + ps: []Permission{ + {Action: "dashboards:read", Scope: "dashboards:uid:123"}, + {Action: "dashboards:read", Scope: "dashboards:uid:1*"}, + }, + want: map[string][]string{ + "dashboards:read": {"dashboards:uid:123", "dashboards:uid:1*"}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index eadfaf0a028..a3d279e103f 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -91,7 +91,7 @@ func (api *AccessControlAPI) searchUsersPermissions(c *models.ReqContext) respon permsByAction := map[int64]map[string][]string{} for userID, userPerms := range permissions { - permsByAction[userID] = ac.GroupScopesByAction(userPerms) + permsByAction[userID] = ac.Reduce(userPerms) } return response.JSON(http.StatusOK, permsByAction) @@ -121,5 +121,5 @@ func (api *AccessControlAPI) searchUserPermissions(c *models.ReqContext) respons response.Error(http.StatusInternalServerError, "could not search user permissions", err) } - return response.JSON(http.StatusOK, ac.GroupScopesByAction(permissions)) + return response.JSON(http.StatusOK, ac.Reduce(permissions)) } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 476477980ee..bf3d8fe217c 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -117,8 +117,8 @@ func ProvideDashboardPermissions( license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*DashboardPermissionsService, error) { - getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) { - query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID} + getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { + query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { return nil, err } @@ -145,12 +145,12 @@ func ProvideDashboardPermissions( if err != nil { return nil, err } - if dashboard.FolderId > 0 { - query := &models.GetDashboardQuery{Id: dashboard.FolderId, OrgId: orgID} + if dashboard.FolderID > 0 { + query := &dashboards.GetDashboardQuery{ID: dashboard.FolderID, OrgID: orgID} if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { return nil, err } - return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.Result.Uid)}, nil + return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.Result.UID)}, nil } return []string{}, nil }, @@ -200,7 +200,7 @@ func ProvideFolderPermissions( Resource: "folders", ResourceAttribute: "uid", ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { - query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID} + query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { return err } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index c67804c163c..41aec12ccea 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -29,7 +29,7 @@ type EvalContext struct { Rule *Rule Log log.Logger - dashboardRef *models.DashboardRef + dashboardRef *dashboards.DashboardRef ImagePublicURL string ImageOnDiskPath string @@ -116,13 +116,13 @@ func (c *EvalContext) GetNotificationTitle() string { } // GetDashboardUID returns the dashboard uid for the alert rule. -func (c *EvalContext) GetDashboardUID() (*models.DashboardRef, error) { +func (c *EvalContext) GetDashboardUID() (*dashboards.DashboardRef, error) { if c.dashboardRef != nil { return c.dashboardRef, nil } - uidQuery := &models.GetDashboardRefByIdQuery{Id: c.Rule.DashboardID} - if err := c.dashboardService.GetDashboardUIDById(c.Ctx, uidQuery); err != nil { + uidQuery := &dashboards.GetDashboardRefByIDQuery{ID: c.Rule.DashboardID} + if err := c.dashboardService.GetDashboardUIDByID(c.Ctx, uidQuery); err != nil { return nil, err } @@ -142,7 +142,7 @@ func (c *EvalContext) GetRuleURL() (string, error) { if err != nil { return "", err } - return fmt.Sprintf(urlFormat, models.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelID, c.Rule.OrgID), nil + return fmt.Sprintf(urlFormat, models.GetFullDashboardUrl(ref.UID, ref.Slug), c.Rule.PanelID, c.Rule.OrgID), nil } // GetNewState returns the new state from the alert rule evaluation. diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index b91a4578aa7..edb9c82dc01 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -149,7 +149,7 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json PanelID: panelID, } if dashAlertInfo.Dash != nil { - ve.DashboardID = dashAlertInfo.Dash.Id + ve.DashboardID = dashAlertInfo.Dash.ID } return ve } @@ -175,7 +175,7 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json } alert := &models.Alert{ - DashboardId: dashAlertInfo.Dash.Id, + DashboardId: dashAlertInfo.Dash.ID, OrgId: dashAlertInfo.OrgID, PanelId: panelID, Id: jsonAlert.Get("id").MustInt64(), diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 85f5d9e0eef..42d8b57154b 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" @@ -59,7 +60,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, _ = extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -73,7 +74,7 @@ func TestAlertRuleExtraction(t *testing.T) { dsService.ExpectedDatasource = &datasources.DataSource{Id: 12} alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -125,7 +126,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, err = extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -141,7 +142,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, err = extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -156,7 +157,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, err = extractor.GetAlerts(WithUAEnabled(context.Background(), true), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Equal(t, "alert validation error: Alert on PanelId: 2 refers to query(B) that cannot be found. Legacy alerting queries are not able to be removed at this time in order to preserve the ability to rollback to previous versions of Grafana", err.Error()) @@ -172,7 +173,7 @@ func TestAlertRuleExtraction(t *testing.T) { dsService.ExpectedDatasource = &datasources.DataSource{Id: 12} alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Nil(t, err) @@ -191,7 +192,7 @@ func TestAlertRuleExtraction(t *testing.T) { alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Nil(t, err) @@ -218,7 +219,7 @@ func TestAlertRuleExtraction(t *testing.T) { alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Nil(t, err) @@ -242,7 +243,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) + dash := dashboards.NewDashboardFromJson(dashJSON) alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, @@ -263,7 +264,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashAlertInfo := DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, } @@ -284,7 +285,7 @@ func TestAlertRuleExtraction(t *testing.T) { dsService.ExpectedDatasource = graphite2Ds dashAlertInfo := DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, } @@ -355,7 +356,7 @@ func TestFilterPermissionsErrors(t *testing.T) { dsPermissions.ErrResult = test.err _, err = extractor.GetAlerts(WithUAEnabled(context.Background(), true), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) assert.Equal(t, err, test.expectedErr) diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index c484c10a23a..3193b54c655 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -4,7 +4,7 @@ import ( "sync" "github.com/grafana/grafana/pkg/components/null" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" ) @@ -47,6 +47,6 @@ type EvalMatch struct { type DashAlertInfo struct { User *user.SignedInUser - Dash *models.Dashboard + Dash *dashboards.Dashboard OrgID int64 } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index e938c8a2785..bed6ad22211 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -225,7 +225,7 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout return err } - renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.Uid, ref.Slug, evalCtx.Rule.OrgID, evalCtx.Rule.PanelID) + renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.UID, ref.Slug, evalCtx.Rule.OrgID, evalCtx.Rule.PanelID) n.log.Debug("Rendering alert panel image", "ruleId", evalCtx.Rule.ID, "urlPath", renderOpts.Path) start := time.Now() diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index 334bac71823..070b8454488 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/validations" @@ -176,7 +177,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext Factory: newTestNotifier, }) - evalCtx.dashboardRef = &models.DashboardRef{Uid: "db-uid"} + evalCtx.dashboardRef = &dashboards.DashboardRef{UID: "db-uid"} store := evalCtx.Store.(*AlertStoreMock) diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 92876853470..274a5bb05f3 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -175,7 +175,7 @@ func (am *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error errCnt := 0 for _, url := range am.URL { - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: strings.TrimSuffix(url, "/") + "/api/v1/alerts", User: am.BasicAuthUser, Password: am.BasicAuthPassword, diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 7767f791df2..d88320f0d12 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -86,7 +86,7 @@ func (dd *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: dd.URL, Body: string(body), } diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index f90d1e12e06..7e7ff16f1d3 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -161,7 +161,7 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { json, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: dn.WebhookURL, HttpMethod: "POST", ContentType: "application/json", @@ -185,7 +185,7 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { return nil } -func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath string, existingJSONBody []byte) error { +func (dn *DiscordNotifier) embedImage(cmd *notifications.SendWebhookSync, imagePath string, existingJSONBody []byte) error { // nolint:gosec // We can ignore the gosec G304 warning on this one because `imagePath` comes // from the alert `evalContext` that generates the images. diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index fb072d59617..69c485cfbf2 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -82,8 +82,8 @@ func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { error = evalContext.Error.Error() } - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := ¬ifications.SendEmailCommandSync{ + SendEmailCommand: notifications.SendEmailCommand{ Subject: evalContext.GetNotificationTitle(), Data: map[string]interface{}{ "Title": evalContext.GetNotificationTitle(), diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go index 90b2f2e1c1f..6eeeda4eb85 100644 --- a/pkg/services/alerting/notifiers/googlechat.go +++ b/pkg/services/alerting/notifiers/googlechat.go @@ -220,7 +220,7 @@ func (gcn *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error { } body, _ := json.Marshal(res1D) - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: gcn.URL, HttpMethod: "POST", HttpHeader: headers, diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index c625c96bdd5..5b8757934f5 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -173,7 +173,7 @@ func (hc *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { hipURL := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", hc.URL, hc.RoomID, hc.APIKey) data, _ := json.Marshal(&body) hc.log.Info("Request payload", "json", string(data)) - cmd := &models.SendWebhookSync{Url: hipURL, Body: string(data)} + cmd := ¬ifications.SendWebhookSync{Url: hipURL, Body: string(data)} if err := hc.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { hc.log.Error("Failed to send hipchat notification", "error", err, "webhook", hc.Name) diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index c9faaea1027..55613d17cbb 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -114,7 +114,7 @@ func (kn *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { topicURL := kn.Endpoint + "/topics/" + kn.Topic - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: topicURL, Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 9e360ecfecd..7ddbb7c0465 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -82,7 +82,7 @@ func (ln *LineNotifier) createAlert(evalContext *alerting.EvalContext) error { form.Add("imageFullsize", evalContext.ImagePublicURL) } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: lineNotifyURL, HttpMethod: "POST", HttpHeader: map[string]string{ diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 8cf3a9ebfe8..4703415f84b 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -195,7 +195,7 @@ func (on *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error body, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: on.APIUrl, Body: string(body), HttpMethod: "POST", @@ -219,7 +219,7 @@ func (on *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error bodyJSON.Set("source", "Grafana") body, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", on.APIUrl, evalContext.Rule.ID), Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 358a23d14ec..7c464a668e0 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -231,7 +231,7 @@ func (pn *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: pagerdutyEventAPIURL, Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 6a6fedce24e..322da15632b 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -280,7 +280,7 @@ func (pn *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: pushoverEndpoint, HttpMethod: "POST", HttpHeader: headers, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index b41cf17ca18..655a3ec9a43 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -138,7 +138,7 @@ func (sn *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { body, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: sn.URL, User: sn.User, Password: sn.Password, diff --git a/pkg/services/alerting/notifiers/sensugo.go b/pkg/services/alerting/notifiers/sensugo.go index 045484be0bb..802681fd647 100644 --- a/pkg/services/alerting/notifiers/sensugo.go +++ b/pkg/services/alerting/notifiers/sensugo.go @@ -188,7 +188,7 @@ func (sn *SensuGoNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.URL, "/"), namespace), Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 86f1b7a4f59..092b366b559 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -415,7 +415,7 @@ func (sn *SlackNotifier) slackFileUpload(evalContext *alerting.EvalContext, log if err != nil { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: "https://slack.com/api/files.upload", Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST", } if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index e85f4a12d82..0add355228d 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -133,7 +133,7 @@ func (tn *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { } data, _ := json.Marshal(&body) - cmd := &models.SendWebhookSync{Url: tn.URL, Body: string(data)} + cmd := ¬ifications.SendWebhookSync{Url: tn.URL, Body: string(data)} if err := tn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { tn.log.Error("Failed to send teams notification", "error", err, "webhook", tn.Name) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index d93d776b853..92b77d27a4d 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -89,7 +89,7 @@ func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecrypt }, nil } -func (tn *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) (*notifications.SendWebhookSync, error) { if sendImageInline { cmd, err := tn.buildMessageInlineImage(evalContext) if err == nil { @@ -102,7 +102,7 @@ func (tn *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, send return tn.buildMessageLinkedImage(evalContext) } -func (tn *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) (*notifications.SendWebhookSync, error) { message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) ruleURL, err := evalContext.GetRuleURL() @@ -132,7 +132,7 @@ func (tn *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalCo }) } -func (tn *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*notifications.SendWebhookSync, error) { var imageFile *os.File var err error @@ -169,7 +169,7 @@ func (tn *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalCo }) } -func (tn *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) (*notifications.SendWebhookSync, error) { var body bytes.Buffer w := multipart.NewWriter(&body) defer func() { @@ -203,7 +203,7 @@ func (tn *TelegramNotifier) generateTelegramCmd(message string, messageField str tn.log.Info("Sending telegram notification", "chat_id", tn.ChatID, "bot_token", tn.BotToken, "apiAction", apiAction) url := fmt.Sprintf(telegramAPIURL, tn.BotToken, apiAction) - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: url, Body: body.String(), HttpMethod: "POST", @@ -260,7 +260,7 @@ func appendIfPossible(tlog log.Logger, message string, extra string, sizeLimit i // Notify send an alert notification to Telegram. func (tn *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { - var cmd *models.SendWebhookSync + var cmd *notifications.SendWebhookSync var err error if evalContext.ImagePublicURL == "" && tn.UploadImage { cmd, err = tn.buildMessage(evalContext, true) diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index 19b7ecd6324..07111646d45 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -152,7 +152,7 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error headers := map[string]string{ "Content-Type": "application/x-www-form-urlencoded", } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: url, Body: body, HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index 9cc1416c911..f2904ca39e6 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -154,7 +154,7 @@ func (vn *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { } data, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{Url: vn.URL, Body: string(data)} + cmd := ¬ifications.SendWebhookSync{Url: vn.URL, Body: string(data)} if err := vn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { vn.log.Error("Failed to send Victorops notification", "error", err, "webhook", vn.Name) diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 5ec1659718b..4baade18f5c 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -145,7 +145,7 @@ func (wn *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { bodyJSON, _ := json.Marshal(body) - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: wn.URL, User: wn.User, Password: wn.Password, diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index 9e3c3e1e4e7..3632d030d5d 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -5,14 +5,14 @@ import ( "fmt" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" ) // AlertTest makes a test alert. func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID int64, user *user.SignedInUser) (*EvalContext, error) { - dash := models.NewDashboardFromJson(dashboard) + dash := dashboards.NewDashboardFromJson(dashboard) dashInfo := DashAlertInfo{ User: user, Dash: dash, diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go index fbe44f9c680..cb752e988ce 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "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/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -61,9 +60,9 @@ func TestIntegrationAnnotations(t *testing.T) { dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService) require.NoError(t, err) - testDashboard1 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard1 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 1", }), @@ -72,9 +71,9 @@ func TestIntegrationAnnotations(t *testing.T) { dashboard, err := dashboardStore.SaveDashboard(context.Background(), testDashboard1) require.NoError(t, err) - testDashboard2 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard2 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 2", }), @@ -85,7 +84,7 @@ func TestIntegrationAnnotations(t *testing.T) { annotation := &annotations.Item{ OrgId: 1, UserId: 1, - DashboardId: dashboard.Id, + DashboardId: dashboard.ID, Text: "hello", Type: "alert", Epoch: 10, @@ -100,7 +99,7 @@ func TestIntegrationAnnotations(t *testing.T) { annotation2 := &annotations.Item{ OrgId: 1, UserId: 1, - DashboardId: dashboard2.Id, + DashboardId: dashboard2.ID, Text: "hello", Type: "alert", Epoch: 21, // Should swap epoch & epochEnd @@ -139,7 +138,7 @@ func TestIntegrationAnnotations(t *testing.T) { t.Run("Can query for annotation by dashboard id", func(t *testing.T) { items, err := repo.Get(context.Background(), &annotations.ItemQuery{ OrgId: 1, - DashboardId: dashboard.Id, + DashboardId: dashboard.ID, From: 0, To: 15, SignedInUser: testUser, @@ -417,7 +416,7 @@ func TestIntegrationAnnotations(t *testing.T) { annotation3 := &annotations.Item{ OrgId: 1, UserId: 1, - DashboardId: dashboard2.Id, + DashboardId: dashboard2.ID, Text: "toBeDeletedWithPanelId", Type: "alert", Epoch: 11, @@ -501,9 +500,9 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService) require.NoError(t, err) - testDashboard1 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard1 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 1", @@ -511,11 +510,11 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { } dashboard, err := dashboardStore.SaveDashboard(context.Background(), testDashboard1) require.NoError(t, err) - dash1UID := dashboard.Uid + dash1UID := dashboard.UID - testDashboard2 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard2 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 2", }), diff --git a/pkg/services/apikey/apikeyimpl/sqlx_store.go b/pkg/services/apikey/apikeyimpl/sqlx_store.go index b9935a58123..7476976f496 100644 --- a/pkg/services/apikey/apikeyimpl/sqlx_store.go +++ b/pkg/services/apikey/apikeyimpl/sqlx_store.go @@ -161,7 +161,7 @@ func (ss *sqlxStore) Count(ctx context.Context, scopeParams *quota.ScopeParamete u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.sess.Get(ctx, &r, `SELECT COUNT(*) AS count FROM api_key WHERE org_id = ?`, scopeParams.OrgID); err != nil { return u, err } else { diff --git a/pkg/services/apikey/apikeyimpl/xorm_store.go b/pkg/services/apikey/apikeyimpl/xorm_store.go index bf2ba4ce6d4..c777b659346 100644 --- a/pkg/services/apikey/apikeyimpl/xorm_store.go +++ b/pkg/services/apikey/apikeyimpl/xorm_store.go @@ -200,7 +200,7 @@ func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := "SELECT COUNT(*) AS count FROM api_key WHERE org_id = ?" if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil { diff --git a/pkg/services/auth/jwt/auth.go b/pkg/services/auth/jwt/auth.go index 469eb6ce129..28adad3a2ff 100644 --- a/pkg/services/auth/jwt/auth.go +++ b/pkg/services/auth/jwt/auth.go @@ -6,11 +6,12 @@ import ( "errors" "strings" + "gopkg.in/square/go-jose.v2/jwt" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - "gopkg.in/square/go-jose.v2/jwt" ) const ServiceName = "AuthService" @@ -102,3 +103,19 @@ func (s *AuthService) Verify(ctx context.Context, strToken string) (models.JWTCl return claims, nil } + +// HasSubClaim checks if the provided JWT token contains a non-empty "sub" claim. +// Returns true if it contains, otherwise returns false. +func HasSubClaim(jwtToken string) bool { + parsed, err := jwt.ParseSigned(sanitizeJWT(jwtToken)) + if err != nil { + return false + } + + var claims jwt.Claims + if err := parsed.UnsafeClaimsWithoutVerification(&claims); err != nil { + return false + } + + return claims.Subject != "" +} diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index a6394b77308..ce6e9f20a47 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -24,6 +24,8 @@ const ( ClientJWT = "auth.client.jwt" ClientRender = "auth.client.render" ClientSession = "auth.client.session" + ClientForm = "auth.client.form" + ClientProxy = "auth.client.proxy" ) const ( @@ -71,6 +73,10 @@ type PasswordClient interface { AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error) } +type ProxyClient interface { + AuthenticateProxy(ctx context.Context, r *Request, username string, additional map[string]string) (*Identity, error) +} + type Request struct { // OrgID will be populated by authn.Service OrgID int64 diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 37dcc4b324d..01c30af77d5 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -40,6 +40,7 @@ func ProvideService( accessControlService accesscontrol.Service, apikeyService apikey.Service, userService user.Service, jwtService auth.JWTVerifierService, + userProtectionService login.UserProtectionService, loginAttempts loginattempt.Service, quotaService quota.Service, authInfoService login.AuthInfoService, renderService rendering.Service, ) *Service { @@ -63,19 +64,39 @@ func ProvideService( s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService) } + var proxyClients []authn.ProxyClient var passwordClients []authn.PasswordClient + if s.cfg.LDAPEnabled { + ldap := clients.ProvideLDAP(cfg) + proxyClients = append(proxyClients, ldap) + passwordClients = append(passwordClients, ldap) + } if !s.cfg.DisableLogin { - passwordClients = append(passwordClients, clients.ProvideGrafana(userService)) + grafana := clients.ProvideGrafana(cfg, userService) + proxyClients = append(proxyClients, grafana) + passwordClients = append(passwordClients, grafana) } - if s.cfg.LDAPEnabled { - passwordClients = append(passwordClients, clients.ProvideLDAP(cfg)) + // if we have password clients configure check if basic auth or form auth is enabled + if len(passwordClients) > 0 { + passwordClient := clients.ProvidePassword(loginAttempts, passwordClients...) + if s.cfg.BasicAuthEnabled { + s.clients[authn.ClientBasic] = clients.ProvideBasic(passwordClient) + } + // FIXME (kalleep): Remove the global variable and stick it into cfg + if !setting.DisableLoginForm { + s.clients[authn.ClientForm] = clients.ProvideForm(passwordClient) + } } - // only configure basic auth client if it is enabled, and we have at least one password client enabled - if s.cfg.BasicAuthEnabled && len(passwordClients) > 0 { - s.clients[authn.ClientBasic] = clients.ProvideBasic(loginAttempts, passwordClients...) + if s.cfg.AuthProxyEnabled && len(proxyClients) > 0 { + proxy, err := clients.ProvideProxy(cfg, proxyClients...) + if err != nil { + s.log.Error("failed to configure auth proxy", "err", err) + } else { + s.clients[authn.ClientProxy] = proxy + } } if s.cfg.JWTAuthEnabled { @@ -83,10 +104,12 @@ func ProvideService( } // FIXME (jguer): move to User package - userSyncService := sync.ProvideUserSync(userService, authInfoService, quotaService) + userSyncService := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService) orgUserSyncService := sync.ProvideOrgSync(userService, orgService, accessControlService) s.RegisterPostAuthHook(userSyncService.SyncUser) s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser) + s.RegisterPostAuthHook(sync.ProvideUserLastSeenSync(userService).SyncLastSeen) + s.RegisterPostAuthHook(sync.ProvideAPIKeyLastSeenSync(apikeyService).SyncLastSeen) return s } @@ -127,6 +150,8 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ return nil, true, err } + // FIXME (kalleep): Handle disabled identities + for _, hook := range s.postAuthHooks { if err := hook(ctx, identity, r); err != nil { s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity) diff --git a/pkg/services/authn/authnimpl/usersync/apikey_last_seen_sync.go b/pkg/services/authn/authnimpl/usersync/apikey_last_seen_sync.go new file mode 100644 index 00000000000..293fc3e88bf --- /dev/null +++ b/pkg/services/authn/authnimpl/usersync/apikey_last_seen_sync.go @@ -0,0 +1,38 @@ +package usersync + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apikey" + "github.com/grafana/grafana/pkg/services/authn" +) + +func ProvideAPIKeyLastSeenSync(service apikey.Service) *APIKeyLastSeenSync { + return &APIKeyLastSeenSync{log.New("apikeylastseen.sync"), service} +} + +type APIKeyLastSeenSync struct { + log log.Logger + service apikey.Service +} + +func (s *APIKeyLastSeenSync) SyncLastSeen(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + namespace, id := identity.NamespacedID() + if namespace != authn.NamespaceAPIKey { + return nil + } + + go func(apikeyID int64) { + defer func() { + if err := recover(); err != nil { + s.log.Error("panic during user last seen sync", "err", err) + } + }() + if err := s.service.UpdateAPIKeyLastUsedDate(context.Background(), apikeyID); err != nil { + s.log.Warn("failed to update last use date for api key", "id", apikeyID) + } + }(id) + + return nil +} diff --git a/pkg/services/authn/authnimpl/usersync/orgsync.go b/pkg/services/authn/authnimpl/usersync/orgsync.go index e51745f29e8..2c24e0438e0 100644 --- a/pkg/services/authn/authnimpl/usersync/orgsync.go +++ b/pkg/services/authn/authnimpl/usersync/orgsync.go @@ -3,10 +3,8 @@ package usersync import ( "context" "errors" - "fmt" "sort" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authn" @@ -28,26 +26,27 @@ type OrgSync struct { func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn.Request) error { if !id.ClientParams.SyncUser { - s.log.Debug("Not syncing org user", "auth_module", id.AuthModule, "auth_id", id.AuthID) return nil } namespace, userID := id.NamespacedID() - if namespace != "user" && userID <= 0 { - return fmt.Errorf("invalid namespace %q for user ID %q", namespace, userID) + if namespace != "user" || userID <= 0 { + s.log.Warn("invalid namespace %q for user ID %q", namespace, userID) + return nil } - s.log.Debug("Syncing organization roles", "id", userID, "extOrgRoles", id.OrgRoles) + s.log.Debug("syncing organization roles", "id", userID, "extOrgRoles", id.OrgRoles) // don't sync org roles if none is specified if len(id.OrgRoles) == 0 { - s.log.Debug("Not syncing organization roles since external user doesn't have any") + s.log.Debug("not syncing organization roles since external user doesn't have any") return nil } orgsQuery := &org.GetUserOrgListQuery{UserID: userID} result, err := s.orgService.GetUserOrgList(ctx, orgsQuery) if err != nil { - return err + s.log.Error("failed to get user's organizations", "userId", userID, "error", err) + return nil } handledOrgIds := map[int64]bool{} @@ -64,7 +63,8 @@ func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn. // update role cmd := &org.UpdateOrgUserCommand{OrgID: orga.OrgID, UserID: userID, Role: extRole} if err := s.orgService.UpdateOrgUser(ctx, cmd); err != nil { - return err + s.log.Error("failed to update active org user", "userId", userID, "error", err) + return nil } } } @@ -81,7 +81,8 @@ func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn. cmd := &org.AddOrgUserCommand{UserID: userID, Role: orgRole, OrgID: orgId} err := s.orgService.AddOrgUser(ctx, cmd) if err != nil && !errors.Is(err, org.ErrOrgNotFound) { - return err + s.log.Error("failed to update active org user", "userId", userID, "error", err) + return nil } } @@ -92,16 +93,17 @@ func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn. cmd := &org.RemoveOrgUserCommand{OrgID: orgId, UserID: userID} if err := s.orgService.RemoveOrgUser(ctx, cmd); err != nil { if errors.Is(err, org.ErrLastOrgAdmin) { - logger.Error(err.Error(), "userId", cmd.UserID, "orgId", cmd.OrgID) + s.log.Error(err.Error(), "userId", cmd.UserID, "orgId", cmd.OrgID) continue } - return err + s.log.Error("failed to delete user org membership", "userId", userID, "error", err) + return nil } if err := s.accessControl.DeleteUserPermissions(ctx, orgId, cmd.UserID); err != nil { - logger.Error("failed to delete permissions for user", "error", err, "userID", cmd.UserID, "orgID", orgId) - return err + s.log.Error("failed to delete permissions for user", "error", err, "userID", cmd.UserID, "orgID", orgId) + return nil } } diff --git a/pkg/services/authn/authnimpl/usersync/user_last_seen_sync.go b/pkg/services/authn/authnimpl/usersync/user_last_seen_sync.go new file mode 100644 index 00000000000..881cd18e333 --- /dev/null +++ b/pkg/services/authn/authnimpl/usersync/user_last_seen_sync.go @@ -0,0 +1,50 @@ +package usersync + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/user" +) + +func ProvideUserLastSeenSync(service user.Service) *UserLastSeenSync { + return &UserLastSeenSync{log.New("userlastseen.sync"), service} +} + +type UserLastSeenSync struct { + log log.Logger + service user.Service +} + +func (s *UserLastSeenSync) SyncLastSeen(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + namespace, id := identity.NamespacedID() + + if namespace != authn.NamespaceUser && namespace != authn.NamespaceServiceAccount { + // skip sync + return nil + } + + if !shouldUpdateLastSeen(identity.LastSeenAt) { + return nil + } + + go func(userID int64) { + defer func() { + if err := recover(); err != nil { + s.log.Error("panic during user last seen sync", "err", err) + } + }() + + if err := s.service.UpdateLastSeenAt(context.Background(), &user.UpdateUserLastSeenAtCommand{UserID: userID}); err != nil { + s.log.Error("failed to update last_seen_at", "err", err, "userId", userID) + } + }(id) + + return nil +} + +func shouldUpdateLastSeen(t time.Time) bool { + return time.Since(t) > time.Minute*5 +} diff --git a/pkg/services/authn/authnimpl/usersync/usersync.go b/pkg/services/authn/authnimpl/usersync/usersync.go index 2fd6803440a..a292f6c2f2d 100644 --- a/pkg/services/authn/authnimpl/usersync/usersync.go +++ b/pkg/services/authn/authnimpl/usersync/usersync.go @@ -9,59 +9,112 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util/errutil" ) -func ProvideUserSync(userService user.Service, authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync { - return &UserSync{userService, authInfoService, quotaService, log.New("user.sync")} +var ( + errSyncUserForbidden = errutil.NewBase(errutil.StatusForbidden, + "user.sync.forbidden", errutil.WithPublicMessage("User sync forbidden")) + errSyncUserInternal = errutil.NewBase(errutil.StatusInternal, + "user.sync.forbidden", errutil.WithPublicMessage("User sync failed")) + errUserProtection = errutil.NewBase(errutil.StatusForbidden, + "user.sync.protectedrole", errutil.WithPublicMessage("Unable to sync due to protected role")) +) + +func ProvideUserSync(userService user.Service, + userProtectionService login.UserProtectionService, + authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync { + return &UserSync{ + userService: userService, + authInfoService: authInfoService, + userProtectionService: userProtectionService, + quotaService: quotaService, + log: log.New("user.sync"), + } } type UserSync struct { - userService user.Service - authInfoService login.AuthInfoService - quotaService quota.Service - log log.Logger + userService user.Service + authInfoService login.AuthInfoService + userProtectionService login.UserProtectionService + quotaService quota.Service + log log.Logger } // SyncUser syncs a user with the database func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Request) error { if !id.ClientParams.SyncUser { - s.log.Debug("Not syncing user", "auth_module", id.AuthModule, "auth_id", id.AuthID) return nil } // Does user exist in the database? usr, errUserInDB := s.UserInDB(ctx, &id.AuthModule, &id.AuthID, id.ClientParams.LookUpParams) if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) { - return errUserInDB + s.log.Error("error retrieving user", "error", errUserInDB, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + "lookup_params", id.ClientParams.LookUpParams, + ) + return errSyncUserInternal.Errorf("unable to retrieve user") } if errors.Is(errUserInDB, user.ErrUserNotFound) { if !id.ClientParams.AllowSignUp { - s.log.Warn("Not allowing login, user not found in internal user database and allow signup = false", + s.log.Warn("not allowing login, user not found in internal user database and allow signup = false", "auth_module", id.AuthModule) - return login.ErrSignupNotAllowed + return errSyncUserForbidden.Errorf("%w", login.ErrSignupNotAllowed) + } + + // quota check (FIXME: (jguer) this should be done in the user service) + // we may insert in both user and org_user tables + // therefore we need to query check quota for both user and org services + for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} { + limitReached, errLimit := s.quotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil) + if errLimit != nil { + s.log.Error("error getting user quota", "error", errLimit) + return errSyncUserInternal.Errorf("%w", login.ErrGettingUserQuota) + } + if limitReached { + return errSyncUserForbidden.Errorf("%w", login.ErrUsersQuotaReached) + } } // create user var errCreate error usr, errCreate = s.createUser(ctx, id) if errCreate != nil { - return errCreate + s.log.Error("error creating user", "error", errCreate, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + "id_login", id.Login, "id_email", id.Email, + ) + return errSyncUserInternal.Errorf("unable to create user") } } + if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthModule); errProtection != nil { + return errUserProtection.Errorf("user mapping not allowed: %w", errProtection) + } + // update user if errUpdate := s.updateUserAttributes(ctx, usr, id); errUpdate != nil { - return errUpdate + s.log.Error("error creating user", "error", errUpdate, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + "login", usr.Login, "email", usr.Email, + "id_login", id.Login, "id_email", id.Email, + ) + return errSyncUserInternal.Errorf("unable to update user") } syncUserToIdentity(usr, id) // persist latest auth info token if errAuthInfo := s.updateAuthInfo(ctx, id); errAuthInfo != nil { - return errAuthInfo + s.log.Error("error creating user", "error", errAuthInfo, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + ) + return errSyncUserInternal.Errorf("unable to update auth info") } return nil diff --git a/pkg/services/authn/authnimpl/usersync/usersync_test.go b/pkg/services/authn/authnimpl/usersync/usersync_test.go index 634f6b3c93d..f836bd01071 100644 --- a/pkg/services/authn/authnimpl/usersync/usersync_test.go +++ b/pkg/services/authn/authnimpl/usersync/usersync_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -29,6 +29,8 @@ func ptrInt64(i int64) *int64 { } func TestUserSync_SyncUser(t *testing.T) { + userProtection := &authinfoservice.OSSUserProtectionImpl{} + authFakeNil := &logintest.AuthInfoServiceFake{ ExpectedUser: nil, ExpectedError: user.ErrUserNotFound, @@ -82,7 +84,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService user.Service authInfoService login.AuthInfoService quotaService quota.Service - log log.Logger } type args struct { ctx context.Context @@ -101,7 +102,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -140,7 +140,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -182,7 +181,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -224,7 +222,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -266,7 +263,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeUserID, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -309,7 +305,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -338,7 +333,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userServiceNil, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -389,7 +383,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userServiceMod, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -433,12 +426,7 @@ func TestUserSync_SyncUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := &UserSync{ - userService: tt.fields.userService, - authInfoService: tt.fields.authInfoService, - quotaService: tt.fields.quotaService, - log: tt.fields.log, - } + s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService) err := s.SyncUser(tt.args.ctx, tt.args.id, nil) if tt.wantErr { require.Error(t, err) diff --git a/pkg/services/authn/authntest/mock.go b/pkg/services/authn/authntest/mock.go index a3e35ec6770..7bb7f1c92df 100644 --- a/pkg/services/authn/authntest/mock.go +++ b/pkg/services/authn/authntest/mock.go @@ -26,3 +26,16 @@ func (m MockClient) Test(ctx context.Context, r *authn.Request) bool { } return false } + +var _ authn.ProxyClient = new(MockProxyClient) + +type MockProxyClient struct { + AuthenticateProxyFunc func(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) +} + +func (m MockProxyClient) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + if m.AuthenticateProxyFunc != nil { + return m.AuthenticateProxyFunc(ctx, r, username, additional) + } + return nil, nil +} diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index ab684a5672d..958cdf7fb63 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -56,17 +56,6 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide return nil, errAPIKeyRevoked.Errorf("Api key is revoked") } - go func(id int64) { - defer func() { - if err := recover(); err != nil { - s.log.Error("api key authentication panic", "err", err) - } - }() - if err := s.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil { - s.log.Warn("failed to update last use date for api key", "id", id) - } - }(apiKey.Id) - // if the api key don't belong to a service account construct the identity and return it if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 { return &authn.Identity{ diff --git a/pkg/services/authn/clients/basic.go b/pkg/services/authn/clients/basic.go index e3fcf608908..4c8c0c529cf 100644 --- a/pkg/services/authn/clients/basic.go +++ b/pkg/services/authn/clients/basic.go @@ -2,30 +2,25 @@ package clients import ( "context" - "errors" "strings" "github.com/grafana/grafana/pkg/services/authn" - "github.com/grafana/grafana/pkg/services/loginattempt" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" - "github.com/grafana/grafana/pkg/web" ) var ( errDecodingBasicAuthHeader = errutil.NewBase(errutil.StatusBadRequest, "basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header")) - errBasicAuthCredentials = errutil.NewBase(errutil.StatusUnauthorized, "basic-auth.invalid-credentials", errutil.WithPublicMessage("Invalid username or password")) ) var _ authn.Client = new(Basic) -func ProvideBasic(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Basic { - return &Basic{clients, loginAttempts} +func ProvideBasic(client authn.PasswordClient) *Basic { + return &Basic{client} } type Basic struct { - clients []authn.PasswordClient - loginAttempts loginattempt.Service + client authn.PasswordClient } func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { @@ -34,44 +29,10 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden return nil, errDecodingBasicAuthHeader.Errorf("failed to decode basic auth header: %w", err) } - r.SetMeta(authn.MetaKeyUsername, username) - - ok, err := c.loginAttempts.Validate(ctx, username) - if err != nil { - return nil, err - } - if !ok { - return nil, errBasicAuthCredentials.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") - } - - if len(password) == 0 { - return nil, errBasicAuthCredentials.Errorf("no password provided") - } - - for _, pwClient := range c.clients { - identity, err := pwClient.AuthenticatePassword(ctx, r, username, password) - if err != nil { - if errors.Is(err, errIdentityNotFound) { - // continue to next password client if identity could not be found - continue - } - if errors.Is(err, errInvalidPassword) { - // only add login attempt if identity was found but the provided password was invalid - _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) - } - return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity: %w", err) - } - - return identity, nil - } - - return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity using basic auth") + return c.client.AuthenticatePassword(ctx, r, username, password) } func (c *Basic) Test(ctx context.Context, r *authn.Request) bool { - if len(c.clients) == 0 { - return false - } return looksLikeBasicAuthRequest(r) } diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index 1f61ade0dc7..81b974ee20d 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" - "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "github.com/stretchr/testify/assert" ) @@ -15,8 +14,7 @@ func TestBasic_Authenticate(t *testing.T) { type TestCase struct { desc string req *authn.Request - blockLogin bool - clients []authn.PasswordClient + client authn.PasswordClient expectedErr error expectedIdentity *authn.Identity } @@ -25,40 +23,19 @@ func TestBasic_Authenticate(t *testing.T) { { desc: "should success when password client return identity", req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}}, + client: authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}, expectedIdentity: &authn.Identity{ID: "user:1"}, }, { - desc: "should success when found in second client", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:2"}}}, - expectedIdentity: &authn.Identity{ID: "user:2"}, - }, - { - desc: "should fail for empty password", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}}, - expectedErr: errBasicAuthCredentials, - }, - { - desc: "should if login is blocked by to many attempts", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}}, - blockLogin: true, - expectedErr: errBasicAuthCredentials, - }, - { - desc: "should fail when not found in any clients", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}}, - expectedErr: errBasicAuthCredentials, + desc: "should fail when basic auth header could not be decoded", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {}}}}, + expectedErr: errDecodingBasicAuthHeader, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideBasic( - loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, - tt.clients..., - ) + c := ProvideBasic(tt.client) identity, err := c.Authenticate(context.Background(), tt.req) if tt.expectedErr != nil { @@ -74,10 +51,9 @@ func TestBasic_Authenticate(t *testing.T) { func TestBasic_Test(t *testing.T) { type TestCase struct { - desc string - req *authn.Request - noClients bool - expected bool + desc string + req *authn.Request + expected bool } tests := []TestCase{ @@ -92,18 +68,6 @@ func TestBasic_Test(t *testing.T) { }, expected: true, }, - { - desc: "should fail when no password client is configured", - req: &authn.Request{ - HTTPRequest: &http.Request{ - Header: map[string][]string{ - authorizationHeaderName: {encodeBasicAuth("user", "password")}, - }, - }, - }, - noClients: true, - expected: false, - }, { desc: "should fail when no http request is passed", req: &authn.Request{}, @@ -124,10 +88,7 @@ func TestBasic_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideBasic(loginattempttest.FakeLoginAttemptService{}, authntest.FakePasswordClient{}) - if tt.noClients { - c.clients = nil - } + c := ProvideBasic(authntest.FakePasswordClient{}) assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) }) } diff --git a/pkg/services/authn/clients/constants.go b/pkg/services/authn/clients/constants.go index d0c2f477f31..0d8734c61fa 100644 --- a/pkg/services/authn/clients/constants.go +++ b/pkg/services/authn/clients/constants.go @@ -10,5 +10,4 @@ const ( var ( errIdentityNotFound = errutil.NewBase(errutil.StatusNotFound, "identity.not-found") - errInvalidPassword = errutil.NewBase(errutil.StatusBadRequest, "identity.invalid-password", errutil.WithPublicMessage("Invalid password or username")) ) diff --git a/pkg/services/authn/clients/form.go b/pkg/services/authn/clients/form.go new file mode 100644 index 00000000000..f8e4de8bdb0 --- /dev/null +++ b/pkg/services/authn/clients/form.go @@ -0,0 +1,42 @@ +package clients + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/web" +) + +var ( + errBadForm = errutil.NewBase(errutil.StatusBadRequest, "form-auth.invalid", errutil.WithPublicMessage("bad login data")) +) + +var _ authn.Client = new(Form) + +func ProvideForm(client authn.PasswordClient) *Form { + return &Form{client} +} + +type Form struct { + client authn.PasswordClient +} + +type loginForm struct { + Username string `json:"user" binding:"Required"` + Password string `json:"password" binding:"Required"` +} + +func (f *Form) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + form := loginForm{} + if err := web.Bind(r.HTTPRequest, &form); err != nil { + return nil, errBadForm.Errorf("failed to parse request: %w", err) + } + return f.client.AuthenticatePassword(ctx, r, form.Username, form.Password) +} + +func (f *Form) Test(ctx context.Context, r *authn.Request) bool { + // FIXME: How should we detect this?? + // Maybe create client test interface and not all clients has to implement this?? + return true +} diff --git a/pkg/services/authn/clients/form_test.go b/pkg/services/authn/clients/form_test.go new file mode 100644 index 00000000000..6c3c0b30f9e --- /dev/null +++ b/pkg/services/authn/clients/form_test.go @@ -0,0 +1,48 @@ +package clients + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" +) + +func TestForm_Authenticate(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + expectedErr error + } + + tests := []testCase{ + { + desc: "should success on valid request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"user": "test", "password": "test"}`)), + }}, + }, + { + desc: "should return error for bad request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{}`)), + }}, + expectedErr: errBadForm, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvideForm(&authntest.FakePasswordClient{}) + _, err := c.Authenticate(context.Background(), tt.req) + assert.ErrorIs(t, err, tt.expectedErr) + }) + } +} diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index a14be60f163..ca830973627 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -4,23 +4,88 @@ import ( "context" "crypto/subtle" "errors" + "net/mail" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) +var _ authn.ProxyClient = new(Grafana) var _ authn.PasswordClient = new(Grafana) -func ProvideGrafana(userService user.Service) *Grafana { - return &Grafana{userService} +func ProvideGrafana(cfg *setting.Cfg, userService user.Service) *Grafana { + return &Grafana{cfg, userService} } type Grafana struct { + cfg *setting.Cfg userService user.Service } -func (c Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { +func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + identity := &authn.Identity{ + AuthModule: login.AuthProxyAuthModule, + AuthID: username, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: c.cfg.AuthProxyAutoSignUp, + }, + } + + switch c.cfg.AuthProxyHeaderProperty { + case "username": + identity.Login = username + addr, err := mail.ParseAddress(username) + if err == nil { + identity.Email = addr.Address + } + case "email": + identity.Login = username + identity.Email = username + default: + return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxyHeaderProperty) + } + + if v, ok := additional[proxyFieldName]; ok { + identity.Name = v + } + + if v, ok := additional[proxyFieldEmail]; ok { + identity.Email = v + } + + if v, ok := additional[proxyFieldLogin]; ok { + identity.Login = v + } + + if v, ok := additional[proxyFieldRole]; ok { + role := org.RoleType(v) + if role.IsValid() { + orgID := int64(1) + if c.cfg.AutoAssignOrg && c.cfg.AutoAssignOrgId > 0 { + orgID = int64(c.cfg.AutoAssignOrgId) + } + identity.OrgID = orgID + identity.OrgRoles = map[int64]org.RoleType{orgID: role} + } + } + + if v, ok := additional[proxyFieldGroups]; ok { + identity.Groups = util.SplitString(v) + } + + identity.ClientParams.LookUpParams.Email = &identity.Email + identity.ClientParams.LookUpParams.Login = &identity.Login + + return identity, nil +} + +func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username}) if err != nil { if errors.Is(err, user.ErrUserNotFound) { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index 4d17bfa8795..918888f5a23 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -2,16 +2,125 @@ package clients import ( "context" + "net/http" "testing" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/assert" ) +func TestGrafana_AuthenticateProxy(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + username string + proxyProperty string + additional map[string]string + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []testCase{ + { + desc: "expect valid identity", + username: "test", + req: &authn.Request{HTTPRequest: &http.Request{}}, + proxyProperty: "username", + additional: map[string]string{ + proxyFieldName: "name", + proxyFieldRole: "Viewer", + proxyFieldGroups: "grp1,grp2", + proxyFieldEmail: "email@email.com", + }, + expectedIdentity: &authn.Identity{ + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + Login: "test", + Name: "name", + Email: "email@email.com", + AuthModule: "authproxy", + AuthID: "test", + Groups: []string{"grp1", "grp2"}, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: true, + LookUpParams: models.UserLookupParams{ + Email: strPtr("email@email.com"), + Login: strPtr("test"), + }, + }, + }, + }, + { + desc: "should set email as both email and login when configured proxy auth header property is email", + username: "test@test.com", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}}, + additional: map[string]string{}, + expectedIdentity: &authn.Identity{ + Login: "test@test.com", + Email: "test@test.com", + AuthModule: "authproxy", + AuthID: "test@test.com", + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: true, + LookUpParams: models.UserLookupParams{ + Email: strPtr("test@test.com"), + Login: strPtr("test@test.com"), + }, + }, + }, + proxyProperty: "email", + }, + { + desc: "should return error on invalid auth proxy header property", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}}, + proxyProperty: "other", + expectedErr: errInvalidProxyHeader, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.AuthProxyAutoSignUp = true + cfg.AuthProxyHeaderProperty = tt.proxyProperty + c := ProvideGrafana(cfg, usertest.NewUserServiceFake()) + + identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional) + assert.ErrorIs(t, err, tt.expectedErr) + if tt.expectedIdentity != nil { + assert.Equal(t, tt.expectedIdentity.OrgID, identity.OrgID) + assert.Equal(t, tt.expectedIdentity.Login, identity.Login) + assert.Equal(t, tt.expectedIdentity.Name, identity.Name) + assert.Equal(t, tt.expectedIdentity.Email, identity.Email) + assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID) + assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule) + assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups) + + assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser) + assert.Equal(t, tt.expectedIdentity.ClientParams.AllowSignUp, identity.ClientParams.AllowSignUp) + assert.Equal(t, tt.expectedIdentity.ClientParams.SyncTeamMembers, identity.ClientParams.SyncTeamMembers) + assert.Equal(t, tt.expectedIdentity.ClientParams.EnableDisabledUsers, identity.ClientParams.EnableDisabledUsers) + + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Email, identity.ClientParams.LookUpParams.Email) + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Login, identity.ClientParams.LookUpParams.Login) + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.UserID, identity.ClientParams.LookUpParams.UserID) + } else { + assert.Nil(t, tt.expectedIdentity) + } + }) + } +} + func TestGrafana_AuthenticatePassword(t *testing.T) { type testCase struct { desc string @@ -60,7 +169,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { userService.ExpectedError = user.ErrUserNotFound } - c := ProvideGrafana(userService) + c := ProvideGrafana(setting.NewCfg(), userService) identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password) assert.ErrorIs(t, err, tt.expectedErr) assert.EqualValues(t, tt.expectedIdentity, identity) diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 4d4f5256918..5c3a23e523f 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/auth" + authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -143,21 +144,14 @@ func (s *JWT) Test(ctx context.Context, r *authn.Request) bool { return false } - // The header is Authorization and the token does not look like a JWT, - // this is likely an API key. Pass it on. - if s.cfg.JWTAuthHeaderName == "Authorization" && !looksLikeJWT(jwtToken) { + // If the "sub" claim is missing or empty then pass the control to the next handler + if !authJWT.HasSubClaim(jwtToken) { return false } return true } -func looksLikeJWT(token string) bool { - // A JWT must have 3 parts separated by `.`. - parts := strings.Split(token, ".") - return len(parts) == 3 -} - const roleGrafanaAdmin = "GrafanaAdmin" func (s *JWT) extractRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) { diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 7b8cea2e86f..c511ac2fe4d 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -7,12 +7,13 @@ import ( "net/url" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func stringPtr(s string) *string { @@ -87,8 +88,13 @@ func TestAuthenticateJWT(t *testing.T) { func TestJWTTest(t *testing.T) { jwtService := &models.FakeJWTService{} jwtHeaderName := "X-Forwarded-User" - validFormatToken := "sample.token.valid" + // #nosec G101 -- This is dummy/test token + validFormatToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" invalidFormatToken := "sampletokeninvalid" + // #nosec G101 -- This is dummy/test token + missingSubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.8nYFUX869Y1mnDDDU4yL11aANgVRuifoxrE8BHZY1iE" + // #nosec G101 -- This is dummy/test token + emptySubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIiLCJpYXQiOjE1MTYyMzkwMjJ9.tnwtOHK58d47dO4DHW4b9MzeToxa1kGiko5Oo887Rqc" type testCase struct { desc string @@ -144,6 +150,20 @@ func TestJWTTest(t *testing.T) { token: validFormatToken, want: false, }, + { + desc: "token without a sub claim", + reqHeaderName: "Authorization", + cfgHeaderName: "Authorization", + token: missingSubToken, + want: false, + }, + { + desc: "token with an empty sub claim", + reqHeaderName: "Authorization", + cfgHeaderName: "Authorization", + token: emptySubToken, + want: false, + }, } for _, tc := range testCases { diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 364be4bb65b..0e8f14a5be0 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +var _ authn.ProxyClient = new(LDAP) var _ authn.PasswordClient = new(LDAP) func ProvideLDAP(cfg *setting.Cfg) *LDAP { @@ -21,6 +22,19 @@ type LDAP struct { service ldapService } +func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, _ map[string]string) (*authn.Identity, error) { + info, err := c.service.User(username) + if errors.Is(err, multildap.ErrDidNotFindUser) { + return nil, errIdentityNotFound.Errorf("no user found: %w", err) + } + + if err != nil { + return nil, err + } + + return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil +} + func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { info, err := c.service.Login(&models.LoginUserQuery{ Username: username, @@ -28,6 +42,7 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern }) if errors.Is(err, multildap.ErrCouldNotFindUser) { + // FIXME: disable user in grafana if not found return nil, errIdentityNotFound.Errorf("no user found: %w", err) } @@ -35,7 +50,6 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern r.SetMeta(authn.MetaKeyAuthModule, "ldap") if errors.Is(err, multildap.ErrInvalidCredentials) { - // FIXME: disable user in grafana if not found return nil, errInvalidPassword.Errorf("invalid password: %w", err) } @@ -43,31 +57,12 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern return nil, err } - return &authn.Identity{ - OrgID: r.OrgID, - OrgRoles: info.OrgRoles, - Login: info.Login, - Name: info.Name, - Email: info.Email, - IsGrafanaAdmin: info.IsGrafanaAdmin, - AuthModule: info.AuthModule, - AuthID: info.AuthId, - Groups: info.Groups, - ClientParams: authn.ClientParams{ - SyncUser: true, - SyncTeamMembers: true, - AllowSignUp: c.cfg.LDAPAllowSignup, - EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ - Login: &info.Login, - Email: &info.Email, - }, - }, - }, nil + return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil } type ldapService interface { Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) + User(username string) (*models.ExternalUserInfo, error) } // FIXME: remove the implementation if we convert ldap to an actual service @@ -83,3 +78,37 @@ func (s *ldapServiceImpl) Login(query *models.LoginUserQuery) (*models.ExternalU return multildap.New(cfg.Servers).Login(query) } + +func (s *ldapServiceImpl) User(username string) (*models.ExternalUserInfo, error) { + cfg, err := multildap.GetConfig(s.cfg) + if err != nil { + return nil, err + } + + user, _, err := multildap.New(cfg.Servers).User(username) + return user, err +} + +func identityFromLDAPInfo(orgID int64, info *models.ExternalUserInfo, allowSignup bool) *authn.Identity { + return &authn.Identity{ + OrgID: orgID, + OrgRoles: info.OrgRoles, + Login: info.Login, + Name: info.Name, + Email: info.Email, + IsGrafanaAdmin: info.IsGrafanaAdmin, + AuthModule: info.AuthModule, + AuthID: info.AuthId, + Groups: info.Groups, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: allowSignup, + EnableDisabledUsers: true, + LookUpParams: models.UserLookupParams{ + Login: &info.Login, + Email: &info.Email, + }, + }, + } +} diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index 91971225519..e59516f2cf1 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -8,11 +8,74 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" ) +func TestLDAP_AuthenticateProxy(t *testing.T) { + type testCase struct { + desc string + username string + expectedLDAPErr error + expectedLDAPInfo *models.ExternalUserInfo + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []testCase{ + { + desc: "should return valid identity when found by ldap service", + username: "test", + expectedLDAPInfo: &models.ExternalUserInfo{ + AuthModule: login.LDAPAuthModule, + AuthId: "123", + Email: "test@test.com", + Login: "test", + Name: "test test", + Groups: []string{"1", "2"}, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + }, + expectedIdentity: &authn.Identity{ + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + Login: "test", + Name: "test test", + Email: "test@test.com", + AuthModule: login.LDAPAuthModule, + AuthID: "123", + Groups: []string{"1", "2"}, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: false, + EnableDisabledUsers: true, + LookUpParams: models.UserLookupParams{ + Email: strPtr("test@test.com"), + Login: strPtr("test"), + }, + }, + }, + }, + { + desc: "should return error when user is not found", + username: "test", + expectedLDAPErr: multildap.ErrDidNotFindUser, + expectedErr: errIdentityNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedLDAPInfo, ExpectedErr: tt.expectedLDAPErr}} + identity, err := c.AuthenticateProxy(context.Background(), &authn.Request{OrgID: 1}, tt.username, nil) + assert.ErrorIs(t, err, tt.expectedErr) + assert.EqualValues(t, tt.expectedIdentity, identity) + }) + } +} + func TestLDAP_AuthenticatePassword(t *testing.T) { type testCase struct { desc string @@ -20,7 +83,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { password string expectedErr error expectedLDAPErr error - expectedInfo *models.ExternalUserInfo + expectedLDAPInfo *models.ExternalUserInfo expectedIdentity *authn.Identity } @@ -29,7 +92,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { desc: "should successfully authenticate with correct username and password", username: "test", password: "test123", - expectedInfo: &models.ExternalUserInfo{ + expectedLDAPInfo: &models.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: "123", Email: "test@test.com", @@ -77,7 +140,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedInfo, ExpectedErr: tt.expectedLDAPErr}} + c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedLDAPInfo, ExpectedErr: tt.expectedLDAPErr}} identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password) assert.ErrorIs(t, err, tt.expectedErr) @@ -100,3 +163,7 @@ type fakeLDAPService struct { func (f fakeLDAPService) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { return f.ExpectedInfo, f.ExpectedErr } + +func (f fakeLDAPService) User(username string) (*models.ExternalUserInfo, error) { + return f.ExpectedInfo, f.ExpectedErr +} diff --git a/pkg/services/authn/clients/password.go b/pkg/services/authn/clients/password.go new file mode 100644 index 00000000000..50e76b4c35f --- /dev/null +++ b/pkg/services/authn/clients/password.go @@ -0,0 +1,67 @@ +package clients + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/loginattempt" + "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/web" +) + +var ( + errEmptyPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.empty", errutil.WithPublicMessage("Invalid username or password")) + errPasswordAuthFailed = errutil.NewBase(errutil.StatusBadRequest, "password-auth.failed", errutil.WithPublicMessage("Invalid username or password")) + errInvalidPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.invalid", errutil.WithPublicMessage("Invalid password or username")) + errLoginAttemptBlocked = errutil.NewBase(errutil.StatusUnauthorized, "login-attempt.blocked", errutil.WithPublicMessage("Invalid username or password")) +) + +var _ authn.PasswordClient = new(Password) + +func ProvidePassword(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Password { + return &Password{loginAttempts, clients} +} + +type Password struct { + loginAttempts loginattempt.Service + clients []authn.PasswordClient +} + +func (c *Password) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + r.SetMeta(authn.MetaKeyUsername, username) + + ok, err := c.loginAttempts.Validate(ctx, username) + if err != nil { + return nil, err + } + if !ok { + return nil, errLoginAttemptBlocked.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") + } + + if len(password) == 0 { + return nil, errEmptyPassword.Errorf("no password provided") + } + + var clientErr error + for _, pwClient := range c.clients { + var identity *authn.Identity + identity, clientErr = pwClient.AuthenticatePassword(ctx, r, username, password) + // for invalid password or if the identity is not found by a client continue to next one + if errors.Is(clientErr, errInvalidPassword) || errors.Is(clientErr, errIdentityNotFound) { + continue + } + + if clientErr != nil { + return nil, errPasswordAuthFailed.Errorf("failed to authenticate identity: %w", clientErr) + } + + return identity, nil + } + + if errors.Is(clientErr, errInvalidPassword) { + _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) + } + + return nil, errPasswordAuthFailed.Errorf("failed to authenticate identity: %w", clientErr) +} diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go new file mode 100644 index 00000000000..e064265e90a --- /dev/null +++ b/pkg/services/authn/clients/password_test.go @@ -0,0 +1,82 @@ +package clients + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" +) + +func TestPassword_AuthenticatePassword(t *testing.T) { + type TestCase struct { + desc string + username string + password string + req *authn.Request + blockLogin bool + clients []authn.PasswordClient + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []TestCase{ + { + desc: "should success when password client return identity", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}}, + expectedIdentity: &authn.Identity{ID: "user:1"}, + }, + { + desc: "should success when found in second client", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:2"}}}, + expectedIdentity: &authn.Identity{ID: "user:2"}, + }, + { + desc: "should fail for empty password", + username: "test", + password: "", + req: &authn.Request{}, + expectedErr: errEmptyPassword, + }, + { + desc: "should if login is blocked by to many attempts", + username: "test", + password: "test", + req: &authn.Request{}, + blockLogin: true, + expectedErr: errLoginAttemptBlocked, + }, + { + desc: "should fail when not found in any clients", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}}, + expectedErr: errPasswordAuthFailed, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tt.clients...) + + identity, err := c.AuthenticatePassword(context.Background(), tt.req, tt.username, tt.password) + if tt.expectedErr != nil { + assert.ErrorIs(t, err, tt.expectedErr) + assert.Nil(t, identity) + } else { + assert.NoError(t, err) + assert.EqualValues(t, *tt.expectedIdentity, *identity) + } + }) + } +} diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go new file mode 100644 index 00000000000..1f8a7d0ad69 --- /dev/null +++ b/pkg/services/authn/clients/proxy.go @@ -0,0 +1,147 @@ +package clients + +import ( + "context" + "fmt" + "net" + "path" + "strings" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errutil" +) + +const ( + proxyFieldName = "Name" + proxyFieldEmail = "Email" + proxyFieldLogin = "Login" + proxyFieldRole = "Role" + proxyFieldGroups = "Groups" +) + +var proxyFields = [...]string{proxyFieldName, proxyFieldEmail, proxyFieldLogin, proxyFieldRole, proxyFieldGroups} + +var ( + errNotAcceptedIP = errutil.NewBase(errutil.StatusUnauthorized, "auth-proxy.invalid-ip") + errEmptyProxyHeader = errutil.NewBase(errutil.StatusUnauthorized, "auth-proxy.empty-header") + errInvalidProxyHeader = errutil.NewBase(errutil.StatusInternal, "auth-proxy.invalid-proxy-header") +) + +var _ authn.Client = new(Proxy) + +func ProvideProxy(cfg *setting.Cfg, clients ...authn.ProxyClient) (*Proxy, error) { + list, err := parseAcceptList(cfg.AuthProxyWhitelist) + if err != nil { + return nil, err + } + return &Proxy{cfg, clients, list}, nil +} + +type Proxy struct { + cfg *setting.Cfg + clients []authn.ProxyClient + acceptedIPs []*net.IPNet +} + +func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + if !c.isAllowedIP(r) { + return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list") + } + + username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded) + if len(username) == 0 { + return nil, errEmptyProxyHeader.Errorf("no username provided in auth proxy header") + } + + additional := getAdditionalProxyHeaders(r, c.cfg) + + // FIXME: add cache to prevent sync on every request + + var clientErr error + for _, proxyClient := range c.clients { + var identity *authn.Identity + identity, clientErr = proxyClient.AuthenticateProxy(ctx, r, username, additional) + if identity != nil { + return identity, nil + } + } + + return nil, clientErr +} + +func (c *Proxy) Test(ctx context.Context, r *authn.Request) bool { + return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0 +} + +func (c *Proxy) isAllowedIP(r *authn.Request) bool { + if len(c.acceptedIPs) == 0 { + return true + } + + host, _, err := net.SplitHostPort(r.HTTPRequest.RemoteAddr) + if err != nil { + return false + } + + ip := net.ParseIP(host) + for _, v := range c.acceptedIPs { + if v.Contains(ip) { + return true + } + } + + return false +} + +func parseAcceptList(s string) ([]*net.IPNet, error) { + if len(strings.TrimSpace(s)) == 0 { + return nil, nil + } + addresses := strings.Split(s, ",") + list := make([]*net.IPNet, 0, len(addresses)) + for _, addr := range addresses { + result, err := coerceProxyAddress(addr) + if err != nil { + return nil, err + } + list = append(list, result) + } + return list, nil +} + +// coerceProxyAddress gets network of the presented CIDR notation +func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) { + proxyAddr = strings.TrimSpace(proxyAddr) + if !strings.Contains(proxyAddr, "/") { + proxyAddr = path.Join(proxyAddr, "32") + } + + _, network, err := net.ParseCIDR(proxyAddr) + if err != nil { + return nil, fmt.Errorf("could not parse the network: %w", err) + } + return network, nil +} + +func getProxyHeader(r *authn.Request, headerName string, encoded bool) string { + if r.HTTPRequest == nil { + return "" + } + v := r.HTTPRequest.Header.Get(headerName) + if encoded { + v = util.DecodeQuotedPrintable(v) + } + return v +} + +func getAdditionalProxyHeaders(r *authn.Request, cfg *setting.Cfg) map[string]string { + additional := make(map[string]string, len(proxyFields)) + for _, k := range proxyFields { + if v := getProxyHeader(r, cfg.AuthProxyHeaders[k], cfg.AuthProxyHeadersEncoded); v != "" { + additional[k] = v + } + } + return additional +} diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go new file mode 100644 index 00000000000..62a72e32ff8 --- /dev/null +++ b/pkg/services/authn/clients/proxy_test.go @@ -0,0 +1,172 @@ +package clients + +import ( + "context" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/services/authn/authntest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/setting" +) + +func TestProxy_Authenticate(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + ips string + proxyHeader string + proxyHeaders map[string]string + expectedErr error + expectedUsername string + expectedAdditional map[string]string + } + + tests := []testCase{ + { + desc: "should authenticate using passed in proxy client", + ips: "127.0.0.1", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{ + "X-Username": {"username"}, + "X-Name": {"name"}, + "X-Email": {"email"}, + "X-Login": {"login"}, + "X-Role": {"Viewer"}, + "X-Group": {"grp1,grp2"}, + }, + RemoteAddr: "127.0.0.1:333", + }, + }, + proxyHeader: "X-Username", + proxyHeaders: map[string]string{ + proxyFieldName: "X-Name", + proxyFieldEmail: "X-Email", + proxyFieldLogin: "X-Login", + proxyFieldRole: "X-Role", + proxyFieldGroups: "X-Group", + }, + expectedUsername: "username", + expectedAdditional: map[string]string{ + proxyFieldName: "name", + proxyFieldEmail: "email", + proxyFieldLogin: "login", + proxyFieldRole: "Viewer", + proxyFieldGroups: "grp1,grp2", + }, + }, + { + desc: "should fail when proxy header is empty", + req: &authn.Request{ + HTTPRequest: &http.Request{Header: map[string][]string{ + "X-Username": {""}, + "X-Name": {"name"}, + "X-Email": {"email"}, + "X-Login": {"login"}, + "X-Role": {"Viewer"}, + "X-Group": {"grp1,grp2"}, + }}, + }, + proxyHeader: "X-Username", + proxyHeaders: map[string]string{ + proxyFieldName: "X-Name", + proxyFieldEmail: "X-Email", + proxyFieldLogin: "X-Login", + proxyFieldRole: "X-Role", + proxyFieldGroups: "X-Group", + }, + expectedErr: errEmptyProxyHeader, + }, + { + desc: "should fail when caller ip is not in accept list", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{}, + RemoteAddr: "127.0.0.2:333", + }, + }, + ips: "127.0.0.1", + expectedErr: errNotAcceptedIP, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.AuthProxyHeaderName = "X-Username" + cfg.AuthProxyHeaders = tt.proxyHeaders + cfg.AuthProxyWhitelist = tt.ips + + calledUsername := "" + var calledAdditional map[string]string + + proxyClient := authntest.MockProxyClient{AuthenticateProxyFunc: func(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + calledUsername = username + calledAdditional = additional + return nil, nil + }} + c, err := ProvideProxy(cfg, proxyClient) + require.NoError(t, err) + + _, err = c.Authenticate(context.Background(), tt.req) + assert.ErrorIs(t, err, tt.expectedErr) + assert.Equal(t, tt.expectedUsername, calledUsername) + assert.EqualValues(t, tt.expectedAdditional, calledAdditional) + }) + } +} + +func TestProxy_Test(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + expectedOK bool + } + + tests := []testCase{ + { + desc: "should return true when proxy header exists", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Proxy-Header": {"some value"}}, + }, + }, + expectedOK: true, + }, + { + desc: "should return false when proxy header exists but has no value", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Proxy-Header": {""}}, + }, + }, + expectedOK: false, + }, + { + desc: "should return false when no proxy header is set on request", + req: &authn.Request{ + HTTPRequest: &http.Request{Header: map[string][]string{}}, + }, + expectedOK: false, + }, + { + desc: "should return false when no http request is present", + req: &authn.Request{}, + expectedOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.AuthProxyHeaderName = "Proxy-Header" + + c, _ := ProvideProxy(cfg, nil) + assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req)) + }) + } +} diff --git a/pkg/services/comments/commentmodel/permissions.go b/pkg/services/comments/commentmodel/permissions.go index a57d19645e8..c11f54179e4 100644 --- a/pkg/services/comments/commentmodel/permissions.go +++ b/pkg/services/comments/commentmodel/permissions.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -29,16 +28,16 @@ func NewPermissionChecker(sqlStore db.DB, features featuremgmt.FeatureToggles, return &PermissionChecker{sqlStore: sqlStore, features: features, accessControl: accessControl, annotationsRepo: annotationsRepo} } -func (c *PermissionChecker) getDashboardByUid(ctx context.Context, orgID int64, uid string) (*models.Dashboard, error) { - query := models.GetDashboardQuery{Uid: uid, OrgId: orgID} +func (c *PermissionChecker) getDashboardByUid(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) { + query := dashboards.GetDashboardQuery{UID: uid, OrgID: orgID} if err := c.dashboardService.GetDashboard(ctx, &query); err != nil { return nil, err } return query.Result, nil } -func (c *PermissionChecker) getDashboardById(ctx context.Context, orgID int64, id int64) (*models.Dashboard, error) { - query := models.GetDashboardQuery{Id: id, OrgId: orgID} +func (c *PermissionChecker) getDashboardById(ctx context.Context, orgID int64, id int64) (*dashboards.Dashboard, error) { + query := dashboards.GetDashboardQuery{ID: id, OrgID: orgID} if err := c.dashboardService.GetDashboard(ctx, &query); err != nil { return nil, err } diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index 25ed124412b..79b51216eff 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -6,13 +6,15 @@ import ( "net/http" "strings" + "github.com/jmespath/go-jmespath" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" + authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/jmespath/go-jmespath" ) const ( @@ -59,9 +61,8 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) // Strip the 'Bearer' prefix if it exists. jwtToken = strings.TrimPrefix(jwtToken, "Bearer ") - // The header is Authorization and the token does not look like a JWT, - // this is likely an API key. Pass it on. - if h.Cfg.JWTAuthHeaderName == "Authorization" && !looksLikeJWT(jwtToken) { + // If the "sub" claim is missing or empty then pass the control to the next handler + if !authJWT.HasSubClaim(jwtToken) { return false } @@ -222,9 +223,3 @@ func searchClaimsForStringAttr(attributePath string, claims map[string]interface return "", nil } - -func looksLikeJWT(token string) bool { - // A JWT must have 3 parts separated by `.`. - parts := strings.Split(token, ".") - return len(parts) == 3 -} diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 72981b2e32b..38b9f3e27dd 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/rendering" @@ -106,7 +107,7 @@ func getContextHandler(t *testing.T) *ContextHandler { return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, - &userService, orgService, nil, nil, &authntest.FakeService{}) + &userService, orgService, nil, featuremgmt.WithFeatures(), &authntest.FakeService{}) } type fakeAuthenticator struct{} diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index b31a0e8b72a..1f84d4484f3 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -180,11 +180,14 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { {Num: reqContext.UserID}}, ) - // update last seen every 5min - if reqContext.ShouldUpdateLastSeenAt() { - reqContext.Logger.Debug("Updating last user_seen_at", "user_id", reqContext.UserID) - if err := h.userService.UpdateLastSeenAt(mContext.Req.Context(), &user.UpdateUserLastSeenAtCommand{UserID: reqContext.UserID}); err != nil { - reqContext.Logger.Error("Failed to update last_seen_at", "error", err) + // when using authn service this is implemented as a post auth hook + if !h.features.IsEnabled(featuremgmt.FlagAuthnService) { + // update last seen every 5min + if reqContext.ShouldUpdateLastSeenAt() { + reqContext.Logger.Debug("Updating last user_seen_at", "user_id", reqContext.UserID) + if err := h.userService.UpdateLastSeenAt(mContext.Req.Context(), &user.UpdateUserLastSeenAtCommand{UserID: reqContext.UserID}); err != nil { + reqContext.Logger.Error("Failed to update last_seen_at", "error", err) + } } } @@ -350,11 +353,17 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo return true } - // update api_key last used date - if err := h.apiKeyService.UpdateAPIKeyLastUsedDate(reqContext.Req.Context(), apikey.Id); err != nil { - reqContext.JsonApiErr(http.StatusInternalServerError, InvalidAPIKey, errKey) - return true - } + // non-blocking update api_key last used date + go func(id int64) { + defer func() { + if err := recover(); err != nil { + reqContext.Logger.Error("api key authentication panic", "err", err) + } + }() + if err := h.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil { + reqContext.Logger.Warn("failed to update last use date for api key", "id", id) + } + }(apikey.Id) if apikey.ServiceAccountId == nil || *apikey.ServiceAccountId < 1 { //There is no service account attached to the apikey // Use the old APIkey method. This provides backwards compatibility. @@ -708,6 +717,28 @@ func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCo } func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, orgID int64) bool { + if h.features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientProxy, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp}) + if !ok { + return false + } + + if err != nil { + writeErr(reqContext, err) + } + + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName) + for _, header := range h.Cfg.AuthProxyHeaders { + if header != "" { + ctx = WithAuthHTTPHeader(ctx, header) + } + } + + *reqContext.Req = *reqContext.Req.WithContext(ctx) + reqContext.IsSignedIn = true + reqContext.SignedInUser = identity.SignedInUser() + return true + } username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName) logger := log.New("auth.proxy") diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 1178654d299..19234f0463c 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -106,17 +106,17 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb req.FolderUid = folder.UID } - saveCmd := models.SaveDashboardCommand{ + saveCmd := dashboards.SaveDashboardCommand{ Dashboard: generatedDash, - OrgId: req.User.OrgID, - UserId: req.User.UserID, + OrgID: req.User.OrgID, + UserID: req.User.UserID, Overwrite: req.Overwrite, - PluginId: req.PluginId, - FolderId: req.FolderId, + PluginID: req.PluginId, + FolderID: req.FolderId, } dto := &dashboards.SaveDashboardDTO{ - OrgId: saveCmd.OrgId, + OrgID: saveCmd.OrgID, Dashboard: saveCmd.GetDashboardModel(), Overwrite: saveCmd.Overwrite, User: req.User, @@ -138,18 +138,18 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb } return &dashboardimport.ImportDashboardResponse{ - UID: savedDashboard.Uid, + UID: savedDashboard.UID, PluginId: req.PluginId, Title: savedDashboard.Title, Path: req.Path, Revision: savedDashboard.Data.Get("revision").MustInt64(1), - FolderId: savedDashboard.FolderId, + FolderId: savedDashboard.FolderID, FolderUID: req.FolderUid, ImportedUri: "db/" + savedDashboard.Slug, - ImportedUrl: savedDashboard.GetUrl(), + ImportedUrl: savedDashboard.GetURL(), ImportedRevision: savedDashboard.Data.Get("revision").MustInt64(1), Imported: true, - DashboardId: savedDashboard.Id, + DashboardId: savedDashboard.ID, Slug: savedDashboard.Slug, }, nil } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index 1222ccada0e..41c4fedec52 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -27,16 +27,16 @@ func TestImportDashboardService(t *testing.T) { var importDashboardArg *dashboards.SaveDashboardDTO dashboardService := &dashboardServiceMock{ - importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { + importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { importDashboardArg = dto - return &models.Dashboard{ - Id: 4, - Uid: dto.Dashboard.Uid, + return &dashboards.Dashboard{ + ID: 4, + UID: dto.Dashboard.UID, Slug: dto.Dashboard.Slug, - OrgId: 3, + OrgID: 3, Version: dto.Dashboard.Version, - PluginId: "prometheus", - FolderId: dto.Dashboard.FolderId, + PluginID: "prometheus", + FolderID: dto.Dashboard.FolderID, Title: dto.Dashboard.Title, Data: dto.Dashboard.Data, }, nil @@ -50,7 +50,7 @@ func TestImportDashboardService(t *testing.T) { importLibraryPanelsForDashboard = true return nil }, - connectLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { + connectLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { connectLibraryPanelsForDashboardCalled = true return nil }, @@ -84,10 +84,10 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, "UDdpyzz7z", resp.UID) require.NotNil(t, importDashboardArg) - require.Equal(t, int64(3), importDashboardArg.OrgId) + require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), importDashboardArg.User.UserID) - require.Equal(t, "prometheus", importDashboardArg.Dashboard.PluginId) - require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderId) + require.Equal(t, "prometheus", importDashboardArg.Dashboard.PluginID) + require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -99,16 +99,16 @@ func TestImportDashboardService(t *testing.T) { t.Run("When importing a non-plugin dashboard should save dashboard and sync library panels", func(t *testing.T) { var importDashboardArg *dashboards.SaveDashboardDTO dashboardService := &dashboardServiceMock{ - importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { + importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { importDashboardArg = dto - return &models.Dashboard{ - Id: 4, - Uid: dto.Dashboard.Uid, + return &dashboards.Dashboard{ + ID: 4, + UID: dto.Dashboard.UID, Slug: dto.Dashboard.Slug, - OrgId: 3, + OrgID: 3, Version: dto.Dashboard.Version, - PluginId: "prometheus", - FolderId: dto.Dashboard.FolderId, + PluginID: "prometheus", + FolderID: dto.Dashboard.FolderID, Title: dto.Dashboard.Title, Data: dto.Dashboard.Data, }, nil @@ -148,10 +148,10 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, "UDdpyzz7z", resp.UID) require.NotNil(t, importDashboardArg) - require.Equal(t, int64(3), importDashboardArg.OrgId) + require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), importDashboardArg.User.UserID) - require.Equal(t, "", importDashboardArg.Dashboard.PluginId) - require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderId) + require.Equal(t, "", importDashboardArg.Dashboard.PluginID) + require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -191,10 +191,10 @@ func (m *pluginDashboardServiceMock) LoadPluginDashboard(ctx context.Context, re type dashboardServiceMock struct { dashboards.DashboardService - importDashboardFunc func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) + importDashboardFunc func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) } -func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { +func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { if s.importDashboardFunc != nil { return s.importDashboardFunc(ctx, dto) } @@ -204,11 +204,11 @@ func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboa type libraryPanelServiceMock struct { librarypanels.Service - connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error + connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *user.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error } -func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { +func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { if s.connectLibraryPanelsForDashboardFunc != nil { return s.connectLibraryPanelsForDashboardFunc(ctx, signedInUser, dash) } diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index 49df72b9b45..6350c35188d 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -4,7 +4,6 @@ import ( "context" "strings" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" ) @@ -97,7 +96,7 @@ func NewDashboardIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) { return nil, err } - dashboard, err := db.GetDashboard(ctx, &models.GetDashboardQuery{Id: id, OrgId: orgID}) + dashboard, err := db.GetDashboard(ctx, &GetDashboardQuery{ID: id, OrgID: orgID}) if err != nil { return nil, err } @@ -120,7 +119,7 @@ func NewDashboardUIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) return nil, err } - dashboard, err := db.GetDashboard(ctx, &models.GetDashboardQuery{Uid: uid, OrgId: orgID}) + dashboard, err := db.GetDashboard(ctx, &GetDashboardQuery{UID: uid, OrgID: orgID}) if err != nil { return nil, err } @@ -129,16 +128,16 @@ func NewDashboardUIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) }) } -func resolveDashboardScope(ctx context.Context, db Store, orgID int64, dashboard *models.Dashboard) ([]string, error) { +func resolveDashboardScope(ctx context.Context, db Store, orgID int64, dashboard *Dashboard) ([]string, error) { var folderUID string - if dashboard.FolderId < 0 { - return []string{ScopeDashboardsProvider.GetResourceScopeUID(dashboard.Uid)}, nil + if dashboard.FolderID < 0 { + return []string{ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)}, nil } - if dashboard.FolderId == 0 { + if dashboard.FolderID == 0 { folderUID = ac.GeneralFolderUID } else { - folder, err := db.GetFolderByID(ctx, orgID, dashboard.FolderId) + folder, err := db.GetFolderByID(ctx, orgID, dashboard.FolderID) if err != nil { return nil, err } @@ -146,7 +145,7 @@ func resolveDashboardScope(ctx context.Context, db Store, orgID int64, dashboard } return []string{ - ScopeDashboardsProvider.GetResourceScopeUID(dashboard.Uid), + ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID), ScopeFoldersProvider.GetResourceScopeUID(folderUID), }, nil } diff --git a/pkg/services/dashboards/accesscontrol_test.go b/pkg/services/dashboards/accesscontrol_test.go index d59f739aec0..062994c87c3 100644 --- a/pkg/services/dashboards/accesscontrol_test.go +++ b/pkg/services/dashboards/accesscontrol_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/util" @@ -157,16 +156,16 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { orgID := rand.Int63() folder := &folder.Folder{ID: 2, UID: "2"} - dashboard := &models.Dashboard{Id: 1, FolderId: folder.ID, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: folder.ID, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil).Once() store.On("GetFolderByID", mock.Anything, orgID, folder.ID).Return(folder, nil).Once() - scope := ac.Scope("dashboards", "id", strconv.FormatInt(dashboard.Id, 10)) + scope := ac.Scope("dashboards", "id", strconv.FormatInt(dashboard.ID, 10)) resolvedScopes, err := resolver.Resolve(context.Background(), orgID, scope) require.NoError(t, err) require.Len(t, resolvedScopes, 2) - require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.Uid), resolvedScopes[0]) + require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.UID), resolvedScopes[0]) require.Equal(t, fmt.Sprintf("folders:uid:%s", folder.UID), resolvedScopes[1]) }) @@ -180,7 +179,7 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { store := &FakeDashboardStore{} _, resolver := NewDashboardIDScopeResolver(store) - dashboard := &models.Dashboard{Id: 1, FolderId: 0, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: 0, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil) resolved, err := resolver.Resolve(context.Background(), 1, ac.Scope("dashboards", "id", "1")) require.NoError(t, err) @@ -203,16 +202,16 @@ func TestNewDashboardUIDScopeResolver(t *testing.T) { orgID := rand.Int63() folder := &folder.Folder{ID: 2, UID: "2"} - dashboard := &models.Dashboard{Id: 1, FolderId: folder.ID, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: folder.ID, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil).Once() store.On("GetFolderByID", mock.Anything, orgID, folder.ID).Return(folder, nil).Once() - scope := ac.Scope("dashboards", "uid", dashboard.Uid) + scope := ac.Scope("dashboards", "uid", dashboard.UID) resolvedScopes, err := resolver.Resolve(context.Background(), orgID, scope) require.NoError(t, err) require.Len(t, resolvedScopes, 2) - require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.Uid), resolvedScopes[0]) + require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.UID), resolvedScopes[0]) require.Equal(t, fmt.Sprintf("folders:uid:%s", folder.UID), resolvedScopes[1]) }) @@ -226,7 +225,7 @@ func TestNewDashboardUIDScopeResolver(t *testing.T) { store := &FakeDashboardStore{} _, resolver := NewDashboardUIDScopeResolver(store) - dashboard := &models.Dashboard{Id: 1, FolderId: 0, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: 0, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil) resolved, err := resolver.Resolve(context.Background(), 1, ac.Scope("dashboards", "uid", "1")) require.NoError(t, err) diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 6493b51b9d7..7c91092f410 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -12,19 +12,19 @@ import ( // //go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go type DashboardService interface { - BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) + BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) - GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error + GetDashboard(ctx context.Context, query *GetDashboardQuery) error GetDashboardACLInfoList(ctx context.Context, query *models.GetDashboardACLInfoListQuery) error - GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error + GetDashboards(ctx context.Context, query *GetDashboardsQuery) error GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error - GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error + GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error - ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) + ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error - SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) + SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardACL) error DeleteACLByUser(ctx context.Context, userID int64) error @@ -45,8 +45,8 @@ type DashboardProvisioningService interface { GetProvisionedDashboardData(ctx context.Context, name string) ([]*models.DashboardProvisioning, error) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*models.DashboardProvisioning, error) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) - SaveFolderForProvisionedDashboards(context.Context, *SaveDashboardDTO) (*models.Dashboard, error) - SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) + SaveFolderForProvisionedDashboards(context.Context, *SaveDashboardDTO) (*Dashboard, error) + SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) UnprovisionDashboard(ctx context.Context, dashboardID int64) error } @@ -57,10 +57,10 @@ type Store interface { DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) - GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) + GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) GetDashboardACLInfoList(ctx context.Context, query *models.GetDashboardACLInfoListQuery) error - GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error - GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error + GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error + GetDashboards(ctx context.Context, query *GetDashboardsQuery) error // GetDashboardsByPluginID retrieves dashboards identified by plugin. GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error @@ -71,12 +71,12 @@ type Store interface { HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error // SaveAlerts saves dashboard alerts. SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error - SaveDashboard(ctx context.Context, cmd models.SaveDashboardCommand) (*models.Dashboard, error) - SaveProvisionedDashboard(ctx context.Context, cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) + SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) + SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) UnprovisionDashboard(ctx context.Context, id int64) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardACL) error // ValidateDashboardBeforeSave validates a dashboard before save. - ValidateDashboardBeforeSave(ctx context.Context, dashboard *models.Dashboard, overwrite bool) (bool, error) + ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) DeleteACLByUser(context.Context, int64) error Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) diff --git a/pkg/services/dashboards/dashboard_provisioning_mock.go b/pkg/services/dashboards/dashboard_provisioning_mock.go index 32493be45a7..b1e95a608b2 100644 --- a/pkg/services/dashboards/dashboard_provisioning_mock.go +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboards @@ -112,15 +112,15 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(c } // SaveFolderForProvisionedDashboards provides a mock function with given fields: _a0, _a1 -func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *SaveDashboardDTO) (*models.Dashboard, error) { +func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *SaveDashboardDTO) (*Dashboard, error) { ret := _m.Called(_a0, _a1) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *Dashboard); ok { r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } @@ -135,20 +135,20 @@ func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 cont } // SaveProvisionedDashboard provides a mock function with given fields: ctx, dto, provisioning -func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { +func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) { ret := _m.Called(ctx, dto, provisioning) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *models.DashboardProvisioning) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) *Dashboard); ok { r0 = rf(ctx, dto, provisioning) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO, *models.DashboardProvisioning) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) error); ok { r1 = rf(ctx, dto, provisioning) } else { r1 = ret.Error(1) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 180dd8016bd..ddf875a84c0 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboards @@ -15,15 +15,15 @@ type FakeDashboardService struct { } // BuildSaveDashboardCommand provides a mock function with given fields: ctx, dto, shouldValidateAlerts, validateProvisionedDashboard -func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { +func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) { ret := _m.Called(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) - var r0 *models.SaveDashboardCommand - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool, bool) *models.SaveDashboardCommand); ok { + var r0 *SaveDashboardCommand + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool, bool) *SaveDashboardCommand); ok { r0 = rf(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.SaveDashboardCommand) + r0 = ret.Get(0).(*SaveDashboardCommand) } } @@ -110,11 +110,11 @@ func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *model } // GetDashboard provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error { +func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDashboardQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -151,12 +151,12 @@ func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *mod return r0 } -// GetDashboardUIDById provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { +// GetDashboardUIDByID provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardRefByIdQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -166,11 +166,11 @@ func (_m *FakeDashboardService) GetDashboardUIDById(ctx context.Context, query * } // GetDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDashboardsQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardsQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -208,15 +208,15 @@ func (_m *FakeDashboardService) HasEditPermissionInFolders(ctx context.Context, } // ImportDashboard provides a mock function with given fields: ctx, dto -func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { +func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) { ret := _m.Called(ctx, dto) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *Dashboard); ok { r0 = rf(ctx, dto) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } @@ -245,15 +245,15 @@ func (_m *FakeDashboardService) MakeUserAdmin(ctx context.Context, orgID int64, } // SaveDashboard provides a mock function with given fields: ctx, dto, allowUiUpdate -func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { +func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) { ret := _m.Called(ctx, dto, allowUiUpdate) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool) *Dashboard); ok { r0 = rf(ctx, dto, allowUiUpdate) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go index ae776c8a867..bb62752b290 100644 --- a/pkg/services/dashboards/database/acl_test.go +++ b/pkg/services/dashboards/database/acl_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" @@ -25,7 +26,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { } var sqlStore *sqlstore.SQLStore var currentUser user.User - var savedFolder, childDash *models.Dashboard + var savedFolder, childDash *dashboards.Dashboard var dashboardStore *DashboardStore setup := func(t *testing.T) { @@ -36,14 +37,14 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.NoError(t, err) currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") + childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.ID, false, "prod", "webapp") } t.Run("Dashboard permission with userId and teamId set to 0", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Equal(t, models.ErrDashboardACLInfoMissing, err) @@ -51,7 +52,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Folder acl should include default acl", func(t *testing.T) { setup(t) - query := models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -68,7 +69,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Dashboard acl should include acl for parent folder", func(t *testing.T) { setup(t) - query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -85,10 +86,10 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Folder with removed default permissions returns no acl items", func(t *testing.T) { setup(t) - err := dashboardStore.UpdateDashboardACL(context.Background(), savedFolder.Id, nil) + err := dashboardStore.UpdateDashboardACL(context.Background(), savedFolder.ID, nil) require.Nil(t, err) - query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -98,43 +99,43 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Given a dashboard folder and a user", func(t *testing.T) { t.Run("Given dashboard folder permission", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) t.Run("When reading dashboard acl should include acl for parent folder", func(t *testing.T) { - query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 1, len(query.Result)) - require.Equal(t, savedFolder.Id, query.Result[0].DashboardId) + require.Equal(t, savedFolder.ID, query.Result[0].DashboardId) }) t.Run("Given child dashboard permission", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: childDash.Id, + DashboardID: childDash.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) t.Run("When reading dashboard acl should include acl for parent folder and child", func(t *testing.T) { - query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.Id} + query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.ID} err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 2, len(query.Result)) - require.Equal(t, savedFolder.Id, query.Result[0].DashboardId) + require.Equal(t, savedFolder.ID, query.Result[0].DashboardId) require.True(t, query.Result[0].Inherited) - require.Equal(t, childDash.Id, query.Result[1].DashboardId) + require.Equal(t, childDash.ID, query.Result[1].DashboardId) require.False(t, query.Result[1].Inherited) }) }) @@ -142,15 +143,15 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Reading dashboard acl should include default acl for parent folder and the child acl", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: childDash.Id, + DashboardID: childDash.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) - query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.Id} + query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.ID} err = dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -163,35 +164,35 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId) require.Equal(t, org.RoleEditor, *query.Result[1].Role) require.True(t, query.Result[1].Inherited) - require.Equal(t, childDash.Id, query.Result[2].DashboardId) + require.Equal(t, childDash.ID, query.Result[2].DashboardId) require.False(t, query.Result[2].Inherited) }) t.Run("Add and delete dashboard permission", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) - q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), q1) require.Nil(t, err) - require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) + require.Equal(t, savedFolder.ID, q1.Result[0].DashboardId) require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission) require.Equal(t, "Edit", q1.Result[0].PermissionName) require.Equal(t, currentUser.ID, q1.Result[0].UserId) require.Equal(t, currentUser.Login, q1.Result[0].UserLogin) require.Equal(t, currentUser.Email, q1.Result[0].UserEmail) - err = updateDashboardACL(t, dashboardStore, savedFolder.Id) + err = updateDashboardACL(t, dashboardStore, savedFolder.ID) require.Nil(t, err) - q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), q3) require.Nil(t, err) require.Equal(t, 0, len(q3.Result)) @@ -203,18 +204,18 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { team1, err := teamSvc.CreateTeam("group1 name", "", 1) require.Nil(t, err) - err = updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err = updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, TeamID: team1.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) - q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), q1) require.Nil(t, err) - require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) + require.Equal(t, savedFolder.ID, q1.Result[0].DashboardId) require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission) require.Equal(t, team1.ID, q1.Result[0].TeamId) }) @@ -224,19 +225,19 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { teamSvc := teamimpl.ProvideService(sqlStore, sqlStore.Cfg) team1, err := teamSvc.CreateTeam("group1 name", "", 1) require.Nil(t, err) - err = updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err = updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, TeamID: team1.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_ADMIN, }) require.Nil(t, err) - q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), q3) require.Nil(t, err) require.Equal(t, 1, len(q3.Result)) - require.Equal(t, savedFolder.Id, q3.Result[0].DashboardId) + require.Equal(t, savedFolder.ID, q3.Result[0].DashboardId) require.Equal(t, models.PERMISSION_ADMIN, q3.Result[0].Permission) require.Equal(t, team1.ID, q3.Result[0].TeamId) }) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 46ef267d094..4ad536c683d 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -69,11 +69,11 @@ func (d *DashboardStore) emitEntityEvent() bool { return d.features != nil && d.features.IsEnabled(featuremgmt.FlagPanelTitleSearch) } -func (d *DashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *models.Dashboard, overwrite bool) (bool, error) { +func (d *DashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *dashboards.Dashboard, overwrite bool) (bool, error) { isParentFolderChanged := false err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { var err error - isParentFolderChanged, err = getExistingDashboardByIdOrUidForUpdate(sess, dashboard, d.store.GetDialect(), overwrite) + isParentFolderChanged, err = getExistingDashboardByIDOrUIDForUpdate(sess, dashboard, d.store.GetDialect(), overwrite) if err != nil { return err } @@ -204,7 +204,7 @@ func (d *DashboardStore) GetProvisionedDashboardData(ctx context.Context, name s return result, err } -func (d *DashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { +func (d *DashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand, provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) { err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { if err := saveDashboard(sess, &cmd, d.emitEntityEvent()); err != nil { return err @@ -220,7 +220,7 @@ func (d *DashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd model return cmd.Result, err } -func (d *DashboardStore) SaveDashboard(ctx context.Context, cmd models.SaveDashboardCommand) (*models.Dashboard, error) { +func (d *DashboardStore) SaveDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) { err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { return saveDashboard(sess, &cmd, d.emitEntityEvent()) }) @@ -333,7 +333,7 @@ func (d *DashboardStore) Count(ctx context.Context, scopeParams *quota.ScopePara u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := d.store.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM dashboard WHERE org_id=? AND is_folder=%s", d.store.GetDialect().BooleanStr(false)) if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil { @@ -354,14 +354,14 @@ func (d *DashboardStore) Count(ctx context.Context, scopeParams *quota.ScopePara return u, nil } -func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashboard, dialect migrator.Dialect, overwrite bool) (bool, error) { +func getExistingDashboardByIDOrUIDForUpdate(sess *db.Session, dash *dashboards.Dashboard, dialect migrator.Dialect, overwrite bool) (bool, error) { dashWithIdExists := false isParentFolderChanged := false var existingById models.Dashboard - if dash.Id > 0 { + if dash.ID > 0 { var err error - dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById) + dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existingById) if err != nil { return false, fmt.Errorf("SQL query for existing dashboard by ID failed: %w", err) } @@ -370,25 +370,25 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb return false, dashboards.ErrDashboardNotFound } - if dash.Uid == "" { - dash.SetUid(existingById.Uid) + if dash.UID == "" { + dash.SetUID(existingById.Uid) } } dashWithUidExists := false var existingByUid models.Dashboard - if dash.Uid != "" { + if dash.UID != "" { var err error - dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid) + dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgID, dash.UID).Get(&existingByUid) if err != nil { return false, fmt.Errorf("SQL query for existing dashboard by UID failed: %w", err) } } - if dash.FolderId > 0 { + if dash.FolderID > 0 { var existingFolder models.Dashboard - folderExists, err := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, + folderExists, err := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgID, dash.FolderID, dialect.BooleanStr(true)).Get(&existingFolder) if err != nil { return false, fmt.Errorf("SQL query for folder failed: %w", err) @@ -410,8 +410,8 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb existing := existingById if !dashWithIdExists && dashWithUidExists { - dash.SetId(existingByUid.Id) - dash.SetUid(existingByUid.Uid) + dash.SetID(existingByUid.Id) + dash.SetUID(existingByUid.Uid) existing = existingByUid } @@ -420,7 +420,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb return isParentFolderChanged, dashboards.ErrDashboardTypeMismatch } - if !dash.IsFolder && dash.FolderId != existing.FolderId { + if !dash.IsFolder && dash.FolderID != existing.FolderId { isParentFolderChanged = true } @@ -441,16 +441,16 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb return isParentFolderChanged, nil } -func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *models.Dashboard, dialect migrator.Dialect, overwrite, +func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *dashboards.Dashboard, dialect migrator.Dialect, overwrite, isParentFolderChanged bool) (bool, error) { var existing models.Dashboard - exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, - dialect.BooleanStr(true), dash.FolderId).Get(&existing) + exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgID, dash.Slug, + dialect.BooleanStr(true), dash.FolderID).Get(&existing) if err != nil { return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by org ID or folder ID failed: %w", err) } - if exists && dash.Id != existing.Id { + if exists && dash.ID != existing.Id { if existing.IsFolder && !dash.IsFolder { return isParentFolderChanged, dashboards.ErrDashboardWithSameNameAsFolder } @@ -459,13 +459,13 @@ func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *models.Dashboa return isParentFolderChanged, dashboards.ErrDashboardFolderWithSameNameAsDashboard } - if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) { + if !dash.IsFolder && (dash.FolderID != existing.FolderId || dash.ID == 0) { isParentFolderChanged = true } if overwrite { - dash.SetId(existing.Id) - dash.SetUid(existing.Uid) + dash.SetID(existing.Id) + dash.SetUID(existing.Uid) dash.SetVersion(existing.Version) } else { return isParentFolderChanged, dashboards.ErrDashboardWithSameNameInFolderExists @@ -475,18 +475,18 @@ func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *models.Dashboa return isParentFolderChanged, nil } -func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntityEvent bool) error { +func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitEntityEvent bool) error { dash := cmd.GetDashboardModel() - userId := cmd.UserId + userId := cmd.UserID if userId == 0 { userId = -1 } - if dash.Id > 0 { + if dash.ID > 0 { var existing models.Dashboard - dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) + dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existing) if err != nil { return err } @@ -509,19 +509,19 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit } } - if dash.Uid == "" { - uid, err := generateNewDashboardUid(sess, dash.OrgId) + if dash.UID == "" { + uid, err := generateNewDashboardUid(sess, dash.OrgID) if err != nil { return err } - dash.SetUid(uid) + dash.SetUID(uid) } parentVersion := dash.Version var affectedRows int64 var err error - if dash.Id == 0 { + if dash.ID == 0 { dash.SetVersion(1) dash.Created = time.Now() dash.CreatedBy = userId @@ -540,7 +540,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit dash.UpdatedBy = userId - affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) + affectedRows, err = sess.MustCols("folder_id").ID(dash.ID).Update(dash) } if err != nil { @@ -552,7 +552,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit } dashVersion := &dashver.DashboardVersion{ - DashboardID: dash.Id, + DashboardID: dash.ID, ParentVersion: parentVersion, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, @@ -570,7 +570,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit } // delete existing tags - if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id); err != nil { + if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.ID); err != nil { return err } @@ -578,7 +578,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit tags := dash.GetTags() if len(tags) > 0 { for _, tag := range tags { - if _, err := sess.Insert(DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil { + if _, err := sess.Insert(DashboardTag{DashboardId: dash.ID, Term: tag}); err != nil { return err } } @@ -612,16 +612,16 @@ func generateNewDashboardUid(sess *db.Session, orgId int64) (string, error) { return "", dashboards.ErrDashboardFailedGenerateUniqueUid } -func saveProvisionedData(sess *db.Session, provisioning *models.DashboardProvisioning, dashboard *models.Dashboard) error { +func saveProvisionedData(sess *db.Session, provisioning *dashboards.DashboardProvisioning, dashboard *dashboards.Dashboard) error { result := &models.DashboardProvisioning{} - exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, provisioning.Name).Get(result) + exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.ID, provisioning.Name).Get(result) if err != nil { return err } - provisioning.Id = result.Id - provisioning.DashboardId = dashboard.Id + provisioning.ID = result.Id + provisioning.DashboardID = dashboard.ID if exist { _, err = sess.ID(result.Id).Update(provisioning) @@ -767,7 +767,7 @@ func (d *DashboardStore) DeleteDashboard(ctx context.Context, cmd *models.Delete } func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, sess *db.Session, emitEntityEvent bool) error { - dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId} + dashboard := dashboards.Dashboard{ID: cmd.Id, OrgID: cmd.OrgId} has, err := sess.Get(&dashboard) if err != nil { return err @@ -794,7 +794,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses Id int64 Uid string } - err := sess.SQL("SELECT id, uid FROM dashboard WHERE folder_id = ?", dashboard.Id).Find(&dashIds) + err := sess.SQL("SELECT id, uid FROM dashboard WHERE folder_id = ?", dashboard.ID).Find(&dashIds) if err != nil { return err } @@ -806,7 +806,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses } // remove all access control permission with folder scope - _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.Uid)) + _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.UID)) if err != nil { return err } @@ -830,7 +830,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses "DELETE FROM dashboard_public WHERE dashboard_uid IN (SELECT uid FROM dashboard WHERE org_id = ? AND folder_id = ?)", } for _, sql := range childrenDeletes { - _, err := sess.Exec(sql, dashboard.OrgId, dashboard.Id) + _, err := sess.Exec(sql, dashboard.OrgID, dashboard.ID) if err != nil { return err } @@ -838,7 +838,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses } var existingRuleID int64 - exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", dashboard.Id).Cols("id").Get(&existingRuleID) + exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", dashboard.ID).Cols("id").Get(&existingRuleID) if err != nil { return err } @@ -854,25 +854,25 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses } for _, sql := range deleteNGAlertsByFolder { - _, err := sess.Exec(sql, dashboard.Id) + _, err := sess.Exec(sql, dashboard.ID) if err != nil { return err } } } } else { - _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.GetResourceScopeUID("dashboards", dashboard.Uid)) + _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.GetResourceScopeUID("dashboards", dashboard.UID)) if err != nil { return err } } - if err := d.deleteAlertDefinition(dashboard.Id, sess); err != nil { + if err := d.deleteAlertDefinition(dashboard.ID, sess); err != nil { return err } for _, sql := range deletes { - _, err := sess.Exec(sql, dashboard.Id) + _, err := sess.Exec(sql, dashboard.ID) if err != nil { return err } @@ -887,18 +887,18 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses return nil } -func createEntityEvent(dashboard *models.Dashboard, eventType store.EntityEventType) *store.EntityEvent { +func createEntityEvent(dashboard *dashboards.Dashboard, eventType store.EntityEventType) *store.EntityEvent { var entityEvent *store.EntityEvent if dashboard.IsFolder { entityEvent = &store.EntityEvent{ EventType: eventType, - EntityId: store.CreateDatabaseEntityId(dashboard.Uid, dashboard.OrgId, store.EntityTypeFolder), + EntityId: store.CreateDatabaseEntityId(dashboard.UID, dashboard.OrgID, store.EntityTypeFolder), Created: time.Now().Unix(), } } else { entityEvent = &store.EntityEvent{ EventType: eventType, - EntityId: store.CreateDatabaseEntityId(dashboard.Uid, dashboard.OrgId, store.EntityTypeDashboard), + EntityId: store.CreateDatabaseEntityId(dashboard.UID, dashboard.OrgID, store.EntityTypeDashboard), Created: time.Now().Unix(), } } @@ -922,13 +922,13 @@ func (d *DashboardStore) deleteAlertDefinition(dashboardId int64, sess *db.Sessi return nil } -func (d *DashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) { +func (d *DashboardStore) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { err := d.store.WithDbSession(ctx, func(sess *db.Session) error { - if query.Id == 0 && len(query.Slug) == 0 && len(query.Uid) == 0 { + if query.ID == 0 && len(query.Slug) == 0 && len(query.UID) == 0 { return dashboards.ErrDashboardIdentifierNotSet } - dashboard := models.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid} + dashboard := dashboards.Dashboard{Slug: query.Slug, OrgID: query.OrgID, ID: query.ID, UID: query.UID} has, err := sess.Get(&dashboard) if err != nil { @@ -937,8 +937,8 @@ func (d *DashboardStore) GetDashboard(ctx context.Context, query *models.GetDash return dashboards.ErrDashboardNotFound } - dashboard.SetId(dashboard.Id) - dashboard.SetUid(dashboard.Uid) + dashboard.SetID(dashboard.ID) + dashboard.SetUID(dashboard.UID) query.Result = &dashboard return nil }) @@ -946,11 +946,11 @@ func (d *DashboardStore) GetDashboard(ctx context.Context, query *models.GetDash return query.Result, err } -func (d *DashboardStore) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { +func (d *DashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) error { return d.store.WithDbSession(ctx, func(sess *db.Session) error { var rawSQL = `SELECT uid, slug from dashboard WHERE Id=?` - us := &models.DashboardRef{} - exists, err := sess.SQL(rawSQL, query.Id).Get(us) + us := &dashboards.DashboardRef{} + exists, err := sess.SQL(rawSQL, query.ID).Get(us) if err != nil { return err } else if !exists { @@ -961,18 +961,21 @@ func (d *DashboardStore) GetDashboardUIDById(ctx context.Context, query *models. }) } -func (d *DashboardStore) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (d *DashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) error { return d.store.WithDbSession(ctx, func(sess *db.Session) error { - if len(query.DashboardIds) == 0 && len(query.DashboardUIds) == 0 { + if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 { return star.ErrCommandValidationFailed } - var dashboards = make([]*models.Dashboard, 0) + var dashboards = make([]*dashboards.Dashboard, 0) var session *xorm.Session - if len(query.DashboardIds) > 0 { - session = sess.In("id", query.DashboardIds) + if len(query.DashboardIDs) > 0 { + session = sess.In("id", query.DashboardIDs) } else { - session = sess.In("uid", query.DashboardUIds) + session = sess.In("uid", query.DashboardUIDs) + } + if query.OrgID > 0 { + session = sess.Where("org_id = ?", query.OrgID) } err := session.Find(&dashboards) diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 2b0addedad7..24c9059d87d 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -27,7 +27,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { } t.Run("Testing DB", func(t *testing.T) { var sqlStore *sqlstore.SQLStore - var folder, dashInRoot, childDash *models.Dashboard + var folder, dashInRoot, childDash *dashboards.Dashboard var currentUser user.User var dashboardStore *DashboardStore @@ -40,8 +40,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) folder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp") - childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, folder.Id, false, "prod", "webapp") - insertTestDashboard(t, dashboardStore, "test dash 45", 1, folder.Id, false, "prod") + childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, folder.ID, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, folder.ID, false, "prod") currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) } @@ -53,20 +53,20 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id}, + DashboardIds: []int64{folder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder.Id) - require.Equal(t, query.Result[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) t.Run("and acl is set for dashboard folder", func(t *testing.T) { var otherUser int64 = 999 - err := updateDashboardACL(t, dashboardStore, folder.Id, models.DashboardACL{ - DashboardID: folder.Id, + err := updateDashboardACL(t, dashboardStore, folder.ID, models.DashboardACL{ + DashboardID: folder.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, @@ -76,18 +76,18 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("should not return folder", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, - OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}, + OrgId: 1, DashboardIds: []int64{folder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, dashInRoot.ID) }) t.Run("when the user is given permission", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, folder.Id, models.DashboardACL{ - DashboardID: folder.Id, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder.ID, models.DashboardACL{ + DashboardID: folder.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -95,13 +95,13 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id}, + DashboardIds: []int64{folder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder.Id) - require.Equal(t, query.Result[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) @@ -114,49 +114,49 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { OrgRole: org.RoleAdmin, }, OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id}, + DashboardIds: []int64{folder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder.Id) - require.Equal(t, query.Result[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) }) t.Run("and acl is set for dashboard child and folder has all permissions removed", func(t *testing.T) { var otherUser int64 = 999 - err := updateDashboardACL(t, dashboardStore, folder.Id) + err := updateDashboardACL(t, dashboardStore, folder.ID) require.NoError(t, err) - err = updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ - DashboardID: folder.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + err = updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ + DashboardID: folder.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("should not return folder or child", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ - SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}, + SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder.ID, childDash.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, dashInRoot.ID) }) t.Run("when the user is given permission to child", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ - DashboardID: childDash.Id, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ + DashboardID: childDash.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("should be able to search for child dashboard but not folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} + query := &models.FindPersistedDashboardsQuery{SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder.ID, childDash.ID, dashInRoot.ID}} err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, childDash.Id) - require.Equal(t, query.Result[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, childDash.ID) + require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) @@ -169,14 +169,14 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { OrgRole: org.RoleAdmin, }, OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id}, + DashboardIds: []int64{folder.ID, dashInRoot.ID, childDash.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 3) - require.Equal(t, query.Result[0].ID, folder.Id) - require.Equal(t, query.Result[1].ID, childDash.Id) - require.Equal(t, query.Result[2].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[1].ID, childDash.ID) + require.Equal(t, query.Result[2].ID, dashInRoot.ID) }) }) }) @@ -184,7 +184,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func(t *testing.T) { var sqlStore *sqlstore.SQLStore - var folder1, folder2, dashInRoot, childDash1, childDash2 *models.Dashboard + var folder1, folder2, dashInRoot, childDash1, childDash2 *dashboards.Dashboard var currentUser user.User var rootFolderId int64 = 0 @@ -196,8 +196,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod") folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod") dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") - childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.Id, false, "prod") - childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.Id, false, "prod") + childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.ID, false, "prod") + childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.ID, false, "prod") currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) } @@ -207,7 +207,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("should return dashboards in root and expanded folder", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ FolderIds: []int64{ - rootFolderId, folder1.Id}, SignedInUser: &user.SignedInUser{UserID: currentUser.ID, + rootFolderId, folder1.ID}, SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer, }, OrgId: 1, @@ -215,76 +215,76 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 4) - require.Equal(t, query.Result[0].ID, folder1.Id) - require.Equal(t, query.Result[1].ID, folder2.Id) - require.Equal(t, query.Result[2].ID, childDash1.Id) - require.Equal(t, query.Result[3].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) + require.Equal(t, query.Result[1].ID, folder2.ID) + require.Equal(t, query.Result[2].ID, childDash1.ID) + require.Equal(t, query.Result[3].ID, dashInRoot.ID) }) }) t.Run("and acl is set for one dashboard folder", func(t *testing.T) { const otherUser int64 = 999 - err := updateDashboardACL(t, dashboardStore, folder1.Id, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("and a dashboard is moved from folder without acl to the folder with an acl", func(t *testing.T) { - moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.Id) + moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.ID) t.Run("should not return folder with acl or its children", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + DashboardIds: []int64{folder1.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, dashInRoot.ID) }) }) t.Run("and a dashboard is moved from folder with acl to the folder without an acl", func(t *testing.T) { setup2() - moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.Id) + moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID) t.Run("should return folder without acl and its children", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + DashboardIds: []int64{folder2.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 4) - require.Equal(t, query.Result[0].ID, folder2.Id) - require.Equal(t, query.Result[1].ID, childDash1.Id) - require.Equal(t, query.Result[2].ID, childDash2.Id) - require.Equal(t, query.Result[3].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder2.ID) + require.Equal(t, query.Result[1].ID, childDash1.ID) + require.Equal(t, query.Result[2].ID, childDash2.ID) + require.Equal(t, query.Result[3].ID, dashInRoot.ID) }) }) t.Run("and a dashboard with an acl is moved to the folder without an acl", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, childDash1.Id, models.DashboardACL{ - DashboardID: childDash1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, childDash1.ID, models.DashboardACL{ + DashboardID: childDash1.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) - moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.Id) + moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID) t.Run("should return folder without acl but not the dashboard with acl", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + DashboardIds: []int64{folder2.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, } err = testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 4) - require.Equal(t, query.Result[0].ID, folder2.Id) - require.Equal(t, query.Result[1].ID, childDash1.Id) - require.Equal(t, query.Result[2].ID, childDash2.Id) - require.Equal(t, query.Result[3].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder2.ID) + require.Equal(t, query.Result[1].ID, childDash1.ID) + require.Equal(t, query.Result[2].ID, childDash2.ID) + require.Equal(t, query.Result[3].ID, dashInRoot.ID) }) }) }) @@ -292,7 +292,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("Given two dashboard folders", func(t *testing.T) { var sqlStore *sqlstore.SQLStore - var folder1, folder2 *models.Dashboard + var folder1, folder2 *dashboards.Dashboard var adminUser, editorUser, viewerUser user.User setup3 := func() { @@ -323,8 +323,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder1.Id) - require.Equal(t, query.Result[1].ID, folder2.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) + require.Equal(t, query.Result[1].ID, folder2.ID) }) t.Run("should have edit permission in folders", func(t *testing.T) { @@ -358,13 +358,13 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder1.Id) - require.Equal(t, query.Result[1].ID, folder2.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) + require.Equal(t, query.Result[1].ID, folder2.ID) }) t.Run("Should have write access to one dashboard folder if default role changed to view for one folder", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, folder1.Id, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: editorUser.ID, Permission: models.PERMISSION_VIEW, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: editorUser.ID, Permission: models.PERMISSION_VIEW, }) require.NoError(t, err) @@ -372,7 +372,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, folder2.Id) + require.Equal(t, query.Result[0].ID, folder2.ID) }) t.Run("should have edit permission in folders", func(t *testing.T) { @@ -409,8 +409,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("Should be able to get one dashboard folder if default role changed to edit for one folder", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, folder1.Id, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -418,7 +418,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, folder1.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) }) t.Run("should not have edit permission in folders", func(t *testing.T) { @@ -442,8 +442,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("and admin permission is given for user with org role viewer in one dashboard folder", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, folder1.Id, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_ADMIN, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_ADMIN, }) require.NoError(t, err) @@ -458,8 +458,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("and edit permission is given for user with org role viewer in one dashboard folder", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, folder1.Id, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -479,19 +479,19 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { var orgId int64 = 1 title := "Very Unique Name" var sqlStore *sqlstore.SQLStore - var folder1, folder2 *models.Dashboard + var folder1, folder2 *dashboards.Dashboard sqlStore = db.InitTestDB(t) quotaService := quotatest.New(false, nil) dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) folder2 = insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod") - _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.Id, false, "prod") + _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.ID, false, "prod") folder1 = insertTestDashboard(t, dashboardStore, title, orgId, 0, true, "prod") t.Run("GetFolderByTitle should find the folder", func(t *testing.T) { result, err := dashboardStore.GetFolderByTitle(context.Background(), orgId, title) require.NoError(t, err) - require.Equal(t, folder1.Id, result.ID) + require.Equal(t, folder1.ID, result.ID) }) }) @@ -502,20 +502,20 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod") - dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod") + dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, false, "prod") t.Run("should return folder by UID", func(t *testing.T) { - d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, folder.Uid) - require.Equal(t, folder.Id, d.ID) + d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, folder.UID) + require.Equal(t, folder.ID, d.ID) require.NoError(t, err) }) t.Run("should not find dashboard", func(t *testing.T) { - d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, dash.Uid) + d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, dash.UID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) t.Run("should search in organization", func(t *testing.T) { - d, err := dashboardStore.GetFolderByUID(context.Background(), orgId+1, folder.Uid) + d, err := dashboardStore.GetFolderByUID(context.Background(), orgId+1, folder.UID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) @@ -528,20 +528,20 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod") - dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod") + dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, false, "prod") t.Run("should return folder by ID", func(t *testing.T) { - d, err := dashboardStore.GetFolderByID(context.Background(), orgId, folder.Id) - require.Equal(t, folder.Id, d.ID) + d, err := dashboardStore.GetFolderByID(context.Background(), orgId, folder.ID) + require.Equal(t, folder.ID, d.ID) require.NoError(t, err) }) t.Run("should not find dashboard", func(t *testing.T) { - d, err := dashboardStore.GetFolderByID(context.Background(), orgId, dash.Id) + d, err := dashboardStore.GetFolderByID(context.Background(), orgId, dash.ID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) t.Run("should search in organization", func(t *testing.T) { - d, err := dashboardStore.GetFolderByID(context.Background(), orgId+1, folder.Id) + d, err := dashboardStore.GetFolderByID(context.Background(), orgId+1, folder.ID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) @@ -550,12 +550,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { } func moveDashboard(t *testing.T, dashboardStore *DashboardStore, orgId int64, dashboard *simplejson.Json, - newFolderId int64) *models.Dashboard { + newFolderId int64) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: newFolderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: newFolderId, Dashboard: dashboard, Overwrite: true, } diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go index 2bfd0feb0cf..bd911d8e27c 100644 --- a/pkg/services/dashboards/database/database_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" ) @@ -23,9 +24,9 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - folderCmd := models.SaveDashboardCommand{ - OrgId: 1, - FolderId: 0, + folderCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + FolderID: 0, IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -36,10 +37,10 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { dash, err := dashboardStore.SaveDashboard(context.Background(), folderCmd) require.Nil(t, err) - saveDashboardCmd := models.SaveDashboardCommand{ - OrgId: 1, + saveDashboardCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, IsFolder: false, - FolderId: dash.Id, + FolderID: dash.ID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "test dashboard", @@ -49,38 +50,38 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { t.Run("Saving dashboards with provisioning meta data", func(t *testing.T) { now := time.Now() - provisioning := &models.DashboardProvisioning{ + provisioning := &dashboards.DashboardProvisioning{ Name: "default", - ExternalId: "/var/grafana.json", + ExternalID: "/var/grafana.json", Updated: now.Unix(), } dash, err := dashboardStore.SaveProvisionedDashboard(context.Background(), saveDashboardCmd, provisioning) require.Nil(t, err) require.NotNil(t, dash) - require.NotEqual(t, 0, dash.Id) - dashId := dash.Id + require.NotEqual(t, 0, dash.ID) + dashId := dash.ID t.Run("Deleting orphaned provisioned dashboards", func(t *testing.T) { - saveCmd := models.SaveDashboardCommand{ - OrgId: 1, + saveCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, IsFolder: false, - FolderId: dash.Id, + FolderID: dash.ID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "another_dashboard", }), } - provisioning := &models.DashboardProvisioning{ + provisioning := &dashboards.DashboardProvisioning{ Name: "another_reader", - ExternalId: "/var/grafana.json", + ExternalID: "/var/grafana.json", Updated: now.Unix(), } anotherDash, err := dashboardStore.SaveProvisionedDashboard(context.Background(), saveCmd, provisioning) require.Nil(t, err) - query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}} + query := &dashboards.GetDashboardsQuery{DashboardIDs: []int64{anotherDash.ID}} err = dashboardStore.GetDashboards(context.Background(), query) require.Nil(t, err) require.NotNil(t, query.Result) @@ -88,12 +89,12 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} require.Nil(t, dashboardStore.DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd)) - query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}} + query = &dashboards.GetDashboardsQuery{DashboardIDs: []int64{dash.ID, anotherDash.ID}} err = dashboardStore.GetDashboards(context.Background(), query) require.Nil(t, err) require.Equal(t, 1, len(query.Result)) - require.Equal(t, dashId, query.Result[0].Id) + require.Equal(t, dashId, query.Result[0].ID) }) t.Run("Can query for provisioned dashboards", func(t *testing.T) { @@ -106,7 +107,7 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { }) t.Run("Can query for one provisioned dashboard", func(t *testing.T) { - data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.Id) + data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.ID) require.Nil(t, err) require.NotNil(t, data) }) @@ -119,13 +120,13 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { t.Run("Deleting folder should delete provision meta data", func(t *testing.T) { deleteCmd := &models.DeleteDashboardCommand{ - Id: dash.Id, + Id: dash.ID, OrgId: 1, } require.Nil(t, dashboardStore.DeleteDashboard(context.Background(), deleteCmd)) - data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.Id) + data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.ID) require.Nil(t, err) require.Nil(t, data) }) diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index dac7c7b784e..0be79da7727 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -35,7 +35,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { } var sqlStore *sqlstore.SQLStore var cfg *setting.Cfg - var savedFolder, savedDash, savedDash2 *models.Dashboard + var savedFolder, savedDash, savedDash2 *dashboards.Dashboard var dashboardStore *DashboardStore var starService star.Service var publicDashboardStore *database.PublicDashboardStoreImpl @@ -48,10 +48,10 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp") - insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.Id, false, "prod") + savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, false, "prod") savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") - insertTestRule(t, sqlStore, savedFolder.OrgId, savedFolder.Uid) + insertTestRule(t, sqlStore, savedFolder.OrgID, savedFolder.UID) publicDashboardStore = database.ProvideStore(sqlStore) } @@ -60,24 +60,24 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() require.Equal(t, savedDash.Title, "test dash 23") require.Equal(t, savedDash.Slug, "test-dash-23") - require.NotEqual(t, savedDash.Id, 0) + require.NotEqual(t, savedDash.ID, 0) require.False(t, savedDash.IsFolder) - require.Positive(t, savedDash.FolderId) - require.Positive(t, len(savedDash.Uid)) + require.Positive(t, savedDash.FolderID) + require.Positive(t, len(savedDash.UID)) require.Equal(t, savedFolder.Title, "1 test dash folder") require.Equal(t, savedFolder.Slug, "1-test-dash-folder") - require.NotEqual(t, savedFolder.Id, 0) + require.NotEqual(t, savedFolder.ID, 0) require.True(t, savedFolder.IsFolder) - require.EqualValues(t, savedFolder.FolderId, 0) - require.Positive(t, len(savedFolder.Uid)) + require.EqualValues(t, savedFolder.FolderID, 0) + require.Positive(t, len(savedFolder.UID)) }) t.Run("Should be able to get dashboard by id", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ - Id: savedDash.Id, - OrgId: 1, + query := dashboards.GetDashboardQuery{ + ID: savedDash.ID, + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -85,16 +85,16 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, query.Result.Title, "test dash 23") require.Equal(t, query.Result.Slug, "test-dash-23") - require.Equal(t, query.Result.Id, savedDash.Id) - require.Equal(t, query.Result.Uid, savedDash.Uid) + require.Equal(t, query.Result.ID, savedDash.ID) + require.Equal(t, query.Result.UID, savedDash.UID) require.False(t, query.Result.IsFolder) }) t.Run("Should be able to get dashboard by slug", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ + query := dashboards.GetDashboardQuery{ Slug: "test-dash-23", - OrgId: 1, + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -102,16 +102,16 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, query.Result.Title, "test dash 23") require.Equal(t, query.Result.Slug, "test-dash-23") - require.Equal(t, query.Result.Id, savedDash.Id) - require.Equal(t, query.Result.Uid, savedDash.Uid) + require.Equal(t, query.Result.ID, savedDash.ID) + require.Equal(t, query.Result.UID, savedDash.UID) require.False(t, query.Result.IsFolder) }) t.Run("Should be able to get dashboard by uid", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ - Uid: savedDash.Uid, - OrgId: 1, + query := dashboards.GetDashboardQuery{ + UID: savedDash.UID, + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -119,23 +119,23 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, query.Result.Title, "test dash 23") require.Equal(t, query.Result.Slug, "test-dash-23") - require.Equal(t, query.Result.Id, savedDash.Id) - require.Equal(t, query.Result.Uid, savedDash.Uid) + require.Equal(t, query.Result.ID, savedDash.ID) + require.Equal(t, query.Result.UID, savedDash.UID) require.False(t, query.Result.IsFolder) }) t.Run("Should be able to get a dashboard UID by ID", func(t *testing.T) { setup() - query := models.GetDashboardRefByIdQuery{Id: savedDash.Id} - err := dashboardStore.GetDashboardUIDById(context.Background(), &query) + query := dashboards.GetDashboardRefByIDQuery{ID: savedDash.ID} + err := dashboardStore.GetDashboardUIDByID(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.Uid, savedDash.Uid) + require.Equal(t, query.Result.UID, savedDash.UID) }) t.Run("Shouldn't be able to get a dashboard with just an OrgID", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ - OrgId: 1, + query := dashboards.GetDashboardQuery{ + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -144,12 +144,12 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to get dashboards by IDs & UIDs", func(t *testing.T) { setup() - query := models.GetDashboardsQuery{DashboardIds: []int64{savedDash.Id, savedDash2.Id}} + query := dashboards.GetDashboardsQuery{DashboardIDs: []int64{savedDash.ID, savedDash2.ID}} err := dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) assert.Equal(t, len(query.Result), 2) - query = models.GetDashboardsQuery{DashboardUIds: []string{savedDash.Uid, savedDash2.Uid}} + query = dashboards.GetDashboardsQuery{DashboardUIDs: []string{savedDash.UID, savedDash2.UID}} err = dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) assert.Equal(t, len(query.Result), 2) @@ -160,7 +160,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, false, "delete this") err := dashboardStore.DeleteDashboard(context.Background(), &models.DeleteDashboardCommand{ - Id: dash.Id, + Id: dash.ID, OrgId: 1, }) require.NoError(t, err) @@ -168,13 +168,13 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to create dashboard", func(t *testing.T) { setup() - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "folderId", "tags": []interface{}{}, }), - UserId: 100, + UserID: 100, } dashboard, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) @@ -186,43 +186,43 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to update dashboard by id and remove folderId", func(t *testing.T) { setup() - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": savedDash.Id, + "id": savedDash.ID, "title": "folderId", "tags": []interface{}{}, }), Overwrite: true, - FolderId: 2, - UserId: 100, + FolderID: 2, + UserID: 100, } dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) - require.EqualValues(t, dash.FolderId, 2) + require.EqualValues(t, dash.FolderID, 2) - cmd = models.SaveDashboardCommand{ - OrgId: 1, + cmd = dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": savedDash.Id, + "id": savedDash.ID, "title": "folderId", "tags": []interface{}{}, }), - FolderId: 0, + FolderID: 0, Overwrite: true, - UserId: 100, + UserID: 100, } _, err = dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) - query := models.GetDashboardQuery{ - Id: savedDash.Id, - OrgId: 1, + query := dashboards.GetDashboardQuery{ + ID: savedDash.ID, + OrgID: 1, } _, err = dashboardStore.GetDashboard(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.FolderId, int64(0)) + require.Equal(t, query.Result.FolderID, int64(0)) require.Equal(t, query.Result.CreatedBy, savedDash.CreatedBy) require.WithinDuration(t, query.Result.Created, savedDash.Created, 3*time.Second) require.Equal(t, query.Result.UpdatedBy, int64(100)) @@ -233,14 +233,14 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() emptyFolder := insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod", "webapp") - deleteCmd := &models.DeleteDashboardCommand{Id: emptyFolder.Id} + deleteCmd := &models.DeleteDashboardCommand{Id: emptyFolder.ID} err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) }) t.Run("Should be not able to delete a dashboard if force delete rules is disabled", func(t *testing.T) { setup() - deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.Id, ForceDeleteFolderRules: false} + deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.ID, ForceDeleteFolderRules: false} err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.True(t, errors.Is(err, dashboards.ErrFolderContainsAlertRules)) }) @@ -252,8 +252,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { cmd := publicDashboardModels.SavePublicDashboardCommand{ PublicDashboard: publicDashboardModels.PublicDashboard{ Uid: uid, - DashboardUid: savedDash.Uid, - OrgId: savedDash.OrgId, + DashboardUid: savedDash.UID, + OrgId: savedDash.OrgID, IsEnabled: true, TimeSettings: &publicDashboardModels.TimeSettings{}, CreatedBy: 1, @@ -266,11 +266,11 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") require.NotNil(t, pubdashConfig) - deleteCmd := &models.DeleteDashboardCommand{Id: savedDash.Id, OrgId: savedDash.OrgId} + deleteCmd := &models.DeleteDashboardCommand{Id: savedDash.ID, OrgId: savedDash.OrgID} err = dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) - query := models.GetDashboardQuery{Uid: savedDash.Uid, OrgId: savedDash.OrgId} + query := dashboards.GetDashboardQuery{UID: savedDash.UID, OrgID: savedDash.OrgID} dash, getErr := dashboardStore.GetDashboard(context.Background(), &query) require.Equal(t, getErr, dashboards.ErrDashboardNotFound) assert.Nil(t, dash) @@ -287,8 +287,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { cmd := publicDashboardModels.SavePublicDashboardCommand{ PublicDashboard: publicDashboardModels.PublicDashboard{ Uid: uid, - DashboardUid: savedDash.Uid, - OrgId: savedDash.OrgId, + DashboardUid: savedDash.UID, + OrgId: savedDash.OrgID, IsEnabled: true, TimeSettings: &publicDashboardModels.TimeSettings{}, CreatedBy: 1, @@ -301,12 +301,12 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") require.NotNil(t, pubdashConfig) - deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.Id, ForceDeleteFolderRules: true} + deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.ID, ForceDeleteFolderRules: true} err = dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) - query := models.GetDashboardsQuery{ - DashboardIds: []int64{savedFolder.Id, savedDash.Id}, + query := dashboards.GetDashboardsQuery{ + DashboardIDs: []int64{savedFolder.ID, savedDash.ID}, } err = dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) @@ -319,13 +319,13 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to delete a dashboard folder and its children if force delete rules is enabled", func(t *testing.T) { setup() - deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.Id, ForceDeleteFolderRules: true} + deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.ID, ForceDeleteFolderRules: true} err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) query := models.FindPersistedDashboardsQuery{ OrgId: 1, - FolderIds: []int64{savedFolder.Id}, + FolderIds: []int64{savedFolder.ID}, SignedInUser: &user.SignedInUser{}, } @@ -335,12 +335,12 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { var existingRuleID int64 - exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.Id).Cols("id").Get(&existingRuleID) + exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.ID).Cols("id").Get(&existingRuleID) require.NoError(t, err) require.False(t, exists) var existingRuleVersionID int64 - exists, err = sess.Table("alert_rule_version").Where("rule_namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.Id).Cols("id").Get(&existingRuleVersionID) + exists, err = sess.Table("alert_rule_version").Where("rule_namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.ID).Cols("id").Get(&existingRuleVersionID) require.NoError(t, err) require.False(t, exists) @@ -350,8 +350,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { }) t.Run("Should return error if no dashboard is found for update when dashboard id is greater than zero", func(t *testing.T) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Overwrite: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(123412321), @@ -365,8 +365,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { }) t.Run("Should not return error if no dashboard is found for update when dashboard id is zero", func(t *testing.T) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Overwrite: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": 0, @@ -408,7 +408,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, len(query.Result), 1) hit := query.Result[0] require.Equal(t, hit.Type, models.DashHitFolder) - require.Equal(t, hit.URL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) + require.Equal(t, hit.URL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug)) require.Equal(t, hit.FolderTitle, "") }) @@ -484,7 +484,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() query := models.FindPersistedDashboardsQuery{ OrgId: 1, - FolderIds: []int64{savedFolder.Id}, + FolderIds: []int64{savedFolder.ID}, SignedInUser: &user.SignedInUser{ OrgID: 1, OrgRole: org.RoleEditor, @@ -499,18 +499,18 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, len(query.Result), 2) hit := query.Result[0] - require.Equal(t, hit.ID, savedDash.Id) - require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug)) - require.Equal(t, hit.FolderID, savedFolder.Id) - require.Equal(t, hit.FolderUID, savedFolder.Uid) + require.Equal(t, hit.ID, savedDash.ID) + require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.UID, savedDash.Slug)) + require.Equal(t, hit.FolderID, savedFolder.ID) + require.Equal(t, hit.FolderUID, savedFolder.UID) require.Equal(t, hit.FolderTitle, savedFolder.Title) - require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) + require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug)) }) t.Run("Should be able to find dashboards by ids", func(t *testing.T) { setup() query := models.FindPersistedDashboardsQuery{ - DashboardIds: []int64{savedDash.Id, savedDash2.Id}, + DashboardIds: []int64{savedDash.ID, savedDash2.ID}, SignedInUser: &user.SignedInUser{ OrgID: 1, OrgRole: org.RoleEditor, @@ -536,13 +536,13 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() starredDash := insertTestDashboard(t, dashboardStore, "starred dash", 1, 0, false) err := starService.Add(context.Background(), &star.StarDashboardCommand{ - DashboardID: starredDash.Id, + DashboardID: starredDash.ID, UserID: 10, }) require.NoError(t, err) err = starService.Add(context.Background(), &star.StarDashboardCommand{ - DashboardID: savedDash.Id, + DashboardID: savedDash.ID, UserID: 1, }) require.NoError(t, err) @@ -576,7 +576,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { count, err = dashboardStore.CountDashboardsInFolder( context.Background(), - &dashboards.CountDashboardsInFolderRequest{FolderID: savedFolder.Id, OrgID: 1}) + &dashboards.CountDashboardsInFolderRequest{FolderID: savedFolder.ID, OrgID: 1}) require.NoError(t, err) require.Equal(t, int64(2), count) }) @@ -595,8 +595,8 @@ func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *test pluginId := "test-app" appFolder := insertTestDashboardForPlugin(t, dashboardStore, "app-test", 1, 0, true, pluginId) - insertTestDashboardForPlugin(t, dashboardStore, "app-dash1", 1, appFolder.Id, false, pluginId) - insertTestDashboardForPlugin(t, dashboardStore, "app-dash2", 1, appFolder.Id, false, pluginId) + insertTestDashboardForPlugin(t, dashboardStore, "app-dash1", 1, appFolder.ID, false, pluginId) + insertTestDashboardForPlugin(t, dashboardStore, "app-dash2", 1, appFolder.ID, false, pluginId) query := models.GetDashboardsByPluginIdQuery{ PluginId: pluginId, @@ -621,8 +621,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false) dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false) - assert.NotZero(t, dashA.Id) - assert.Less(t, dashB.Id, dashA.Id) + assert.NotZero(t, dashA.ID) + assert.Less(t, dashB.ID, dashA.ID) qNoSort := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ OrgID: 1, @@ -636,8 +636,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { results, err := dashboardStore.FindDashboards(context.Background(), qNoSort) require.NoError(t, err) require.Len(t, results, 2) - assert.Equal(t, dashA.Id, results[0].ID) - assert.Equal(t, dashB.Id, results[1].ID) + assert.Equal(t, dashA.ID, results[0].ID) + assert.Equal(t, dashB.ID, results[1].ID) qSort := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ @@ -657,8 +657,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { results, err = dashboardStore.FindDashboards(context.Background(), qSort) require.NoError(t, err) require.Len(t, results, 2) - assert.Equal(t, dashB.Id, results[0].ID) - assert.Equal(t, dashA.Id, results[1].ID) + assert.Equal(t, dashB.ID, results[0].ID) + assert.Equal(t, dashA.ID, results[1].ID) } func TestIntegrationDashboard_Filter(t *testing.T) { @@ -706,7 +706,7 @@ func TestIntegrationDashboard_Filter(t *testing.T) { results, err = dashboardStore.FindDashboards(context.Background(), qFilter) require.NoError(t, err) require.Len(t, results, 1) - assert.Equal(t, dashB.Id, results[0].ID) + assert.Equal(t, dashB.ID, results[0].ID) } func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID string) { @@ -783,11 +783,11 @@ func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID st } func insertTestDashboard(t *testing.T, dashboardStore *DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -798,23 +798,23 @@ func insertTestDashboard(t *testing.T, dashboardStore *DashboardStore, title str dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) + dash.Data.Set("id", dash.ID) + dash.Data.Set("uid", dash.UID) return dash } func insertTestDashboardForPlugin(t *testing.T, dashboardStore *DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, pluginId string) *models.Dashboard { + folderId int64, isFolder bool, pluginId string) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": title, }), - PluginId: pluginId, + PluginID: pluginId, } dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index f35cfcd686a..76ed476e483 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -1,20 +1,300 @@ package dashboards import ( + "fmt" "time" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) +const RootFolderName = "General" + +const ( + DashTypeDB = "db" + DashTypeSnapshot = "snapshot" +) + +// Dashboard model +type Dashboard struct { + ID int64 `xorm:"pk autoincr 'id'"` + UID string `xorm:"uid"` + Slug string + OrgID int64 `xorm:"org_id"` + GnetID int64 `xorm:"gnet_id"` + Version int + PluginID string `xorm:"plugin_id"` + + Created time.Time + Updated time.Time + + UpdatedBy int64 + CreatedBy int64 + FolderID int64 `xorm:"folder_id"` + IsFolder bool + HasACL bool `xorm:"has_acl"` + + Title string + Data *simplejson.Json +} + +func (d *Dashboard) SetID(id int64) { + d.ID = id + d.Data.Set("id", id) +} + +func (d *Dashboard) SetUID(uid string) { + d.UID = uid + d.Data.Set("uid", uid) +} + +func (d *Dashboard) SetVersion(version int) { + d.Version = version + d.Data.Set("version", version) +} + +// NewDashboard creates a new dashboard +func NewDashboard(title string) *Dashboard { + dash := &Dashboard{} + dash.Data = simplejson.New() + dash.Data.Set("title", title) + dash.Title = title + dash.Created = time.Now() + dash.Updated = time.Now() + dash.UpdateSlug() + return dash +} + +// NewDashboardFolder creates a new dashboard folder +func NewDashboardFolder(title string) *Dashboard { + folder := NewDashboard(title) + folder.IsFolder = true + folder.Data.Set("schemaVersion", 17) + folder.Data.Set("version", 0) + folder.IsFolder = true + return folder +} + +// GetTags turns the tags in data json into go string array +func (d *Dashboard) GetTags() []string { + return d.Data.Get("tags").MustStringArray() +} + +func NewDashboardFromJson(data *simplejson.Json) *Dashboard { + dash := &Dashboard{} + dash.Data = data + dash.Title = dash.Data.Get("title").MustString() + dash.UpdateSlug() + update := false + + if id, err := dash.Data.Get("id").Float64(); err == nil { + dash.ID = int64(id) + update = true + } + + if uid, err := dash.Data.Get("uid").String(); err == nil { + dash.UID = uid + update = true + } + + if version, err := dash.Data.Get("version").Float64(); err == nil && update { + dash.Version = int(version) + dash.Updated = time.Now() + } else { + dash.Data.Set("version", 0) + dash.Created = time.Now() + dash.Updated = time.Now() + } + + if gnetId, err := dash.Data.Get("gnetId").Float64(); err == nil { + dash.GnetID = int64(gnetId) + } + + return dash +} + +// GetDashboardModel turns the command into the saveable model +func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { + dash := NewDashboardFromJson(cmd.Dashboard) + userID := cmd.UserID + + if userID == 0 { + userID = -1 + } + + dash.UpdatedBy = userID + dash.OrgID = cmd.OrgID + dash.PluginID = cmd.PluginID + dash.IsFolder = cmd.IsFolder + dash.FolderID = cmd.FolderID + dash.UpdateSlug() + return dash +} + +// UpdateSlug updates the slug +func (d *Dashboard) UpdateSlug() { + title := d.Data.Get("title").MustString() + d.Slug = slugify.Slugify(title) +} + +// GetURL return the html url for a folder if it's folder, otherwise for a dashboard +func (d *Dashboard) GetURL() string { + return GetDashboardFolderURL(d.IsFolder, d.UID, d.Slug) +} + +// GetDashboardFolderURL return the html url for a folder if it's folder, otherwise for a dashboard +func GetDashboardFolderURL(isFolder bool, uid string, slug string) string { + if isFolder { + return GetFolderURL(uid, slug) + } + + return GetDashboardURL(uid, slug) +} + +// GetDashboardURL returns the HTML url for a dashboard. +func GetDashboardURL(uid string, slug string) string { + return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug) +} + +// GetKioskModeDashboardUrl returns the HTML url for a dashboard in kiosk mode. +func GetKioskModeDashboardURL(uid string, slug string, theme models.Theme) string { + return fmt.Sprintf("%s?kiosk&theme=%s", GetDashboardURL(uid, slug), string(theme)) +} + +// GetFullDashboardURL returns the full URL for a dashboard. +func GetFullDashboardURL(uid string, slug string) string { + return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug) +} + +// GetFolderURL returns the HTML url for a folder. +func GetFolderURL(folderUID string, slug string) string { + return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUID, slug) +} + +type ValidateDashboardBeforeSaveResult struct { + IsParentFolderChanged bool +} + +// +// COMMANDS +// + +type SaveDashboardCommand struct { + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + UserID int64 `json:"userId" xorm:"user_id"` + Overwrite bool `json:"overwrite"` + Message string `json:"message"` + OrgID int64 `json:"-" xorm:"org_id"` + RestoredFrom int `json:"-"` + PluginID string `json:"-" xorm:"plugin_id"` + FolderID int64 `json:"folderId" xorm:"folder_id"` + FolderUID string `json:"folderUid" xorm:"folder_uid"` + IsFolder bool `json:"isFolder"` + + UpdatedAt time.Time + + Result *Dashboard `json:"-"` +} + +type ValidateDashboardCommand struct { + Dashboard string `json:"dashboard" binding:"Required"` +} + +type TrimDashboardCommand struct { + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + Meta *simplejson.Json `json:"meta"` + Result *Dashboard `json:"-"` +} + +type DashboardProvisioning struct { + ID int64 `xorm:"pk autoincr 'id'"` + DashboardID int64 `xorm:"dashboard_id"` + Name string + ExternalID string `xorm:"external_id"` + CheckSum string + Updated int64 +} + +type DeleteDashboardCommand struct { + ID int64 + OrgID int64 + ForceDeleteFolderRules bool +} + +type DeleteOrphanedProvisionedDashboardsCommand struct { + ReaderNames []string +} + +// +// QUERIES +// + +type GetDashboardQuery struct { + Slug string // required if no ID or Uid is specified + ID int64 // optional if slug is set + UID string // optional if slug is set + OrgID int64 + + Result *Dashboard +} + +type DashboardTagCloudItem struct { + Term string `json:"term"` + Count int `json:"count"` +} + +type GetDashboardTagsQuery struct { + OrgID int64 + Result []*DashboardTagCloudItem +} + +type GetDashboardsQuery struct { + DashboardIDs []int64 + DashboardUIDs []string + OrgID int64 + Result []*Dashboard +} + +type GetDashboardsByPluginIdQuery struct { + OrgID int64 + PluginId string + Result []*Dashboard +} + +type GetDashboardSlugByIdQuery struct { + ID int64 + Result string +} + +type GetDashboardsBySlugQuery struct { + OrgID int64 + Slug string + + Result []*Dashboard +} + +type DashboardRef struct { + UID string `xorm:"uid"` + Slug string +} + +type GetDashboardRefByIDQuery struct { + ID int64 + Result *DashboardRef +} + type SaveDashboardDTO struct { - OrgId int64 + OrgID int64 UpdatedAt time.Time User *user.SignedInUser Message string Overwrite bool - Dashboard *models.Dashboard + Dashboard *Dashboard } type DashboardSearchProjection struct { @@ -48,3 +328,18 @@ type CountDashboardsInFolderRequest struct { FolderID int64 OrgID int64 } + +func FromDashboard(dash *Dashboard) *folder.Folder { + return &folder.Folder{ + ID: dash.ID, + UID: dash.UID, + Title: dash.Title, + HasACL: dash.HasACL, + Url: models.GetFolderUrl(dash.UID, dash.Slug), + Version: dash.Version, + Created: dash.Created, + CreatedBy: dash.CreatedBy, + Updated: dash.Updated, + UpdatedBy: dash.UpdatedBy, + } +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 8a3f7ebf55b..a5eb63bebdc 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -77,19 +77,19 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co } func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO, shouldValidateAlerts bool, - validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { + validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) { dash := dto.Dashboard - dash.OrgId = dto.OrgId + dash.OrgID = dto.OrgID dash.Title = strings.TrimSpace(dash.Title) dash.Data.Set("title", dash.Title) - dash.SetUid(strings.TrimSpace(dash.Uid)) + dash.SetUID(strings.TrimSpace(dash.UID)) if dash.Title == "" { return nil, dashboards.ErrDashboardTitleEmpty } - if dash.IsFolder && dash.FolderId > 0 { + if dash.IsFolder && dash.FolderID > 0 { return nil, dashboards.ErrDashboardFolderCannotHaveParent } @@ -97,9 +97,9 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d return nil, dashboards.ErrDashboardFolderNameExists } - if !util.IsValidShortUID(dash.Uid) { + if !util.IsValidShortUID(dash.UID) { return nil, dashboards.ErrDashboardInvalidUid - } else if util.IsShortUIDTooLong(dash.Uid) { + } else if util.IsShortUIDTooLong(dash.UID) { return nil, dashboards.ErrDashboardUidTooLong } @@ -108,7 +108,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if shouldValidateAlerts { - dashAlertInfo := alerting.DashAlertInfo{Dash: dash, User: dto.User, OrgID: dash.OrgId} + dashAlertInfo := alerting.DashAlertInfo{Dash: dash, User: dto.User, OrgID: dash.OrgID} if err := dr.dashAlertExtractor.ValidateAlerts(ctx, dashAlertInfo); err != nil { return nil, err } @@ -121,11 +121,11 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d if isParentFolderChanged { // Check that the user is allowed to add a dashboard to the folder - guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgId, dto.User) + guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgID, dto.User) if err != nil { return nil, err } - if canSave, err := guardian.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canSave { + if canSave, err := guardian.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canSave { if err != nil { return nil, err } @@ -134,7 +134,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if validateProvisionedDashboard { - provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.Id) + provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.ID) if err != nil { return nil, err } @@ -149,8 +149,8 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d return nil, err } - if dash.Id == 0 { - if canCreate, err := guard.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canCreate { + if dash.ID == 0 { + if canCreate, err := guard.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canCreate { if err != nil { return nil, err } @@ -165,15 +165,15 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } } - cmd := &models.SaveDashboardCommand{ + cmd := &dashboards.SaveDashboardCommand{ Dashboard: dash.Data, Message: dto.Message, - OrgId: dto.OrgId, + OrgID: dto.OrgID, Overwrite: dto.Overwrite, - UserId: dto.User.UserID, - FolderId: dash.FolderId, + UserID: dto.User.UserID, + FolderID: dash.FolderID, IsFolder: dash.IsFolder, - PluginId: dash.PluginId, + PluginID: dash.PluginID, } if !dto.UpdatedAt.IsZero() { @@ -193,25 +193,25 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. // getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard // It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck() -func getGuardianForSavePermissionCheck(ctx context.Context, d *models.Dashboard, user *user.SignedInUser) (guardian.DashboardGuardian, error) { - newDashboard := d.Id == 0 +func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user *user.SignedInUser) (guardian.DashboardGuardian, error) { + newDashboard := d.ID == 0 if newDashboard { // if it's a new dashboard/folder check the parent folder permissions - guard, err := guardian.New(ctx, d.FolderId, d.OrgId, user) + guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user) if err != nil { return nil, err } return guard, nil } - guard, err := guardian.NewByDashboard(ctx, d, d.OrgId, user) + guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) if err != nil { return nil, err } return guard, nil } -func validateDashboardRefreshInterval(dash *models.Dashboard) error { +func validateDashboardRefreshInterval(dash *dashboards.Dashboard) error { if setting.MinRefreshInterval == "" { return nil } @@ -239,14 +239,14 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { } func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, - provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid", - dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) + dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } - dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgId, org.RoleAdmin, provisionerPermissions) + dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, setting.IsLegacyAlertingEnabled(), false) if err != nil { @@ -263,7 +263,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, - OrgID: dto.OrgId, + OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled @@ -273,13 +273,13 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt return nil, err } - err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } - if dto.Dashboard.Id == 0 { + if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, true); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } @@ -288,8 +288,8 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt return dash, nil } -func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { - dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgId, org.RoleAdmin, provisionerPermissions) +func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { + dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false) if err != nil { return nil, err @@ -303,7 +303,7 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, - OrgID: dto.OrgId, + OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled @@ -313,13 +313,13 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C return nil, err } - err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } - if dto.Dashboard.Id == 0 { + if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, true); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } @@ -329,10 +329,10 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C } func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, - allowUiUpdate bool) (*models.Dashboard, error) { + allowUiUpdate bool) (*dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", - "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", + "dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } @@ -350,7 +350,7 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, - OrgID: dto.OrgId, + OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled @@ -360,14 +360,14 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar return nil, err } - err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } // new dashboard created - if dto.Dashboard.Id == 0 { + if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, false); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } @@ -450,10 +450,10 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId } func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) ( - *models.Dashboard, error) { + *dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", - "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, + "dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } @@ -485,8 +485,8 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que return dr.dashboardStore.GetDashboardsByPluginID(ctx, query) } -func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *models.Dashboard, provisioned bool) error { - inFolder := dash.FolderId > 0 +func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) error { + inFolder := dash.FolderID > 0 if !accesscontrol.IsDisabled(dr.cfg) { var permissions []accesscontrol.SetResourcePermissionCommand if !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous { @@ -507,12 +507,12 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * svc = dr.folderPermissions } - _, err := svc.SetPermissions(ctx, dto.OrgId, dash.Uid, permissions...) + _, err := svc.SetPermissions(ctx, dto.OrgID, dash.UID, permissions...) if err != nil { return err } } else if dr.cfg.EditorsCanAdmin && !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous { - if err := dr.MakeUserAdmin(ctx, dto.OrgId, dto.User.UserID, dash.Id, !inFolder); err != nil { + if err := dr.MakeUserAdmin(ctx, dto.OrgID, dto.User.UserID, dash.ID, !inFolder); err != nil { return err } } @@ -520,16 +520,16 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * return nil } -func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error { +func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) error { _, err := dr.dashboardStore.GetDashboard(ctx, query) return err } -func (dr *DashboardServiceImpl) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { - return dr.dashboardStore.GetDashboardUIDById(ctx, query) +func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) error { + return dr.dashboardStore.GetDashboardUIDByID(ctx, query) } -func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) error { return dr.dashboardStore.GetDashboards(ctx, query) } diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index e70e4f391c2..51d34a60e4a 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -35,8 +35,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When saving a dashboard with non-existing id", true, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(123412321), "title": "Expect error", @@ -54,10 +54,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", true, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: otherOrgId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "Expect error", }), Overwrite: false, @@ -70,10 +70,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", true, func(t *testing.T, sc *permissionScenarioContext) { const otherOrgId int64 = 2 - cmd := models.SaveDashboardCommand{ - OrgId: otherOrgId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Dash with existing uid in other org", }), Overwrite: false, @@ -82,9 +82,9 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - OrgId: otherOrgId, - Uid: sc.savedDashInFolder.Uid, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + OrgID: otherOrgId, + UID: sc.savedDashInFolder.UID, }) require.NoError(t, err) }) @@ -96,12 +96,12 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a new dashboard in the General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { sqlStore := db.InitTestDB(t) - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dash", }), - UserId: 10000, + UserID: 10000, Overwrite: true, } @@ -109,195 +109,195 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) assert.Equal(t, "", sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When creating a new dashboard in other folder, it should create dashboard guardian for other folder with correct arguments and rsult in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dash", }), - FolderId: sc.otherSavedFolder.Id, - UserId: 10000, + FolderID: sc.otherSavedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.otherSavedFolder.ID, sc.dashboardGuardianMock.DashID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": sc.savedDashInFolder.Title, }), - FolderId: sc.savedFolder.Id, - UserId: 10000, + FolderID: sc.savedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "New dash", }), - FolderId: sc.savedFolder.Id, - UserId: 10000, + FolderID: sc.savedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When updating a dashboard by existing id in the General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Dash", }), - FolderId: sc.savedDashInGeneralFolder.FolderId, - UserId: 10000, + FolderID: sc.savedDashInGeneralFolder.FolderID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When updating a dashboard by existing id in other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "Dash", }), - FolderId: sc.savedDashInFolder.FolderId, - UserId: 10000, + FolderID: sc.savedDashInFolder.FolderID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Dash", }), - FolderId: sc.otherSavedFolder.Id, - UserId: 10000, + FolderID: sc.otherSavedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "Dash", }), - FolderId: 0, - UserId: 10000, + FolderID: 0, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInGeneralFolder.Uid, + "uid": sc.savedDashInGeneralFolder.UID, "title": "Dash", }), - FolderId: sc.otherSavedFolder.Id, - UserId: 10000, + FolderID: sc.otherSavedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Dash", }), - FolderId: 0, - UserId: 10000, + FolderID: 0, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) }) @@ -309,22 +309,22 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) @@ -332,32 +332,32 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInGeneralFolder.Title, }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id) + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When creating a folder with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, @@ -369,20 +369,20 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id) + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) assert.True(t, res.IsFolder) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dash without id and uid", }), @@ -392,19 +392,19 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Greater(t, res.Id, int64(0)) - assert.NotEmpty(t, res.Uid) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + assert.Greater(t, res.ID, int64(0)) + assert.NotEmpty(t, res.UID) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When saving a dashboard when dashboard id is zero ", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": 0, "title": "Dash with zero id", @@ -415,21 +415,21 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When saving a dashboard in non-existing folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Expect error", }), - FolderId: 123412321, + FolderID: 123412321, Overwrite: shouldOverwrite, } @@ -439,13 +439,13 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "test dash 23", }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } @@ -455,23 +455,23 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by id with current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Updated title", "version": sc.savedDashInGeneralFolder.Version, }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInGeneralFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) @@ -479,13 +479,13 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "test dash 23", }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } @@ -495,36 +495,36 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by uid with current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Updated title", "version": sc.savedDashInFolder.Version, }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, }), - FolderId: sc.savedDashInFolder.FolderId, + FolderID: sc.savedDashInFolder.FolderID, Overwrite: shouldOverwrite, } @@ -534,13 +534,13 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInGeneralFolder.Title, }), - FolderId: sc.savedDashInGeneralFolder.FolderId, + FolderID: sc.savedDashInGeneralFolder.FolderID, Overwrite: shouldOverwrite, } @@ -550,8 +550,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a folder with same name as existing folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedFolder.Title, @@ -570,54 +570,54 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Updated title", }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInGeneralFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Updated title", }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating uid for existing dashboard using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "uid": "new-uid", "title": sc.savedDashInFolder.Title, }), @@ -626,23 +626,23 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.Id, res.Id) - assert.Equal(t, "new-uid", res.Uid) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, "new-uid", res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, - "uid": sc.savedDashInGeneralFolder.Uid, + "id": sc.savedDashInFolder.ID, + "uid": sc.savedDashInGeneralFolder.UID, "title": sc.savedDashInFolder.Title, }), Overwrite: shouldOverwrite, @@ -654,58 +654,58 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, }), - FolderId: sc.savedDashInFolder.FolderId, + FolderID: sc.savedDashInFolder.FolderID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.Id, res.Id) - assert.Equal(t, sc.savedDashInFolder.Uid, res.Uid) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInFolder.UID, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInGeneralFolder.Title, }), - FolderId: sc.savedDashInGeneralFolder.FolderId, + FolderID: sc.savedDashInGeneralFolder.FolderID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Equal(t, sc.savedDashInGeneralFolder.Id, res.Id) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, res.Uid) + assert.Equal(t, sc.savedDashInGeneralFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating existing folder to a dashboard using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedFolder.Id, + "id": sc.savedFolder.ID, "title": "new title", }), IsFolder: false, @@ -718,10 +718,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing dashboard to a folder using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "new folder title", }), IsFolder: true, @@ -734,10 +734,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing folder to a dashboard using uid", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedFolder.Uid, + "uid": sc.savedFolder.UID, "title": "new title", }), IsFolder: false, @@ -750,10 +750,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing dashboard to a folder using uid", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "new folder title", }), IsFolder: true, @@ -766,8 +766,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing folder to a dashboard using title", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": sc.savedFolder.Title, }), @@ -781,8 +781,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing dashboard to a folder using title", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": sc.savedDashInGeneralFolder.Title, }), @@ -802,10 +802,10 @@ type permissionScenarioContext struct { dashboardGuardianMock *guardian.FakeDashboardGuardian sqlStore db.DB dashboardStore dashboards.Store - savedFolder *models.Dashboard - savedDashInFolder *models.Dashboard - otherSavedFolder *models.Dashboard - savedDashInGeneralFolder *models.Dashboard + savedFolder *dashboards.Dashboard + savedDashInFolder *dashboards.Dashboard + otherSavedFolder *dashboards.Dashboard + savedDashInGeneralFolder *dashboards.Dashboard } type permissionScenarioFunc func(t *testing.T, sc *permissionScenarioContext) @@ -835,24 +835,24 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc guardian.InitLegacyGuardian(sqlStore, service, &teamtest.FakeService{}) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) - savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.Id, sqlStore) - saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.Id, sqlStore) + savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.ID, sqlStore) + saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.ID, sqlStore) savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgID, 0, sqlStore) otherSavedFolder := saveTestFolder(t, "Other saved folder", testOrgID, sqlStore) require.Equal(t, "Saved folder", savedFolder.Title) require.Equal(t, "saved-folder", savedFolder.Slug) - require.NotEqual(t, int64(0), savedFolder.Id) + require.NotEqual(t, int64(0), savedFolder.ID) require.True(t, savedFolder.IsFolder) - require.Equal(t, int64(0), savedFolder.FolderId) - require.NotEmpty(t, savedFolder.Uid) + require.Equal(t, int64(0), savedFolder.FolderID) + require.NotEmpty(t, savedFolder.UID) require.Equal(t, "Saved dash in folder", savedDashInFolder.Title) require.Equal(t, "saved-dash-in-folder", savedDashInFolder.Slug) - require.NotEqual(t, int64(0), savedDashInFolder.Id) + require.NotEqual(t, int64(0), savedDashInFolder.ID) require.False(t, savedDashInFolder.IsFolder) - require.Equal(t, savedFolder.Id, savedDashInFolder.FolderId) - require.NotEmpty(t, savedDashInFolder.Uid) + require.Equal(t, savedFolder.ID, savedDashInFolder.FolderID) + require.NotEmpty(t, savedDashInFolder.UID) origNewDashboardGuardian := guardian.New t.Cleanup(func() { @@ -874,7 +874,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc }) } -func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore db.DB) *models.Dashboard { +func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) *dashboards.Dashboard { t.Helper() dto := toSaveDashboardDto(cmd) @@ -897,7 +897,7 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore return res } -func callSaveWithError(t *testing.T, cmd models.SaveDashboardCommand, sqlStore db.DB) error { +func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) error { dto := toSaveDashboardDto(cmd) cfg := setting.NewCfg() cfg.RBACEnabled = false @@ -916,12 +916,12 @@ func callSaveWithError(t *testing.T, cmd models.SaveDashboardCommand, sqlStore d return err } -func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlStore db.DB) *models.Dashboard { +func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlStore db.DB) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgID, - FolderId: folderID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgID, + FolderID: folderID, IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -930,7 +930,7 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto } dto := dashboards.SaveDashboardDTO{ - OrgId: orgID, + OrgID: orgID, Dashboard: cmd.GetDashboardModel(), User: &user.SignedInUser{ UserID: 1, @@ -957,11 +957,11 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto return res } -func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *models.Dashboard { +func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgID, - FolderId: 0, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgID, + FolderID: 0, IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -970,7 +970,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *mo } dto := dashboards.SaveDashboardDTO{ - OrgId: orgID, + OrgID: orgID, Dashboard: cmd.GetDashboardModel(), User: &user.SignedInUser{ UserID: 1, @@ -997,14 +997,14 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *mo return res } -func toSaveDashboardDto(cmd models.SaveDashboardCommand) dashboards.SaveDashboardDTO { +func toSaveDashboardDto(cmd dashboards.SaveDashboardCommand) dashboards.SaveDashboardDTO { dash := (&cmd).GetDashboardModel() return dashboards.SaveDashboardDTO{ Dashboard: dash, Message: cmd.Message, - OrgId: cmd.OrgId, - User: &user.SignedInUser{UserID: cmd.UserId}, + OrgID: cmd.OrgID, + User: &user.SignedInUser{UserID: cmd.UserID}, Overwrite: cmd.Overwrite, } } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 3464677f40e..fd9c4a87b98 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -43,21 +43,21 @@ func TestDashboardService(t *testing.T) { titles := []string{"", " ", " \t "} for _, title := range titles { - dto.Dashboard = models.NewDashboard(title) + dto.Dashboard = dashboards.NewDashboard(title) _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardTitleEmpty) } }) t.Run("Should return validation error if it's a folder and have a folder id", func(t *testing.T) { - dto.Dashboard = models.NewDashboardFolder("Folder") - dto.Dashboard.FolderId = 1 + dto.Dashboard = dashboards.NewDashboardFolder("Folder") + dto.Dashboard.FolderID = 1 _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardFolderCannotHaveParent) }) t.Run("Should return validation error if folder is named General", func(t *testing.T) { - dto.Dashboard = models.NewDashboardFolder("General") + dto.Dashboard = dashboards.NewDashboardFolder("General") _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardFolderNameExists) }) @@ -77,8 +77,8 @@ func TestDashboardService(t *testing.T) { } for _, tc := range testCases { - dto.Dashboard = models.NewDashboard("title") - dto.Dashboard.SetUid(tc.Uid) + dto.Dashboard = dashboards.NewDashboard("title") + dto.Dashboard.SetUID(tc.Uid) dto.User = &user.SignedInUser{} if tc.Error == nil { @@ -93,8 +93,8 @@ func TestDashboardService(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&models.DashboardProvisioning{}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardCannotSaveProvisionedDashboard) @@ -102,10 +102,10 @@ func TestDashboardService(t *testing.T) { t.Run("Should not return validation error if dashboard is provisioned but UI updates allowed", func(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() - fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveDashboard(context.Background(), dto, true) require.NoError(t, err) @@ -128,10 +128,10 @@ func TestDashboardService(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(nil, nil).Once() - fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() fakeStore.On("SaveAlerts", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("alert validation error")).Once() - dto.Dashboard = models.NewDashboard("Dash") + dto.Dashboard = dashboards.NewDashboard("Dash") dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveDashboard(context.Background(), dto, false) require.Error(t, err) @@ -144,10 +144,10 @@ func TestDashboardService(t *testing.T) { t.Run("Should not return validation error if dashboard is provisioned", func(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() - fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand"), mock.AnythingOfType("*models.DashboardProvisioning")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand"), mock.AnythingOfType("*dashboards.DashboardProvisioning")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveProvisionedDashboard(context.Background(), dto, nil) require.NoError(t, err) @@ -155,14 +155,14 @@ func TestDashboardService(t *testing.T) { t.Run("Should override invalid refresh interval if dashboard is provisioned", func(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() - fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand"), mock.AnythingOfType("*models.DashboardProvisioning")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand"), mock.AnythingOfType("*dashboards.DashboardProvisioning")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() oldRefreshInterval := setting.MinRefreshInterval setting.MinRefreshInterval = "5m" defer func() { setting.MinRefreshInterval = oldRefreshInterval }() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} dto.Dashboard.Data.Set("refresh", "1s") _, err := service.SaveProvisionedDashboard(context.Background(), dto, nil) @@ -178,8 +178,8 @@ func TestDashboardService(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&models.DashboardProvisioning{}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.ImportDashboard(context.Background(), dto) require.Equal(t, err, dashboards.ErrDashboardCannotSaveProvisionedDashboard) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 9cdd1a9aa41..d6ee0dc6434 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -1,14 +1,16 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboards import ( context "context" - models "github.com/grafana/grafana/pkg/models" folder "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/quota" mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/models" + + quota "github.com/grafana/grafana/pkg/services/quota" ) // FakeDashboardStore is an autogenerated mock type for the Store type @@ -16,6 +18,29 @@ type FakeDashboardStore struct { mock.Mock } +// Count provides a mock function with given fields: _a0, _a1 +func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParameters) (*quota.Map, error) { + ret := _m.Called(_a0, _a1) + + var r0 *quota.Map + if rf, ok := ret.Get(0).(func(context.Context, *quota.ScopeParameters) *quota.Map); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*quota.Map) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *quota.ScopeParameters) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CountDashboardsInFolder provides a mock function with given fields: ctx, request func (_m *FakeDashboardStore) CountDashboardsInFolder(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) { ret := _m.Called(ctx, request) @@ -103,20 +128,20 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models. } // GetDashboard provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) { +func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) { ret := _m.Called(ctx, query) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardQuery) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) *Dashboard); ok { r0 = rf(ctx, query) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.GetDashboardQuery) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardQuery) error); ok { r1 = rf(ctx, query) } else { r1 = ret.Error(1) @@ -153,12 +178,12 @@ func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *model return r0 } -// GetDashboardUIDById provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { +// GetDashboardUIDByID provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardRefByIdQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -168,11 +193,11 @@ func (_m *FakeDashboardStore) GetDashboardUIDById(ctx context.Context, query *mo } // GetDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardsQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -376,20 +401,20 @@ func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, aler } // SaveDashboard provides a mock function with given fields: ctx, cmd -func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd models.SaveDashboardCommand) (*models.Dashboard, error) { +func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) { ret := _m.Called(ctx, cmd) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, models.SaveDashboardCommand) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand) *Dashboard); ok { r0 = rf(ctx, cmd) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, models.SaveDashboardCommand) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, SaveDashboardCommand) error); ok { r1 = rf(ctx, cmd) } else { r1 = ret.Error(1) @@ -399,20 +424,20 @@ func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd models.Save } // SaveProvisionedDashboard provides a mock function with given fields: ctx, cmd, provisioning -func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { +func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) { ret := _m.Called(ctx, cmd, provisioning) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, models.SaveDashboardCommand, *models.DashboardProvisioning) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand, *DashboardProvisioning) *Dashboard); ok { r0 = rf(ctx, cmd, provisioning) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, models.SaveDashboardCommand, *models.DashboardProvisioning) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, SaveDashboardCommand, *DashboardProvisioning) error); ok { r1 = rf(ctx, cmd, provisioning) } else { r1 = ret.Error(1) @@ -450,18 +475,18 @@ func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64, } // ValidateDashboardBeforeSave provides a mock function with given fields: ctx, dashboard, overwrite -func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *models.Dashboard, overwrite bool) (bool, error) { +func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) { ret := _m.Called(ctx, dashboard, overwrite) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, bool) bool); ok { + if rf, ok := ret.Get(0).(func(context.Context, *Dashboard, bool) bool); ok { r0 = rf(ctx, dashboard, overwrite) } else { r0 = ret.Get(0).(bool) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, bool) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *Dashboard, bool) error); ok { r1 = rf(ctx, dashboard, overwrite) } else { r1 = ret.Error(1) @@ -475,10 +500,6 @@ type mockConstructorTestingTNewFakeDashboardStore interface { Cleanup(func()) } -func (_m *FakeDashboardStore) Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) { - return nil, nil -} - // NewFakeDashboardStore creates a new instance of FakeDashboardStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func NewFakeDashboardStore(t mockConstructorTestingTNewFakeDashboardStore) *FakeDashboardStore { mock := &FakeDashboardStore{} diff --git a/pkg/services/datasources/service/store.go b/pkg/services/datasources/service/store.go index 9cc07b3a2b8..306f43d2c35 100644 --- a/pkg/services/datasources/service/store.go +++ b/pkg/services/datasources/service/store.go @@ -198,7 +198,7 @@ func (ss *SqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := "SELECT COUNT(*) AS count FROM data_source WHERE org_id=?" if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5dd409da495..1575c038bd9 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -83,12 +83,6 @@ var ( Description: "Experimental. Allow override default AAD audience for Azure Prometheus endpoint", State: FeatureStateBeta, }, - { - Name: "influxdbBackendMigration", - Description: "Query InfluxDB InfluxQL without the proxy", - State: FeatureStateAlpha, - FrontendOnly: true, - }, { Name: "showFeatureFlagsInUI", Description: "Show feature flags in the settings UI", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 1ec1a331b04..3dde0372da3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -63,10 +63,6 @@ const ( // Experimental. Allow override default AAD audience for Azure Prometheus endpoint FlagPrometheusAzureOverrideAudience = "prometheusAzureOverrideAudience" - // FlagInfluxdbBackendMigration - // Query InfluxDB InfluxQL without the proxy - FlagInfluxdbBackendMigration = "influxdbBackendMigration" - // FlagShowFeatureFlagsInUI // Show feature flags in the settings UI FlagShowFeatureFlagsInUI = "showFeatureFlagsInUI" diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 3890db59bc1..23dfaf60606 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -293,15 +293,15 @@ func (s *Service) getFolderByTitle(ctx context.Context, user *user.SignedInUser, func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) { logger := s.log.FromContext(ctx) - dashFolder := models.NewDashboardFolder(cmd.Title) - dashFolder.OrgId = cmd.OrgID + dashFolder := dashboards.NewDashboardFolder(cmd.Title) + dashFolder.OrgID = cmd.OrgID trimmedUID := strings.TrimSpace(cmd.UID) if trimmedUID == accesscontrol.GeneralFolderUID { return nil, dashboards.ErrFolderInvalidUID } - dashFolder.SetUid(trimmedUID) + dashFolder.SetUID(trimmedUID) if cmd.SignedInUser == nil { return nil, folder.ErrBadRequest.Errorf("missing signed in user") @@ -317,7 +317,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( dto := &dashboards.SaveDashboardDTO{ Dashboard: dashFolder, - OrgId: cmd.OrgID, + OrgID: cmd.OrgID, User: user, } @@ -332,7 +332,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( } var createdFolder *folder.Folder - createdFolder, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.Id) + createdFolder, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.ID) if err != nil { return nil, err } @@ -366,7 +366,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( // TODO: Today, if a UID isn't specified, the dashboard store // generates a new UID. The new folder store will need to do this as // well, but for now we take the UID from the newly created folder. - UID: dash.Uid, + UID: dash.UID, OrgID: cmd.OrgID, Title: cmd.Title, Description: cmd.Description, @@ -384,11 +384,11 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( }); deleteErr != nil { logger.Error("error deleting folder after failed save to nested folder store", "error", err) } - return folder.FromDashboard(dash), err + return dashboards.FromDashboard(dash), err } } - f := folder.FromDashboard(dash) + f := dashboards.FromDashboard(dash) if nestedFolder != nil && nestedFolder.ParentUID != "" { f.ParentUID = nestedFolder.ParentUID } @@ -439,7 +439,7 @@ func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) ( func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) { logger := s.log.FromContext(ctx) - query := models.GetDashboardQuery{OrgId: cmd.OrgID, Uid: cmd.UID} + query := dashboards.GetDashboardQuery{OrgID: cmd.OrgID, UID: cmd.UID} _, err := s.dashboardStore.GetDashboard(ctx, &query) if err != nil { return nil, toFolderError(err) @@ -461,7 +461,7 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm dto := &dashboards.SaveDashboardDTO{ Dashboard: dashFolder, - OrgId: cmd.OrgID, + OrgID: cmd.OrgID, User: cmd.SignedInUser, Overwrite: cmd.Overwrite, } @@ -477,7 +477,7 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm } var foldr *folder.Folder - foldr, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.Id) + foldr, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.ID) if err != nil { return nil, err } @@ -486,8 +486,8 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm if err := s.bus.Publish(ctx, &events.FolderTitleUpdated{ Timestamp: foldr.Updated, Title: foldr.Title, - ID: dash.Id, - UID: dash.Uid, + ID: dash.ID, + UID: dash.UID, OrgID: cmd.OrgID, }); err != nil { logger.Error("failed to publish FolderTitleUpdated event", "folder", foldr.Title, "user", user.UserID, "error", err) @@ -497,8 +497,8 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm } // prepareForUpdate updates an existing dashboard model from command into model for folder update -func prepareForUpdate(dashFolder *models.Dashboard, orgId int64, userId int64, cmd *folder.UpdateFolderCommand) { - dashFolder.OrgId = orgId +func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int64, cmd *folder.UpdateFolderCommand) { + dashFolder.OrgID = orgId title := dashFolder.Title if cmd.NewTitle != nil && *cmd.NewTitle != "" { @@ -508,7 +508,7 @@ func prepareForUpdate(dashFolder *models.Dashboard, orgId int64, userId int64, c dashFolder.Data.Set("title", dashFolder.Title) if cmd.NewUID != nil && *cmd.NewUID != "" { - dashFolder.SetUid(*cmd.NewUID) + dashFolder.SetUID(*cmd.NewUID) } dashFolder.SetVersion(cmd.Version) diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 99c10c86422..2a7a99657cd 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -116,7 +116,7 @@ func TestIntegrationFolderService(t *testing.T) { }) t.Run("When creating folder should return access denied error", func(t *testing.T) { - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil).Times(2) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil).Times(2) _, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: f.Title, @@ -128,11 +128,11 @@ func TestIntegrationFolderService(t *testing.T) { title := "Folder-TEST" t.Run("When updating folder should return access denied error", func(t *testing.T) { - dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - folder := args.Get(1).(*models.GetDashboardQuery) - folder.Result = models.NewDashboard("dashboard-test") + dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + folder := args.Get(1).(*dashboards.GetDashboardQuery) + folder.Result = dashboards.NewDashboard("dashboard-test") folder.Result.IsFolder = true - }).Return(&models.Dashboard{}, nil) + }).Return(&dashboards.Dashboard{}, nil) _, err := service.Update(context.Background(), &folder.UpdateFolderCommand{ UID: folderUID, OrgID: orgID, @@ -169,13 +169,13 @@ func TestIntegrationFolderService(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Run("When creating folder should not return access denied error", func(t *testing.T) { - dash := models.NewDashboardFolder("Test-Folder") - dash.Id = rand.Int63() - f := folder.FromDashboard(dash) + dash := dashboards.NewDashboardFolder("Test-Folder") + dash.ID = rand.Int63() + f := dashboards.FromDashboard(dash) - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(dash, nil).Once() - dashStore.On("GetFolderByID", mock.Anything, orgID, dash.Id).Return(f, nil) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil).Once() + dashStore.On("GetFolderByID", mock.Anything, orgID, dash.ID).Return(f, nil) actualFolder, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -188,8 +188,8 @@ func TestIntegrationFolderService(t *testing.T) { }) t.Run("When creating folder should return error if uid is general", func(t *testing.T) { - dash := models.NewDashboardFolder("Test-Folder") - dash.Id = rand.Int63() + dash := dashboards.NewDashboardFolder("Test-Folder") + dash.ID = rand.Int63() _, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -201,18 +201,18 @@ func TestIntegrationFolderService(t *testing.T) { }) t.Run("When updating folder should not return access denied error", func(t *testing.T) { - dashboardFolder := models.NewDashboardFolder("Folder") - dashboardFolder.Id = rand.Int63() - dashboardFolder.Uid = util.GenerateShortUID() - f := folder.FromDashboard(dashboardFolder) + dashboardFolder := dashboards.NewDashboardFolder("Folder") + dashboardFolder.ID = rand.Int63() + dashboardFolder.UID = util.GenerateShortUID() + f := dashboards.FromDashboard(dashboardFolder) - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(dashboardFolder, nil) - dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.Id).Return(f, nil) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dashboardFolder, nil) + dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.ID).Return(f, nil) title := "TEST-Folder" req := &folder.UpdateFolderCommand{ - UID: dashboardFolder.Uid, + UID: dashboardFolder.UID, OrgID: orgID, NewTitle: &title, SignedInUser: usr, @@ -324,9 +324,9 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { dashboardsvc := dashboards.FakeDashboardService{} dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) dashStore := dashboards.FakeDashboardStore{} - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) cfg := setting.NewCfg() cfg.RBACEnabled = false @@ -368,8 +368,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) _, err := foldersvc.Create(context.Background(), &folder.CreateFolderCommand{ @@ -427,8 +427,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) _, err := foldersvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -447,8 +447,8 @@ func TestNestedFolderService(t *testing.T) { foldersvc.dashboardStore = dashStore dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{Uid: "newUID"}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{UID: "newUID"}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) f, err := foldersvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -463,16 +463,16 @@ func TestNestedFolderService(t *testing.T) { t.Run("create failed because of circular reference", func(t *testing.T) { // dashboard store & service commands that should be called. - dashboardFolder := models.NewDashboardFolder("myFolder") - dashboardFolder.Id = rand.Int63() - dashboardFolder.Uid = "myFolder" - f := folder.FromDashboard(dashboardFolder) + dashboardFolder := dashboards.NewDashboardFolder("myFolder") + dashboardFolder.ID = rand.Int63() + dashboardFolder.UID = "myFolder" + f := dashboards.FromDashboard(dashboardFolder) dashStore = &dashboards.FakeDashboardStore{} foldersvc.dashboardStore = dashStore - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(dashboardFolder, nil) - dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.Id).Return(f, nil) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dashboardFolder, nil) + dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.ID).Return(f, nil) var actualCmd *models.DeleteDashboardCommand dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { actualCmd = args.Get(1).(*models.DeleteDashboardCommand) @@ -509,8 +509,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) dashStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) var actualCmd *models.DeleteDashboardCommand @@ -670,8 +670,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) dashStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) var actualCmd *models.DeleteDashboardCommand diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index e2f1018dae3..e583b18f0b9 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -30,11 +30,11 @@ func NewAccessControlDashboardGuardian( dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardService dashboards.DashboardService, ) (*AccessControlDashboardGuardian, error) { - var dashboard *models.Dashboard + var dashboard *dashboards.Dashboard if dashboardId != 0 { - q := &models.GetDashboardQuery{ - Id: dashboardId, - OrgId: user.OrgID, + q := &dashboards.GetDashboardQuery{ + ID: dashboardId, + OrgID: user.OrgID, } if err := dashboardService.GetDashboard(ctx, q); err != nil { @@ -67,11 +67,11 @@ func NewAccessControlDashboardGuardianByUID( dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardService dashboards.DashboardService, ) (*AccessControlDashboardGuardian, error) { - var dashboard *models.Dashboard + var dashboard *dashboards.Dashboard if dashboardUID != "" { - q := &models.GetDashboardQuery{ - Uid: dashboardUID, - OrgId: user.OrgID, + q := &dashboards.GetDashboardQuery{ + UID: dashboardUID, + OrgID: user.OrgID, } if err := dashboardService.GetDashboard(ctx, q); err != nil { @@ -100,7 +100,7 @@ func NewAccessControlDashboardGuardianByUID( // This constructor should be preferred over the other two if the dashboard in available // since it avoids querying the database for fetching the dashboard. func NewAccessControlDashboardGuardianByDashboard( - ctx context.Context, dashboard *models.Dashboard, user *user.SignedInUser, + ctx context.Context, dashboard *dashboards.Dashboard, user *user.SignedInUser, store db.DB, ac accesscontrol.AccessControl, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, @@ -122,7 +122,7 @@ func NewAccessControlDashboardGuardianByDashboard( type AccessControlDashboardGuardian struct { ctx context.Context log log.Logger - dashboard *models.Dashboard + dashboard *dashboards.Dashboard user *user.SignedInUser store db.DB ac accesscontrol.AccessControl @@ -137,11 +137,11 @@ func (a *AccessControlDashboardGuardian) CanSave() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -155,11 +155,11 @@ func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -169,11 +169,11 @@ func (a *AccessControlDashboardGuardian) CanView() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -184,14 +184,14 @@ func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) { if a.dashboard.IsFolder { return a.evaluate(accesscontrol.EvalAll( - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid)), - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID)), + accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID)), )) } return a.evaluate(accesscontrol.EvalAll( - accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), - accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), )) } @@ -201,11 +201,11 @@ func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -217,17 +217,17 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool if err != nil { return false, err } - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID))) } func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator) if err != nil { - a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", a.dashboard.Id) + a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", a.dashboard.ID) } if !ok && err == nil { - a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", a.dashboard.Id, "permissions", evaluator.GoString()) + a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", a.dashboard.ID, "permissions", evaluator.GoString()) } return ok, err @@ -251,7 +251,7 @@ func (a *AccessControlDashboardGuardian) GetACL() ([]*models.DashboardACLInfoDTO svc = a.dashboardPermissionsService } - permissions, err := svc.GetPermissions(a.ctx, a.user, a.dashboard.Uid) + permissions, err := svc.GetPermissions(a.ctx, a.user, a.dashboard.UID) if err != nil { return nil, err } @@ -269,9 +269,9 @@ func (a *AccessControlDashboardGuardian) GetACL() ([]*models.DashboardACLInfoDTO } acl = append(acl, &models.DashboardACLInfoDTO{ - OrgId: a.dashboard.OrgId, - DashboardId: a.dashboard.Id, - FolderId: a.dashboard.FolderId, + OrgId: a.dashboard.OrgID, + DashboardId: a.dashboard.ID, + FolderId: a.dashboard.FolderID, Created: p.Created, Updated: p.Updated, UserId: p.UserId, @@ -283,11 +283,11 @@ func (a *AccessControlDashboardGuardian) GetACL() ([]*models.DashboardACLInfoDTO Role: role, Permission: permissionMap[svc.MapActions(p)], PermissionName: permissionMap[svc.MapActions(p)].String(), - Uid: a.dashboard.Uid, + Uid: a.dashboard.UID, Title: a.dashboard.Title, Slug: a.dashboard.Slug, IsFolder: a.dashboard.IsFolder, - Url: a.dashboard.GetUrl(), + Url: a.dashboard.GetURL(), Inherited: false, }) } @@ -332,11 +332,11 @@ func (a *AccessControlDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*mode return hiddenACL, nil } -func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*models.Dashboard, error) { +func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*dashboards.Dashboard, error) { if folderID == 0 { - return &models.Dashboard{Uid: accesscontrol.GeneralFolderUID}, nil + return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil } - folderQuery := &models.GetDashboardQuery{Id: folderID, OrgId: a.user.OrgID} + folderQuery := &dashboards.GetDashboardQuery{ID: folderID, OrgID: a.user.OrgID} if err := a.dashboardService.GetDashboard(a.ctx, folderQuery); err != nil { return nil, err } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 41ad86297be..9a45bae5878 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -584,7 +584,7 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) { } } -func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []accesscontrol.Permission, dashboardSvc dashboards.DashboardService) (*AccessControlDashboardGuardian, *models.Dashboard) { +func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []accesscontrol.Permission, dashboardSvc dashboards.DashboardService) (*AccessControlDashboardGuardian, *dashboards.Dashboard) { t.Helper() store := db.InitTestDB(t) @@ -595,10 +595,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce quotaService := quotatest.New(false, nil) dashStore, err := dashdb.ProvideDashboardStore(store, store.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(store, store.Cfg), quotaService) require.NoError(t, err) - dash, err := dashStore.SaveDashboard(context.Background(), models.SaveDashboardCommand{ + dash, err := dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ Dashboard: toSave.Data, - UserId: 1, - OrgId: 1, + UserID: 1, + OrgID: 1, }) require.NoError(t, err) ac := accesscontrolmock.New().WithPermissions(permissions) @@ -617,29 +617,29 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce require.NoError(t, err) if dashboardSvc == nil { fakeDashboardService := dashboards.NewFakeDashboardService(t) - fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, + OrgID: q.OrgID, } }).Return(nil) dashboardSvc = fakeDashboardService } - g, err := NewAccessControlDashboardGuardian(context.Background(), dash.Id, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc) + g, err := NewAccessControlDashboardGuardian(context.Background(), dash.ID, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc) require.NoError(t, err) return g, dash } func testDashSvc(t *testing.T) dashboards.DashboardService { dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - d := models.NewDashboard("mocked") - d.Id = 1 - d.Uid = "1" + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + d := dashboards.NewDashboard("mocked") + d.ID = 1 + d.UID = "1" q.Result = d }).Return(nil) return dashSvc diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 5bc5f790b3a..24d1cb7761d 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -69,16 +69,16 @@ var NewByUID = func(ctx context.Context, dashUID string, orgId int64, user *user // NewByDashboard factory for creating a new dashboard guardian instance // When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned -var NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { +var NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { panic("no guardian factory implementation provided") } // newDashboardGuardian creates a dashboard guardian by the provided dashId. func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { if dashId != 0 { - q := &models.GetDashboardQuery{ - Id: dashId, - OrgId: orgId, + q := &dashboards.GetDashboardQuery{ + ID: dashId, + OrgID: orgId, } if err := dashSvc.GetDashboard(ctx, q); err != nil { @@ -105,9 +105,9 @@ func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user * func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { dashID := int64(0) if dashUID != "" { - q := &models.GetDashboardQuery{ - Uid: dashUID, - OrgId: orgId, + q := &dashboards.GetDashboardQuery{ + UID: dashUID, + OrgID: orgId, } if err := dashSvc.GetDashboard(ctx, q); err != nil { @@ -116,7 +116,7 @@ func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, } return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err) } - dashID = q.Result.Id + dashID = q.Result.ID } return &dashboardGuardianImpl{ @@ -134,10 +134,10 @@ func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, // newDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboard. // This constructor should be preferred over the other two if the dashboard in available // since it avoids querying the database for fetching the dashboard. -func newDashboardGuardianByDashboard(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { +func newDashboardGuardianByDashboard(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { return &dashboardGuardianImpl{ user: user, - dashId: dash.Id, + dashId: dash.ID, orgId: orgId, log: log.New("dashboard.permissions"), ctx: ctx, @@ -470,10 +470,10 @@ func MockDashboardGuardian(mock *FakeDashboardGuardian) { return mock, nil } - NewByDashboard = func(_ context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { + NewByDashboard = func(_ context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { mock.OrgId = orgId - mock.DashUID = dash.Uid - mock.DashID = dash.Id + mock.DashUID = dash.UID + mock.DashID = dash.ID mock.User = user return mock, nil } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index 85b82e44252..126b3b165ae 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -698,12 +698,12 @@ func TestGuardianGetHiddenACL(t *testing.T) { {Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, } }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, + OrgID: q.OrgID, } }).Return(nil) @@ -734,11 +734,11 @@ func TestGuardianGetHiddenACL(t *testing.T) { IsGrafanaAdmin: true, } dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil) g, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) @@ -769,12 +769,12 @@ func TestGuardianGetACLWithoutDuplicates(t *testing.T) { {Inherited: false, UserId: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, } }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, + OrgID: q.OrgID, } }).Return(nil) diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go index 18af2889dfe..aa69dc3a5b3 100644 --- a/pkg/services/guardian/guardian_util_test.go +++ b/pkg/services/guardian/guardian_util_test.go @@ -47,11 +47,11 @@ func orgRoleScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFu store := dbtest.NewFakeDB() fakeDashboardService := dashboards.NewFakeDashboardService(t) - fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil) guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, fakeDashboardService, &teamtest.FakeService{}) @@ -78,11 +78,11 @@ func apiKeyScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFun } store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil) guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) @@ -118,12 +118,12 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, q := args.Get(1).(*models.GetDashboardACLInfoListQuery) q.Result = permissions }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, + OrgID: q.OrgID, } }).Return(nil) diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go index 4dc0cdc71b7..82d839db7b0 100644 --- a/pkg/services/guardian/provider.go +++ b/pkg/services/guardian/provider.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/team" @@ -36,7 +35,7 @@ func InitLegacyGuardian(store db.DB, dashSvc dashboards.DashboardService, teamSv return newDashboardGuardianByUID(ctx, dashUID, orgId, user, store, dashSvc, teamSvc) } - NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { + NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { return newDashboardGuardianByDashboard(ctx, dash, orgId, user, store, dashSvc, teamSvc) } } @@ -53,7 +52,7 @@ func InitAccessControlGuardian( return NewAccessControlDashboardGuardianByUID(ctx, dashUID, user, store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService) } - NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { + NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { return NewAccessControlDashboardGuardianByDashboard(ctx, dash, user, store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService) } } diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index 0bd640e646d..42f76f77179 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -5,11 +5,10 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/models" ) func TestDeleteLibraryElement(t *testing.T) { @@ -69,12 +68,12 @@ func TestDeleteLibraryElement(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing deleteHandler ", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index a1ae8276351..ff587a20d85 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" @@ -115,12 +116,12 @@ func TestGetLibraryElement(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing getHandler", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) expected := func(res libraryElementResult) libraryElementResult { diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 180188d6454..1ca4a0c8304 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -72,12 +72,12 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing DeleteLibraryElementsInFolder", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID) @@ -145,12 +145,12 @@ func TestGetLibraryPanelConnections(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing GetLibraryPanelConnections", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) var expected = func(res LibraryElementConnectionsResponse) LibraryElementConnectionsResponse { @@ -160,8 +160,8 @@ func TestGetLibraryPanelConnections(t *testing.T) { ID: sc.initialResult.Result.ID, Kind: sc.initialResult.Result.Kind, ElementID: 1, - ConnectionID: dashInDB.Id, - ConnectionUID: dashInDB.Uid, + ConnectionID: dashInDB.ID, + ConnectionUID: dashInDB.UID, Created: res.Result[0].Created, CreatedBy: LibraryElementDTOMetaUser{ ID: 1, @@ -269,12 +269,12 @@ type folderACLItem struct { permission models.PermissionType } -func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard { - dash.FolderId = folderID +func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { + dash.FolderID = folderID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", - OrgId: user.OrgID, + OrgID: user.OrgID, User: &user, Overwrite: false, } diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 514272e156f..41209c98898 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -28,7 +29,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout // Service is a service for operating on library panels. type Service interface { - ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error + ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error ImportLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error } @@ -47,7 +48,7 @@ type LibraryPanelService struct { } // ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard. -func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { +func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { panels := dash.Data.Get("panels").MustArray() libraryPanels := make(map[string]string) err := connectLibraryPanelsRecursively(c, panels, libraryPanels) @@ -60,7 +61,7 @@ func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Conte elementUIDs = append(elementUIDs, libraryPanel) } - return lps.LibraryElementService.ConnectElementsToDashboard(c, signedInUser, elementUIDs, dash.Id) + return lps.LibraryElementService.ConnectElementsToDashboard(c, signedInUser, elementUIDs, dash.ID) } func isLibraryPanelOrRow(panel *simplejson.Json, panelType string) bool { diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 90eb5f4125a..ed1c760d64d 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -72,7 +72,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } @@ -81,7 +81,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) - elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id) + elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID) require.NoError(t, err) require.Len(t, elements, 1) require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) @@ -170,7 +170,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } @@ -179,7 +179,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) - elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id) + elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID) require.NoError(t, err) require.Len(t, elements, 2) require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) @@ -216,7 +216,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } @@ -272,12 +272,12 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) - err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) panelJSON := []interface{}{ @@ -310,7 +310,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) - elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id) + elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID) require.NoError(t, err) require.Len(t, elements, 1) require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) @@ -684,12 +684,12 @@ func getExpected(t *testing.T, res libraryelements.LibraryElementDTO, UID string } } -func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard { - dash.FolderId = folderID +func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { + dash.FolderID = folderID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", - OrgId: user.OrgID, + OrgID: user.OrgID, User: user, Overwrite: false, } @@ -767,11 +767,11 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ + ID: q.ID, + UID: q.UID, } }).Return(nil) guardian.InitLegacyGuardian(store, dashSvc, &teamtest.FakeService{}) diff --git a/pkg/services/live/features/dashboard.go b/pkg/services/live/features/dashboard.go index c2d81dbf3ff..2474bf23085 100644 --- a/pkg/services/live/features/dashboard.go +++ b/pkg/services/live/features/dashboard.go @@ -29,13 +29,13 @@ const ( // DashboardEvent events related to dashboards type dashboardEvent struct { - UID string `json:"uid"` - Action actionType `json:"action"` // saved, editing, deleted - User *user.UserDisplayDTO `json:"user,omitempty"` - SessionID string `json:"sessionId,omitempty"` - Message string `json:"message,omitempty"` - Dashboard *models.Dashboard `json:"dashboard,omitempty"` - Error string `json:"error,omitempty"` + UID string `json:"uid"` + Action actionType `json:"action"` // saved, editing, deleted + User *user.UserDisplayDTO `json:"user,omitempty"` + SessionID string `json:"sessionId,omitempty"` + Message string `json:"message,omitempty"` + Dashboard *dashboards.Dashboard `json:"dashboard,omitempty"` + Error string `json:"error,omitempty"` } // DashboardHandler manages all the `grafana/dashboard/*` channels @@ -66,7 +66,7 @@ func (h *DashboardHandler) OnSubscribe(ctx context.Context, user *user.SignedInU // make sure can view this dashboard if len(parts) == 2 && parts[0] == "uid" { - query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgID} + query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.OrgID} if err := h.DashboardService.GetDashboard(ctx, &query); err != nil { logger.Error("Error getting dashboard", "query", query, "error", err) return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil @@ -116,7 +116,7 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, user *user.SignedInUse // just ignore the event return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???") } - query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgID} + query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.OrgID} if err := h.DashboardService.GetDashboard(ctx, &query); err != nil { logger.Error("Unknown dashboard", "query", query) return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil @@ -171,13 +171,13 @@ func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error { } // DashboardSaved will broadcast to all connected dashboards -func (h *DashboardHandler) DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *models.Dashboard, err error) error { +func (h *DashboardHandler) DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *dashboards.Dashboard, err error) error { if err != nil && !h.HasGitOpsObserver(orgID) { return nil // only broadcast if it was OK } msg := dashboardEvent{ - UID: dashboard.Uid, + UID: dashboard.UID, Action: ActionSaved, User: user, Message: message, diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index f56c35c3ad3..40020873f89 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -69,7 +69,7 @@ type CoreGrafanaScope struct { Features map[string]models.ChannelHandlerFactory // The generic service to advertise dashboard changes - Dashboards models.DashboardActivityChannel + Dashboards DashboardActivityChannel } func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister, @@ -437,6 +437,22 @@ type GrafanaLive struct { usageStats usageStats } +// DashboardActivityChannel is a service to advertise dashboard activity +type DashboardActivityChannel interface { + // Called when a dashboard is saved -- this includes the error so we can support a + // gitops workflow that knows if the value was saved to the local database or not + // in many cases all direct save requests will fail, but the request should be forwarded + // to any gitops observers + DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *dashboards.Dashboard, err error) error + + // Called when a dashboard is deleted + DashboardDeleted(orgID int64, user *user.UserDisplayDTO, uid string) error + + // Experimental! Indicate is GitOps is active. This really means + // someone is subscribed to the `grafana/dashboards/gitops` channel + HasGitOpsObserver(orgID int64) bool +} + func (g *GrafanaLive) getStreamPlugin(ctx context.Context, pluginID string) (backend.StreamHandler, error) { plugin, exists := g.pluginStore.Plugin(ctx, pluginID) if !exists { diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index e9231ee6995..37f38af1373 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -62,10 +62,11 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser return login.ErrSignupNotAllowed } + // quota check (FIXME: (jguer) this should be done in the user service) // we may insert in both user and org_user tables // therefore we need to query check quota for both user and org services for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} { - limitReached, errLimit := ls.QuotaService.QuotaReached(cmd.ReqContext, quota.TargetSrv(srv)) + limitReached, errLimit := ls.QuotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil) if errLimit != nil { cmd.ReqContext.Logger.Warn("Error getting user quota.", "error", errLimit) return login.ErrGettingUserQuota diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index f77e85e1d28..d1e8f740182 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -219,10 +219,10 @@ func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) } if prefs.HomeDashboardID != 0 { - slugQuery := models.GetDashboardRefByIdQuery{Id: prefs.HomeDashboardID} - err := s.dashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery) + slugQuery := dashboards.GetDashboardRefByIDQuery{ID: prefs.HomeDashboardID} + err := s.dashboardService.GetDashboardUIDByID(c.Req.Context(), &slugQuery) if err == nil { - homeUrl = models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) + homeUrl = models.GetDashboardUrl(slugQuery.Result.UID, slugQuery.Result.Slug) } } @@ -331,7 +331,7 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre return nil, err } - starredDashboards := []*models.Dashboard{} + starredDashboards := []*dashboards.Dashboard{} starredDashboardsCounter := 0 for dashboardId := range starredDashboardResult.UserStars { // Set a loose limit to the first 50 starred dashboards found @@ -339,9 +339,9 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre break } starredDashboardsCounter++ - query := &models.GetDashboardQuery{ - Id: dashboardId, - OrgId: c.OrgID, + query := &dashboards.GetDashboardQuery{ + ID: dashboardId, + OrgID: c.OrgID, } err := s.dashboardService.GetDashboard(c.Req.Context(), query) if err == nil { @@ -355,9 +355,9 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre }) for _, starredItem := range starredDashboards { starredItemsChildNavs = append(starredItemsChildNavs, &navtree.NavLink{ - Id: "starred/" + starredItem.Uid, + Id: "starred/" + starredItem.UID, Text: starredItem.Title, - Url: starredItem.GetUrl(), + Url: starredItem.GetURL(), }) } } diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 3defbc4c995..045fc3e0e06 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -156,7 +156,13 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { func (api *API) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) { u := "a.Map{} - if orgUsage, err := api.RuleStore.Count(ctx, scopeParams.OrgID); err != nil { + + var orgID int64 = 0 + if scopeParams != nil { + orgID = scopeParams.OrgID + } + + if orgUsage, err := api.RuleStore.Count(ctx, orgID); err != nil { return u, err } else { tag, err := quota.NewTag(models.QuotaTargetSrv, models.QuotaTarget, quota.OrgScope) diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 29d538e8f99..82b0ab7d443 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -60,7 +60,7 @@ func AlertRuleGen(mutators ...AlertRuleMutator) func() *AlertRule { rule := &AlertRule{ ID: rand.Int63n(1500), - OrgID: rand.Int63n(1500), + OrgID: rand.Int63n(1500) + 1, // Prevent OrgID=0 as this does not pass alert rule validation. Title: "TEST-ALERT-" + util.GenerateShortUID(), Condition: "A", Data: []AlertQuery{GenerateAlertQuery()}, diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 993b35bd2da..fa9622b919a 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -387,7 +387,15 @@ func configureHistorianBackend(cfg setting.UnifiedAlertingStateHistorySettings, return historian.NewAnnotationBackend(ar, ds), nil } if cfg.Backend == "loki" { - return historian.NewRemoteLokiBackend(), nil + baseURL, err := url.Parse(cfg.LokiRemoteURL) + if err != nil { + return nil, fmt.Errorf("failed to parse remote loki URL: %w", err) + } + backend := historian.NewRemoteLokiBackend(baseURL) + if err := backend.TestConnection(); err != nil { + return nil, fmt.Errorf("failed to ping the remote loki historian: %w", err) + } + return backend, nil } if cfg.Backend == "sql" { return historian.NewSqlBackend(), nil diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 97fcfe5695f..543f792dd24 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -403,7 +403,7 @@ func (am *Alertmanager) buildIntegrationsMap(receivers []*apimodels.PostableApiR // buildReceiverIntegrations builds a list of integration notifiers off of a receiver config. func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableApiReceiver, tmpl *alerting.Template) ([]*alerting.Integration, error) { - var integrations []*alerting.Integration + integrations := make([]*alerting.Integration, 0, len(receiver.GrafanaManagedReceivers)) for i, r := range receiver.GrafanaManagedReceivers { n, err := am.buildReceiverIntegration(r, tmpl) if err != nil { diff --git a/pkg/services/ngalert/notifier/email_test.go b/pkg/services/ngalert/notifier/email_test.go index e1428ed8c17..19c659c3fb5 100644 --- a/pkg/services/ngalert/notifier/email_test.go +++ b/pkg/services/ngalert/notifier/email_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -240,15 +239,15 @@ func (e emailSender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookS } func (e emailSender) SendEmail(ctx context.Context, cmd *channels.SendEmailSettings) error { - attached := make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) + attached := make([]*notifications.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) for _, file := range cmd.AttachedFiles { - attached = append(attached, &models.SendEmailAttachFile{ + attached = append(attached, ¬ifications.SendEmailAttachFile{ Name: file.Name, Content: file.Content, }) } - return e.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + return e.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{ + SendEmailCommand: notifications.SendEmailCommand{ To: cmd.To, SingleEmail: cmd.SingleEmail, Template: cmd.Template, diff --git a/pkg/services/ngalert/notifier/receivers.go b/pkg/services/ngalert/notifier/receivers.go index 3c49a292353..2897f9799b9 100644 --- a/pkg/services/ngalert/notifier/receivers.go +++ b/pkg/services/ngalert/notifier/receivers.go @@ -130,7 +130,7 @@ func (am *Alertmanager) GetReceivers(ctx context.Context) []apimodels.Receiver { am.reloadConfigMtx.RLock() defer am.reloadConfigMtx.RUnlock() - var apiReceivers []apimodels.Receiver + apiReceivers := make([]apimodels.Receiver, 0, len(am.Base.GetReceivers())) for _, rcv := range am.Base.GetReceivers() { // Build integrations slice for each receiver. integrations := make([]*models.Integration, 0, len(rcv.Integrations())) diff --git a/pkg/services/ngalert/notifier/sender.go b/pkg/services/ngalert/notifier/sender.go index fd53694b399..fd016cabec9 100644 --- a/pkg/services/ngalert/notifier/sender.go +++ b/pkg/services/ngalert/notifier/sender.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/alerting/alerting/notifier/channels" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/notifications" ) @@ -14,7 +13,7 @@ type sender struct { } func (s sender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookSettings) error { - return s.ns.SendWebhookSync(ctx, &models.SendWebhookSync{ + return s.ns.SendWebhookSync(ctx, ¬ifications.SendWebhookSync{ Url: cmd.URL, User: cmd.User, Password: cmd.Password, @@ -27,18 +26,18 @@ func (s sender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookSettin } func (s sender) SendEmail(ctx context.Context, cmd *channels.SendEmailSettings) error { - var attached []*models.SendEmailAttachFile + var attached []*notifications.SendEmailAttachFile if cmd.AttachedFiles != nil { - attached = make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) + attached = make([]*notifications.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) for _, file := range cmd.AttachedFiles { - attached = append(attached, &models.SendEmailAttachFile{ + attached = append(attached, ¬ifications.SendEmailAttachFile{ Name: file.Name, Content: file.Content, }) } } - return s.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + return s.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{ + SendEmailCommand: notifications.SendEmailCommand{ To: cmd.To, SingleEmail: cmd.SingleEmail, Template: cmd.Template, diff --git a/pkg/services/ngalert/state/historian/dashboard.go b/pkg/services/ngalert/state/historian/dashboard.go index e2d3213a3a2..ec311e6ff05 100644 --- a/pkg/services/ngalert/state/historian/dashboard.go +++ b/pkg/services/ngalert/state/historian/dashboard.go @@ -8,7 +8,6 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/patrickmn/go-cache" "golang.org/x/sync/singleflight" @@ -51,9 +50,9 @@ func (r *dashboardResolver) getID(ctx context.Context, orgID int64, uid string) r.log.Debug("Dashboard cache miss, querying dashboards", "dashboardUID", uid) var result interface{} - query := &models.GetDashboardQuery{ - Uid: uid, - OrgId: orgID, + query := &dashboards.GetDashboardQuery{ + UID: uid, + OrgID: orgID, } err := r.dashboards.GetDashboard(ctx, query) // We also cache lookups where we don't find anything. @@ -64,7 +63,7 @@ func (r *dashboardResolver) getID(ctx context.Context, orgID int64, uid string) } else if query.Result == nil { result = dashboards.ErrDashboardNotFound } else { - result = query.Result.Id + result = query.Result.ID } // By setting the cache inside the singleflighted routine, we avoid any accidental re-queries that could get initiated after the query completes. diff --git a/pkg/services/ngalert/state/historian/dashboard_test.go b/pkg/services/ngalert/state/historian/dashboard_test.go index 904e8929982..f8e7dba20a4 100644 --- a/pkg/services/ngalert/state/historian/dashboard_test.go +++ b/pkg/services/ngalert/state/historian/dashboard_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -16,7 +15,7 @@ func TestDashboardResolver(t *testing.T) { dbs := &dashboards.FakeDashboardService{} exp := int64(14) dbs.On("GetDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - args.Get(1).(*models.GetDashboardQuery).Result = &models.Dashboard{Id: exp} + args.Get(1).(*dashboards.GetDashboardQuery).Result = &dashboards.Dashboard{ID: exp} }).Return(nil) sut := createDashboardResolverSut(dbs) @@ -29,7 +28,7 @@ func TestDashboardResolver(t *testing.T) { t.Run("fetches dashboardNotFound if underlying dashboard does not exist", func(t *testing.T) { dbs := &dashboards.FakeDashboardService{} dbs.On("GetDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - args.Get(1).(*models.GetDashboardQuery).Result = nil + args.Get(1).(*dashboards.GetDashboardQuery).Result = nil }).Return(dashboards.ErrDashboardNotFound) sut := createDashboardResolverSut(dbs) diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index a1ebacddc06..038f4e98065 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -2,21 +2,35 @@ package historian import ( "context" + "net/url" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" ) -type RemoteLokiBackend struct { - log log.Logger +type remoteLokiClient interface { + ping() error } -func NewRemoteLokiBackend() *RemoteLokiBackend { +type RemoteLokiBackend struct { + client remoteLokiClient + log log.Logger +} + +func NewRemoteLokiBackend(url *url.URL) *RemoteLokiBackend { + logger := log.New("ngalert.state.historian", "backend", "loki") return &RemoteLokiBackend{ - log: log.New("ngalert.state.historian"), + client: newLokiClient(url, logger), + log: logger, } } +func (h *RemoteLokiBackend) TestConnection() error { + return h.client.ping() +} + func (h *RemoteLokiBackend) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) { + logger := h.log.FromContext(ctx) + logger.Debug("Remote Loki state history backend was called with states") } diff --git a/pkg/services/ngalert/state/historian/loki_http.go b/pkg/services/ngalert/state/historian/loki_http.go new file mode 100644 index 00000000000..78232dde025 --- /dev/null +++ b/pkg/services/ngalert/state/historian/loki_http.go @@ -0,0 +1,54 @@ +package historian + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/grafana/grafana/pkg/infra/log" +) + +const defaultClientTimeout = 30 * time.Second + +type httpLokiClient struct { + client http.Client + url *url.URL + log log.Logger +} + +func newLokiClient(u *url.URL, logger log.Logger) *httpLokiClient { + return &httpLokiClient{ + client: http.Client{ + Timeout: defaultClientTimeout, + }, + url: u, + log: logger.New("protocol", "http"), + } +} + +func (c *httpLokiClient) ping() error { + uri := c.url.JoinPath("/loki/api/v1/status/buildinfo") + req, err := http.NewRequest(http.MethodGet, uri.String(), nil) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + res, err := c.client.Do(req) + if res != nil { + defer func() { + if err := res.Body.Close(); err != nil { + c.log.Warn("Failed to close response body", "err", err) + } + }() + } + if err != nil { + return fmt.Errorf("error sending request: %w", err) + } + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return fmt.Errorf("request to the loki buildinfo endpoint returned a non-200 status code: %d", res.StatusCode) + } + c.log.Debug("Request to Loki buildinfo endpoint succeeded", "status", res.StatusCode) + return nil +} diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index d937a7328a5..41b06c37f41 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -184,7 +184,7 @@ func (st *Manager) ResetStateByRuleUID(ctx context.Context, ruleKey ngModels.Ale func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels) []StateTransition { logger := st.log.FromContext(ctx) logger.Debug("State manager processing evaluation results", "resultCount", len(results)) - var states []StateTransition + states := make([]StateTransition, 0, len(results)) for _, result := range results { s := st.setNextState(ctx, alertRule, result, extraLabels, logger) @@ -383,10 +383,10 @@ func (st *Manager) deleteStaleStatesFromCache(ctx context.Context, logger log.Lo // TODO: We will need to change this when we support images without screenshots as each series will have a different image var resolvedImage *ngModels.Image - var resolvedStates []StateTransition staleStates := st.cache.deleteRuleStates(alertRule.GetKey(), func(s *State) bool { return stateIsStale(evaluatedAt, s.LastEvaluationTime, alertRule.IntervalSeconds) }) + resolvedStates := make([]StateTransition, 0, len(staleStates)) for _, s := range staleStates { logger.Info("Detected stale state entry", "cacheID", s.CacheID, "state", s.State, "reason", s.StateReason) diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index a3f589a8cc7..bfb4a8daa15 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -58,7 +58,9 @@ func CalculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model existingGroupRulesUIDs[r.UID] = r } - var toAdd, toDelete []*models.AlertRule + //nolint:prealloc // difficult logic + var toAdd []*models.AlertRule + //nolint:prealloc // difficult logic var toUpdate []RuleDelta loadedRulesByUID := map[string]*models.AlertRule{} // auxiliary cache to avoid unnecessary queries if there are multiple moves from the same group for _, r := range submittedRules { @@ -110,6 +112,7 @@ func CalculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model continue } + toDelete := make([]*models.AlertRule, 0, len(existingGroupRulesUIDs)) for _, rule := range existingGroupRulesUIDs { toDelete = append(toDelete, rule) } diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 1ba3b85e40a..827e2f4973e 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -10,7 +10,6 @@ import ( "html/template" "net/mail" - "github.com/grafana/grafana/pkg/models" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -54,9 +53,9 @@ func (ns *NotificationService) Send(msg *Message) (int, error) { return ns.mailer.Send(messages...) } -func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (*Message, error) { +func (ns *NotificationService) buildEmailMessage(cmd *SendEmailCommand) (*Message, error) { if !ns.Cfg.Smtp.Enabled { - return nil, models.ErrSmtpNotEnabled + return nil, ErrSmtpNotEnabled } data := cmd.Data @@ -120,7 +119,7 @@ func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) ( // buildAttachedFiles build attached files func buildAttachedFiles( - attached []*models.SendEmailAttachFile, + attached []*SendEmailAttachFile, ) []*AttachedFile { result := make([]*AttachedFile, 0) diff --git a/pkg/services/notifications/mock.go b/pkg/services/notifications/mock.go index 14229fc797d..2e874ad6570 100644 --- a/pkg/services/notifications/mock.go +++ b/pkg/services/notifications/mock.go @@ -2,22 +2,20 @@ package notifications import ( "context" - - "github.com/grafana/grafana/pkg/models" ) type NotificationServiceMock struct { - Webhook models.SendWebhookSync - EmailSync models.SendEmailCommandSync - Email models.SendEmailCommand + Webhook SendWebhookSync + EmailSync SendEmailCommandSync + Email SendEmailCommand ShouldError error - WebhookHandler func(context.Context, *models.SendWebhookSync) error - EmailHandlerSync func(context.Context, *models.SendEmailCommandSync) error - EmailHandler func(context.Context, *models.SendEmailCommand) error + WebhookHandler func(context.Context, *SendWebhookSync) error + EmailHandlerSync func(context.Context, *SendEmailCommandSync) error + EmailHandler func(context.Context, *SendEmailCommand) error } -func (ns *NotificationServiceMock) SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error { +func (ns *NotificationServiceMock) SendWebhookSync(ctx context.Context, cmd *SendWebhookSync) error { ns.Webhook = *cmd if ns.WebhookHandler != nil { return ns.WebhookHandler(ctx, cmd) @@ -25,7 +23,7 @@ func (ns *NotificationServiceMock) SendWebhookSync(ctx context.Context, cmd *mod return ns.ShouldError } -func (ns *NotificationServiceMock) SendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error { +func (ns *NotificationServiceMock) SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error { ns.EmailSync = *cmd if ns.EmailHandlerSync != nil { return ns.EmailHandlerSync(ctx, cmd) @@ -33,7 +31,7 @@ func (ns *NotificationServiceMock) SendEmailCommandHandlerSync(ctx context.Conte return ns.ShouldError } -func (ns *NotificationServiceMock) SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error { +func (ns *NotificationServiceMock) SendEmailCommandHandler(ctx context.Context, cmd *SendEmailCommand) error { ns.Email = *cmd if ns.EmailHandler != nil { return ns.EmailHandler(ctx, cmd) diff --git a/pkg/models/notifications.go b/pkg/services/notifications/models.go similarity index 98% rename from pkg/models/notifications.go rename to pkg/services/notifications/models.go index 159d6a04edb..117f354ce82 100644 --- a/pkg/models/notifications.go +++ b/pkg/services/notifications/models.go @@ -1,4 +1,4 @@ -package models +package notifications import ( "errors" diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index bca81599d79..f481f8b355d 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -10,10 +10,10 @@ import ( "strings" "github.com/Masterminds/sprig/v3" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -21,11 +21,11 @@ import ( ) type WebhookSender interface { - SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error + SendWebhookSync(ctx context.Context, cmd *SendWebhookSync) error } type EmailSender interface { - SendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error - SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error + SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error + SendEmailCommandHandler(ctx context.Context, cmd *SendEmailCommand) error } type Service interface { WebhookSender @@ -130,7 +130,7 @@ func (ns *NotificationService) GetMailer() Mailer { return ns.mailer } -func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error { +func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *SendWebhookSync) error { return ns.sendWebRequestSync(ctx, &Webhook{ Url: cmd.Url, User: cmd.User, @@ -148,8 +148,8 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string { return "" } -func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error { - message, err := ns.buildEmailMessage(&models.SendEmailCommand{ +func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error { + message, err := ns.buildEmailMessage(&SendEmailCommand{ Data: cmd.Data, Info: cmd.Info, Template: cmd.Template, @@ -169,7 +169,7 @@ func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, return err } -func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error { +func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd *SendEmailCommand) error { message, err := ns.buildEmailMessage(cmd) if err != nil { @@ -180,12 +180,12 @@ func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd return nil } -func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd *models.SendResetPasswordEmailCommand) error { +func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd *SendResetPasswordEmailCommand) error { code, err := createUserEmailCode(ns.Cfg, cmd.User, "") if err != nil { return err } - return ns.SendEmailCommandHandler(ctx, &models.SendEmailCommand{ + return ns.SendEmailCommandHandler(ctx, &SendEmailCommand{ To: []string{cmd.User.Email}, Template: tmplResetPassword, Data: map[string]interface{}{ @@ -197,10 +197,10 @@ func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd * type GetUserByLoginFunc = func(c context.Context, login string) (*user.User, error) -func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, query *models.ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) error { +func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, query *ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) error { login := getLoginForEmailCode(query.Code) if login == "" { - return models.ErrInvalidEmailCode + return ErrInvalidEmailCode } user, err := userByLogin(ctx, login) @@ -213,7 +213,7 @@ func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, qu return err } if !validEmailCode { - return models.ErrInvalidEmailCode + return ErrInvalidEmailCode } query.Result = user @@ -231,7 +231,7 @@ func (ns *NotificationService) signUpStartedHandler(ctx context.Context, evt *ev return nil } - err := ns.SendEmailCommandHandler(ctx, &models.SendEmailCommand{ + err := ns.SendEmailCommandHandler(ctx, &SendEmailCommand{ To: []string{evt.Email}, Template: tmplSignUpStarted, Data: map[string]interface{}{ @@ -254,7 +254,7 @@ func (ns *NotificationService) signUpCompletedHandler(ctx context.Context, evt * return nil } - return ns.SendEmailCommandHandler(ctx, &models.SendEmailCommand{ + return ns.SendEmailCommandHandler(ctx, &SendEmailCommand{ To: []string{evt.Email}, Template: tmplWelcomeOnSignUp, Data: map[string]interface{}{ diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 48ee121127d..4254a8f93a5 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -5,13 +5,13 @@ import ( "regexp" "testing" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) func newBus(t *testing.T) bus.Bus { @@ -53,8 +53,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When sending emails synchronously", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"asdf@grafana.com"}, SingleEmail: false, @@ -72,8 +72,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When using Single Email mode with multiple recipients", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: true, @@ -89,8 +89,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When using Multi Email mode with multiple recipients", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -106,13 +106,13 @@ func TestSendEmailSync(t *testing.T) { t.Run("When attaching files to emails", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"asdf@grafana.com"}, SingleEmail: true, Template: "welcome_on_signup", - AttachedFiles: []*models.SendEmailAttachFile{ + AttachedFiles: []*SendEmailAttachFile{ { Name: "attachment.txt", Content: []byte("text file content"), @@ -137,8 +137,8 @@ func TestSendEmailSync(t *testing.T) { cfg.Smtp.Enabled = false ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: true, @@ -148,7 +148,7 @@ func TestSendEmailSync(t *testing.T) { err = ns.SendEmailCommandHandlerSync(context.Background(), cmd) - require.ErrorIs(t, err, models.ErrSmtpNotEnabled) + require.ErrorIs(t, err, ErrSmtpNotEnabled) require.Empty(t, mailer.Sent) }) @@ -157,8 +157,8 @@ func TestSendEmailSync(t *testing.T) { cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -174,8 +174,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When SMTP dialer is disconnected", func(t *testing.T) { ns := createDisconnectedSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -195,7 +195,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When sending reset email password", func(t *testing.T) { sut, _ := createSut(t, bus) testuser := user.User{Email: "asd@asd.com", Login: "asd@asd.com"} - err := sut.SendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &testuser}) + err := sut.SendResetPasswordEmail(context.Background(), &SendResetPasswordEmailCommand{User: &testuser}) require.NoError(t, err) @@ -212,7 +212,7 @@ func TestSendEmailAsync(t *testing.T) { code := match[len("code="):] // verify code - query := models.ValidateResetPasswordCodeQuery{Code: code} + query := ValidateResetPasswordCodeQuery{Code: code} getUserByLogin := func(ctx context.Context, login string) (*user.User, error) { return &testuser, nil } @@ -225,7 +225,7 @@ func TestSendEmailAsync(t *testing.T) { cfg.Smtp.Enabled = false ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: true, @@ -234,7 +234,7 @@ func TestSendEmailAsync(t *testing.T) { err = ns.SendEmailCommandHandler(context.Background(), cmd) - require.ErrorIs(t, err, models.ErrSmtpNotEnabled) + require.ErrorIs(t, err, ErrSmtpNotEnabled) require.Empty(t, mailer.Sent) }) @@ -243,7 +243,7 @@ func TestSendEmailAsync(t *testing.T) { cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -258,7 +258,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When SMTP dialer is disconnected", func(t *testing.T) { ns := createDisconnectedSut(t, bus) - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go index a33d2781fa4..99f0cee48ab 100644 --- a/pkg/services/notifications/send_email_integration_test.go +++ b/pkg/services/notifications/send_email_integration_test.go @@ -5,7 +5,6 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" @@ -28,7 +27,7 @@ func TestEmailIntegrationTest(t *testing.T) { ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"} t.Run("When sending reset email password", func(t *testing.T) { - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Data: map[string]interface{}{ "Title": "[CRITICAL] Imaginary timeseries alert", diff --git a/pkg/services/org/orgimpl/store.go b/pkg/services/org/orgimpl/store.go index f281d8ccb70..aafa97ac96d 100644 --- a/pkg/services/org/orgimpl/store.go +++ b/pkg/services/org/orgimpl/store.go @@ -420,7 +420,7 @@ func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM (SELECT user_id FROM org_user WHERE org_id=? AND user_id IN (SELECT id AS user_id FROM %s WHERE is_service_account=%s)) as subq", ss.db.GetDialect().Quote("user"), @@ -441,7 +441,7 @@ func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter } } - if scopeParams.UserID != 0 { + if scopeParams != nil && scopeParams.UserID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { // should we exclude service accounts? rawSQL := "SELECT COUNT(*) AS count FROM org_user WHERE user_id=?" diff --git a/pkg/services/pluginsettings/service/service.go b/pkg/services/pluginsettings/service/service.go index d1534398aae..8ec61036d16 100644 --- a/pkg/services/pluginsettings/service/service.go +++ b/pkg/services/pluginsettings/service/service.go @@ -49,7 +49,7 @@ func (s *Service) GetPluginSettings(ctx context.Context, args *pluginsettings.Ge return nil, err } - var result []*pluginsettings.InfoDTO + result := make([]*pluginsettings.InfoDTO, 0, len(ps)) for _, p := range ps { result = append(result, &pluginsettings.InfoDTO{ OrgID: p.OrgID, diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index 071c549edad..5d816b325be 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" alert_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -99,9 +98,9 @@ func (prov *defaultAlertRuleProvisioner) provisionRule( func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( ctx context.Context, folderName string, orgID int64) (string, error) { - cmd := &models.GetDashboardQuery{ + cmd := &dashboards.GetDashboardQuery{ Slug: slugify.Slugify(folderName), - OrgId: orgID, + OrgID: orgID, } err := prov.dashboardService.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { @@ -111,22 +110,22 @@ func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( // dashboard folder not found. create one. if errors.Is(err, dashboards.ErrDashboardNotFound) { dash := &dashboards.SaveDashboardDTO{} - dash.Dashboard = models.NewDashboardFolder(folderName) + dash.Dashboard = dashboards.NewDashboardFolder(folderName) dash.Dashboard.IsFolder = true dash.Overwrite = true - dash.OrgId = orgID - dash.Dashboard.SetUid(util.GenerateShortUID()) + dash.OrgID = orgID + dash.Dashboard.SetUID(util.GenerateShortUID()) dbDash, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, dash) if err != nil { return "", err } - return dbDash.Uid, nil + return dbDash.UID, nil } if !cmd.Result.IsFolder { return "", fmt.Errorf("got invalid response. expected folder, found dashboard") } - return cmd.Result.Uid, nil + return cmd.Result.UID, nil } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index d87b4407e7c..3f1772c84c6 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -244,26 +244,26 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i // keeps track of which UIDs and titles we have already provisioned dash := jsonFile.dashboard - provisioningMetadata.uid = dash.Dashboard.Uid - provisioningMetadata.identity = dashboardIdentity{title: dash.Dashboard.Title, folderID: dash.Dashboard.FolderId} + provisioningMetadata.uid = dash.Dashboard.UID + provisioningMetadata.identity = dashboardIdentity{title: dash.Dashboard.Title, folderID: dash.Dashboard.FolderID} if upToDate { return provisioningMetadata, nil } - if dash.Dashboard.Id != 0 { + if dash.Dashboard.ID != 0 { dash.Dashboard.Data.Set("id", nil) - dash.Dashboard.Id = 0 + dash.Dashboard.ID = 0 } if alreadyProvisioned { - dash.Dashboard.SetId(provisionedData.DashboardId) + dash.Dashboard.SetID(provisionedData.DashboardId) } if !fr.isDatabaseAccessRestricted() { - fr.log.Debug("saving new dashboard", "provisioner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId) - dp := &models.DashboardProvisioning{ - ExternalId: path, + fr.log.Debug("saving new dashboard", "provisioner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderID) + dp := &dashboards.DashboardProvisioning{ + ExternalID: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix(), CheckSum: jsonFile.checkSum, @@ -274,7 +274,7 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i } } else { fr.log.Warn("Not saving new dashboard due to restricted database access", "provisioner", fr.Cfg.Name, - "file", path, "folderId", dash.Dashboard.FolderId) + "file", path, "folderId", dash.Dashboard.FolderID) } return provisioningMetadata, nil @@ -300,7 +300,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv return 0, ErrFolderNameMissing } - cmd := &models.GetDashboardQuery{Slug: slugify.Slugify(folderName), OrgId: cfg.OrgID} + cmd := &dashboards.GetDashboardQuery{Slug: slugify.Slugify(folderName), OrgID: cfg.OrgID} err := fr.dashboardStore.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { @@ -310,28 +310,28 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv // dashboard folder not found. create one. if errors.Is(err, dashboards.ErrDashboardNotFound) { dash := &dashboards.SaveDashboardDTO{} - dash.Dashboard = models.NewDashboardFolder(folderName) + dash.Dashboard = dashboards.NewDashboardFolder(folderName) dash.Dashboard.IsFolder = true dash.Overwrite = true - dash.OrgId = cfg.OrgID + dash.OrgID = cfg.OrgID // set dashboard folderUid if given if cfg.FolderUID == accesscontrol.GeneralFolderUID { return 0, dashboards.ErrFolderInvalidUID } - dash.Dashboard.SetUid(cfg.FolderUID) + dash.Dashboard.SetUID(cfg.FolderUID) dbDash, err := service.SaveFolderForProvisionedDashboards(ctx, dash) if err != nil { return 0, err } - return dbDash.Id, nil + return dbDash.ID, nil } if !cmd.Result.IsFolder { return 0, fmt.Errorf("got invalid response. expected folder, found dashboard") } - return cmd.Result.Id, nil + return cmd.Result.ID, nil } func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) { diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index d29015b88bd..463bf88ea47 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -114,8 +114,8 @@ func TestDashboardFileReader(t *testing.T) { cfg.Folder = "Team A" fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 2}, nil).Times(2) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 2}, nil).Times(2) reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -132,7 +132,7 @@ func TestDashboardFileReader(t *testing.T) { inserted := 0 fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything). - Return(&models.Dashboard{}, nil).Once(). + Return(&dashboards.Dashboard{}, nil).Once(). Run(func(args mock.Arguments) { inserted++ }) @@ -200,7 +200,7 @@ func TestDashboardFileReader(t *testing.T) { } fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -263,7 +263,7 @@ func TestDashboardFileReader(t *testing.T) { } fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -278,7 +278,7 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["path"] = containingID fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -294,8 +294,8 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["foldersFromFilesStructure"] = true fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(3) reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -332,8 +332,8 @@ func TestDashboardFileReader(t *testing.T) { cfg2 := &config{Name: "2", Type: "file", OrgID: 1, Folder: "f2", Options: map[string]interface{}{"path": containingID}} fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return(nil, nil).Times(2) - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2) reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore) reader1.dashboardProvisioningService = fakeService @@ -380,7 +380,7 @@ func TestDashboardFileReader(t *testing.T) { "folder": defaultDashboards, }, } - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1}, nil).Once() r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) @@ -453,7 +453,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() fakeService.On("UnprovisionDashboard", mock.Anything, mock.Anything).Return(nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() cfg.DisableDeletion = true @@ -469,7 +469,7 @@ func TestDashboardFileReader(t *testing.T) { setupFakeService() fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) @@ -513,6 +513,6 @@ func (ffi FakeFileInfo) Sys() interface{} { type fakeDashboardStore struct{} -func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *models.GetDashboardQuery) error { +func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *dashboards.GetDashboardQuery) error { return dashboards.ErrDashboardNotFound } diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 8a4c200d5f8..8e259379b2a 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -5,7 +5,6 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/values" ) @@ -59,12 +58,12 @@ type configs struct { func createDashboardJSON(data *simplejson.Json, lastModified time.Time, cfg *config, folderID int64) (*dashboards.SaveDashboardDTO, error) { dash := &dashboards.SaveDashboardDTO{} - dash.Dashboard = models.NewDashboardFromJson(data) + dash.Dashboard = dashboards.NewDashboardFromJson(data) dash.UpdatedAt = lastModified dash.Overwrite = true - dash.OrgId = cfg.OrgID - dash.Dashboard.OrgId = cfg.OrgID - dash.Dashboard.FolderId = folderID + dash.OrgID = cfg.OrgID + dash.Dashboard.OrgID = cfg.OrgID + dash.Dashboard.FolderID = folderID if dash.Dashboard.Title == "" { return nil, dashboards.ErrDashboardTitleEmpty diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index 380bb1db995..0c8243a7140 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -37,9 +37,9 @@ func TestDuplicatesValidator(t *testing.T) { fakeStore := &fakeDashboardStore{} r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(6) fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*models.DashboardProvisioning{}, nil).Times(4) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) require.NoError(t, err) @@ -151,9 +151,9 @@ func TestDuplicatesValidator(t *testing.T) { }) t.Run("Duplicates validator should restrict write access only for readers with duplicates", func(t *testing.T) { - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*models.DashboardProvisioning{}, nil).Times(3) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) fakeStore := &fakeDashboardStore{} cfg1 := &config{ diff --git a/pkg/services/provisioning/utils/utils.go b/pkg/services/provisioning/utils/utils.go index a1054436ea9..40153769f6f 100644 --- a/pkg/services/provisioning/utils/utils.go +++ b/pkg/services/provisioning/utils/utils.go @@ -5,12 +5,12 @@ import ( "errors" "fmt" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" ) type DashboardStore interface { - GetDashboard(context.Context, *models.GetDashboardQuery) error + GetDashboard(context.Context, *dashboards.GetDashboardQuery) error } func CheckOrgExists(ctx context.Context, orgService org.Service, orgID int64) error { diff --git a/pkg/services/provisioning/values/values.go b/pkg/services/provisioning/values/values.go index 20c0de27b9a..b384317f9d4 100644 --- a/pkg/services/provisioning/values/values.go +++ b/pkg/services/provisioning/values/values.go @@ -255,8 +255,8 @@ func transformInterface(i interface{}) (interface{}, interface{}, error) { } func transformSlice(i []interface{}) (interface{}, interface{}, error) { - var transformedSlice []interface{} - var rawSlice []interface{} + transformedSlice := make([]interface{}, 0, len(i)) + rawSlice := make([]interface{}, 0, len(i)) for _, val := range i { transformed, raw, err := transformInterface(val) if err != nil { diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index a1bf190112f..233e067604b 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -40,7 +40,7 @@ func (api *Api) ViewPublicDashboard(c *models.ReqContext) response.Response { Updated: dash.Updated, Version: dash.Version, IsFolder: false, - FolderId: dash.FolderId, + FolderId: dash.FolderID, PublicDashboardAccessToken: pubdash.AccessToken, } dash.Data.Get("timepicker").Set("hidden", !pubdash.TimeSelectionEnabled) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 44b9466920b..b69e7a91918 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -17,9 +17,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/dashboards" dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/datasources" datasourcesService "github.com/grafana/grafana/pkg/services/datasources/service" @@ -46,7 +46,7 @@ func TestAPIViewPublicDashboard(t *testing.T) { Name string AccessToken string ExpectedHttpResponse int - DashboardResult *models.Dashboard + DashboardResult *dashboards.Dashboard Err error FixedErrorResponse string }{ @@ -54,7 +54,7 @@ func TestAPIViewPublicDashboard(t *testing.T) { Name: "It gets a public dashboard", AccessToken: validAccessToken, ExpectedHttpResponse: http.StatusOK, - DashboardResult: &models.Dashboard{ + DashboardResult: &dashboards.Dashboard{ Data: simplejson.NewFromAny(map[string]interface{}{ "Uid": DashboardUid, }), @@ -279,9 +279,9 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) }) // Create Dashboard - saveDashboardCmd := models.SaveDashboardCommand{ - OrgId: 1, - FolderId: 1, + saveDashboardCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + FolderID: 1, IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -311,8 +311,8 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) // Create public dashboard savePubDashboardCmd := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, PublicDashboard: &PublicDashboard{ IsEnabled: true, }, diff --git a/pkg/services/publicdashboards/commands/generate_datasources/generate.go b/pkg/services/publicdashboards/commands/generate_datasources/generate.go index d36852860e2..9a98454badc 100644 --- a/pkg/services/publicdashboards/commands/generate_datasources/generate.go +++ b/pkg/services/publicdashboards/commands/generate_datasources/generate.go @@ -56,7 +56,7 @@ func getDatasourcePluginSlugs(baseUrl string) ([]string, error) { if err != nil { return nil, err } - var slugs []string + slugs := make([]string, 0, len(res.Items)) for _, meta := range res.Items { slugs = append(slugs, meta.Slug) } diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index b0f4ade154f..681dbea3cc2 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -56,8 +55,8 @@ func (d *PublicDashboardStoreImpl) FindAll(ctx context.Context, orgId int64) ([] } // FindDashboard returns a dashboard by orgId and dashboardUid -func (d *PublicDashboardStoreImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) { - dashboard := &models.Dashboard{OrgId: orgId, Uid: dashboardUid} +func (d *PublicDashboardStoreImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { + dashboard := &dashboards.Dashboard{OrgID: orgId, UID: dashboardUid} var found bool err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 37526c5fdc8..ba4a6eab5ad 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -48,9 +47,9 @@ func TestIntegrationListPublicDashboard(t *testing.T) { cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true) // these are in order of how they should be returned from ListPUblicDashboards - a := insertPublicDashboard(t, publicdashboardStore, bDash.Uid, orgId, true) - b := insertPublicDashboard(t, publicdashboardStore, cDash.Uid, orgId, true) - c := insertPublicDashboard(t, publicdashboardStore, aDash.Uid, orgId, false) + a := insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true) + b := insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true) + c := insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false) // this is case that can happen as of now, however, postgres and mysql sort // null in the exact opposite fashion and there is no shared syntax to sort @@ -77,7 +76,7 @@ func TestIntegrationFindDashboard(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -92,10 +91,10 @@ func TestIntegrationFindDashboard(t *testing.T) { t.Run("FindDashboard can get original dashboard by uid", func(t *testing.T) { setup() - dashboard, err := publicdashboardStore.FindDashboard(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + dashboard, err := publicdashboardStore.FindDashboard(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) - require.Equal(t, savedDashboard.Uid, dashboard.Uid) + require.Equal(t, savedDashboard.UID, dashboard.UID) }) } @@ -107,7 +106,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -125,8 +124,8 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -147,8 +146,8 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: false, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -180,7 +179,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -199,8 +198,8 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "NOTAREALUUID", @@ -208,7 +207,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.Uid) + res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.UID) require.NoError(t, err) require.True(t, res) @@ -221,8 +220,8 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: false, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "NOTAREALUUID", @@ -230,7 +229,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.Uid) + res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.UID) require.NoError(t, err) require.False(t, res) @@ -245,7 +244,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -259,15 +258,15 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { t.Run("returns public dashboard by dashboardUid", func(t *testing.T) { setup() - savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.Uid, savedDashboard.OrgId, false) - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, false) + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.Equal(t, savedPubdash, pubdash) }) t.Run("returns nil when identifier is not set", func(t *testing.T) { setup() - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, "") + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, "") assert.Nil(t, err) assert.Nil(t, pubdash) }) @@ -278,8 +277,8 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -291,7 +290,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { require.NoError(t, err) // retrieve from db - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.True(t, assert.ObjectsAreEqualValues(&cmd.PublicDashboard, pubdash)) @@ -313,7 +312,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard var err error setup := func() { @@ -326,7 +325,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) { t.Run("returns public dashboard by accessToken", func(t *testing.T) { setup() - savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.Uid, savedDashboard.OrgId, false) + savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, false) pubdash, err := publicdashboardStore.FindByAccessToken(context.Background(), savedPubdash.AccessToken) require.NoError(t, err) assert.Equal(t, savedPubdash, pubdash) @@ -345,8 +344,8 @@ func TestIntegrationFindByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -381,8 +380,8 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard - var savedDashboard2 *models.Dashboard + var savedDashboard *dashboards.Dashboard + var savedDashboard2 *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) @@ -393,7 +392,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true) - insertPublicDashboard(t, publicdashboardStore, savedDashboard2.Uid, savedDashboard2.OrgId, false) + insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, false) } t.Run("saves new public dashboard", func(t *testing.T) { @@ -404,8 +403,8 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { AnnotationsEnabled: true, TimeSelectionEnabled: true, Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -416,7 +415,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, affectedRows, 1) - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.Equal(t, pubdash.AccessToken, "NOTAREALUUID") assert.True(t, pubdash.IsEnabled) @@ -424,7 +423,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { assert.True(t, pubdash.TimeSelectionEnabled) // verify we didn't update all dashboards - pubdash2, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid) + pubdash2, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard2.OrgID, savedDashboard2.UID) require.NoError(t, err) assert.False(t, pubdash2.IsEnabled) }) @@ -436,7 +435,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { IsEnabled: true, Uid: "pubdash-uid", DashboardUid: "", - OrgId: savedDashboard.OrgId, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -458,8 +457,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard - var anotherSavedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard + var anotherSavedDashboard *dashboards.Dashboard var err error setup := func() { @@ -479,8 +478,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { cmd := SavePublicDashboardCommand{ PublicDashboard: PublicDashboard{ Uid: pdUid, - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, IsEnabled: false, AnnotationsEnabled: true, TimeSelectionEnabled: true, @@ -498,8 +497,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { cmd = SavePublicDashboardCommand{ PublicDashboard: PublicDashboard{ Uid: anotherPdUid, - DashboardUid: anotherSavedDashboard.Uid, - OrgId: anotherSavedDashboard.OrgId, + DashboardUid: anotherSavedDashboard.UID, + OrgId: anotherSavedDashboard.OrgID, IsEnabled: true, AnnotationsEnabled: false, TimeSelectionEnabled: false, @@ -515,8 +514,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { updatedPublicDashboard := PublicDashboard{ Uid: pdUid, - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, IsEnabled: false, AnnotationsEnabled: true, TimeSelectionEnabled: true, @@ -532,7 +531,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { assert.EqualValues(t, rowsAffected, 1) // updated dashboard should have changed - pdRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pdRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.Equal(t, updatedPublicDashboard.UpdatedAt, pdRetrieved.UpdatedAt) @@ -543,7 +542,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { assert.Equal(t, updatedPublicDashboard.TimeSelectionEnabled, pdRetrieved.TimeSelectionEnabled) // not updated dashboard shouldn't have changed - pdNotUpdatedRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), anotherSavedDashboard.OrgId, anotherSavedDashboard.Uid) + pdNotUpdatedRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), anotherSavedDashboard.OrgID, anotherSavedDashboard.UID) require.NoError(t, err) assert.NotEqual(t, updatedPublicDashboard.UpdatedAt, pdNotUpdatedRetrieved.UpdatedAt) assert.NotEqual(t, updatedPublicDashboard.IsEnabled, pdNotUpdatedRetrieved.IsEnabled) @@ -559,7 +558,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard var err error setup := func() { @@ -576,8 +575,8 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -589,7 +588,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken") require.NoError(t, err) - assert.Equal(t, savedDashboard.OrgId, orgId) + assert.Equal(t, savedDashboard.OrgID, orgId) }) t.Run("GetOrgIdByAccessToken will return 0 when IsEnabled=false", func(t *testing.T) { @@ -598,8 +597,8 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: false, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -611,7 +610,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken") require.NoError(t, err) - assert.NotEqual(t, savedDashboard.OrgId, orgId) + assert.NotEqual(t, savedDashboard.OrgID, orgId) }) t.Run("GetOrgIdByAccessToken will return 0 when no public dashboard has matching access token", func(t *testing.T) { @@ -619,7 +618,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "nonExistentAccessToken") require.NoError(t, err) - assert.NotEqual(t, savedDashboard.OrgId, orgId) + assert.NotEqual(t, savedDashboard.OrgID, orgId) }) } @@ -631,7 +630,7 @@ func TestIntegrationDelete(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard var savedPublicDashboard *PublicDashboard var err error @@ -641,7 +640,7 @@ func TestIntegrationDelete(t *testing.T) { require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.Uid, savedDashboard.OrgId, true) + savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, true) } t.Run("Delete success", func(t *testing.T) { @@ -668,11 +667,11 @@ func TestIntegrationDelete(t *testing.T) { // helper function to insert a dashboard func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -683,8 +682,8 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) + dash.Data.Set("id", dash.ID) + dash.Data.Set("uid", dash.UID) return dash } diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index 1de844522d5..8274e6c8551 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -6,7 +6,7 @@ import ( "time" "github.com/grafana/grafana/pkg/kinds/dashboard" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) @@ -95,7 +95,7 @@ func (ts *TimeSettings) ToDB() ([]byte, error) { } // BuildTimeSettings build time settings object using selected values if enabled and are valid or dashboard default values -func (pd PublicDashboard) BuildTimeSettings(dashboard *models.Dashboard, reqDTO PublicDashboardQueryDTO) TimeSettings { +func (pd PublicDashboard) BuildTimeSettings(dashboard *dashboards.Dashboard, reqDTO PublicDashboardQueryDTO) TimeSettings { from := dashboard.Data.GetPath("time", "from").MustString() to := dashboard.Data.GetPath("time", "to").MustString() diff --git a/pkg/services/publicdashboards/models/models_test.go b/pkg/services/publicdashboards/models/models_test.go index c7fc470e084..fe60b4aa9c0 100644 --- a/pkg/services/publicdashboards/models/models_test.go +++ b/pkg/services/publicdashboards/models/models_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/internal" "github.com/stretchr/testify/assert" ) @@ -24,14 +24,14 @@ func TestBuildTimeSettings(t *testing.T) { testCases := []struct { name string - dashboard *models.Dashboard + dashboard *dashboards.Dashboard pubdash *PublicDashboard timeResult TimeSettings reqDTO PublicDashboardQueryDTO }{ { name: "should use dashboard time if pubdash time empty", - dashboard: &models.Dashboard{Data: dashboardData}, + dashboard: &dashboards.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, timeResult: TimeSettings{ From: defaultFromMs, @@ -41,7 +41,7 @@ func TestBuildTimeSettings(t *testing.T) { }, { name: "should use dashboard time when time selection is disabled", - dashboard: &models.Dashboard{Data: dashboardData}, + dashboard: &dashboards.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: false, TimeSettings: &TimeSettings{From: "now-12", To: "now"}}, timeResult: TimeSettings{ From: defaultFromMs, @@ -51,7 +51,7 @@ func TestBuildTimeSettings(t *testing.T) { }, { name: "should use selected values if time selection is enabled", - dashboard: &models.Dashboard{Data: dashboardData}, + dashboard: &dashboards.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: true, TimeSettings: &TimeSettings{From: "now-12", To: "now"}}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeSettings{ diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index a8544cc2b0e..883c51e1b80 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package publicdashboards @@ -7,14 +7,14 @@ import ( backend "github.com/grafana/grafana-plugin-sdk-go/backend" + dashboards "github.com/grafana/grafana/pkg/services/dashboards" + dtos "github.com/grafana/grafana/pkg/api/dtos" mock "github.com/stretchr/testify/mock" models "github.com/grafana/grafana/pkg/services/publicdashboards/models" - pkgmodels "github.com/grafana/grafana/pkg/models" - user "github.com/grafana/grafana/pkg/services/user" ) @@ -195,15 +195,15 @@ func (_m *FakePublicDashboardService) FindByDashboardUid(ctx context.Context, or } // FindDashboard provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakePublicDashboardService) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*pkgmodels.Dashboard, error) { +func (_m *FakePublicDashboardService) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) - var r0 *pkgmodels.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *pkgmodels.Dashboard); ok { + var r0 *dashboards.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *dashboards.Dashboard); ok { r0 = rf(ctx, orgId, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*pkgmodels.Dashboard) + r0 = ret.Get(0).(*dashboards.Dashboard) } } @@ -218,7 +218,7 @@ func (_m *FakePublicDashboardService) FindDashboard(ctx context.Context, orgId i } // FindPublicDashboardAndDashboardByAccessToken provides a mock function with given fields: ctx, accessToken -func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*models.PublicDashboard, *pkgmodels.Dashboard, error) { +func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*models.PublicDashboard, *dashboards.Dashboard, error) { ret := _m.Called(ctx, accessToken) var r0 *models.PublicDashboard @@ -230,12 +230,12 @@ func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessTok } } - var r1 *pkgmodels.Dashboard - if rf, ok := ret.Get(1).(func(context.Context, string) *pkgmodels.Dashboard); ok { + var r1 *dashboards.Dashboard + if rf, ok := ret.Get(1).(func(context.Context, string) *dashboards.Dashboard); ok { r1 = rf(ctx, accessToken) } else { if ret.Get(1) != nil { - r1 = ret.Get(1).(*pkgmodels.Dashboard) + r1 = ret.Get(1).(*dashboards.Dashboard) } } @@ -250,18 +250,18 @@ func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessTok } // GetMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId, reqDTO -func (_m *FakePublicDashboardService) GetMetricRequest(ctx context.Context, dashboard *pkgmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { +func (_m *FakePublicDashboardService) GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { ret := _m.Called(ctx, dashboard, publicDashboard, panelId, reqDTO) var r0 dtos.MetricRequest - if rf, ok := ret.Get(0).(func(context.Context, *pkgmodels.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) dtos.MetricRequest); ok { + if rf, ok := ret.Get(0).(func(context.Context, *dashboards.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) dtos.MetricRequest); ok { r0 = rf(ctx, dashboard, publicDashboard, panelId, reqDTO) } else { r0 = ret.Get(0).(dtos.MetricRequest) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *pkgmodels.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *dashboards.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) error); ok { r1 = rf(ctx, dashboard, publicDashboard, panelId, reqDTO) } else { r1 = ret.Error(1) diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index a664cb9cc2c..16a79872fea 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -1,16 +1,14 @@ -// Code generated by mockery v2.12.1. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package publicdashboards import ( context "context" - models "github.com/grafana/grafana/pkg/services/publicdashboards/models" + dashboards "github.com/grafana/grafana/pkg/services/dashboards" mock "github.com/stretchr/testify/mock" - pkgmodels "github.com/grafana/grafana/pkg/models" - - testing "testing" + models "github.com/grafana/grafana/pkg/services/publicdashboards/models" ) // FakePublicDashboardStore is an autogenerated mock type for the Store type @@ -195,15 +193,15 @@ func (_m *FakePublicDashboardStore) FindByDashboardUid(ctx context.Context, orgI } // FindDashboard provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakePublicDashboardStore) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*pkgmodels.Dashboard, error) { +func (_m *FakePublicDashboardStore) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) - var r0 *pkgmodels.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *pkgmodels.Dashboard); ok { + var r0 *dashboards.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *dashboards.Dashboard); ok { r0 = rf(ctx, orgId, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*pkgmodels.Dashboard) + r0 = ret.Get(0).(*dashboards.Dashboard) } } @@ -259,8 +257,13 @@ func (_m *FakePublicDashboardStore) Update(ctx context.Context, cmd models.SaveP return r0, r1 } -// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakePublicDashboardStore(t testing.TB) *FakePublicDashboardStore { +type mockConstructorTestingTNewFakePublicDashboardStore interface { + mock.TestingT + Cleanup(func()) +} + +// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFakePublicDashboardStore(t mockConstructorTestingTNewFakePublicDashboardStore) *FakePublicDashboardStore { mock := &FakePublicDashboardStore{} mock.Mock.Test(t) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index e78ef487d4e..b301e9be8ad 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -5,7 +5,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/user" ) @@ -15,17 +15,17 @@ import ( //go:generate go run ./commands/generate_datasources/main.go //go:generate mockery --name Service --structname FakePublicDashboardService --inpackage --filename public_dashboard_service_mock.go type Service interface { - FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) + FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) - FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) + FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Delete(ctx context.Context, orgId int64, uid string) error - GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) + GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) GetOrgIdByAccessToken(ctx context.Context, accessToken string) (int64, error) NewPublicDashboardAccessToken(ctx context.Context) (string, error) @@ -40,7 +40,7 @@ type Store interface { Find(ctx context.Context, uid string) (*PublicDashboard, error) FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) - FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) + FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go index 42005beb02d..bb10954a64f 100644 --- a/pkg/services/publicdashboards/service/query.go +++ b/pkg/services/publicdashboards/service/query.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" - dashmodels "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -44,9 +43,9 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT annoQuery := &annotations.ItemQuery{ From: reqDTO.From, To: reqDTO.To, - OrgId: dash.OrgId, - DashboardId: dash.Id, - DashboardUid: dash.Uid, + OrgId: dash.OrgID, + DashboardId: dash.ID, + DashboardUid: dash.UID, Limit: anno.Target.Limit, MatchAny: anno.Target.MatchAny, SignedInUser: anonymousUser, @@ -89,7 +88,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT } } - var results []models.AnnotationEvent + results := make([]models.AnnotationEvent, 0, len(uniqueEvents)) for _, result := range uniqueEvents { results = append(results, result) } @@ -98,7 +97,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT } // GetMetricRequest returns a metric request for the given panel and query -func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { +func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { err := validation.ValidateQueryPublicDashboardRequest(queryDto, publicDashboard) if err != nil { return dtos.MetricRequest{}, err @@ -150,7 +149,7 @@ func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, } // buildMetricRequest merges public dashboard parameters with dashboard and returns a metrics request to be sent to query backend -func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { +func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { // group queries by panel queriesByPanel := groupQueriesByPanelId(dashboard.Data) queries, ok := queriesByPanel[panelId] @@ -175,11 +174,11 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, da } // buildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard -func buildAnonymousUser(ctx context.Context, dashboard *dashmodels.Dashboard) *user.SignedInUser { +func buildAnonymousUser(ctx context.Context, dashboard *dashboards.Dashboard) *user.SignedInUser { datasourceUids := getUniqueDashboardDatasourceUids(dashboard.Data) // Create a user with blank permissions - anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} + anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgID, Permissions: make(map[int64]map[string][]string)} // Scopes needed for Annotation queries annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard} @@ -202,7 +201,7 @@ func buildAnonymousUser(ctx context.Context, dashboard *dashmodels.Dashboard) *u permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes permissions[dashboards.ActionDashboardsRead] = dashboardScopes - anonymousUser.Permissions[dashboard.OrgId] = permissions + anonymousUser.Permissions[dashboard.OrgID] = permissions return anonymousUser } diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index 81885ddcb65..137ce98f003 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -11,9 +11,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" - grafanamodels "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" + "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards" @@ -391,8 +391,8 @@ func TestGetQueryDataResponse(t *testing.T) { dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -426,13 +426,13 @@ func TestGetAnnotations(t *testing.T) { fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")). - Return(grafanamodels.NewDashboard("dash1"), nil) + Return(dashboards.NewDashboard("dash1"), nil) reqDTO := AnnotationsQueryDTO{ From: 1, To: 2, } - dash := grafanamodels.NewDashboard("testDashboard") + dash := dashboards.NewDashboard("testDashboard") items, _ := service.FindAnnotations(context.Background(), reqDTO, "abc123") anonUser := buildAnonymousUser(context.Background(), dash) @@ -442,7 +442,7 @@ func TestGetAnnotations(t *testing.T) { }) t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -478,7 +478,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -515,7 +515,7 @@ func TestGetAnnotations(t *testing.T) { }) t.Run("Test panelId set to zero when annotation event is for a tags query", func(t *testing.T) { - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -538,7 +538,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -575,7 +575,7 @@ func TestGetAnnotations(t *testing.T) { }) t.Run("Test can get grafana annotations and will skip annotation queries and disabled annotations", func(t *testing.T) { - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") disabledGrafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: false, @@ -610,7 +610,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -654,8 +654,8 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - dashboard := grafanamodels.NewDashboard("dashWithNoAnnotations") - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + dashboard := dashboards.NewDashboard("dashWithNoAnnotations") + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -674,7 +674,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -690,7 +690,7 @@ func TestGetAnnotations(t *testing.T) { } annos := []DashAnnotation{grafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: false} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: false} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -709,7 +709,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -724,7 +724,7 @@ func TestGetAnnotations(t *testing.T) { } annos := []DashAnnotation{grafanaAnnotation} dash = AddAnnotationsToDashboard(t, dash, annos) - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dash, nil) @@ -746,7 +746,7 @@ func TestGetMetricRequest(t *testing.T) { dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) publicDashboard := &PublicDashboard{ Uid: "1", - DashboardUid: dashboard.Uid, + DashboardUid: dashboard.UID, IsEnabled: true, AccessToken: "abc123", } @@ -834,8 +834,8 @@ func TestBuildMetricRequest(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: publicDashboard.Uid, - OrgId: publicDashboard.OrgId, + DashboardUid: publicDashboard.UID, + OrgId: publicDashboard.OrgID, PublicDashboard: &PublicDashboard{ IsEnabled: true, DashboardUid: "NOTTHESAME", @@ -848,8 +848,8 @@ func TestBuildMetricRequest(t *testing.T) { require.NoError(t, err) nonPublicDto := &SavePublicDashboardDTO{ - DashboardUid: nonPublicDashboard.Uid, - OrgId: nonPublicDashboard.OrgId, + DashboardUid: nonPublicDashboard.UID, + OrgId: nonPublicDashboard.OrgID, PublicDashboard: &PublicDashboard{ IsEnabled: false, DashboardUid: "NOTTHESAME", @@ -1038,7 +1038,7 @@ func TestBuildAnonymousUser(t *testing.T) { t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { user := buildAnonymousUser(context.Background(), dashboard) - require.Equal(t, dashboard.OrgId, user.OrgID) + require.Equal(t, dashboard.OrgID, user.OrgID) require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:query"][0]) require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:query"][1]) require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:read"][0]) @@ -1047,7 +1047,7 @@ func TestBuildAnonymousUser(t *testing.T) { t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) { user := buildAnonymousUser(context.Background(), dashboard) - require.Equal(t, dashboard.OrgId, user.OrgID) + require.Equal(t, dashboard.OrgID, user.OrgID) require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0]) require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) }) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index adbf48006c2..37ff8c755f9 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "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/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -62,7 +61,7 @@ func ProvideService( } // FindDashboard Gets a dashboard by Uid -func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) { +func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { dash, err := pd.store.FindDashboard(ctx, orgId, dashboardUid) if err != nil { return nil, ErrInternalServerError.Errorf("FindDashboard: failed to find dashboard by orgId: %d and dashboardUid: %s: %w", orgId, dashboardUid, err) @@ -90,7 +89,7 @@ func (pd *PublicDashboardServiceImpl) FindByAccessToken(ctx context.Context, acc } // FindPublicDashboardAndDashboardByAccessToken Gets public dashboard and a dashboard by access token -func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) { +func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) { pubdash, err := pd.FindByAccessToken(ctx, accessToken) if err != nil { return nil, nil, err diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index fcf700b36c4..771f916d53a 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "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/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -44,7 +43,7 @@ func TestLogPrefix(t *testing.T) { func TestGetPublicDashboard(t *testing.T) { type storeResp struct { pd *PublicDashboard - d *models.Dashboard + d *dashboards.Dashboard err error } @@ -53,25 +52,25 @@ func TestGetPublicDashboard(t *testing.T) { AccessToken string StoreResp *storeResp ErrResp error - DashResp *models.Dashboard + DashResp *dashboards.Dashboard }{ { Name: "returns a dashboard", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: true}, - d: &models.Dashboard{Uid: "mydashboard", Data: dashboardData}, + d: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, err: nil, }, ErrResp: nil, - DashResp: &models.Dashboard{Uid: "mydashboard", Data: dashboardData}, + DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, }, { Name: "returns ErrPublicDashboardNotFound when isEnabled is false", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: false}, - d: &models.Dashboard{Uid: "mydashboard"}, + d: &dashboards.Dashboard{UID: "mydashboard"}, err: nil, }, ErrResp: ErrPublicDashboardNotFound, @@ -138,8 +137,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -153,12 +152,12 @@ func TestCreatePublicDashboard(t *testing.T) { _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) - pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid) + pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) // DashboardUid/OrgId/CreatedBy set by the command, not parameters - assert.Equal(t, dashboard.Uid, pubdash.DashboardUid) - assert.Equal(t, dashboard.OrgId, pubdash.OrgId) + assert.Equal(t, dashboard.UID, pubdash.DashboardUid) + assert.Equal(t, dashboard.OrgID, pubdash.OrgId) assert.Equal(t, dto.UserId, pubdash.CreatedBy) assert.Equal(t, dto.PublicDashboard.AnnotationsEnabled, pubdash.AnnotationsEnabled) // ExistsEnabledByDashboardUid set by parameters @@ -186,8 +185,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -199,7 +198,7 @@ func TestCreatePublicDashboard(t *testing.T) { _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) - pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid) + pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) assert.Equal(t, defaultPubdashTimeSettings, pubdash.TimeSettings) }) @@ -219,8 +218,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -234,7 +233,7 @@ func TestCreatePublicDashboard(t *testing.T) { }) t.Run("Throws an error when pubdash with generated access token already exists", func(t *testing.T) { - dashboard := models.NewDashboard("testDashie") + dashboard := dashboards.NewDashboard("testDashie") pubdash := &PublicDashboard{ IsEnabled: true, AnnotationsEnabled: false, @@ -283,8 +282,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ AnnotationsEnabled: false, @@ -298,8 +297,8 @@ func TestCreatePublicDashboard(t *testing.T) { // attempt to overwrite settings dto = &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 8, PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, @@ -336,8 +335,8 @@ func TestUpdatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ AnnotationsEnabled: false, @@ -352,8 +351,8 @@ func TestUpdatePublicDashboard(t *testing.T) { // attempt to overwrite settings dto = &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 8, PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, @@ -400,8 +399,8 @@ func TestUpdatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -414,8 +413,8 @@ func TestUpdatePublicDashboard(t *testing.T) { // attempt to overwrite settings dto = &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 8, PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, @@ -933,7 +932,7 @@ func CreateDatasource(dsType string, uid string) struct { } } -func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard { +func AddAnnotationsToDashboard(t *testing.T, dash *dashboards.Dashboard, annotations []DashAnnotation) *dashboards.Dashboard { type annotationsDto struct { List []DashAnnotation `json:"list"` } @@ -951,7 +950,7 @@ func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations } func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *models.Dashboard { + folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *dashboards.Dashboard { t.Helper() var dashboardPanels []interface{} @@ -999,9 +998,9 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto } } - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -1020,7 +1019,7 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) + dash.Data.Set("id", dash.ID) + dash.Data.Set("uid", dash.UID) return dash } diff --git a/pkg/services/publicdashboards/validation/validation.go b/pkg/services/publicdashboards/validation/validation.go index 70c2d25b703..6f2c57c83dd 100644 --- a/pkg/services/publicdashboards/validation/validation.go +++ b/pkg/services/publicdashboards/validation/validation.go @@ -1,12 +1,12 @@ package validation import ( - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) -func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error { +func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *dashboards.Dashboard) error { if hasTemplateVariables(dashboard) { return ErrPublicDashboardHasTemplateVariables.Errorf("ValidateSavePublicDashboard: public dashboard has template variables") } @@ -14,7 +14,7 @@ func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dash return nil } -func hasTemplateVariables(dashboard *models.Dashboard) bool { +func hasTemplateVariables(dashboard *dashboards.Dashboard) bool { templateVariables := dashboard.Data.Get("templating").Get("list").MustArray() return len(templateVariables) > 0 diff --git a/pkg/services/publicdashboards/validation/validation_test.go b/pkg/services/publicdashboards/validation/validation_test.go index 8b606d05a27..4ee281e20fb 100644 --- a/pkg/services/publicdashboards/validation/validation_test.go +++ b/pkg/services/publicdashboards/validation/validation_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/stretchr/testify/require" ) @@ -21,7 +21,7 @@ func TestValidatePublicDashboard(t *testing.T) { } }`) dashboardData, _ := simplejson.NewJson(templateVars) - dashboard := models.NewDashboardFromJson(dashboardData) + dashboard := dashboards.NewDashboardFromJson(dashboardData) dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil} err := ValidatePublicDashboard(dto, dashboard) @@ -35,7 +35,7 @@ func TestValidatePublicDashboard(t *testing.T) { } }`) dashboardData, _ := simplejson.NewJson(templateVars) - dashboard := models.NewDashboardFromJson(dashboardData) + dashboard := dashboards.NewDashboardFromJson(dashboardData) dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil} err := ValidatePublicDashboard(dto, dashboard) diff --git a/pkg/services/querylibrary/querylibraryimpl/service.go b/pkg/services/querylibrary/querylibraryimpl/service.go index 6078c41af82..15b39bea04f 100644 --- a/pkg/services/querylibrary/querylibraryimpl/service.go +++ b/pkg/services/querylibrary/querylibraryimpl/service.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/x/persistentcollection" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" @@ -67,7 +67,7 @@ type queryLoader interface { byUID(uid string) (*querylibrary.Query, error) } -func (s *service) UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *models.Dashboard) error { +func (s *service) UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *dashboards.Dashboard) error { queryLoader := newPerRequestQueryLoader(ctx, user, s) return s.updateQueriesRecursively(queryLoader, dash.Data) } diff --git a/pkg/services/querylibrary/types.go b/pkg/services/querylibrary/types.go index 2dfbb6247fa..4f2bd11cfc3 100644 --- a/pkg/services/querylibrary/types.go +++ b/pkg/services/querylibrary/types.go @@ -5,8 +5,8 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/services/user" ) @@ -78,7 +78,7 @@ type Service interface { GetBatch(ctx context.Context, user *user.SignedInUser, uids []string) ([]*Query, error) Update(ctx context.Context, user *user.SignedInUser, query *Query) error Delete(ctx context.Context, user *user.SignedInUser, uid string) error - UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *models.Dashboard) error + UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *dashboards.Dashboard) error registry.CanBeDisabled } diff --git a/pkg/services/screenshot/screenshot.go b/pkg/services/screenshot/screenshot.go index e8d6b41b695..eb5bf01ddfa 100644 --- a/pkg/services/screenshot/screenshot.go +++ b/pkg/services/screenshot/screenshot.go @@ -12,7 +12,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/rendering" @@ -86,7 +85,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt start := time.Now() defer func() { s.duration.Observe(time.Since(start).Seconds()) }() - q := models.GetDashboardQuery{Uid: opts.DashboardUID} + q := dashboards.GetDashboardQuery{UID: opts.DashboardUID} if err := s.ds.GetDashboard(ctx, &q); err != nil { s.instrumentError(err) return nil, err @@ -95,9 +94,9 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt opts = opts.SetDefaults() u := url.URL{} - u.Path = path.Join("d-solo", q.Result.Uid, q.Result.Slug) + u.Path = path.Join("d-solo", q.Result.UID, q.Result.Slug) p := u.Query() - p.Add("orgId", strconv.FormatInt(q.Result.OrgId, 10)) + p.Add("orgId", strconv.FormatInt(q.Result.OrgID, 10)) p.Add("panelId", strconv.FormatInt(opts.PanelID, 10)) p.Add("from", opts.From) p.Add("to", opts.To) @@ -105,7 +104,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt renderOpts := rendering.Opts{ AuthOpts: rendering.AuthOpts{ - OrgID: q.Result.OrgId, + OrgID: q.Result.OrgID, OrgRole: org.RoleAdmin, }, ErrorOpts: rendering.ErrorOpts{ diff --git a/pkg/services/screenshot/screenshot_test.go b/pkg/services/screenshot/screenshot_test.go index d4382bbd960..42557f6956e 100644 --- a/pkg/services/screenshot/screenshot_test.go +++ b/pkg/services/screenshot/screenshot_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/rendering" @@ -27,7 +26,7 @@ func TestHeadlessScreenshotService(t *testing.T) { s := NewHeadlessScreenshotService(&d, r, prometheus.NewRegistry()) // a non-existent dashboard should return error - d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Return(dashboards.ErrDashboardNotFound).Once() + d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(dashboards.ErrDashboardNotFound).Once() ctx := context.Background() opts := ScreenshotOptions{} screenshot, err := s.Take(ctx, opts) @@ -35,9 +34,9 @@ func TestHeadlessScreenshotService(t *testing.T) { assert.Nil(t, screenshot) // should take a screenshot - d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Id: 1, Uid: "foo", Slug: "bar", OrgId: 2} + d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.GetDashboardQuery) + q.Result = &dashboards.Dashboard{ID: 1, UID: "foo", Slug: "bar", OrgID: 2} }).Return(nil) renderOpts := rendering.Opts{ diff --git a/pkg/services/searchV2/allowed_actions.go b/pkg/services/searchV2/allowed_actions.go index d6519c9e6b0..2e9dd13907e 100644 --- a/pkg/services/searchV2/allowed_actions.go +++ b/pkg/services/searchV2/allowed_actions.go @@ -100,7 +100,7 @@ func (s *StandardSearchService) createAllowedActions(ctx context.Context, orgId dsActionsByUid = make(map[string][]string) } - var out [][]allowedActions + out := make([][]allowedActions, 0, len(references)) for _, ref := range references { var actions []allowedActions diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index 15632368411..0e92a96993e 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -776,13 +776,12 @@ func (i *searchIndex) updateDashboard(ctx context.Context, orgID int64, index *o return err } - var actualPanelIDs []string - if location != "" { location += "/" } location += dash.uid panelDocs := getDashboardPanelDocs(dash, location) + actualPanelIDs := make([]string, 0, len(panelDocs)) for _, panelDoc := range panelDocs { actualPanelIDs = append(actualPanelIDs, string(panelDoc.ID().Term())) batch.Update(panelDoc.ID(), panelDoc) diff --git a/pkg/services/secrets/kvstore/plugin.go b/pkg/services/secrets/kvstore/plugin.go index 51f0ba02927..d04578bc47d 100644 --- a/pkg/services/secrets/kvstore/plugin.go +++ b/pkg/services/secrets/kvstore/plugin.go @@ -190,7 +190,7 @@ func (kv *SecretsKVStorePlugin) WithFallbackEnabled(fn func() error) error { } func parseKeys(keys []*smp.Key) []Key { - var newKeys []Key + newKeys := make([]Key, 0, len(keys)) for _, k := range keys { newKey := Key{OrgId: k.OrgId, Namespace: k.Namespace, Type: k.Type} @@ -201,7 +201,7 @@ func parseKeys(keys []*smp.Key) []Key { } func parseItems(items []*smp.Item) []Item { - var newItems []Item + newItems := make([]Item, 0, len(items)) for _, i := range items { newItem := Item{OrgId: &i.Key.OrgId, Namespace: &i.Key.Namespace, Type: &i.Key.Type, Value: i.Value} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go index 7e2266163a4..47de5f8fa30 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go @@ -54,7 +54,8 @@ func (m *actionNameMigrator) migrateActionNames() error { "alert.rules:update": accesscontrol.ActionAlertingRuleUpdate, } - var oldActionNames, newActionNames []interface{} + oldActionNames := make([]interface{}, 0, len(actionNameMapping)) + newActionNames := make([]interface{}, 0, len(actionNameMapping)) for oldName, newName := range actionNameMapping { oldActionNames = append(oldActionNames, oldName) newActionNames = append(newActionNames, newName) diff --git a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go index a0be96f92a9..c9e9f29c207 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go @@ -47,7 +47,7 @@ func (p *teamPermissionMigrator) setRolePermissions(roleID int64, permissions [] } // Then insert new permissions - var newPermissions []accesscontrol.Permission + newPermissions := make([]accesscontrol.Permission, 0, len(permissions)) now := time.Now() for _, permission := range permissions { permission.RoleID = roleID diff --git a/pkg/services/sqlstore/migrations/dashboard_public_mig.go b/pkg/services/sqlstore/migrations/dashboard_public_mig.go index f3e932f45ce..252d95e0aaa 100644 --- a/pkg/services/sqlstore/migrations/dashboard_public_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_public_mig.go @@ -84,4 +84,12 @@ func addPublicDashboardMigration(mg *Migrator) { mg.AddMigration("delete orphaned public dashboards", NewRawSQLMigration( "DELETE FROM dashboard_public WHERE dashboard_uid NOT IN (SELECT uid FROM dashboard)")) + + mg.AddMigration("add share column", NewAddColumnMigration(dashboardPublicCfgV2, &Column{ + Name: "share", + Type: DB_NVarchar, + Length: 64, + Nullable: false, + Default: "'public'", + })) } diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 1b52a9ebe60..ce249e6095c 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/session" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" ) @@ -23,10 +24,10 @@ type SQLStoreMock struct { ExpectedUser *user.User ExpectedTeamsByUser []*team.TeamDTO ExpectedAlert *models.Alert - ExpectedSystemStats *models.SystemStats - ExpectedDataSourceStats []*models.DataSourceStats - ExpectedDataSourcesAccessStats []*models.DataSourceAccessStats - ExpectedNotifierUsageStats []*models.NotifierUsageStats + ExpectedSystemStats *stats.SystemStats + ExpectedDataSourceStats []*stats.DataSourceStats + ExpectedDataSourcesAccessStats []*stats.DataSourceAccessStats + ExpectedNotifierUsageStats []*stats.NotifierUsageStats ExpectedSignedInUser *user.SignedInUser ExpectedError error @@ -36,30 +37,6 @@ func NewSQLStoreMock() *SQLStoreMock { return &SQLStoreMock{} } -func (m *SQLStoreMock) GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error { - return m.ExpectedError -} - -func (m *SQLStoreMock) GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error { - query.Result = m.ExpectedNotifierUsageStats - return m.ExpectedError -} - -func (m *SQLStoreMock) GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error { - query.Result = m.ExpectedDataSourceStats - return m.ExpectedError -} - -func (m *SQLStoreMock) GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error { - query.Result = m.ExpectedDataSourcesAccessStats - return m.ExpectedError -} - -func (m *SQLStoreMock) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error { - query.Result = m.ExpectedSystemStats - return m.ExpectedError -} - func (m *SQLStoreMock) GetDialect() migrator.Dialect { return nil } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 81e00eab3ab..28a11c97315 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -24,13 +24,13 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -202,7 +202,7 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser(test bool) error { // If this is a test database, don't exit early when any user is found. if !test { - var stats models.SystemUserCountStats + var stats stats.SystemUserCountStats // TODO: Should be able to rename "Count" to "count", for more standard SQL style // Just have to make sure it gets deserialized properly into models.SystemUserCountStats rawSQL := `SELECT COUNT(id) AS Count FROM ` + ss.Dialect.Quote("user") diff --git a/pkg/models/stats.go b/pkg/services/stats/models.go similarity index 99% rename from pkg/models/stats.go rename to pkg/services/stats/models.go index f7d82218a13..303037108ea 100644 --- a/pkg/models/stats.go +++ b/pkg/services/stats/models.go @@ -1,4 +1,4 @@ -package models +package stats type SystemStats struct { Dashboards int64 diff --git a/pkg/services/stats/stats.go b/pkg/services/stats/stats.go index da2b9a49bf2..986ee5a08d3 100644 --- a/pkg/services/stats/stats.go +++ b/pkg/services/stats/stats.go @@ -2,15 +2,13 @@ package stats import ( "context" - - "github.com/grafana/grafana/pkg/models" ) type Service interface { - GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error - GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error - GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error - GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error - GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error - GetSystemUserCountStats(ctx context.Context, query *models.GetSystemUserCountStatsQuery) error + GetAdminStats(ctx context.Context, query *GetAdminStatsQuery) error + GetAlertNotifiersUsageStats(ctx context.Context, query *GetAlertNotifierUsageStatsQuery) error + GetDataSourceStats(ctx context.Context, query *GetDataSourceStatsQuery) error + GetDataSourceAccessStats(ctx context.Context, query *GetDataSourceAccessStatsQuery) error + GetSystemStats(ctx context.Context, query *GetSystemStatsQuery) error + GetSystemUserCountStats(ctx context.Context, query *GetSystemUserCountStatsQuery) error } diff --git a/pkg/services/stats/statsimpl/stats.go b/pkg/services/stats/statsimpl/stats.go index c0a49f3fc35..d70929500e7 100644 --- a/pkg/services/stats/statsimpl/stats.go +++ b/pkg/services/stats/statsimpl/stats.go @@ -21,28 +21,28 @@ func ProvideService(db db.DB) stats.Service { type sqlStatsService struct{ db db.DB } -func (ss *sqlStatsService) GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error { +func (ss *sqlStatsService) GetAlertNotifiersUsageStats(ctx context.Context, query *stats.GetAlertNotifierUsageStatsQuery) error { return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { var rawSQL = `SELECT COUNT(*) AS count, type FROM ` + ss.db.GetDialect().Quote("alert_notification") + ` GROUP BY type` - query.Result = make([]*models.NotifierUsageStats, 0) + query.Result = make([]*stats.NotifierUsageStats, 0) err := dbSession.SQL(rawSQL).Find(&query.Result) return err }) } -func (ss *sqlStatsService) GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error { +func (ss *sqlStatsService) GetDataSourceStats(ctx context.Context, query *stats.GetDataSourceStatsQuery) error { return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { var rawSQL = `SELECT COUNT(*) AS count, type FROM ` + ss.db.GetDialect().Quote("data_source") + ` GROUP BY type` - query.Result = make([]*models.DataSourceStats, 0) + query.Result = make([]*stats.DataSourceStats, 0) err := dbSession.SQL(rawSQL).Find(&query.Result) return err }) } -func (ss *sqlStatsService) GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error { +func (ss *sqlStatsService) GetDataSourceAccessStats(ctx context.Context, query *stats.GetDataSourceAccessStatsQuery) error { return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { var rawSQL = `SELECT COUNT(*) AS count, type, access FROM ` + ss.db.GetDialect().Quote("data_source") + ` GROUP BY type, access` - query.Result = make([]*models.DataSourceAccessStats, 0) + query.Result = make([]*stats.DataSourceAccessStats, 0) err := dbSession.SQL(rawSQL).Find(&query.Result) return err }) @@ -53,7 +53,7 @@ func notServiceAccount(dialect migrator.Dialect) string { dialect.BooleanStr(false) } -func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error { +func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetSystemStatsQuery) error { return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { sb := &db.SQLBuilder{} sb.Write("SELECT ") @@ -120,7 +120,7 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *models.Get sb.Write(ss.roleCounterSQL(ctx)) - var stats models.SystemStats + var stats stats.SystemStats _, err := dbSession.SQL(sb.GetSQLString(), sb.GetParams()...).Get(&stats) if err != nil { return err @@ -164,7 +164,7 @@ func viewersPermissionsCounterSQL(db db.DB, statName string, isFolder bool, perm ) AS ` + statName + `, ` } -func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error { +func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *stats.GetAdminStatsQuery) error { return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { dialect := ss.db.GetDialect() now := time.Now() @@ -231,7 +231,7 @@ func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *models.GetA FROM ` + dialect.Quote("user_auth_token") + ` WHERE rotated_at > ? ) AS daily_active_sessions` - var stats models.AdminStats + var stats stats.AdminStats _, err := dbSession.SQL(rawSQL, activeEndDate, dailyActiveEndDate, monthlyActiveEndDate, activeEndDate.Unix(), dailyActiveEndDate.Unix()).Get(&stats) if err != nil { return err @@ -242,10 +242,10 @@ func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *models.GetA }) } -func (ss *sqlStatsService) GetSystemUserCountStats(ctx context.Context, query *models.GetSystemUserCountStatsQuery) error { +func (ss *sqlStatsService) GetSystemUserCountStats(ctx context.Context, query *stats.GetSystemUserCountStatsQuery) error { return ss.db.WithDbSession(ctx, func(sess *db.Session) error { var rawSQL = `SELECT COUNT(id) AS Count FROM ` + ss.db.GetDialect().Quote("user") - var stats models.SystemUserCountStats + var stats stats.SystemUserCountStats _, err := sess.SQL(rawSQL).Get(&stats) if err != nil { return err @@ -269,9 +269,9 @@ func (ss *sqlStatsService) updateUserRoleCountsIfNecessary(ctx context.Context, } type memoUserStats struct { - active models.UserStats - dailyActive models.UserStats - total models.UserStats + active stats.UserStats + dailyActive stats.UserStats + total stats.UserStats memoized time.Time } @@ -338,7 +338,7 @@ GROUP BY active, daily_active, role;` }) } -func addToStats(base models.UserStats, role org.RoleType, count int64) models.UserStats { +func addToStats(base stats.UserStats, role org.RoleType, count int64) stats.UserStats { base.Users += count switch role { diff --git a/pkg/services/stats/statsimpl/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go index 938bf16992d..b116ecc107e 100644 --- a/pkg/services/stats/statsimpl/stats_test.go +++ b/pkg/services/stats/statsimpl/stats_test.go @@ -8,11 +8,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" ) @@ -26,7 +26,7 @@ func TestIntegrationStatsDataAccess(t *testing.T) { populateDB(t, db) t.Run("Get system stats should not results in error", func(t *testing.T) { - query := models.GetSystemStatsQuery{} + query := stats.GetSystemStatsQuery{} err := statsService.GetSystemStats(context.Background(), &query) require.NoError(t, err) assert.Equal(t, int64(3), query.Result.Users) @@ -39,31 +39,31 @@ func TestIntegrationStatsDataAccess(t *testing.T) { }) t.Run("Get system user count stats should not results in error", func(t *testing.T) { - query := models.GetSystemUserCountStatsQuery{} + query := stats.GetSystemUserCountStatsQuery{} err := statsService.GetSystemUserCountStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get datasource stats should not results in error", func(t *testing.T) { - query := models.GetDataSourceStatsQuery{} + query := stats.GetDataSourceStatsQuery{} err := statsService.GetDataSourceStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get datasource access stats should not results in error", func(t *testing.T) { - query := models.GetDataSourceAccessStatsQuery{} + query := stats.GetDataSourceAccessStatsQuery{} err := statsService.GetDataSourceAccessStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get alert notifier stats should not results in error", func(t *testing.T) { - query := models.GetAlertNotifierUsageStatsQuery{} + query := stats.GetAlertNotifierUsageStatsQuery{} err := statsService.GetAlertNotifiersUsageStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get admin stats should not result in error", func(t *testing.T) { - query := models.GetAdminStatsQuery{} + query := stats.GetAdminStatsQuery{} err := statsService.GetAdminStats(context.Background(), &query) assert.NoError(t, err) }) @@ -123,7 +123,7 @@ func TestIntegration_GetAdminStats(t *testing.T) { db := sqlstore.InitTestDB(t) statsService := ProvideService(db) - query := models.GetAdminStatsQuery{} + query := stats.GetAdminStatsQuery{} err := statsService.GetAdminStats(context.Background(), &query) require.NoError(t, err) } diff --git a/pkg/services/stats/statstest/stats.go b/pkg/services/stats/statstest/stats.go index 77822bbfffb..c137aee4044 100644 --- a/pkg/services/stats/statstest/stats.go +++ b/pkg/services/stats/statstest/stats.go @@ -3,14 +3,14 @@ package statstest import ( "context" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/stats" ) type FakeService struct { - ExpectedSystemStats *models.SystemStats - ExpectedDataSourceStats []*models.DataSourceStats - ExpectedDataSourcesAccessStats []*models.DataSourceAccessStats - ExpectedNotifierUsageStats []*models.NotifierUsageStats + ExpectedSystemStats *stats.SystemStats + ExpectedDataSourceStats []*stats.DataSourceStats + ExpectedDataSourcesAccessStats []*stats.DataSourceAccessStats + ExpectedNotifierUsageStats []*stats.NotifierUsageStats ExpectedError error } @@ -19,30 +19,30 @@ func NewFakeService() *FakeService { return &FakeService{} } -func (s *FakeService) GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error { +func (s *FakeService) GetAdminStats(ctx context.Context, query *stats.GetAdminStatsQuery) error { return s.ExpectedError } -func (s *FakeService) GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error { +func (s *FakeService) GetAlertNotifiersUsageStats(ctx context.Context, query *stats.GetAlertNotifierUsageStatsQuery) error { query.Result = s.ExpectedNotifierUsageStats return s.ExpectedError } -func (s *FakeService) GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error { +func (s *FakeService) GetDataSourceStats(ctx context.Context, query *stats.GetDataSourceStatsQuery) error { query.Result = s.ExpectedDataSourceStats return s.ExpectedError } -func (s *FakeService) GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error { +func (s *FakeService) GetDataSourceAccessStats(ctx context.Context, query *stats.GetDataSourceAccessStatsQuery) error { query.Result = s.ExpectedDataSourcesAccessStats return s.ExpectedError } -func (s *FakeService) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error { +func (s *FakeService) GetSystemStats(ctx context.Context, query *stats.GetSystemStatsQuery) error { query.Result = s.ExpectedSystemStats return s.ExpectedError } -func (s *FakeService) GetSystemUserCountStats(ctx context.Context, query *models.GetSystemUserCountStatsQuery) error { +func (s *FakeService) GetSystemUserCountStats(ctx context.Context, query *stats.GetSystemUserCountStatsQuery) error { return s.ExpectedError } diff --git a/pkg/services/supportbundles/interface.go b/pkg/services/supportbundles/interface.go index 3a46ab3e1ea..66ed4a1e867 100644 --- a/pkg/services/supportbundles/interface.go +++ b/pkg/services/supportbundles/interface.go @@ -23,10 +23,10 @@ func (s State) String() string { type Bundle struct { UID string `json:"uid"` State State `json:"state"` - FilePath string `json:"filePath"` Creator string `json:"creator"` CreatedAt int64 `json:"createdAt"` ExpiresAt int64 `json:"expiresAt"` + TarBytes []byte `json:"tarBytes,omitempty"` } type CollectorFunc func(context.Context) (*SupportItem, error) diff --git a/pkg/services/supportbundles/supportbundlesimpl/api.go b/pkg/services/supportbundles/supportbundlesimpl/api.go index 9c48dde29d0..aaeab726e3a 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/api.go +++ b/pkg/services/supportbundles/supportbundlesimpl/api.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -72,32 +71,20 @@ func (s *Service) handleCreate(ctx *models.ReqContext) response.Response { return response.JSON(http.StatusCreated, data) } -func (s *Service) handleDownload(ctx *models.ReqContext) { +func (s *Service) handleDownload(ctx *models.ReqContext) response.Response { uid := web.Params(ctx.Req)[":uid"] bundle, err := s.get(ctx.Req.Context(), uid) if err != nil { - ctx.Redirect("/admin/support-bundles") - return + return response.Redirect("/admin/support-bundles") } if bundle.State != supportbundles.StateComplete { - ctx.Redirect("/admin/support-bundles") - return - } - - if bundle.FilePath == "" { - ctx.Redirect("/admin/support-bundles") - return - } - - if _, err := os.Stat(bundle.FilePath); err != nil { - ctx.Redirect("/admin/support-bundles") - return + return response.Redirect("/admin/support-bundles") } ctx.Resp.Header().Set("Content-Type", "application/tar+gzip") - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%d.tar.gz", bundle.CreatedAt)) - http.ServeFile(ctx.Resp, ctx.Req, bundle.FilePath) + ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.tar.gz", uid)) + return response.CreateNormalResponse(ctx.Resp.Header(), bundle.TarBytes, http.StatusOK) } func (s *Service) handleRemove(ctx *models.ReqContext) response.Response { diff --git a/pkg/services/supportbundles/supportbundlesimpl/service.go b/pkg/services/supportbundles/supportbundlesimpl/service.go index b9264fdda33..599e82b492c 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service.go @@ -3,8 +3,6 @@ package supportbundlesimpl import ( "context" "fmt" - "os" - "path/filepath" "time" "github.com/grafana/grafana/pkg/api/routing" @@ -151,12 +149,6 @@ func (s *Service) remove(ctx context.Context, uid string) error { return fmt.Errorf("could not remove a support bundle with uid %s as it is still being created", uid) } - if bundle.FilePath != "" { - if err := os.RemoveAll(filepath.Dir(bundle.FilePath)); err != nil { - return fmt.Errorf("could not remove directory for support bundle %s: %w", uid, err) - } - } - // Remove the KV store entry return s.store.Remove(ctx, uid) } diff --git a/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go b/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go index 2af15e44fec..6a798a0c051 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go @@ -6,11 +6,10 @@ import ( "compress/gzip" "context" "errors" - "fmt" "io" - "os" "path/filepath" "runtime/debug" + "time" "github.com/grafana/grafana/pkg/services/supportbundles" ) @@ -18,8 +17,8 @@ import ( var ErrCollectorPanicked = errors.New("collector panicked") type bundleResult struct { - path string - err error + tarBytes []byte + err error } func (s *Service) startBundleWork(ctx context.Context, collectors []string, uid string) { @@ -33,47 +32,43 @@ func (s *Service) startBundleWork(ctx context.Context, collectors []string, uid } }() - sbFilePath, err := s.bundle(ctx, collectors, uid) + bundleBytes, err := s.bundle(ctx, collectors, uid) if err != nil { result <- bundleResult{err: err} } - result <- bundleResult{ - path: sbFilePath, - } + result <- bundleResult{tarBytes: bundleBytes} close(result) }() select { case <-ctx.Done(): s.log.Warn("Context cancelled while collecting support bundle") - if err := s.store.Update(ctx, uid, supportbundles.StateTimeout, ""); err != nil { + if err := s.store.Update(ctx, uid, supportbundles.StateTimeout, nil); err != nil { s.log.Error("failed to update bundle after timeout") } return case r := <-result: if r.err != nil { - if err := s.store.Update(ctx, uid, supportbundles.StateError, ""); err != nil { + s.log.Error("failed to make bundle", "error", r.err, "uid", uid) + if err := s.store.Update(ctx, uid, supportbundles.StateError, nil); err != nil { s.log.Error("failed to update bundle after error") } return } - if err := s.store.Update(ctx, uid, supportbundles.StateComplete, r.path); err != nil { + if err := s.store.Update(ctx, uid, supportbundles.StateComplete, r.tarBytes); err != nil { s.log.Error("failed to update bundle after completion") } return } } -func (s *Service) bundle(ctx context.Context, collectors []string, uid string) (string, error) { +func (s *Service) bundle(ctx context.Context, collectors []string, uid string) ([]byte, error) { lookup := make(map[string]bool, len(collectors)) for _, c := range collectors { lookup[c] = true } - sbDir, err := os.MkdirTemp("", "") - if err != nil { - return "", err - } + files := map[string][]byte{} for _, collector := range s.collectors { if !lookup[collector.UID] && !collector.IncludedByDefault { @@ -86,70 +81,42 @@ func (s *Service) bundle(ctx context.Context, collectors []string, uid string) ( // write item to file if item != nil { - if err := os.WriteFile(filepath.Join(sbDir, item.Filename), item.FileBytes, 0600); err != nil { - s.log.Warn("Failed to collect support bundle item", "error", err) - } + files[item.Filename] = item.FileBytes } } // create tar.gz file var buf bytes.Buffer - errCompress := compress(sbDir, &buf) + errCompress := compress(files, &buf) if errCompress != nil { - return "", errCompress + return nil, errCompress } - finalFilePath := filepath.Join(sbDir, fmt.Sprintf("%s.tar.gz", uid)) - - // Ignore gosec G304 as this function is only used internally. - //nolint:gosec - fileToWrite, err := os.OpenFile(finalFilePath, os.O_CREATE|os.O_RDWR, 0600) - if err != nil { - return "", err - } - if _, err := io.Copy(fileToWrite, &buf); err != nil { - return "", err - } - - return finalFilePath, nil + return buf.Bytes(), nil } -func compress(src string, buf io.Writer) error { +func compress(files map[string][]byte, buf io.Writer) error { // tar > gzip > buf zr := gzip.NewWriter(buf) tw := tar.NewWriter(zr) - // walk through every file in the folder - err := filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { - // if not a dir, write file content - if !fi.IsDir() { - // generate tar header - header, err := tar.FileInfoHeader(fi, file) - if err != nil { - return err - } + for name, data := range files { + header := &tar.Header{ + Name: name, + ModTime: time.Now(), + Mode: int64(0o644), + Size: int64(len(data)), + } - header.Name = filepath.ToSlash("/bundle/" + header.Name) + header.Name = filepath.ToSlash("/bundle/" + header.Name) + // write header + if err := tw.WriteHeader(header); err != nil { + return err + } - // write header - if err := tw.WriteHeader(header); err != nil { - return err - } - - // Ignore gosec G304 as this function is only used internally. - //nolint:gosec - data, err := os.Open(file) - if err != nil { - return err - } - if _, err := io.Copy(tw, data); err != nil { - return err - } + if _, err := io.Copy(tw, bytes.NewReader(data)); err != nil { + return err } - return nil - }) - if err != nil { - return err } // produce tar diff --git a/pkg/services/supportbundles/supportbundlesimpl/store.go b/pkg/services/supportbundles/supportbundlesimpl/store.go index 01a3fe1becc..ca57a072ece 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/store.go +++ b/pkg/services/supportbundles/supportbundlesimpl/store.go @@ -31,7 +31,7 @@ type bundleStore interface { Get(ctx context.Context, uid string) (*supportbundles.Bundle, error) List() ([]supportbundles.Bundle, error) Remove(ctx context.Context, uid string) error - Update(ctx context.Context, uid string, state supportbundles.State, filePath string) error + Update(ctx context.Context, uid string, state supportbundles.State, tarBytes []byte) error } func (s *store) Create(ctx context.Context, usr *user.SignedInUser) (*supportbundles.Bundle, error) { @@ -54,14 +54,14 @@ func (s *store) Create(ctx context.Context, usr *user.SignedInUser) (*supportbun return &bundle, nil } -func (s *store) Update(ctx context.Context, uid string, state supportbundles.State, filePath string) error { +func (s *store) Update(ctx context.Context, uid string, state supportbundles.State, tarBytes []byte) error { bundle, err := s.Get(ctx, uid) if err != nil { return err } bundle.State = state - bundle.FilePath = filePath + bundle.TarBytes = tarBytes return s.set(ctx, bundle) } @@ -101,13 +101,15 @@ func (s *store) List() ([]supportbundles.Bundle, error) { return nil, err } - var res []supportbundles.Bundle + res := make([]supportbundles.Bundle, 0) for _, items := range data { for _, s := range items { var b supportbundles.Bundle if err := json.NewDecoder(strings.NewReader(s)).Decode(&b); err != nil { return nil, err } + + b.TarBytes = nil res = append(res, b) } } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 921fdadadaf..441adea7d0f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -303,6 +303,7 @@ type Cfg struct { SigV4AuthEnabled bool SigV4VerboseLogging bool AzureAuthEnabled bool + AzureSkipOrgRoleSync bool BasicAuthEnabled bool AdminUser string AdminPassword string @@ -422,6 +423,9 @@ type Cfg struct { ApplicationInsightsEndpointUrl string FeedbackLinksEnabled bool + // AzureAD + AzureADSkipOrgRoleSync bool + // LDAP LDAPEnabled bool LDAPSkipOrgRoleSync bool @@ -1354,6 +1358,10 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { return nil } +func readAuthAzureADSettings(iniFile *ini.File, cfg *Cfg) { + sec := iniFile.Section("auth.azuread") + cfg.AzureADSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) +} func readAuthGrafanaComSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.grafana_com") @@ -1406,6 +1414,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { // Azure Auth AzureAuthEnabled = auth.Key("azure_auth_enabled").MustBool(false) cfg.AzureAuthEnabled = AzureAuthEnabled + readAuthAzureADSettings(iniFile, cfg) // anonymous access AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false) diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index b9441ca5d5a..9663b44426d 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -101,8 +101,9 @@ type UnifiedAlertingReservedLabelSettings struct { } type UnifiedAlertingStateHistorySettings struct { - Enabled bool - Backend string + Enabled bool + Backend string + LokiRemoteURL string } // IsEnabled returns true if UnifiedAlertingSettings.Enabled is either nil or true. @@ -313,8 +314,9 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { stateHistory := iniFile.Section("unified_alerting.state_history") uaCfgStateHistory := UnifiedAlertingStateHistorySettings{ - Enabled: stateHistory.Key("enabled").MustBool(stateHistoryDefaultEnabled), - Backend: stateHistory.Key("backend").MustString("annotations"), + Enabled: stateHistory.Key("enabled").MustBool(stateHistoryDefaultEnabled), + Backend: stateHistory.Key("backend").MustString("annotations"), + LokiRemoteURL: stateHistory.Key("loki_remote_url").MustString(""), } uaCfg.StateHistory = uaCfgStateHistory diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 0a90e4ec626..7396e24dbaf 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -22,10 +22,10 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -740,7 +740,7 @@ func TestIntegrationNotificationChannels(t *testing.T) { env.NotificationService.EmailHandlerSync = mockEmail.sendEmailCommandHandlerSync // As we are using a NotificationService mock here, but the test expects real NotificationService - // we try to issue a real POST request here - env.NotificationService.WebhookHandler = func(_ context.Context, cmd *models.SendWebhookSync) error { + env.NotificationService.WebhookHandler = func(_ context.Context, cmd *notifications.SendWebhookSync) error { if res, err := http.Post(cmd.Url, "", strings.NewReader(cmd.Body)); err == nil { _ = res.Body.Close() } @@ -1165,10 +1165,10 @@ func (nc *mockNotificationChannel) Close() error { } type mockEmailHandler struct { - emails []*models.SendEmailCommandSync + emails []*notifications.SendEmailCommandSync } -func (e *mockEmailHandler) sendEmailCommandHandlerSync(_ context.Context, cmd *models.SendEmailCommandSync) error { +func (e *mockEmailHandler) sendEmailCommandHandlerSync(_ context.Context, cmd *notifications.SendEmailCommandSync) error { // We 0 out the start time since that is a variable that we cannot predict. alerts := cmd.Data["Alerts"].(channels.ExtendedAlerts) for i := range alerts { @@ -1185,7 +1185,7 @@ type mockEmailHandlerWithTimeout struct { timeout time.Duration } -func (e *mockEmailHandlerWithTimeout) sendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error { +func (e *mockEmailHandlerWithTimeout) sendEmailCommandHandlerSync(ctx context.Context, cmd *notifications.SendEmailCommandSync) error { select { case <-time.After(e.timeout): return e.mockEmailHandler.sendEmailCommandHandlerSync(ctx, cmd) @@ -2280,9 +2280,9 @@ var expAlertmanagerConfigFromAPI = ` } ` -var expEmailNotifications = []*models.SendEmailCommandSync{ +var expEmailNotifications = []*notifications.SendEmailCommandSync{ { - SendEmailCommand: models.SendEmailCommand{ + SendEmailCommand: notifications.SendEmailCommand{ To: []string{"test@email.com"}, SingleEmail: true, Template: "ng_alert_notification", diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 1cacce669ef..2b469833e5c 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -1,6 +1,7 @@ package util import ( + "encoding/json" "fmt" "math" "strings" @@ -33,6 +34,16 @@ func SplitString(str string) []string { return []string{} } + // JSON list syntax support + if strings.Index(strings.TrimSpace(str), "[") == 0 { + var res []string + err := json.Unmarshal([]byte(str), &res) + if err != nil { + return []string{} + } + return res + } + return strings.Fields(strings.ReplaceAll(str, ",", " ")) } diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go index efb3dbbb773..876cb705974 100644 --- a/pkg/util/strings_test.go +++ b/pkg/util/strings_test.go @@ -46,12 +46,17 @@ func TestStringsFallback3(t *testing.T) { func TestSplitString(t *testing.T) { tests := map[string][]string{ - "": {}, - "test": {"test"}, - "test1 test2 test3": {"test1", "test2", "test3"}, - "test1,test2,test3": {"test1", "test2", "test3"}, - "test1, test2, test3": {"test1", "test2", "test3"}, - "test1 , test2 test3": {"test1", "test2", "test3"}, + "": {}, + "test": {"test"}, + " test1 test2 test3": {"test1", "test2", "test3"}, + "test1,test2,test3": {"test1", "test2", "test3"}, + "test1, test2, test3": {"test1", "test2", "test3"}, + "test1 , test2 test3": {"test1", "test2", "test3"}, + "foo, bar baz": {"foo", "bar", "baz"}, + `["foo", "bar baz"]`: {"foo", "bar baz"}, + `["foo", "bar \"baz\""]`: {"foo", "bar \"baz\""}, + ` ["foo", "bar baz"]`: {"foo", "bar baz"}, + `[]`: {}, } for input, expected := range tests { assert.EqualValues(t, expected, SplitString(input)) diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index b4203991570..07e871a703e 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -17,6 +17,7 @@ import panelsReducers from 'app/features/panel/state/reducers'; import { reducer as pluginsReducer } from 'app/features/plugins/admin/state/reducer'; import userReducers from 'app/features/profile/state/reducers'; import serviceAccountsReducer from 'app/features/serviceaccounts/state/reducers'; +import supportBundlesReducer from 'app/features/support-bundles/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; import templatingReducers from 'app/features/variables/state/keyedVariablesReducer'; @@ -43,6 +44,7 @@ const rootReducers = { ...panelEditorReducers, ...panelsReducers, ...templatingReducers, + ...supportBundlesReducer, plugins: pluginsReducer, [alertingApi.reducerPath]: alertingApi.reducer, [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 155adb3abd5..71456012b4b 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -39,7 +39,7 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { error?: UserAdminError; } -const SyncedOAuthLabels: string[] = ['GitHub', 'GitLab', 'AzureAD', 'OAuth']; +const SyncedOAuthLabels: string[] = ['GitHub', 'GitLab', 'OAuth']; export class UserAdminPage extends PureComponent { async componentDidMount() { @@ -113,9 +113,8 @@ export class UserAdminPage extends PureComponent { const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML'); const isGoogleUser = user?.isExternal && user?.authLabels?.includes('Google'); const isAuthProxyUser = user?.isExternal && user?.authLabels?.includes('Auth Proxy'); + const isAzureADUser = user?.isExternal && user?.authLabels?.includes('AzureAD'); const isGrafanaComUser = user?.isExternal && user?.authLabels?.includes('grafana.com'); - // isGrafanaComUser true - // isOAuthUserWithSkippableSync true const isUserSynced = !config.auth.DisableSyncLock && ((user?.isExternal && @@ -125,13 +124,15 @@ export class UserAdminPage extends PureComponent { isOAuthUserWithSkippableSync || isSAMLUser || isLDAPUser || + isAzureADUser || isGrafanaComUser )) || (!config.auth.OAuthSkipOrgRoleUpdateSync && isOAuthUserWithSkippableSync) || (!config.auth.SAMLSkipOrgRoleSync && isSAMLUser) || (!config.auth.LDAPSkipOrgRoleSync && isLDAPUser) || - // both OAuthSkipOrgRoleUpdateSync and GrafanaComSkipOrgRoleSync needs to be false for a GrafanaComUser to be synced - (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GrafanaComSkipOrgRoleSync && isGrafanaComUser)); + // both OAuthSkipOrgRoleUpdateSync and specific provider settings needs to be false for a user to be synced + (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GrafanaComSkipOrgRoleSync && isGrafanaComUser) || + (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.AzureADSkipOrgRoleSync && isAzureADUser)); const pageNav: NavModelItem = { text: user?.login ?? '', diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 6daa4946ba1..c9efd56ddab 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -258,7 +258,8 @@ export function fetchRulerRulesIfNotFetchedYet(rulesSourceName: string): ThunkRe return (dispatch, getStore) => { const { rulerRules } = getStore().unifiedAlerting; const resp = rulerRules[rulesSourceName]; - if (!resp?.result && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) { + const emptyResults = isEmpty(resp?.result); + if (emptyResults && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) { dispatch(fetchRulerRulesAction({ rulesSourceName })); } }; diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx index 1d5507eb799..7ee00813631 100644 --- a/public/app/features/commandPalette/CommandPalette.tsx +++ b/public/app/features/commandPalette/CommandPalette.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; @@ -56,7 +56,10 @@ export const CommandPalette = () => {
- +
@@ -80,24 +83,33 @@ const RenderResults = ({ dashboardResults }: RenderResultsProps) => { () => dashboardResults.map((dashboard) => new ActionImpl(dashboard, { store: {} })), [dashboardResults] ); + const items = useMemo( () => (dashboardResultItems.length > 0 ? [...results, dashboardsSectionTitle, ...dashboardResultItems] : results), [results, dashboardsSectionTitle, dashboardResultItems] ); return ( -
- + { + // These items are rendered in a container, in a virtual list, so we cannot + // use :first/last-child selectors, so we must mimic them in JS + const isFirstItem = items[0] === item; + const isLastItem = items[items.length - 1] === item; + + const renderedItem = typeof item === 'string' ? ( -
{item}
+
+
{item}
+
) : ( - ) - } - /> -
+ ); + + return isLastItem ?
{renderedItem}
: renderedItem; + }} + /> ); }; @@ -134,15 +146,30 @@ const getSearchStyles = (theme: GrafanaTheme2) => ({ border: 'none', background: theme.colors.background.canvas, color: theme.colors.text.primary, - borderBottom: `1px solid ${theme.colors.border.weak}`, + borderBottom: `1px solid ${theme.colors.border.medium}`, }), + + // Virtual list measures margin incorrectly, so we need to split padding before/after border + // over and inner and outer element sectionHeader: css({ - padding: theme.spacing(1, 2), + paddingTop: theme.spacing(2), fontSize: theme.typography.h6.fontSize, fontWeight: theme.typography.body.fontWeight, color: theme.colors.text.secondary, }), - resultsContainer: css({ - padding: theme.spacing(2, 0), + sectionHeaderInner: css({ + padding: theme.spacing(1, 2), + borderTop: `1px solid ${theme.colors.border.medium}`, + }), + + // We don't need the header above the first section + sectionHeaderInnerFirst: css({ + borderTop: 'none', + paddingTop: 0, + }), + + // Last item gets extra padding so it's not clipped by the rounded corners on the container + lastItem: css({ + paddingBottom: theme.spacing(1), }), }); diff --git a/public/app/features/commandPalette/ResultItem.tsx b/public/app/features/commandPalette/ResultItem.tsx index 036d70dffb4..fc4858fa00e 100644 --- a/public/app/features/commandPalette/ResultItem.tsx +++ b/public/app/features/commandPalette/ResultItem.tsx @@ -59,15 +59,6 @@ export const ResultItem = React.forwardRef(
{action.subtitle && {action.subtitle}}
- {action.shortcut?.length ? ( -
- {action.shortcut.map((sc) => ( - - {sc} - - ))} -
- ) : null}
); } @@ -87,6 +78,7 @@ const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => { alightItems: 'center', justifyContent: 'space-between', cursor: 'pointer', + position: 'relative', '&:before': { display: isActive ? 'block' : 'none', content: '" "', @@ -116,8 +108,8 @@ const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => { fontSize: theme.typography.fontSize, }), breadcrumbAncestor: css({ - opacity: 0.5, marginRight: theme.spacing(1), + color: theme.colors.text.secondary, }), subtitleText: css({ fontSize: theme.typography.fontSize - 2, diff --git a/public/app/features/commandPalette/actions/dashboardActions.ts b/public/app/features/commandPalette/actions/dashboardActions.ts index e230a2730cd..8c8647293cd 100644 --- a/public/app/features/commandPalette/actions/dashboardActions.ts +++ b/public/app/features/commandPalette/actions/dashboardActions.ts @@ -36,7 +36,7 @@ export async function getRecentDashboardActions(): Promise { locationService.push(locationUtil.stripBaseFromUrl(url)); diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index 0eed8786fd2..835cec7be4f 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -55,7 +55,6 @@ export default (navBarTree: NavModelItem[]): CommandPaletteAction[] => { keywords: 'navigate', perform: () => locationService.push('?search=open'), section: t('command-palette.section.pages', 'Pages'), - shortcut: ['s', 'o'], priority: DEFAULT_PRIORITY, }, { @@ -63,7 +62,6 @@ export default (navBarTree: NavModelItem[]): CommandPaletteAction[] => { name: t('command-palette.action.change-theme', 'Change theme...'), keywords: 'interface color dark light', section: t('command-palette.section.preferences', 'Preferences'), - shortcut: ['c', 't'], priority: PREFERENCES_PRIORITY, }, { diff --git a/public/app/features/connections/tabs/ConnectData/Search/Search.tsx b/public/app/features/connections/tabs/ConnectData/Search/Search.tsx index 2c9726d8dd7..bbe14d8e107 100644 --- a/public/app/features/connections/tabs/ConnectData/Search/Search.tsx +++ b/public/app/features/connections/tabs/ConnectData/Search/Search.tsx @@ -1,13 +1,21 @@ import { css } from '@emotion/css'; import React, { FC } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { Icon, Input, useStyles2 } from '@grafana/ui'; -const getStyles = () => ({ +const getStyles = (theme: GrafanaTheme2) => ({ searchContainer: css` display: flex; margin: 16px 0; justify-content: space-between; + + position: sticky; + top: 0; + background-color: ${theme.colors.background.primary}; + z-index: 2; + padding: ${theme.spacing(2)}; + margin: 0 -${theme.spacing(2)}; `, }); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx index 18e13ca1ccc..a59680e1290 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx @@ -1,7 +1,9 @@ +import { css } from '@emotion/css'; import React, { FC } from 'react'; -import { QueryResultMetaNotice } from '@grafana/data'; -import { Icon, Tooltip } from '@grafana/ui'; +import { GrafanaTheme2, QueryResultMetaNotice } from '@grafana/data'; +import { Icon, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; +import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins'; interface Props { notice: QueryResultMetaNotice; @@ -9,20 +11,67 @@ interface Props { } export const PanelHeaderNotice: FC = ({ notice, onClick }) => { + const styles = useStyles2(getStyles); + const iconName = notice.severity === 'error' || notice.severity === 'warning' ? 'exclamation-triangle' : 'info-circle'; + if (notice.inspect && onClick) { + return ( + onClick(e, notice.inspect!)} + /> + ); + } + + if (notice.link) { + return ( + + + + ); + } + return ( - - {notice.inspect ? ( -
onClick(e, notice.inspect!)}> - -
- ) : ( - - - - )} + + + + ); }; + +const getStyles = (theme: GrafanaTheme2) => ({ + notice: css({ + border: 'none', + borderRadius: theme.shape.borderRadius(), + }), + iconTooltip: css({ + color: `${theme.colors.text.secondary}`, + backgroundColor: `${theme.colors.background.primary}`, + cursor: 'auto', + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + padding: `${theme.spacing(0, 1)}`, + height: ` ${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + color: `${theme.colors.text.primary}`, + background: `${theme.colors.background.secondary}`, + }, + }), +}); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx new file mode 100644 index 00000000000..91967b2ab52 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx @@ -0,0 +1,107 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { PanelData, GrafanaTheme2, PanelModel, LinkModel, AlertState, DataLink } from '@grafana/data'; +import { Icon, Tooltip, useStyles2 } from '@grafana/ui'; +import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins'; + +import { PanelLinks } from '../PanelLinks'; + +import { PanelHeaderNotices } from './PanelHeaderNotices'; + +export interface Props { + alertState?: string; + data: PanelData; + panelId: number; + onShowPanelLinks?: () => Array>; + panelLinks?: DataLink[]; +} + +export function PanelHeaderTitleItems(props: Props) { + const { alertState, data, panelId, onShowPanelLinks, panelLinks } = props; + const styles = useStyles2(getStyles); + + // panel health + const alertStateItem = ( + + + + + + ); + + const timeshift = ( + <> + + + + {data.request?.timeInfo} + + + + ); + + return ( + <> + {panelLinks && panelLinks.length > 0 && onShowPanelLinks && ( + + )} + + {} + {data.request && data.request.timeInfo && timeshift} + {alertState && alertStateItem} + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + item: css({ + label: 'panel-header-item', + backgroundColor: `${theme.colors.background.primary}`, + cursor: 'auto', + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + padding: `${theme.spacing(0, 1)}`, + height: `${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + background: `${theme.colors.background.secondary}`, + }, + }), + ok: css({ + color: theme.colors.success.text, + }), + pending: css({ + color: theme.colors.warning.text, + }), + alerting: css({ + color: theme.colors.error.text, + }), + timeshift: css({ + color: theme.colors.text.link, + + '&:hover': { + color: theme.colors.emphasize(theme.colors.text.link, 0.03), + }, + }), + }; +}; diff --git a/public/app/features/dashboard/dashgrid/PanelLinks.tsx b/public/app/features/dashboard/dashgrid/PanelLinks.tsx new file mode 100644 index 00000000000..706d29fa0ba --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelLinks.tsx @@ -0,0 +1,77 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { DataLink, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { Dropdown, Icon, Menu, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins'; + +interface Props { + panelLinks: DataLink[]; + onShowPanelLinks: () => LinkModel[]; +} + +export function PanelLinks({ panelLinks, onShowPanelLinks }: Props) { + const styles = useStyles2(getStyles); + + const getLinksContent = (): JSX.Element => { + const interpolatedLinks = onShowPanelLinks(); + return ( + + {interpolatedLinks?.map((link, idx) => { + return ; + })} + + ); + }; + + if (panelLinks.length === 1) { + const linkModel = onShowPanelLinks()[0]; + return ( + + + + ); + } else { + return ( + + + + ); + } +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + menuTrigger: css({ + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + cursor: 'context-menu', + }), + singleLink: css({ + color: theme.colors.text.secondary, + padding: `${theme.spacing(0, 1)}`, + height: ` ${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + color: `${theme.colors.text.primary}`, + background: `${theme.colors.background.secondary}`, + }, + }), + }; +}; diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index a6aadfd2a05..b7e2a8daeda 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -11,17 +11,19 @@ import { EventFilterOptions, FieldConfigSource, getDefaultTimeRange, + LinkModel, LoadingState, PanelData, PanelPlugin, PanelPluginMeta, PluginContextProvider, + renderMarkdown, TimeRange, toDataFrameDTO, toUtc, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { config, locationService, RefreshEvent } from '@grafana/runtime'; +import { getTemplateSrv, config, locationService, RefreshEvent } from '@grafana/runtime'; import { VizLegendOptions } from '@grafana/schema'; import { ErrorBoundary, @@ -35,6 +37,7 @@ import { PANEL_BORDER } from 'app/core/constants'; import { profiler } from 'app/core/profiler'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { InspectTab } from 'app/features/inspector/types'; +import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory'; import { RenderEvent } from 'app/types/events'; @@ -47,6 +50,7 @@ import { loadSnapshotData } from '../utils/loadSnapshotData'; import { PanelHeader } from './PanelHeader/PanelHeader'; import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper'; +import { PanelHeaderTitleItems } from './PanelHeader/PanelHeaderTitleItems'; import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory'; import { liveTimer } from './liveTimer'; @@ -567,10 +571,32 @@ export class PanelStateWrapper extends PureComponent { return !panel.hasTitle(); } - onOpenErrorInspect(e: React.SyntheticEvent, tab: string) { + onShowPanelDescription = () => { + const { panel } = this.props; + const descriptionMarkdown = getTemplateSrv().replace(panel.description, panel.scopedVars); + const interpolatedDescription = renderMarkdown(descriptionMarkdown); + return interpolatedDescription; + }; + + onShowPanelLinks = (): LinkModel[] => { + const { panel } = this.props; + const linkSupplier = getPanelLinksSupplier(panel); + if (linkSupplier) { + const panelLinks = linkSupplier && linkSupplier.getLinks(panel.replaceVariables); + return panelLinks; + } + return []; + }; + + onOpenInspector = (e: React.SyntheticEvent, tab: string) => { e.stopPropagation(); locationService.partial({ inspect: this.props.panel.id, inspectTab: tab }); - } + }; + + onOpenErrorInspect = (e: React.SyntheticEvent) => { + e.stopPropagation(); + locationService.partial({ inspect: this.props.panel.id, inspectTab: InspectTab.Error }); + }; render() { const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props; @@ -590,6 +616,17 @@ export class PanelStateWrapper extends PureComponent { const title = panel.getDisplayTitle(); const padding: PanelPadding = plugin.noPadding ? 'none' : 'md'; + const titleItems = [ + , + ]; + let menu; if (!dashboard.meta.publicDashboardAccessToken) { menu = ( @@ -610,14 +647,14 @@ export class PanelStateWrapper extends PureComponent { this.onOpenErrorInspect(e, InspectTab.Error), - }} + statusMessage={errorMessage} + statusMessageOnClick={this.onOpenErrorInspect} + description={!!panel.description ? this.onShowPanelDescription : undefined} + titleItems={titleItems} + menu={menu} + padding={padding} > {(innerWidth, innerHeight) => ( <> diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index fb6073d83e7..51deeecd53c 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -26,8 +26,8 @@ import { import { labelsToFieldsTransformer } from '@grafana/data/src/transformations/transformers/labelsToFields'; import { mergeTransformer } from '@grafana/data/src/transformations/transformers/merge'; import { getDataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; -import { BarGaugeDisplayMode, TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; -import { AxisPlacement, GraphFieldConfig, TableCellDisplayMode } from '@grafana/ui'; +import { AxisPlacement, GraphFieldConfig } from '@grafana/ui'; +import { migrateTableDisplayModeToCellOptions } from '@grafana/ui/src/components/Table/utils'; import { getAllOptionEditors, getAllStandardFieldConfigs } from 'app/core/components/OptionsUI/registry'; import { config } from 'app/core/config'; import { @@ -818,7 +818,7 @@ export class DashboardMigrator { // Update field configuration if (displayMode !== undefined) { // Migrate any options for the panel - panel.fieldConfig.defaults.custom.cellOptions = migrateTableCellConfig(displayMode); + panel.fieldConfig.defaults.custom.cellOptions = migrateTableDisplayModeToCellOptions(displayMode); // Delete the legacy field delete panel.fieldConfig.defaults.custom.displayMode; @@ -831,7 +831,8 @@ export class DashboardMigrator { if (panel.fieldConfig.overrides[i].properties[j].id === 'custom.displayMode') { panel.fieldConfig.overrides[i].properties[j].id = 'custom.cellOptions'; - panel.fieldConfig.overrides[i].properties[j].value = migrateTableCellConfig(overrideDisplayMode); + panel.fieldConfig.overrides[i].properties[j].value = + migrateTableDisplayModeToCellOptions(overrideDisplayMode); } } } @@ -1364,50 +1365,3 @@ function ensureXAxisVisibility(panel: PanelModel) { return panel; } - -/** - * Migrates table cell display mode to new object format. - * - * @param displayMode The display mode of the cell - * @returns TableCellOptions object in the correct format - * relative to the old display mode. - */ -function migrateTableCellConfig(displayMode: TableCellDisplayMode): TableCellOptions { - switch (displayMode) { - // In the case of the gauge we move to a different option - case 'basic': - case 'gradient-gauge': - case 'lcd-gauge': - let gaugeMode = BarGaugeDisplayMode.Basic; - - if (displayMode === 'gradient-gauge') { - gaugeMode = BarGaugeDisplayMode.Gradient; - } else if (displayMode === 'lcd-gauge') { - gaugeMode = BarGaugeDisplayMode.Lcd; - } - - return { - type: TableCellDisplayMode.Gauge, - mode: gaugeMode, - }; - // Also true in the case of the color background - case 'color-background': - case 'color-background-solid': - let mode = TableCellBackgroundDisplayMode.Basic; - - // Set the new mode field, somewhat confusingly the - // color-background mode is for gradient display - if (displayMode === 'color-background') { - mode = TableCellBackgroundDisplayMode.Gradient; - } - - return { - type: TableCellDisplayMode.ColorBackground, - mode: mode, - }; - default: - return { - type: displayMode, - }; - } -} diff --git a/public/app/features/explore/LogsMetaRow.test.tsx b/public/app/features/explore/LogsMetaRow.test.tsx new file mode 100644 index 00000000000..4096e2ab5ed --- /dev/null +++ b/public/app/features/explore/LogsMetaRow.test.tsx @@ -0,0 +1,186 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import saveAs from 'file-saver'; +import React, { ComponentProps } from 'react'; + +import { LogLevel, LogsDedupStrategy, MutableDataFrame } from '@grafana/data'; + +import { MAX_CHARACTERS } from '../logs/components/LogRowMessage'; +import { logRowsToReadableJson } from '../logs/utils'; + +import { LogsMetaRow } from './LogsMetaRow'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: () => null, +})); + +jest.mock('file-saver', () => jest.fn()); + +type LogsMetaRowProps = ComponentProps; +const defaultProps: LogsMetaRowProps = { + meta: [], + dedupStrategy: LogsDedupStrategy.none, + dedupCount: 0, + displayedFields: [], + hasUnescapedContent: false, + forceEscape: false, + logRows: [], + onEscapeNewlines: jest.fn(), + clearDetectedFields: jest.fn(), +}; + +const setup = (propOverrides?: object) => { + const props = { + ...defaultProps, + ...propOverrides, + }; + + return render(); +}; + +describe('LogsMetaRow', () => { + it('renders the dedupe number', async () => { + setup({ dedupStrategy: LogsDedupStrategy.numbers, dedupCount: 1234 }); + expect(await screen.findByText('1234')).toBeInTheDocument(); + }); + + it('renders a highlighting warning', async () => { + setup({ logRows: [{ entry: 'A'.repeat(MAX_CHARACTERS + 1) }] }); + expect( + await screen.findByText('Logs with more than 100,000 characters could not be parsed and highlighted') + ).toBeInTheDocument(); + }); + + it('renders the show original line button', () => { + setup({ displayedFields: ['test'] }); + expect( + screen.getByRole('button', { + name: 'Show original line', + }) + ).toBeInTheDocument(); + }); + + it('renders the displayedfield', async () => { + setup({ displayedFields: ['testField1234'] }); + expect(await screen.findByText('testField1234')).toBeInTheDocument(); + }); + + it('renders a button to clear displayedfields', () => { + const clearSpy = jest.fn(); + setup({ displayedFields: ['testField1234'], clearDetectedFields: clearSpy }); + fireEvent( + screen.getByRole('button', { + name: 'Show original line', + }), + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + expect(clearSpy).toBeCalled(); + }); + + it('renders a button to remove escaping', () => { + setup({ hasUnescapedContent: true, forceEscape: true }); + expect( + screen.getByRole('button', { + name: 'Remove escaping', + }) + ).toBeInTheDocument(); + }); + + it('renders a button to remove escaping', () => { + setup({ hasUnescapedContent: true, forceEscape: false }); + expect( + screen.getByRole('button', { + name: 'Escape newlines', + }) + ).toBeInTheDocument(); + }); + + it('renders a button to remove escaping', () => { + const escapeSpy = jest.fn(); + setup({ hasUnescapedContent: true, forceEscape: false, onEscapeNewlines: escapeSpy }); + fireEvent( + screen.getByRole('button', { + name: 'Escape newlines', + }), + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + expect(escapeSpy).toBeCalled(); + }); + + it('renders a button to show the download menu', () => { + setup(); + expect(screen.getByText('Download').closest('button')).toBeInTheDocument(); + }); + + it('renders a button to show the download menu', async () => { + setup(); + + expect(screen.queryAllByText('txt')).toHaveLength(0); + await userEvent.click(screen.getByText('Download').closest('button')!); + expect( + screen.getByRole('menuitem', { + name: 'txt', + }) + ).toBeInTheDocument(); + }); + + it('renders a button to download txt', async () => { + setup(); + + await userEvent.click(screen.getByText('Download').closest('button')!); + + await userEvent.click( + screen.getByRole('menuitem', { + name: 'txt', + }) + ); + + expect(saveAs).toBeCalled(); + }); + + it('renders a button to download json', async () => { + const rows = [ + { + rowIndex: 1, + entryFieldIndex: 0, + dataFrame: new MutableDataFrame(), + entry: 'test entry', + hasAnsi: false, + hasUnescapedContent: false, + labels: { + foo: 'bar', + }, + logLevel: LogLevel.info, + raw: '', + timeEpochMs: 10, + timeEpochNs: '123456789', + timeFromNow: '', + timeLocal: '', + timeUtc: '', + uid: '2', + }, + ]; + setup({ logRows: rows }); + + await userEvent.click(screen.getByText('Download').closest('button')!); + + await userEvent.click( + screen.getByRole('menuitem', { + name: 'json', + }) + ); + + expect(saveAs).toBeCalled(); + const blob = (saveAs as unknown as jest.Mock).mock.lastCall[0]; + expect(blob.type).toBe('application/json;charset=utf-8'); + const text = await blob.text(); + expect(text).toBe(JSON.stringify(logRowsToReadableJson(rows))); + }); +}); diff --git a/public/app/features/explore/LogsMetaRow.tsx b/public/app/features/explore/LogsMetaRow.tsx index 88642e197a8..402ed08aefb 100644 --- a/public/app/features/explore/LogsMetaRow.tsx +++ b/public/app/features/explore/LogsMetaRow.tsx @@ -1,13 +1,15 @@ import { css } from '@emotion/css'; +import saveAs from 'file-saver'; import React from 'react'; -import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp } from '@grafana/data'; +import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, dateTimeFormat } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { Button, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; +import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; import { downloadLogsModelAsTxt } from '../inspector/utils/download'; import { LogLabels } from '../logs/components/LogLabels'; import { MAX_CHARACTERS } from '../logs/components/LogRowMessage'; +import { logRowsToReadableJson } from '../logs/utils'; import { MetaInfoText, MetaItemProps } from './MetaInfoText'; @@ -31,6 +33,11 @@ export type Props = { clearDetectedFields: () => void; }; +enum DownloadFormat { + Text = 'text', + Json = 'json', +} + export const LogsMetaRow = React.memo( ({ meta, @@ -45,13 +52,27 @@ export const LogsMetaRow = React.memo( }: Props) => { const style = useStyles2(getStyles); - const downloadLogs = () => { + const downloadLogs = (format: DownloadFormat) => { reportInteraction('grafana_logs_download_logs_clicked', { app: CoreApp.Explore, - format: 'logs', + format, area: 'logs-meta-row', }); - downloadLogsModelAsTxt({ meta, rows: logRows }, 'Explore'); + + switch (format) { + case DownloadFormat.Text: + downloadLogsModelAsTxt({ meta, rows: logRows }, 'Explore'); + break; + case DownloadFormat.Json: + const jsonLogs = logRowsToReadableJson(logRows); + const blob = new Blob([JSON.stringify(jsonLogs)], { + type: 'application/json;charset=utf-8', + }); + + const fileName = `Explore-logs-${dateTimeFormat(new Date())}.json`; + saveAs(blob, fileName); + break; + } }; const logsMetaItem: Array = [...meta]; @@ -107,6 +128,12 @@ export const LogsMetaRow = React.memo( ), }); } + const downloadMenu = ( + + downloadLogs(DownloadFormat.Text)} /> + downloadLogs(DownloadFormat.Json)} /> + + ); return ( <> {logsMetaItem && ( @@ -119,9 +146,11 @@ export const LogsMetaRow = React.memo( }; })} /> - - Download logs - + + + Download + +
)} diff --git a/public/app/features/explore/NodeGraphContainer.tsx b/public/app/features/explore/NodeGraphContainer.tsx index 0a0821bfd60..b15870dd27f 100644 --- a/public/app/features/explore/NodeGraphContainer.tsx +++ b/public/app/features/explore/NodeGraphContainer.tsx @@ -59,7 +59,7 @@ export function UnconnectedNodeGraphContainer(props: Props) { toggleOpen(); reportInteraction('grafana_traces_node_graph_panel_clicked', { datasourceType: datasourceType, - expanded: !open, + isExpanded: !open, }); }; diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 6bdadacea77..1d908a0ed64 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -29,13 +29,14 @@ export function TraceViewContainer(props: Props) { const datasource = useSelector( (state: StoreState) => state.explore[props.exploreId!]?.datasourceInstance ?? undefined ); + const datasourceType = datasource ? datasource?.type : 'unknown'; if (!traceProp) { return null; } return ( - <> + - - - - - + + ); } diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index 23c2f7be0b3..1c60871ed87 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -133,7 +133,7 @@ describe('getFieldLinksForExplore', () => { expect(links).toHaveLength(0); }); - it('returns internal links when target contains defined template variables', () => { + it('returns internal links when target contains __data template variables', () => { const { field, range, dataFrame } = setup({ title: '', url: '', @@ -145,6 +145,100 @@ describe('getFieldLinksForExplore', () => { }); const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}' + )}` + ); + }); + + it('returns internal links when target contains targetField template variable', () => { + const { field, range, dataFrame } = setup({ + title: '', + url: '', + internal: { + query: { query: 'query_1-${__targetField}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}' + )}` + ); + }); + + it('returns internal links when target contains field name template variable', () => { + // field cannot be hyphenated, change field name to non-hyphenated + const noHyphenLink = { + title: '', + url: '', + internal: { + query: { query: 'query_1-${fluxDimensions}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }; + const { field, range, dataFrame } = setup(noHyphenLink, true, { + name: 'fluxDimensions', + type: FieldType.string, + values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]), + config: { + links: [noHyphenLink], + }, + }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}' + )}` + ); + }); + + it('returns internal links when target contains other field name template variables', () => { + // field cannot be hyphenated, change field name to non-hyphenated + const noHyphenLink = { + title: '', + url: '', + internal: { + query: { query: 'query_1-${fluxDimensions}-${fluxDimension2}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }; + const { field, range, dataFrame } = setup( + noHyphenLink, + true, + { + name: 'fluxDimensions', + type: FieldType.string, + values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]), + config: { + links: [noHyphenLink], + }, + }, + [ + { + name: 'fluxDimension2', + type: FieldType.string, + values: new ArrayVector(['foo2', ROW_WITH_NULL_VALUE.value]), + config: { + links: [noHyphenLink], + }, + }, + ] + ); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo-foo2"}],"panelsState":{}}' + )}` + ); }); it('returns no internal links when target contains empty template variables', () => { @@ -165,7 +259,12 @@ describe('getFieldLinksForExplore', () => { const ROW_WITH_TEXT_VALUE = { value: 'foo', index: 0 }; const ROW_WITH_NULL_VALUE = { value: null, index: 1 }; -function setup(link: DataLink, hasAccess = true) { +function setup( + link: DataLink, + hasAccess = true, + fieldOverride?: Field, + dataFrameOtherFieldOverride?: Field[] +) { setLinkSrv({ getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel { return { @@ -196,8 +295,14 @@ function setup(link: DataLink, hasAccess = true) { }, }; + let fieldsArr = [fieldOverride || field]; + + if (dataFrameOtherFieldOverride) { + fieldsArr = [...fieldsArr, ...dataFrameOtherFieldOverride]; + } + const dataFrame: DataFrame = toDataFrame({ - fields: [field], + fields: fieldsArr, }); const range: TimeRange = { @@ -209,5 +314,5 @@ function setup(link: DataLink, hasAccess = true) { }, }; - return { range, field, dataFrame }; + return { range, field: fieldOverride || field, dataFrame }; } diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts index ad0f27341e0..a8e507a0b59 100644 --- a/public/app/features/explore/utils/links.ts +++ b/public/app/features/explore/utils/links.ts @@ -11,6 +11,7 @@ import { getFieldDisplayValuesProxy, SplitOpen, DataLink, + DisplayValue, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; @@ -72,19 +73,36 @@ export const getFieldLinksForExplore = (options: { text: 'Raw value', }; + let fieldDisplayValuesProxy: Record | undefined = undefined; + // If we have a dataFrame we can allow referencing other columns and their values in the interpolation. if (dataFrame) { + fieldDisplayValuesProxy = getFieldDisplayValuesProxy({ + frame: dataFrame, + rowIndex, + }); + scopedVars['__data'] = { value: { name: dataFrame.name, refId: dataFrame.refId, - fields: getFieldDisplayValuesProxy({ - frame: dataFrame, - rowIndex, - }), + fields: fieldDisplayValuesProxy, }, text: 'Data', }; + + dataFrame.fields.forEach((f) => { + if (fieldDisplayValuesProxy && fieldDisplayValuesProxy[f.name]) { + scopedVars[f.name] = { + value: fieldDisplayValuesProxy[f.name], + }; + } + }); + + // add this for convenience + scopedVars['__targetField'] = { + value: fieldDisplayValuesProxy[field.name], + }; } if (field.config.links) { diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index d32db19ff51..22359e26080 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -27,7 +27,7 @@ export const getAllFields = memoizeOne( /** * creates fields from the dataframe-fields, adding data-links, when field.config.links exists */ -const getDataframeFields = memoizeOne( +export const getDataframeFields = memoizeOne( ( row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> diff --git a/public/app/features/logs/utils.test.ts b/public/app/features/logs/utils.test.ts index b6e3e9f290f..fed857ac22d 100644 --- a/public/app/features/logs/utils.test.ts +++ b/public/app/features/logs/utils.test.ts @@ -7,6 +7,7 @@ import { getLogLevelFromKey, sortLogsResult, checkLogsError, + logRowsToReadableJson, } from './utils'; describe('getLoglevel()', () => { @@ -205,3 +206,58 @@ describe('checkLogsError()', () => { expect(checkLogsError(log)).toStrictEqual({ hasError: true, errorMessage: 'Error Message' }); }); }); + +describe('logRowsToReadableJson', () => { + const testRow: LogRowModel = { + rowIndex: 1, + entryFieldIndex: 0, + dataFrame: new MutableDataFrame(), + entry: 'test entry', + hasAnsi: false, + hasUnescapedContent: false, + labels: { + foo: 'bar', + }, + logLevel: LogLevel.info, + raw: '', + timeEpochMs: 10, + timeEpochNs: '123456789', + timeFromNow: '', + timeLocal: '', + timeUtc: '', + uid: '2', + }; + const testDf = new MutableDataFrame(); + testDf.addField({ name: 'foo2', values: ['bar2'] }); + const testRow2: LogRowModel = { + rowIndex: 0, + entryFieldIndex: -1, + dataFrame: testDf, + entry: 'test entry', + hasAnsi: false, + hasUnescapedContent: false, + labels: { + foo: 'bar', + }, + logLevel: LogLevel.info, + raw: '', + timeEpochMs: 10, + timeEpochNs: '123456789', + timeFromNow: '', + timeLocal: '', + timeUtc: '', + uid: '2', + }; + + it('should format a single row', () => { + const result = logRowsToReadableJson([testRow]); + + expect(result).toEqual([{ line: 'test entry', timestamp: '123456789', fields: { foo: 'bar' } }]); + }); + + it('should format a df field row', () => { + const result = logRowsToReadableJson([testRow2]); + + expect(result).toEqual([{ line: 'test entry', timestamp: '123456789', fields: { foo: 'bar', foo2: 'bar2' } }]); + }); +}); diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 298003d419c..66319a9931e 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -2,6 +2,8 @@ import { countBy, chain } from 'lodash'; import { LogLevel, LogRowModel, LogLabelStatsModel, LogsModel, LogsSortOrder } from '@grafana/data'; +import { getDataframeFields } from './components/logParser'; + /** * Returns the log level of a log line. * Parse the line for level words. If no level is found, it returns `LogLevel.unknown`. @@ -129,3 +131,21 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM export const escapeUnescapedString = (string: string) => string.replace(/\\r\\n|\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); + +export function logRowsToReadableJson(logs: LogRowModel[]) { + return logs.map((log) => { + const fields = getDataframeFields(log).reduce>((acc, field) => { + acc[field.key] = field.value; + return acc; + }, {}); + + return { + line: log.entry, + timestamp: log.timeEpochNs, + fields: { + ...fields, + ...log.labels, + }, + }; + }); +} diff --git a/public/app/features/scenes/SceneEmbeddedPage.tsx b/public/app/features/scenes/SceneEmbeddedPage.tsx index 5728ffa7c0c..7b7b2c06500 100644 --- a/public/app/features/scenes/SceneEmbeddedPage.tsx +++ b/public/app/features/scenes/SceneEmbeddedPage.tsx @@ -10,15 +10,16 @@ import { getSceneByTitle } from './scenes'; export interface Props extends GrafanaRouteComponentProps<{ name: string }> {} export const SceneEmbeddedPage = (props: Props) => { - const scene = getSceneByTitle(props.match.params.name, false); + const scene = getSceneByTitle(props.match.params.name); if (!scene) { return

Scene not found

; } const pageNav: NavModelItem = { - text: scene.state.title, + text: 'Embedded Scene', }; + return ( diff --git a/public/app/features/scenes/SceneListPage.tsx b/public/app/features/scenes/SceneListPage.tsx index 6518394f78d..33078f91313 100644 --- a/public/app/features/scenes/SceneListPage.tsx +++ b/public/app/features/scenes/SceneListPage.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useAsync } from 'react-use'; import { Stack } from '@grafana/experimental'; -import { Card, LinkButton } from '@grafana/ui'; +import { Card } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; // Types @@ -26,16 +26,8 @@ export const SceneListPage = ({}: Props) => {
Test scenes
{scenes.map((scene) => ( - + {scene.title} - - - Open as standalone scene - - - Open as embedded scene - - ))} diff --git a/public/app/features/scenes/ScenePage.tsx b/public/app/features/scenes/ScenePage.tsx index 92587896aee..b755d11491b 100644 --- a/public/app/features/scenes/ScenePage.tsx +++ b/public/app/features/scenes/ScenePage.tsx @@ -1,5 +1,5 @@ // Libraries -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; @@ -8,12 +8,24 @@ import { getSceneByTitle } from './scenes'; export interface Props extends GrafanaRouteComponentProps<{ name: string }> {} export const ScenePage = (props: Props) => { - const scene = getSceneByTitle(props.match.params.name, true); + const scene = getSceneByTitle(props.match.params.name); + const [isInitialized, setInitialized] = useState(false); + + useEffect(() => { + if (scene && !isInitialized) { + scene.initUrlSync(); + setInitialized(true); + } + }, [isInitialized, scene]); if (!scene) { return

Scene not found

; } + if (!isInitialized) { + return null; + } + return ; }; diff --git a/public/app/features/scenes/components/Scene.test.tsx b/public/app/features/scenes/components/Scene.test.tsx deleted file mode 100644 index b6138fca279..00000000000 --- a/public/app/features/scenes/components/Scene.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { SceneFlexLayout } from '@grafana/scenes'; - -import { Scene } from './Scene'; - -describe('Scene', () => { - it('Simple scene', () => { - const scene = new Scene({ - title: 'Hello', - body: new SceneFlexLayout({ - children: [], - }), - }); - - expect(scene.state.title).toBe('Hello'); - }); -}); diff --git a/public/app/features/scenes/components/Scene.tsx b/public/app/features/scenes/components/Scene.tsx deleted file mode 100644 index 18857af0438..00000000000 --- a/public/app/features/scenes/components/Scene.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import { PageLayoutType } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { SceneObjectBase, SceneComponentProps, SceneState, UrlSyncManager } from '@grafana/scenes'; -import { PageToolbar, ToolbarButton } from '@grafana/ui'; -import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; -import { Page } from 'app/core/components/Page/Page'; - -export class Scene extends SceneObjectBase { - public static Component = SceneRenderer; - private urlSyncManager?: UrlSyncManager; - - public activate() { - super.activate(); - this.urlSyncManager = new UrlSyncManager(this); - this.urlSyncManager.initSync(); - } - - public deactivate() { - super.deactivate(); - this.urlSyncManager!.cleanUp(); - } -} - -function SceneRenderer({ model }: SceneComponentProps) { - const { title, body, actions = [], isEditing, $editor, subMenu } = model.useState(); - - const toolbarActions = (actions ?? []).map((action) => ); - - if ($editor) { - toolbarActions.push( - model.setState({ isEditing: !model.state.isEditing })} - /> - ); - } - - const pageToolbar = config.featureToggles.topnav ? ( - - ) : ( - {toolbarActions} - ); - - return ( - -
- {subMenu && } -
- - {$editor && <$editor.Component model={$editor} isEditing={isEditing} />} -
-
-
- ); -} diff --git a/public/app/features/scenes/dashboard/DashboardScene.tsx b/public/app/features/scenes/dashboard/DashboardScene.tsx index dc88bf95a05..5990086af42 100644 --- a/public/app/features/scenes/dashboard/DashboardScene.tsx +++ b/public/app/features/scenes/dashboard/DashboardScene.tsx @@ -1,25 +1,25 @@ +import { css } from '@emotion/css'; import React from 'react'; -import { PageLayoutType } from '@grafana/data'; +import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { UrlSyncManager, SceneObjectBase, SceneComponentProps, - SceneLayout, SceneObject, SceneObjectStatePlain, } from '@grafana/scenes'; -import { PageToolbar, ToolbarButton } from '@grafana/ui'; +import { PageToolbar, ToolbarButton, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { Page } from 'app/core/components/Page/Page'; interface DashboardSceneState extends SceneObjectStatePlain { title: string; - uid: string; - body: SceneLayout; + uid?: string; + body: SceneObject; actions?: SceneObject[]; - subMenu?: SceneObject; + controls?: SceneObject[]; } export class DashboardScene extends SceneObjectBase { @@ -48,7 +48,8 @@ export class DashboardScene extends SceneObjectBase { } function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { title, body, actions = [], uid, subMenu } = model.useState(); + const { title, body, actions = [], uid, controls } = model.useState(); + const styles = useStyles2(getStyles); const toolbarActions = (actions ?? []).map((action) => ); @@ -63,10 +64,31 @@ function DashboardSceneRenderer({ model }: SceneComponentProps) return ( - {subMenu && } -
+ {controls && ( +
+ {controls.map((control) => ( + + ))} +
+ )} +
); } + +function getStyles(theme: GrafanaTheme2) { + return { + body: css({ + flexGrow: 1, + display: 'flex', + gap: '8px', + }), + controls: css({ + display: 'flex', + gap: theme.spacing(1), + alignItems: 'center', + }), + }; +} diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.test.ts b/public/app/features/scenes/dashboard/DashboardsLoader.test.ts index 63e672ace35..8684e5d2b3a 100644 --- a/public/app/features/scenes/dashboard/DashboardsLoader.test.ts +++ b/public/app/features/scenes/dashboard/DashboardsLoader.test.ts @@ -135,7 +135,7 @@ describe('DashboardLoader', () => { expect(scene.state.uid).toBe('test-uid'); expect(scene.state?.$timeRange?.state.value.raw).toEqual(dash.time); expect(scene.state?.$variables?.state.variables).toHaveLength(1); - expect(scene.state.subMenu).toBeDefined(); + expect(scene.state.controls).toBeDefined(); }); }); @@ -162,9 +162,10 @@ describe('DashboardLoader', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel); + const body = scene.state.body as SceneGridLayout; - expect(scene.state.body.state.children).toHaveLength(1); - const rowScene = scene.state.body.state.children[0] as SceneGridRow; + expect(body.state.children).toHaveLength(1); + const rowScene = body.state.children[0] as SceneGridRow; expect(rowScene).toBeInstanceOf(SceneGridRow); expect(rowScene.state.title).toEqual(row.title); expect(rowScene.state.placement?.y).toEqual(row.gridPos!.y); @@ -226,16 +227,17 @@ describe('DashboardLoader', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel); + const body = scene.state.body as SceneGridLayout; - expect(scene.state.body.state.children).toHaveLength(3); - expect(scene.state.body).toBeInstanceOf(SceneGridLayout); + expect(body.state.children).toHaveLength(3); + expect(body).toBeInstanceOf(SceneGridLayout); // Panel out of row - expect(scene.state.body.state.children[0]).toBeInstanceOf(VizPanel); - const panelOutOfRowVizPanel = scene.state.body.state.children[0] as VizPanel; + expect(body.state.children[0]).toBeInstanceOf(VizPanel); + const panelOutOfRowVizPanel = body.state.children[0] as VizPanel; expect(panelOutOfRowVizPanel.state.title).toBe(panelOutOfRow.title); // Row with panel - expect(scene.state.body.state.children[1]).toBeInstanceOf(SceneGridRow); - const rowWithPanelsScene = scene.state.body.state.children[1] as SceneGridRow; + expect(body.state.children[1]).toBeInstanceOf(SceneGridRow); + const rowWithPanelsScene = body.state.children[1] as SceneGridRow; expect(rowWithPanelsScene.state.title).toBe(rowWithPanel.title); expect(rowWithPanelsScene.state.children).toHaveLength(1); // Panel within row @@ -243,8 +245,8 @@ describe('DashboardLoader', () => { const panelInRowVizPanel = rowWithPanelsScene.state.children[0] as VizPanel; expect(panelInRowVizPanel.state.title).toBe(panelInRow.title); // Empty row - expect(scene.state.body.state.children[2]).toBeInstanceOf(SceneGridRow); - const emptyRowScene = scene.state.body.state.children[2] as SceneGridRow; + expect(body.state.children[2]).toBeInstanceOf(SceneGridRow); + const emptyRowScene = body.state.children[2] as SceneGridRow; expect(emptyRowScene.state.title).toBe(emptyRow.title); expect(emptyRowScene.state.children).toHaveLength(0); }); diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.ts b/public/app/features/scenes/dashboard/DashboardsLoader.ts index d2bcd4de2a7..bd19241d56e 100644 --- a/public/app/features/scenes/dashboard/DashboardsLoader.ts +++ b/public/app/features/scenes/dashboard/DashboardsLoader.ts @@ -13,7 +13,6 @@ import { SceneTimeRange, SceneObject, SceneQueryRunner, - SceneSubMenu, SceneVariableSet, VariableValueSelectors, SceneVariable, @@ -150,7 +149,6 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneObjec } export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) { - let subMenu: SceneSubMenu | undefined = undefined; let variables: SceneVariableSet | undefined = undefined; if (oldModel.templating.list.length) { @@ -166,9 +164,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) // TODO: Remove filter // Added temporarily to allow skipping non-compatible variables .filter((v): v is SceneVariable => Boolean(v)); - subMenu = new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }); + variables = new SceneVariableSet({ variables: variableObjects, }); @@ -183,7 +179,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) $timeRange: new SceneTimeRange(oldModel.time), actions: [new SceneTimePicker({})], $variables: variables, - subMenu, + ...(variables && { + controls: [new VariableValueSelectors({})], + }), }); } diff --git a/public/app/features/scenes/scenes/demo.tsx b/public/app/features/scenes/scenes/demo.tsx index a8c3c861987..489e3088a23 100644 --- a/public/app/features/scenes/scenes/demo.tsx +++ b/public/app/features/scenes/scenes/demo.tsx @@ -2,21 +2,21 @@ import { SceneFlexLayout, SceneTimeRange, SceneTimePicker, - ScenePanelRepeater, + SceneByFrameRepeater, VizPanel, SceneCanvasText, SceneToolbarInput, - EmbeddedScene, + SceneDataNode, } from '@grafana/scenes'; import { panelBuilders } from '../builders/panelBuilders'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getFlexLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getFlexLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Flex layout test', body: new SceneFlexLayout({ direction: 'row', @@ -62,47 +62,53 @@ export function getFlexLayoutTest(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } -export function getScenePanelRepeaterTest(standalone: boolean): Scene | EmbeddedScene { +export function getScenePanelRepeaterTest(): DashboardScene { const queryRunner = getQueryRunnerWithRandomWalkQuery({ seriesCount: 2, alias: '__server_names', scenarioId: 'random_walk', }); - const state = { + return new DashboardScene({ title: 'Panel repeater test', - body: new ScenePanelRepeater({ - layout: new SceneFlexLayout({ + body: new SceneByFrameRepeater({ + body: new SceneFlexLayout({ direction: 'column', - children: [ - new SceneFlexLayout({ - direction: 'row', - placement: { minHeight: 200 }, - children: [ - new VizPanel({ - pluginId: 'timeseries', - title: 'Title', - options: { - legend: { displayMode: 'hidden' }, - }, - }), - new VizPanel({ - placement: { width: 300 }, - pluginId: 'stat', - fieldConfig: { defaults: { displayName: 'Last' }, overrides: [] }, - options: { - graphMode: 'none', - }, - }), - ], - }), - ], + children: [], }), + getLayoutChild: (data, frame, frameIndex) => { + return new SceneFlexLayout({ + key: `panel-${frameIndex}`, + $data: new SceneDataNode({ + data: { + ...data, + series: [frame], + }, + }), + direction: 'row', + placement: { minHeight: 200 }, + children: [ + new VizPanel({ + pluginId: 'timeseries', + title: 'Title', + options: { + legend: { displayMode: 'hidden' }, + }, + }), + new VizPanel({ + placement: { width: 300 }, + pluginId: 'stat', + fieldConfig: { defaults: { displayName: 'Last' }, overrides: [] }, + options: { + graphMode: 'none', + }, + }), + ], + }); + }, }), $editor: new SceneEditManager({}), $timeRange: new SceneTimeRange(), @@ -124,7 +130,5 @@ export function getScenePanelRepeaterTest(standalone: boolean): Scene | Embedded }), new SceneTimePicker({}), ], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/grid.tsx b/public/app/features/scenes/scenes/grid.tsx index 6cbfce18580..4b7e85e9aa3 100644 --- a/public/app/features/scenes/scenes/grid.tsx +++ b/public/app/features/scenes/scenes/grid.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneTimePicker, - SceneFlexLayout, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneTimePicker, SceneFlexLayout, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getGridLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Grid layout test', body: new SceneGridLayout({ children: [ @@ -58,7 +51,5 @@ export function getGridLayoutTest(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx index dc856427bb2..052f5ee9903 100644 --- a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx +++ b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx @@ -1,25 +1,18 @@ -import { - VizPanel, - SceneGridRow, - SceneTimePicker, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneGridRow, SceneTimePicker, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithMultipleTimeRanges(standalone: boolean): Scene | EmbeddedScene { +export function getGridWithMultipleTimeRanges(): DashboardScene { const globalTimeRange = new SceneTimeRange(); const row1TimeRange = new SceneTimeRange({ from: 'now-1y', to: 'now', }); - const state = { + return new DashboardScene({ title: 'Grid with rows and different queries and time ranges', body: new SceneGridLayout({ children: [ @@ -66,7 +59,5 @@ export function getGridWithMultipleTimeRanges(standalone: boolean): Scene | Embe $timeRange: globalTimeRange, $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridMultiple.tsx b/public/app/features/scenes/scenes/gridMultiple.tsx index bfd49bc6e00..76cb257b203 100644 --- a/public/app/features/scenes/scenes/gridMultiple.tsx +++ b/public/app/features/scenes/scenes/gridMultiple.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneTimePicker, - SceneFlexLayout, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneTimePicker, SceneFlexLayout, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getMultipleGridLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getMultipleGridLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Multiple grid layouts test', body: new SceneFlexLayout({ children: [ @@ -98,7 +91,5 @@ export function getMultipleGridLayoutTest(standalone: boolean): Scene | Embedded $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridWithMultipleData.tsx b/public/app/features/scenes/scenes/gridWithMultipleData.tsx index 683d118932b..051c539c61a 100644 --- a/public/app/features/scenes/scenes/gridWithMultipleData.tsx +++ b/public/app/features/scenes/scenes/gridWithMultipleData.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneGridRow, - SceneTimePicker, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneGridRow, SceneTimePicker, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithMultipleData(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getGridWithMultipleData(): DashboardScene { + return new DashboardScene({ title: 'Grid with rows and different queries', body: new SceneGridLayout({ children: [ @@ -93,7 +86,5 @@ export function getGridWithMultipleData(standalone: boolean): Scene | EmbeddedSc $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridWithRow.tsx b/public/app/features/scenes/scenes/gridWithRow.tsx index d3739040722..8f3c7f09500 100644 --- a/public/app/features/scenes/scenes/gridWithRow.tsx +++ b/public/app/features/scenes/scenes/gridWithRow.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneGridLayout, - SceneGridRow, - SceneTimePicker, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneGridLayout, SceneGridRow, SceneTimePicker, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithRowLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getGridWithRowLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Grid with row layout test', body: new SceneGridLayout({ children: [ @@ -76,7 +69,5 @@ export function getGridWithRowLayoutTest(standalone: boolean): Scene | EmbeddedS $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridWithRows.tsx b/public/app/features/scenes/scenes/gridWithRows.tsx index c3bde62bb1f..ad1c871d35d 100644 --- a/public/app/features/scenes/scenes/gridWithRows.tsx +++ b/public/app/features/scenes/scenes/gridWithRows.tsx @@ -7,12 +7,11 @@ import { SceneTimeRange, } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; -import { SceneEditManager } from '../editor/SceneEditManager'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithRowsTest(): Scene { +export function getGridWithRowsTest(): DashboardScene { const panel = new VizPanel({ pluginId: 'timeseries', title: 'Fill height', @@ -77,12 +76,12 @@ export function getGridWithRowsTest(): Scene { }), ], }); - const scene = new Scene({ + + const scene = new DashboardScene({ title: 'Grid rows test', body: new SceneGridLayout({ children: [cell1, cell2, row1, row2], }), - $editor: new SceneEditManager({}), $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], diff --git a/public/app/features/scenes/scenes/index.tsx b/public/app/features/scenes/scenes/index.tsx index bdb94daebe4..f398ff1a2ae 100644 --- a/public/app/features/scenes/scenes/index.tsx +++ b/public/app/features/scenes/scenes/index.tsx @@ -1,6 +1,4 @@ -import { EmbeddedScene, SceneObjectBase, SceneState } from '@grafana/scenes'; - -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getFlexLayoutTest, getScenePanelRepeaterTest } from './demo'; import { getGridLayoutTest } from './grid'; @@ -16,7 +14,7 @@ import { getVariablesDemo, getVariablesDemoWithAll } from './variablesDemo'; interface SceneDef { title: string; - getScene: (standalone: boolean) => Scene | EmbeddedScene; + getScene: () => DashboardScene; } export function getScenes(): SceneDef[] { return [ @@ -36,20 +34,18 @@ export function getScenes(): SceneDef[] { ]; } -const cache: Record }> = {}; +const cache: Record = {}; -export function getSceneByTitle(title: string, standalone = true) { +export function getSceneByTitle(title: string) { if (cache[title]) { - if (cache[title].standalone === standalone) { - return cache[title].scene; - } + return cache[title]; } const scene = getScenes().find((x) => x.title === title); if (scene) { - cache[title] = { scene: scene.getScene(standalone), standalone }; + cache[title] = scene.getScene(); } - return cache[title].scene; + return cache[title]; } diff --git a/public/app/features/scenes/scenes/nested.tsx b/public/app/features/scenes/scenes/nested.tsx index 9243a1c1abc..7ac0056e3e8 100644 --- a/public/app/features/scenes/scenes/nested.tsx +++ b/public/app/features/scenes/scenes/nested.tsx @@ -1,18 +1,11 @@ -import { - VizPanel, - NestedScene, - SceneTimePicker, - SceneFlexLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, NestedScene, SceneTimePicker, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getNestedScene(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getNestedScene(): DashboardScene { + return new DashboardScene({ title: 'Nested Scene demo', body: new SceneFlexLayout({ direction: 'column', @@ -28,9 +21,7 @@ export function getNestedScene(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } export function getInnerScene(title: string) { diff --git a/public/app/features/scenes/scenes/queryVariableDemo.tsx b/public/app/features/scenes/scenes/queryVariableDemo.tsx index 4eb211d8d59..69fd91ded75 100644 --- a/public/app/features/scenes/scenes/queryVariableDemo.tsx +++ b/public/app/features/scenes/scenes/queryVariableDemo.tsx @@ -1,7 +1,6 @@ import { VariableRefresh } from '@grafana/data'; import { SceneCanvasText, - SceneSubMenu, SceneTimePicker, SceneFlexLayout, SceneTimeRange, @@ -10,13 +9,12 @@ import { CustomVariable, DataSourceVariable, QueryVariable, - EmbeddedScene, } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; -export function getQueryVariableDemo(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getQueryVariableDemo(): DashboardScene { + return new DashboardScene({ title: 'Query variable', $variables: new SceneVariableSet({ variables: [ @@ -65,10 +63,6 @@ export function getQueryVariableDemo(standalone: boolean): Scene | EmbeddedScene }), $timeRange: new SceneTimeRange(), actions: [new SceneTimePicker({})], - subMenu: new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }), - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + controls: [new VariableValueSelectors({})], + }); } diff --git a/public/app/features/scenes/scenes/sceneWithRows.tsx b/public/app/features/scenes/scenes/sceneWithRows.tsx index c7d5d7f4f1f..51e4352129b 100644 --- a/public/app/features/scenes/scenes/sceneWithRows.tsx +++ b/public/app/features/scenes/scenes/sceneWithRows.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - NestedScene, - SceneTimePicker, - SceneFlexLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, NestedScene, SceneTimePicker, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getSceneWithRows(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getSceneWithRows(): DashboardScene { + return new DashboardScene({ title: 'Scene with rows', body: new SceneFlexLayout({ direction: 'column', @@ -60,7 +53,5 @@ export function getSceneWithRows(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/transformations.tsx b/public/app/features/scenes/scenes/transformations.tsx index 739f88c820f..d8c5a08f335 100644 --- a/public/app/features/scenes/scenes/transformations.tsx +++ b/public/app/features/scenes/scenes/transformations.tsx @@ -1,19 +1,11 @@ -import { - SceneTimePicker, - SceneFlexLayout, - VizPanel, - SceneDataTransformer, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { SceneTimePicker, SceneFlexLayout, VizPanel, SceneDataTransformer, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; -import { SceneEditManager } from '../editor/SceneEditManager'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getTransformationsDemo(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getTransformationsDemo(): DashboardScene { + return new DashboardScene({ title: 'Transformations demo', body: new SceneFlexLayout({ direction: 'row', @@ -63,11 +55,8 @@ export function getTransformationsDemo(standalone: boolean): Scene | EmbeddedSce }), ], }), - $editor: new SceneEditManager({}), $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/variablesDemo.tsx b/public/app/features/scenes/scenes/variablesDemo.tsx index 14547e4302d..aae86008f13 100644 --- a/public/app/features/scenes/scenes/variablesDemo.tsx +++ b/public/app/features/scenes/scenes/variablesDemo.tsx @@ -1,7 +1,6 @@ import { VizPanel, SceneCanvasText, - SceneSubMenu, SceneTimePicker, SceneFlexLayout, SceneTimeRange, @@ -10,15 +9,14 @@ import { CustomVariable, DataSourceVariable, TestVariable, - EmbeddedScene, } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getVariablesDemo(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getVariablesDemo(): DashboardScene { + return new DashboardScene({ title: 'Variables', $variables: new SceneVariableSet({ variables: [ @@ -82,16 +80,12 @@ export function getVariablesDemo(standalone: boolean): Scene | EmbeddedScene { }), $timeRange: new SceneTimeRange(), actions: [new SceneTimePicker({})], - subMenu: new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }), - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + controls: [new VariableValueSelectors({})], + }); } -export function getVariablesDemoWithAll(): Scene { - const scene = new Scene({ +export function getVariablesDemoWithAll(): DashboardScene { + return new DashboardScene({ title: 'Variables with All values', $variables: new SceneVariableSet({ variables: [ @@ -153,10 +147,6 @@ export function getVariablesDemoWithAll(): Scene { }), $timeRange: new SceneTimeRange(), actions: [new SceneTimePicker({})], - subMenu: new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }), + controls: [new VariableValueSelectors({})], }); - - return scene; } diff --git a/public/app/features/search/page/components/SearchView.test.tsx b/public/app/features/search/page/components/SearchView.test.tsx index cb3f230c736..8c594887eca 100644 --- a/public/app/features/search/page/components/SearchView.test.tsx +++ b/public/app/features/search/page/components/SearchView.test.tsx @@ -105,6 +105,46 @@ describe('SearchView', () => { expect(screen.getByRole('button', { name: 'Clear search and filters' })).toBeInTheDocument(); }); + it('shows an empty state if no starred dashboard returned', async () => { + jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue({ + ...mockSearchResult, + totalRows: 0, + view: new DataFrameView({ fields: [], length: 0 }), + }); + + setup(undefined, { starred: true }); + + await waitFor(() => expect(screen.queryByText('No results found for your query.')).toBeInTheDocument()); + expect(screen.getByRole('button', { name: 'Clear search and filters' })).toBeInTheDocument(); + }); + + it('shows empty folder cta for empty folder', async () => { + jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue({ + ...mockSearchResult, + totalRows: 0, + view: new DataFrameView({ fields: [], length: 0 }), + }); + + setup( + { + folderDTO: { + id: 1, + uid: 'abc', + title: 'morning coffee', + url: '/morningcoffee', + version: 1, + canSave: true, + canEdit: true, + canAdmin: true, + canDelete: true, + }, + }, + undefined + ); + + await waitFor(() => expect(screen.queryByText("This folder doesn't have any dashboards yet")).toBeInTheDocument()); + }); + describe('include panels', () => { it('should be enabled when layout is list', async () => { config.featureToggles.panelTitleSearch = true; diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index 5a03908f1df..dbde3e3ad22 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -159,7 +159,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE ); }; - if (folderDTO && !state.loading && !state.result?.totalRows && !state.query.length) { + if (folderDTO && !state.loading && !state.result?.totalRows && !stateManager.hasSearchFilters()) { return ( { store.set(SEARCH_PANELS_LOCAL_STORAGE_KEY, includePanels); }; + hasSearchFilters() { + return this.state.query || this.state.tag.length || this.state.starred; + } + getSearchQuery() { const q: SearchQuery = { query: this.state.query, diff --git a/public/app/features/support-bundles/SupportBundles.tsx b/public/app/features/support-bundles/SupportBundles.tsx index 0e75a1c27a6..a2a6fa98f72 100644 --- a/public/app/features/support-bundles/SupportBundles.tsx +++ b/public/app/features/support-bundles/SupportBundles.tsx @@ -1,10 +1,13 @@ import React, { useEffect } from 'react'; -import { useAsyncFn } from 'react-use'; +import { connect, ConnectedProps } from 'react-redux'; import { dateTimeFormat } from '@grafana/data'; -import { config, getBackendSrv } from '@grafana/runtime'; -import { LinkButton } from '@grafana/ui'; +import { config } from '@grafana/runtime'; +import { LinkButton, Spinner, IconButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types'; + +import { loadBundles, removeBundle, checkBundles } from './state/actions'; const subTitle = ( @@ -13,39 +16,48 @@ const subTitle = ( ); -const newButton = ( +const NewBundleButton = ( New support bundle ); -type SupportBundleState = 'complete' | 'error' | 'timeout' | 'pending'; - -interface SupportBundle { - uid: string; - state: SupportBundleState; - creator: string; - createdAt: number; - expiresAt: number; -} - -const getBundles = () => { - return getBackendSrv().get('/api/support-bundles'); +const mapStateToProps = (state: StoreState) => { + return { + supportBundles: state.supportBundles.supportBundles, + isLoading: state.supportBundles.isLoading, + }; }; -function SupportBundles() { - const [bundlesState, fetchBundles] = useAsyncFn(getBundles, []); +const mapDispatchToProps = { + loadBundles, + removeBundle, + checkBundles, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = ConnectedProps; + +const SupportBundlesUnconnected = ({ supportBundles, isLoading, loadBundles, removeBundle, checkBundles }: Props) => { + const isPending = supportBundles.some((b) => b.state === 'pending'); useEffect(() => { - fetchBundles(); - }, [fetchBundles]); + loadBundles(); + }, [loadBundles]); - const actions = config.featureToggles.topnav ? newButton : undefined; + useEffect(() => { + if (isPending) { + checkBundles(); + } + }); + + const actions = config.featureToggles.topnav ? NewBundleButton : undefined; return ( - - {!config.featureToggles.topnav && newButton} + + {!config.featureToggles.topnav && NewBundleButton} @@ -53,25 +65,31 @@ function SupportBundles() { + - {bundlesState?.value?.map((b) => ( - - - - + {supportBundles?.map((bundle) => ( + + + + + + ))} @@ -79,6 +97,6 @@ function SupportBundles() { ); -} +}; -export default SupportBundles; +export default connector(SupportBundlesUnconnected); diff --git a/public/app/features/support-bundles/SupportBundlesCreate.tsx b/public/app/features/support-bundles/SupportBundlesCreate.tsx index 3ecbc3212f6..d8070c1c096 100644 --- a/public/app/features/support-bundles/SupportBundlesCreate.tsx +++ b/public/app/features/support-bundles/SupportBundlesCreate.tsx @@ -1,29 +1,11 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useAsyncFn } from 'react-use'; +import React, { useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; -import { getBackendSrv, locationService } from '@grafana/runtime'; -import { Form, Button, Field, Checkbox } from '@grafana/ui'; +import { Form, Button, Field, Checkbox, LinkButton, HorizontalGroup, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types'; -// move to types -export interface SupportBundleCreateRequest { - collectors: string[]; -} - -export interface SupportBundleCollector { - uid: string; - displayName: string; - description: string; - includedByDefault: boolean; - default: boolean; -} - -export interface Props {} - -const createSupportBundle = async (data: SupportBundleCreateRequest) => { - const result = await getBackendSrv().post('/api/support-bundles', data); - return result; -}; +import { loadSupportBundleCollectors, createSupportBundle } from './state/actions'; const subTitle = ( @@ -31,50 +13,60 @@ const subTitle = ( ); -export const SupportBundlesCreate = ({}: Props): JSX.Element => { - const onSubmit = useCallback(async (data) => { - try { - const selectedLabelsArray = Object.keys(data).filter((key) => data[key]); - const response = await createSupportBundle({ collectors: selectedLabelsArray }); - console.info(response); - } catch (e) { - console.error(e); - } +const mapStateToProps = (state: StoreState) => { + return { + collectors: state.supportBundles.supportBundleCollectors, + isLoading: state.supportBundles.createBundlePageLoading, + loadCollectorsError: state.supportBundles.loadBundlesError, + createBundleError: state.supportBundles.createBundleError, + }; +}; - locationService.push('/admin/support-bundles'); - }, []); +const mapDispatchToProps = { + loadSupportBundleCollectors, + createSupportBundle, +}; - const [components, setComponents] = useState([]); - // populate components from the backend - const populateComponents = async () => { - return await getBackendSrv().get('/api/support-bundles/collectors'); +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = ConnectedProps; + +export const SupportBundlesCreateUnconnected = ({ + collectors, + isLoading, + loadCollectorsError, + createBundleError, + loadSupportBundleCollectors, + createSupportBundle, +}: Props): JSX.Element => { + const onSubmit = (data: Record) => { + const selectedLabelsArray = Object.keys(data).filter((key) => data[key]); + createSupportBundle({ collectors: selectedLabelsArray }); }; - const [state, fetchComponents] = useAsyncFn(populateComponents); useEffect(() => { - fetchComponents().then((res) => { - setComponents(res); - }); - }, [fetchComponents]); + loadSupportBundleCollectors(); + }, [loadSupportBundleCollectors]); // turn components into a uuid -> enabled map - const values: Record = components.reduce((acc, curr) => { + const values: Record = collectors.reduce((acc, curr) => { return { ...acc, [curr.uid]: curr.default }; }, {}); return ( - +

Create support bundle

- {state.error &&

{state.error}

} - {!!components.length && ( + {loadCollectorsError && } + {createBundleError && } + {!!collectors.length && (
{({ register, errors }) => { return ( <> - {components.map((component) => { + {collectors.map((component) => { return ( { ); })} - + + + + Cancel + + ); }} @@ -99,4 +96,4 @@ export const SupportBundlesCreate = ({}: Props): JSX.Element => { ); }; -export default SupportBundlesCreate; +export default connector(SupportBundlesCreateUnconnected); diff --git a/public/app/features/support-bundles/state/actions.ts b/public/app/features/support-bundles/state/actions.ts new file mode 100644 index 00000000000..6deb633c207 --- /dev/null +++ b/public/app/features/support-bundles/state/actions.ts @@ -0,0 +1,72 @@ +import { throttle } from 'lodash'; + +import { getBackendSrv, locationService } from '@grafana/runtime'; +import { SupportBundle, SupportBundleCollector, SupportBundleCreateRequest, ThunkResult } from 'app/types'; + +import { + collectorsFetchBegin, + collectorsFetchEnd, + fetchBegin, + fetchEnd, + setCreateBundleError, + setLoadBundleError, + supportBundleCollectorsLoaded, + supportBundlesLoaded, +} from './reducers'; + +export function loadBundles(skipPageRefresh = false): ThunkResult { + return async (dispatch) => { + try { + if (!skipPageRefresh) { + dispatch(fetchBegin()); + } + const result = await getBackendSrv().get('/api/support-bundles'); + dispatch(supportBundlesLoaded(result)); + } finally { + dispatch(fetchEnd()); + } + }; +} + +const checkBundlesStatusThrottled = throttle(async (dispatch) => { + const result = await getBackendSrv().get('/api/support-bundles'); + dispatch(supportBundlesLoaded(result)); +}, 1000); + +export function checkBundles(): ThunkResult { + return async (dispatch) => { + dispatch(checkBundlesStatusThrottled); + }; +} + +export function removeBundle(uid: string): ThunkResult { + return async (dispatch) => { + await getBackendSrv().delete(`/api/support-bundles/${uid}`); + dispatch(loadBundles(true)); + }; +} + +export function loadSupportBundleCollectors(): ThunkResult { + return async (dispatch) => { + try { + dispatch(collectorsFetchBegin()); + const result = await getBackendSrv().get('/api/support-bundles/collectors'); + dispatch(supportBundleCollectorsLoaded(result)); + } catch (err) { + dispatch(setLoadBundleError('Error loading support bundles data collectors')); + } finally { + dispatch(collectorsFetchEnd()); + } + }; +} + +export function createSupportBundle(data: SupportBundleCreateRequest): ThunkResult { + return async (dispatch) => { + try { + await getBackendSrv().post('/api/support-bundles', data); + locationService.push('/admin/support-bundles'); + } catch (err) { + dispatch(setCreateBundleError('Error creating support bundle')); + } + }; +} diff --git a/public/app/features/support-bundles/state/reducers.ts b/public/app/features/support-bundles/state/reducers.ts new file mode 100644 index 00000000000..21e32402ec0 --- /dev/null +++ b/public/app/features/support-bundles/state/reducers.ts @@ -0,0 +1,60 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { SupportBundle, SupportBundleCollector, SupportBundlesState } from 'app/types'; + +export const initialState: SupportBundlesState = { + supportBundles: [], + isLoading: false, + supportBundleCollectors: [], + createBundlePageLoading: false, + loadBundlesError: '', + createBundleError: '', +}; + +const supportBundlesSlice = createSlice({ + name: 'supportBundles', + initialState, + reducers: { + supportBundlesLoaded: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, supportBundles: action.payload, isLoading: false }; + }, + fetchBegin: (state): SupportBundlesState => { + return { ...state, isLoading: true }; + }, + fetchEnd: (state): SupportBundlesState => { + return { ...state, isLoading: false }; + }, + collectorsFetchBegin: (state): SupportBundlesState => { + return { ...state, createBundlePageLoading: true }; + }, + collectorsFetchEnd: (state): SupportBundlesState => { + return { ...state, createBundlePageLoading: false }; + }, + supportBundleCollectorsLoaded: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, supportBundleCollectors: action.payload, createBundlePageLoading: false }; + }, + setLoadBundleError: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, loadBundlesError: action.payload, supportBundleCollectors: [] }; + }, + setCreateBundleError: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, createBundleError: action.payload }; + }, + }, +}); + +export const { + supportBundlesLoaded, + fetchBegin, + fetchEnd, + supportBundleCollectorsLoaded, + collectorsFetchBegin, + collectorsFetchEnd, + setLoadBundleError, + setCreateBundleError, +} = supportBundlesSlice.actions; + +export const supportBundlesReducer = supportBundlesSlice.reducer; + +export default { + supportBundles: supportBundlesReducer, +}; diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index a2454b56a35..d7d3ad49de3 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -9,7 +9,7 @@ import { TypedVariableModel, } from '@grafana/data'; import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; -import { SceneObjectBase, sceneGraph, FormatRegistryID, formatRegistry, CustomFormatterFn } from '@grafana/scenes'; +import { sceneGraph, FormatRegistryID, formatRegistry, CustomFormatterFn } from '@grafana/scenes'; import { variableAdapters } from '../variables/adapters'; import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants'; @@ -276,7 +276,7 @@ export class TemplateSrv implements BaseTemplateSrv { } replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { - if (scopedVars && scopedVars.__sceneObject && scopedVars.__sceneObject.value instanceof SceneObjectBase) { + if (scopedVars && scopedVars.__sceneObject) { return sceneGraph.interpolate( scopedVars.__sceneObject.value, target, diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts index 7b10b7b67aa..3b41ccbab8f 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts @@ -21,6 +21,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region'], + keepFields: true, + naming: { + asLabels: false, + }, }; let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); @@ -57,6 +61,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, + naming: { + asLabels: false, + }, }; let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); @@ -100,7 +108,7 @@ describe('Partition by values transformer', () => { expect(partitioned[3].fields[2].values.toArray()).toEqual(['FAIL']); }); - it('should partition by multiple fields with custom frame naming {withFields: true}', () => { + it('should partition by multiple fields with custom frame naming {withNames: true}', () => { const source = [ toDataFrame({ name: 'XYZ', @@ -115,8 +123,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { - withFields: true, + asLabels: false, + withNames: true, }, }; @@ -143,7 +153,9 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { + asLabels: false, append: true, }, }; @@ -156,7 +168,7 @@ describe('Partition by values transformer', () => { expect(partitioned[3].name).toEqual('XYZ China FAIL'); }); - it('should partition by multiple fields with custom frame naming {withFields: true, append: true}', () => { + it('should partition by multiple fields with custom frame naming {withNames: true, append: true}', () => { const source = [ toDataFrame({ name: 'XYZ', @@ -171,8 +183,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { - withFields: true, + asLabels: false, + withNames: true, append: true, }, }; @@ -184,4 +198,70 @@ describe('Partition by values transformer', () => { expect(partitioned[2].name).toEqual('XYZ region=China status=OK'); expect(partitioned[3].name).toEqual('XYZ region=China status=FAIL'); }); + + it('should partition by multiple fields naming: {asLabels: true}', () => { + const source = [ + toDataFrame({ + name: 'XYZ', + refId: 'A', + fields: [ + { name: 'model', type: FieldType.string, values: ['E1', 'E2', 'C1', 'E3', 'C2', 'C3'] }, + { name: 'region', type: FieldType.string, values: ['Europe', 'Europe', 'China', 'Europe', 'China', 'China'] }, + { name: 'status', type: FieldType.string, values: ['OK', 'FAIL', 'OK', 'FAIL', 'OK', 'FAIL'] }, + ], + }), + ]; + + const config: PartitionByValuesTransformerOptions = { + fields: ['region', 'status'], + keepFields: true, + naming: { + asLabels: true, + }, + }; + + let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); + + // all frame names are same + expect(partitioned[0].name).toEqual('XYZ'); + expect(partitioned[1].name).toEqual('XYZ'); + expect(partitioned[2].name).toEqual('XYZ'); + expect(partitioned[3].name).toEqual('XYZ'); + + // all frames contain all fields + expect(partitioned[0].fields[0].name).toEqual('model'); + expect(partitioned[0].fields[1].name).toEqual('region'); + expect(partitioned[0].fields[2].name).toEqual('status'); + + // in each frame, every field has same labels + expect(partitioned[0].fields[0].labels).toEqual({ region: 'Europe', status: 'OK' }); + expect(partitioned[1].fields[0].labels).toEqual({ region: 'Europe', status: 'FAIL' }); + expect(partitioned[2].fields[0].labels).toEqual({ region: 'China', status: 'OK' }); + expect(partitioned[3].fields[0].labels).toEqual({ region: 'China', status: 'FAIL' }); + }); + + it('should partition by multiple fields and omit those fields in result', () => { + const source = [ + toDataFrame({ + name: 'XYZ', + refId: 'A', + fields: [ + { name: 'model', type: FieldType.string, values: ['E1', 'E2', 'C1', 'E3', 'C2', 'C3'] }, + { name: 'region', type: FieldType.string, values: ['Europe', 'Europe', 'China', 'Europe', 'China', 'China'] }, + { name: 'status', type: FieldType.string, values: ['OK', 'FAIL', 'OK', 'FAIL', 'OK', 'FAIL'] }, + ], + }), + ]; + + const config: PartitionByValuesTransformerOptions = { + fields: ['region', 'status'], + }; + + let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); + + // all frames contain only model field + expect(partitioned[0].fields.length).toEqual(1); + expect(partitioned[0].fields[0].name).toEqual('model'); + expect(partitioned[0].fields[0].labels).toEqual({ region: 'Europe', status: 'OK' }); + }); }); diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.ts b/public/app/features/transformers/partitionByValues/partitionByValues.ts index 2b26be6d6b9..1d558728a02 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.ts @@ -14,10 +14,15 @@ import { noopTransformer } from '@grafana/data/src/transformations/transformers/ import { partition } from './partition'; export interface FrameNamingOptions { + /** when true, the frame name is copied unmodified, and discriminator fields' names+values become field labels in new frames */ + asLabels?: boolean; + + /** opts below are used only when asLabels: false */ + /** whether to append to existing frame name, false -> replace */ append?: boolean; // false /** whether to include discriminator field names, e.g. true -> Region=Europe Profession=Chef, false -> 'Europe Chef' */ - withFields?: boolean; // false + withNames?: boolean; // false /** name/value separator, e.g. '=' in 'Region=Europe' */ separator1?: string; /** name/value pair separator, e.g. ' ' in 'Region=Europe Profession=Chef' */ @@ -25,8 +30,10 @@ export interface FrameNamingOptions { } const defaultFrameNameOptions: FrameNamingOptions = { + asLabels: true, + append: false, - withFields: false, + withNames: false, separator1: '=', separator2: ' ', }; @@ -36,14 +43,26 @@ export interface PartitionByValuesTransformerOptions { fields: string[]; /** how the split frames' names should be suffixed (ends up as field prefixes) */ naming?: FrameNamingOptions; + /** should the discriminator fields be kept in the output */ + keepFields?: boolean; } function buildFrameName(opts: FrameNamingOptions, names: string[], values: unknown[]): string { return names - .map((name, i) => (opts.withFields ? `${name}${opts.separator1}${values[i]}` : values[i])) + .map((name, i) => (opts.withNames ? `${name}${opts.separator1}${values[i]}` : values[i])) .join(opts.separator2); } +function buildFieldLabels(names: string[], values: unknown[]) { + const labels: Record = {}; + + names.forEach((name, i) => { + labels[name] = String(values[i]); + }); + + return labels; +} + export const partitionByValuesTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.partitionByValues, name: 'Partition by values', @@ -78,21 +97,40 @@ export const partitionByValuesTransformer: SynchronousDataTransformerInfo { - let name = buildFrameName( - frameNameOpts, - names, - keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) - ); + let frameName = frame.name; + let fieldLabels = {}; - if (options.naming?.append && frame.name) { - name = `${frame.name} ${name}`; + if (frameNameOpts.asLabels) { + fieldLabels = buildFieldLabels( + names, + keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) + ); + } else { + let name = buildFrameName( + frameNameOpts, + names, + keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) + ); + + if (options.naming?.append && frame.name) { + name = `${frame.name} ${name}`; + } + + frameName = name; + } + + let filteredFields = frame.fields; + + if (!options.keepFields) { + const keyFieldNames = new Set(names); + filteredFields = frame.fields.filter((field) => !keyFieldNames.has(field.name)); } return { ...frame, - name, + name: frameName, length: idxs.length, - fields: frame.fields.map((f) => { + fields: filteredFields.map((f) => { const vals = f.values.toArray(); const vals2 = Array(idxs.length); @@ -102,6 +140,10 @@ export const partitionByValuesTransformer: SynchronousDataTransformerInfo) => { templateSrv, getSLOServices: jest.fn().mockResolvedValue([]), migrateQuery: jest.fn().mockImplementation((query) => query), + timeSrv: getTimeSrv(), ...overrides, }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx deleted file mode 100644 index 74973ba7abe..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { render, screen, within } from '@testing-library/react'; -import React from 'react'; -import { openMenu, select } from 'react-select-event'; - -import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource'; -import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor'; -import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery'; - -import { Metrics } from './Metrics'; - -describe('Metrics', () => { - it('renders metrics fields', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource(); - - render( - - {() =>
} - - ); - - expect(await screen.findByLabelText('Service')).toBeInTheDocument(); - expect(await screen.findByLabelText('Metric name')).toBeInTheDocument(); - }); - - it('can select a service', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), - }); - - render( - - {() =>
} - - ); - - const service = await screen.findByLabelText('Service'); - await openMenu(service); - await select(service, 'Srv', { container: document.body }); - expect(onChange).toBeCalledWith(expect.objectContaining({ service: 'service' })); - }); - - it('can select a metric name', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), - }); - - render( - - {() =>
} - - ); - - const metricName = await screen.findByLabelText('Metric name'); - await openMenu(metricName); - await select(metricName, 'metricName', { container: document.body }); - expect(onChange).toBeCalledWith(expect.objectContaining({ type: 'type' })); - }); - - it('should render available metric options according to the selected service', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([ - createMockMetricDescriptor({ - service: 'service_a', - serviceShortName: 'srv_a', - type: 'metric1', - description: 'description_metric1', - displayName: 'displayName_metric1', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric2', - description: 'description_metric2', - displayName: 'displayName_metric2', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric3', - description: 'description_metric3', - displayName: 'displayName_metric3', - }), - ]), - }); - - render( - - {() =>
} - - ); - - const metricName = await screen.findByLabelText('Metric name'); - await openMenu(metricName); - - const metricNameOptions = screen.getByLabelText('Select options menu'); - expect(within(metricNameOptions).getByText('description_metric1')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('displayName_metric1')).toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('displayName_metric2')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('description_metric2')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('displayName_metric3')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('description_metric3')).not.toBeInTheDocument(); - - await select(screen.getByLabelText('Service'), 'Srv B', { container: document.body }); - expect(within(metricNameOptions).queryByText('displayName_metric1')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('description_metric1')).not.toBeInTheDocument(); - expect(within(metricNameOptions).getByText('displayName_metric2')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('description_metric2')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('displayName_metric3')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('description_metric3')).toBeInTheDocument(); - }); - - it('should have a distinct list of services', async () => { - const onChange = jest.fn(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([ - createMockMetricDescriptor({ - service: 'service_a', - serviceShortName: 'srv_a', - type: 'metric1', - description: 'description_metric1', - displayName: 'displayName_metric1', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric2', - description: 'description_metric2', - displayName: 'displayName_metric2', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric3', - description: 'description_metric3', - displayName: 'displayName_metric3', - }), - ]), - }); - const query = createMockTimeSeriesList(); - - render( - - {() =>
} - - ); - const service = await screen.findByLabelText('Service'); - await openMenu(service); - expect(screen.getAllByLabelText('Select option').length).toEqual(2); - }); -}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx deleted file mode 100644 index 980c4ecb6ee..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { css } from '@emotion/css'; -import { startCase, uniqBy } from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; - -import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; -import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui'; - -import CloudMonitoringDatasource from '../datasource'; -import { MetricDescriptor, TimeSeriesList } from '../types'; - -import { Project } from './Project'; - -export interface Props { - refId: string; - onChange: (metricDescriptor: MetricDescriptor) => void; - templateVariableOptions: Array>; - datasource: CloudMonitoringDatasource; - projectName: string; - metricType: string; - query: TimeSeriesList; - children: (metricDescriptor?: MetricDescriptor) => JSX.Element; - onProjectChange: (query: TimeSeriesList) => void; -} - -export function Metrics(props: Props) { - const [metricDescriptors, setMetricDescriptors] = useState([]); - const [metricDescriptor, setMetricDescriptor] = useState(); - const [metrics, setMetrics] = useState>>([]); - const [services, setServices] = useState>>([]); - const [service, setService] = useState(''); - - const theme = useTheme2(); - const selectStyles = getSelectStyles(theme); - - const customStyle = useStyles2(getStyles); - - const { - onProjectChange, - query, - refId, - metricType, - templateVariableOptions, - projectName, - datasource, - onChange, - children, - } = props; - const { templateSrv } = datasource; - - const getSelectedMetricDescriptor = useCallback( - (metricDescriptors: MetricDescriptor[], metricType: string) => { - return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!; - }, - [templateSrv] - ); - - useEffect(() => { - const loadMetricDescriptors = async () => { - if (projectName) { - const metricDescriptors = await datasource.getMetricTypes(projectName); - const services = getServicesList(metricDescriptors); - setMetricDescriptors(metricDescriptors); - setServices(services); - } - }; - loadMetricDescriptors(); - }, [datasource, projectName, customStyle, selectStyles.optionDescription]); - - useEffect(() => { - const getMetricsList = (metricDescriptors: MetricDescriptor[]) => { - const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); - if (!selectedMetricDescriptor) { - return []; - } - - const metricsByService = metricDescriptors - .filter((m) => m.service === selectedMetricDescriptor.service) - .map((m) => ({ - service: m.service, - value: m.type, - label: m.displayName, - component: function optionComponent() { - return ( -
-
{m.type}
-
{m.description}
-
- ); - }, - })); - return metricsByService; - }; - - const metrics = getMetricsList(metricDescriptors); - const service = metrics.length > 0 ? metrics[0].service : ''; - const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); - setMetricDescriptor(metricDescriptor); - setMetrics(metrics); - setService(service); - }, [metricDescriptors, getSelectedMetricDescriptor, metricType, customStyle, selectStyles.optionDescription]); - - const onServiceChange = ({ value: service }: any) => { - const metrics = metricDescriptors - .filter((m: MetricDescriptor) => m.service === templateSrv.replace(service)) - .map((m: MetricDescriptor) => ({ - service: m.service, - value: m.type, - label: m.displayName, - description: m.description, - })); - - if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) { - onMetricTypeChange(metrics[0]); - setService(service); - setMetrics(metrics); - } else { - setService(service); - setMetrics(metrics); - } - }; - - const onMetricTypeChange = ({ value }: SelectableValue) => { - const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!); - setMetricDescriptor(metricDescriptor); - onChange({ ...metricDescriptor, type: value! }); - }; - - const getServicesList = (metricDescriptors: MetricDescriptor[]) => { - const services = metricDescriptors.map((m) => ({ - value: m.service, - label: startCase(m.serviceShortName), - })); - - return services.length > 0 ? uniqBy(services, (s) => s.value) : []; - }; - - return ( - <> - - - { - onProjectChange({ ...query, projectName }); - }} - /> - - - s.value === metricType)} - options={[ - { - label: 'Template Variables', - options: templateVariableOptions, - }, - ...metrics, - ]} - placeholder="Select Metric" - inputId={`${props.refId}-select-metric`} - /> - - - - - {children(metricDescriptor)} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => css` - label: grafana-select-option-description; - font-weight: normal; - font-style: italic; - color: ${theme.colors.text.secondary}; -`; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx new file mode 100644 index 00000000000..b408714dc49 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx @@ -0,0 +1,286 @@ +import { act, render, screen, waitFor, within } from '@testing-library/react'; +import React from 'react'; +import { openMenu, select } from 'react-select-event'; + +import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { TemplateSrv } from 'app/features/templating/template_srv'; + +import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource'; +import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor'; +import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery'; +import { MetricKind, PreprocessorType } from '../types'; + +import { defaultTimeSeriesList } from './MetricQueryEditor'; +import { VisualMetricQueryEditor } from './VisualMetricQueryEditor'; + +const defaultProps = { + refId: 'refId', + customMetaData: {}, + variableOptionGroup: { options: [] }, + aliasBy: '', + onChangeAliasBy: jest.fn(), +}; + +describe('VisualMetricQueryEditor', () => { + it('renders metrics fields', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const datasource = createMockDatasource(); + + render(); + + expect(await screen.findByLabelText('Service')).toBeInTheDocument(); + expect(await screen.findByLabelText('Metric name')).toBeInTheDocument(); + }); + + it('can select a service', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const mockMetricDescriptor = createMockMetricDescriptor(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([mockMetricDescriptor]), + getLabels: jest.fn().mockResolvedValue([]), + }); + + render(); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv', { container: document.body }); + expect(onChange).toBeCalledWith( + expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] }) + ); + }); + }); + + it('can select a metric name', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const mockMetricDescriptor = createMockMetricDescriptor({ displayName: 'metricName_test', type: 'test_type' }); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]), + getLabels: jest.fn().mockResolvedValue([]), + }); + + render(); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv', { container: document.body }); + }); + const metricName = await screen.findByLabelText('Metric name'); + await openMenu(metricName); + await waitFor(() => expect(document.body).toHaveTextContent('metricName_test')); + await act(async () => { + await select(metricName, 'metricName_test', { container: document.body }); + expect(onChange).toBeCalledWith( + expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] }) + ); + }); + }); + + it('should render available metric options according to the selected service', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([ + createMockMetricDescriptor({ + service: 'service_a', + serviceShortName: 'srv_a', + type: 'metric1', + description: 'description_metric1', + displayName: 'displayName_metric1', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric2', + description: 'description_metric2', + displayName: 'displayName_metric2', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric3', + description: 'description_metric3', + displayName: 'displayName_metric3', + }), + ]), + getLabels: jest.fn().mockResolvedValue([]), + }); + + render(); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv A', { container: document.body }); + }); + const metricName = await screen.findByLabelText('Metric name'); + await openMenu(metricName); + + const metricNameOptions = screen.getByLabelText('Select options menu'); + expect(within(metricNameOptions).getByText('description_metric1')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('displayName_metric1')).toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('displayName_metric2')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('description_metric2')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('displayName_metric3')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('description_metric3')).not.toBeInTheDocument(); + + await openMenu(service); + await act(async () => { + await select(service, 'Srv B', { container: document.body }); + }); + expect(within(metricNameOptions).queryByText('displayName_metric1')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('description_metric1')).not.toBeInTheDocument(); + expect(within(metricNameOptions).getByText('displayName_metric2')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('description_metric2')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('displayName_metric3')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('description_metric3')).toBeInTheDocument(); + }); + + it('should have a distinct list of services', async () => { + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([ + createMockMetricDescriptor({ + service: 'service_a', + serviceShortName: 'srv_a', + type: 'metric1', + description: 'description_metric1', + displayName: 'displayName_metric1', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric2', + description: 'description_metric2', + displayName: 'displayName_metric2', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric3', + description: 'description_metric3', + displayName: 'displayName_metric3', + }), + ]), + }); + const query = createMockTimeSeriesList(); + + render(); + const service = await screen.findByLabelText('Service'); + await openMenu(service); + expect(screen.getAllByLabelText('Select option').length).toEqual(2); + }); + + it('resets query to default when service changes', async () => { + const query = createMockTimeSeriesList({ filters: ['metric.test_label', '=', 'test', 'AND'] }); + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest + .fn() + .mockResolvedValue([ + createMockMetricDescriptor(), + createMockMetricDescriptor({ type: 'type2', service: 'service2', serviceShortName: 'srv2' }), + ]), + getLabels: jest.fn().mockResolvedValue([]), + }); + const defaultQuery = { ...query, ...defaultTimeSeriesList(datasource), filters: ['metric.type', '=', 'type2'] }; + + render(); + + expect(screen.getByText('metric.test_label')).toBeInTheDocument(); + const service = await screen.findByLabelText('Service'); + openMenu(service); + await select(service, 'Srv 2', { container: document.body }); + expect(onChange).toBeCalledWith(expect.objectContaining({ filters: ['metric.type', '=', 'type2'] })); + expect(query).toEqual(defaultQuery); + expect(screen.queryByText('metric.test_label')).not.toBeInTheDocument(); + }); + + it('resets query to defaults (except filters) when metric changes', async () => { + const groupBys = ['metric.test_groupby']; + const query = createMockTimeSeriesList({ + filters: ['metric.test_label', '=', 'test', 'AND', 'metric.type', '=', 'type'], + groupBys, + preprocessor: PreprocessorType.Delta, + }); + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest + .fn() + .mockResolvedValue([ + createMockMetricDescriptor(), + createMockMetricDescriptor({ type: 'type2', displayName: 'metricName2', metricKind: MetricKind.GAUGE }), + ]), + getLabels: jest.fn().mockResolvedValue({ 'metric.test_groupby': '' }), + templateSrv: new TemplateSrv(), + }); + const defaultQuery = { ...query, ...defaultTimeSeriesList(datasource), filters: query.filters }; + + render(); + expect(document.body).toHaveTextContent('metric.test_label'); + expect(await screen.findByText('Delta')).toBeInTheDocument(); + expect(await screen.findByText('metric.test_groupby')).toBeInTheDocument(); + const metric = await screen.findByLabelText('Metric name'); + openMenu(metric); + await select(metric, 'metricName2', { container: document.body }); + expect(onChange).toBeCalledWith( + expect.objectContaining({ filters: ['metric.test_label', '=', 'test', 'AND', 'metric.type', '=', 'type2'] }) + ); + expect(query).toEqual(defaultQuery); + expect(document.body).toHaveTextContent('metric.test_label'); + expect(await screen.queryByText('Delta')).not.toBeInTheDocument(); + expect(await screen.queryByText('metric.test_groupby')).not.toBeInTheDocument(); + }); + + it('updates labels on time range change', async () => { + const timeSrv = getTimeSrv(); + const query = createMockTimeSeriesList(); + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), + getLabels: jest + .fn() + .mockResolvedValue( + timeSrv.time.from === 'now-6h' ? { 'metric.test_groupby': '' } : { 'metric.test_groupby_1': '' } + ), + templateSrv: new TemplateSrv(), + timeSrv, + }); + + const { rerender } = render( + + ); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv', { container: document.body }); + }); + const metricName = await screen.findByLabelText('Metric name'); + await openMenu(metricName); + await waitFor(() => expect(document.body).toHaveTextContent('metricName')); + await act(async () => { + await select(metricName, 'metricName', { container: document.body }); + }); + const groupBy = await screen.findByLabelText('Group by'); + await openMenu(groupBy); + await waitFor(() => expect(document.body).toHaveTextContent('metric.test_groupby')); + await act(async () => { + timeSrv.setTime({ from: 'now-12h', to: 'now' }); + const datasourceUpdated = createMockDatasource({ + timeSrv, + getLabels: jest.fn().mockResolvedValue({ 'metric.test_groupby_1': '' }), + }); + rerender( + + ); + await openMenu(groupBy); + await waitFor(() => expect(document.body).toHaveTextContent('metric.test_groupby_1')); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx index ebaae3bf0aa..be1fe2a1b03 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx @@ -1,7 +1,10 @@ +import { css } from '@emotion/css'; +import { startCase, uniqBy } from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; -import { SelectableValue } from '@grafana/data'; -import { EditorRow } from '@grafana/experimental'; +import { GrafanaTheme2, SelectableValue, TimeRange } from '@grafana/data'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; +import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; import { getAlignmentPickerData, getMetricType, setMetricType } from '../functions'; @@ -11,106 +14,263 @@ import { AliasBy } from './AliasBy'; import { Alignment } from './Alignment'; import { GroupBy } from './GroupBy'; import { LabelFilter } from './LabelFilter'; -import { Metrics } from './Metrics'; +import { defaultTimeSeriesList } from './MetricQueryEditor'; import { Preprocessor } from './Preprocessor'; +import { Project } from './Project'; export interface Props { refId: string; customMetaData: CustomMetaData; - variableOptionGroup: SelectableValue; onChange: (query: TimeSeriesList) => void; - query: TimeSeriesList; datasource: CloudMonitoringDatasource; + query: TimeSeriesList; + variableOptionGroup: SelectableValue; aliasBy?: string; onChangeAliasBy: (aliasBy: string) => void; } -function Editor({ +export function Editor({ refId, - query, - datasource, onChange, - customMetaData, + datasource, + query, variableOptionGroup, + customMetaData, aliasBy, onChangeAliasBy, }: React.PropsWithChildren) { const [labels, setLabels] = useState<{ [k: string]: any }>({}); + const [metricDescriptors, setMetricDescriptors] = useState([]); + const [metricDescriptor, setMetricDescriptor] = useState(); + const [metrics, setMetrics] = useState>>([]); + const [services, setServices] = useState>>([]); + const [service, setService] = useState(''); + const [timeRange, setTimeRange] = useState({ ...datasource.timeSrv.timeRange() }); + + const useTime = (time: TimeRange) => { + if (timeRange !== null && (timeRange.raw.from !== time.raw.from || timeRange.raw.to !== time.raw.to)) { + setTimeRange({ ...time }); + } + }; + + useTime(datasource.timeSrv.timeRange()); + + const theme = useTheme2(); + const selectStyles = getSelectStyles(theme); + + const customStyle = useStyles2(getStyles); + const { projectName, groupBys, crossSeriesReducer } = query; const metricType = getMetricType(query); + const { templateSrv } = datasource; + + const getSelectedMetricDescriptor = useCallback( + (metricDescriptors: MetricDescriptor[], metricType: string) => { + return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!; + }, + [templateSrv] + ); useEffect(() => { if (projectName && metricType) { - datasource.getLabels(metricType, refId, projectName).then((labels) => setLabels(labels)); + datasource + .getLabels(metricType, refId, projectName, { groupBys, crossSeriesReducer }, timeRange) + .then((labels) => setLabels(labels)); } - }, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer]); + }, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer, timeRange]); - const onMetricTypeChange = useCallback( - ({ valueType, metricKind, type }: MetricDescriptor) => { - const preprocessor = - metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION - ? PreprocessorType.None - : PreprocessorType.Rate; - const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor); - onChange({ - ...setMetricType( - { - ...query, - perSeriesAligner, + useEffect(() => { + const loadMetricDescriptors = async () => { + if (projectName) { + const metricDescriptors = await datasource.getMetricTypes(projectName); + const services = getServicesList(metricDescriptors); + setMetricDescriptors(metricDescriptors); + setServices(services); + } + }; + loadMetricDescriptors(); + }, [datasource, projectName, customStyle, selectStyles.optionDescription]); + + useEffect(() => { + const getMetricsList = (metricDescriptors: MetricDescriptor[]) => { + const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); + if (!selectedMetricDescriptor) { + return []; + } + + const metricsByService = metricDescriptors + .filter((m) => m.service === selectedMetricDescriptor.service) + .map((m) => ({ + service: m.service, + value: m.type, + label: m.displayName, + component: function optionComponent() { + return ( +
+
{m.type}
+
{m.description}
+
+ ); }, - type - ), - preprocessor, - }); - }, - [onChange, query] - ); + })); + return metricsByService; + }; + + const metrics = getMetricsList(metricDescriptors); + const service = metrics.length > 0 ? metrics[0].service : ''; + const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); + setMetricDescriptor(metricDescriptor); + setMetrics(metrics); + setService(service); + }, [metricDescriptors, getSelectedMetricDescriptor, metricType, customStyle, selectStyles.optionDescription]); + + const onServiceChange = ({ value: service }: SelectableValue) => { + const metrics = metricDescriptors + .filter((m: MetricDescriptor) => m.service === templateSrv.replace(service)) + .map((m: MetricDescriptor) => ({ + service: m.service, + value: m.type, + label: m.displayName, + description: m.description, + })); + // On service change reset all query values except the project name + query.filters = []; + + if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) { + onMetricTypeChange(metrics[0]); + setService(service!); + setMetrics(metrics); + } else { + setService(service!); + setMetrics(metrics); + } + }; + + const getServicesList = (metricDescriptors: MetricDescriptor[]) => { + const services = metricDescriptors.map((m) => ({ + value: m.service, + label: startCase(m.serviceShortName), + })); + + return services.length > 0 ? uniqBy(services, (s) => s.value) : []; + }; + + const onMetricTypeChange = ({ value }: SelectableValue) => { + const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!); + setMetricDescriptor(metricDescriptor); + const { metricKind, valueType } = metricDescriptor; + const preprocessor = + metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION + ? PreprocessorType.None + : PreprocessorType.Rate; + const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor); + + // On metric name change reset query to defaults except project name and filters + Object.assign(query, { + ...defaultTimeSeriesList(datasource), + projectName: query.projectName, + filters: query.filters, + }); + onChange({ + ...setMetricType( + { + ...query, + perSeriesAligner, + }, + value! + ), + preprocessor, + }); + }; return ( - - {(metric) => ( - <> - onChange({ ...query, filters })} - variableOptionGroup={variableOptionGroup} + <> + + + { + onChange({ ...query, projectName }); + }} /> - - - + s.value === metricType)} + options={[ + { + label: 'Template Variables', + options: variableOptionGroup.options, + }, + ...metrics, + ]} + placeholder="Select Metric" + inputId={`${refId}-select-metric`} /> - - - - )} - + + + + + <> + onChange({ ...query, filters })} + variableOptionGroup={variableOptionGroup} + /> + + + + + + + + ); } +const getStyles = (theme: GrafanaTheme2) => css` + label: grafana-select-option-description; + font-weight: normal; + font-style: italic; + color: ${theme.colors.text.secondary}; +`; + export const VisualMetricQueryEditor = React.memo(Editor); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/index.ts b/public/app/plugins/datasource/cloud-monitoring/components/index.ts index 1651a77898c..734d8c47c22 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/index.ts +++ b/public/app/plugins/datasource/cloud-monitoring/components/index.ts @@ -1,5 +1,4 @@ export { Project } from './Project'; -export { Metrics } from './Metrics'; export { GroupBy } from './GroupBy'; export { Alignment } from './Alignment'; export { LabelFilter } from './LabelFilter'; diff --git a/public/app/plugins/datasource/cloud-monitoring/datasource.ts b/public/app/plugins/datasource/cloud-monitoring/datasource.ts index 2211c09942a..60df2221082 100644 --- a/public/app/plugins/datasource/cloud-monitoring/datasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/datasource.ts @@ -8,6 +8,7 @@ import { DataSourceInstanceSettings, ScopedVars, SelectableValue, + TimeRange, } from '@grafana/data'; import { DataSourceWithBackend, getBackendSrv, toDataQueryResponse, BackendSrv } from '@grafana/runtime'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -39,7 +40,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< constructor( private instanceSettings: DataSourceInstanceSettings, public templateSrv: TemplateSrv = getTemplateSrv(), - private readonly timeSrv: TimeSrv = getTimeSrv() + readonly timeSrv: TimeSrv = getTimeSrv() ) { super(instanceSettings); this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt'; @@ -89,7 +90,13 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< }; } - async getLabels(metricType: string, refId: string, projectName: string, aggregation?: Aggregation) { + async getLabels( + metricType: string, + refId: string, + projectName: string, + aggregation?: Aggregation, + timeRange?: TimeRange + ) { const options = { targets: [ { @@ -107,7 +114,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< ), }, ], - range: this.timeSrv.timeRange(), + range: timeRange ?? this.timeSrv.timeRange(), }; const queries = options.targets; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index 52f31219faf..72221bc968a 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -80,12 +80,12 @@ export function setupMockedDataSource({ const timeSrv = getTimeSrv(); const datasource = new CloudWatchDatasource(customInstanceSettings, templateService, timeSrv); datasource.getVariables = () => ['test']; - datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); - datasource.api.getRegions = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getAccounts = jest.fn().mockResolvedValue([]); - datasource.api.getLogGroups = jest.fn().mockResolvedValue([]); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); + datasource.resources.getRegions = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); + datasource.resources.getLogGroups = jest.fn().mockResolvedValue([]); const fetchMock = jest.fn().mockReturnValue(of({})); setBackendSrv({ ...getBackendSrv(), diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts similarity index 84% rename from public/app/plugins/datasource/cloudwatch/__mocks__/API.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts index 662fc010006..69effa8d23b 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts @@ -3,11 +3,11 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { CustomVariableModel } from 'app/features/variables/types'; -import { CloudWatchAPI } from '../api'; +import { ResourcesAPI } from '../resources/ResourcesAPI'; import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource'; -export function setupMockedAPI({ +export function setupMockedResourcesAPI({ variables, response, getMock, @@ -20,7 +20,7 @@ export function setupMockedAPI({ let templateService = variables ? setupMockedTemplateService(variables) : new TemplateSrv(); const timeSrv = getTimeSrv(); - const api = new CloudWatchAPI(CloudWatchSettings, templateService); + const api = new ResourcesAPI(CloudWatchSettings, templateService); let resourceRequestMock = getMock ? getMock : jest.fn().mockReturnValue(response); setBackendSrv({ ...getBackendSrv(), diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts index 75e2a192c03..27e0670047d 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts @@ -3,11 +3,11 @@ import { uniq } from 'lodash'; import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import type { Monaco, monacoTypes } from '@grafana/ui'; -import { CloudWatchAPI } from '../../api'; import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; import { LinkedToken } from '../../monarch/LinkedToken'; import { TRIGGER_SUGGEST } from '../../monarch/commands'; import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types'; +import { ResourcesAPI } from '../../resources/ResourcesAPI'; import { BY, FROM, @@ -34,9 +34,9 @@ type CompletionItem = monacoTypes.languages.CompletionItem; export class SQLCompletionItemProvider extends CompletionItemProvider { region: string; - constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) { - super(api, templateSrv); - this.region = api.getActualRegion() ?? ''; + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + super(resources, templateSrv); + this.region = resources.getActualRegion() ?? ''; this.getStatementPosition = getStatementPosition; this.getSuggestionKinds = getSuggestionKinds; this.tokenTypes = SQLTokenTypes; @@ -112,14 +112,14 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { const namespaceToken = getNamespaceToken(currentToken); if (namespaceToken?.value) { // if a namespace is specified, only suggest metrics for the namespace - const metrics = await this.api.getMetrics({ + const metrics = await this.resources.getMetrics({ namespace: namespaceToken?.value.replace(/\"/g, ''), region: this.region, }); metrics.forEach((m) => m.value && addSuggestion(m.value)); } else { // If no namespace is specified in the query, just list all metrics - const metrics = await this.api.getAllMetrics({ region: this.region }); + const metrics = await this.resources.getAllMetrics({ region: this.region }); uniq(metrics.map((m) => m.metricName)).forEach((m) => m && addSuggestion(m, { insertText: m })); } } @@ -147,12 +147,12 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { let namespaces = []; if (metricNameToken?.value) { // if a metric is specified, only suggest namespaces that actually have that metric - const metrics = await this.api.getMetrics({ region: this.region }); + const metrics = await this.resources.getMetrics({ region: this.region }); const metricName = this.templateSrv.replace(metricNameToken.value); namespaces = metrics.filter((m) => m.metricName === metricName).map((m) => m.namespace); } else { // if no metric is specified, just suggest all namespaces - const ns = await this.api.getNamespaces(); + const ns = await this.resources.getNamespaces(); namespaces = ns.map((n) => n.value); } namespaces.map((n) => addSuggestion(`"${n}"`, { insertText: `"${n}"` })); @@ -179,7 +179,7 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { dimensionFilters = (labelKeyTokens || []).reduce((acc, curr) => { return { ...acc, [curr.value]: null }; }, {}); - const keys = await this.api.getDimensionKeys({ + const keys = await this.resources.getDimensionKeys({ namespace: this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')), region: this.templateSrv.replace(this.region), metricName: metricNameToken?.value, @@ -199,7 +199,7 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { const metricNameToken = getMetricNameToken(currentToken); const labelKey = currentToken?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken(); if (namespaceToken?.value && labelKey?.value && metricNameToken?.value) { - const values = await this.api.getDimensionValues({ + const values = await this.resources.getDimensionValues({ region: this.region, namespace: namespaceToken.value.replace(/\"/g, ''), metricName: metricNameToken.value, diff --git a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx index 082d8ee0583..65de0fc59f7 100644 --- a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx @@ -29,10 +29,10 @@ const q: CloudWatchQuery = { alarmNamePrefix: '', }; -ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); -ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); const props: QueryEditorProps = { @@ -51,7 +51,9 @@ describe('AnnotationQueryEditor', () => { }); it('should return an error component in case CloudWatchQuery is not CloudWatchAnnotationQuery', async () => { - ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + ds.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); render( ); @@ -59,7 +61,9 @@ describe('AnnotationQueryEditor', () => { }); it('should not display wildcard option in dimension value dropdown', async () => { - ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + ds.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); (props.query as CloudWatchAnnotationQuery).dimensions = { instanceId: 'instance-123' }; render(); const valueElement = screen.getByText('instance-123'); @@ -72,7 +76,9 @@ describe('AnnotationQueryEditor', () => { }); it('should not display Accounts component', async () => { - ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + ds.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); (props.query as CloudWatchAnnotationQuery).dimensions = { instanceId: 'instance-123' }; await waitFor(() => render()); expect(await screen.queryByText('Account')).toBeNull(); diff --git a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx index e535aa91d38..2ba3364df49 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx @@ -40,7 +40,7 @@ export function CloudWatchLink({ panelData, query, datasource }: Props) { source: sources ?? [], }; - setHref(encodeUrl(urlProps, datasource.api.getActualRegion(query.region))); + setHref(encodeUrl(urlProps, datasource.resources.getActualRegion(query.region))); } }, [panelData, prevPanelData, datasource, query]); diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx index d2579d9fc21..d44e1755b7e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx @@ -99,7 +99,7 @@ describe('Render', () => { putMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource })); getMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource })); loadDataSourceMock.mockResolvedValue(datasource); - datasource.api.getRegions = jest.fn().mockResolvedValue([ + datasource.resources.getRegions = jest.fn().mockResolvedValue([ { label: 'ap-east-1', value: 'ap-east-1', diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index e13ce093c1d..61a01609f39 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -14,8 +14,8 @@ import { createWarningNotification } from 'app/core/copy/appNotification'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { store } from 'app/store/store'; -import { SelectableResourceValue } from '../api'; import { CloudWatchDatasource } from '../datasource'; +import { SelectableResourceValue } from '../resources/types'; import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types'; import { LogGroupsField } from './LogGroups/LogGroupsField'; @@ -45,7 +45,7 @@ export const ConfigEditor: FC = (props: Props) => { loadRegions={ datasource && (async () => { - return datasource.api + return datasource.resources .getRegions() .then((regions) => regions.reduce( diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx index 7b038b53379..6132944f607 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx @@ -10,9 +10,9 @@ const ds = setupMockedDataSource({ variables: [], }); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); -ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); const q: CloudWatchMetricsQuery = { id: '', diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx index 4dea46fead0..ed814a6d99a 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx @@ -51,7 +51,7 @@ export const FilterItem: FunctionComponent = ({ return []; } - return datasource.api + return datasource.resources .getDimensionValues({ dimensionKey: filter.key, dimensionFilters: dimensionsExcludingCurrentKey, diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx index 4e8d6684a76..b670c8486ae 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx @@ -32,13 +32,13 @@ describe('LogGroupSelection', () => { it('should call getLogGroups to get associated log group arns and then update props if rendered with legacy log group names', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - defaultProps.datasource.api.getLogGroups = jest + defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); render(); await waitFor(async () => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); - expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledWith({ + expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledWith({ region: defaultProps.region, logGroupNamePrefix: 'loggroupname', }); @@ -48,14 +48,14 @@ describe('LogGroupSelection', () => { it('should not call getLogGroups to get associated log group arns for template variables that were part of the legacy log group names array, only include them in the call to onChange', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; defaultProps.datasource = setupMockedDataSource({ variables: [logGroupNamesVariable] }).datasource; - defaultProps.datasource.api.getLogGroups = jest + defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); render(); await waitFor(async () => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); - expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledTimes(1); - expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledWith({ + expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledTimes(1); + expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledWith({ region: defaultProps.region, logGroupNamePrefix: 'loggroupname', }); @@ -67,12 +67,12 @@ describe('LogGroupSelection', () => { it('should not call getLogGroups and update props if rendered with log groups', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - defaultProps.datasource.api.getLogGroups = jest + defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); render(); await waitFor(() => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); - expect(defaultProps.datasource.api.getLogGroups).not.toHaveBeenCalled(); + expect(defaultProps.datasource.resources.getLogGroups).not.toHaveBeenCalled(); expect(defaultProps.onChange).not.toHaveBeenCalled(); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx index 1ec17498541..efb53a7c8fb 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx @@ -3,7 +3,8 @@ import React, { useEffect, useState } from 'react'; import { CloudWatchDatasource } from '../../datasource'; import { useAccountOptions } from '../../hooks'; -import { DescribeLogGroupsRequest, LogGroup } from '../../types'; +import { DescribeLogGroupsRequest } from '../../resources/types'; +import { LogGroup } from '../../types'; import { isTemplateVariable } from '../../utils/templateVariableUtils'; import { LogGroupsSelector } from './LogGroupsSelector'; @@ -32,7 +33,7 @@ export const LogGroupsField = ({ maxNoOfVisibleLogGroups, onBeforeOpen, }: Props) => { - const accountState = useAccountOptions(datasource?.api, region); + const accountState = useAccountOptions(datasource?.resources, region); const [loadingLogGroupsStarted, setLoadingLogGroupsStarted] = useState(false); useEffect(() => { @@ -41,13 +42,15 @@ export const LogGroupsField = ({ setLoadingLogGroupsStarted(true); // there's no need to migrate variables, they will be taken care of in the logs query runner - const variables = legacyLogGroupNames.filter((lgn) => isTemplateVariable(datasource.api.templateSrv, lgn)); + const variables = legacyLogGroupNames.filter((lgn) => isTemplateVariable(datasource.resources.templateSrv, lgn)); const legacyLogGroupNameValues = legacyLogGroupNames.filter( - (lgn) => !isTemplateVariable(datasource.api.templateSrv, lgn) + (lgn) => !isTemplateVariable(datasource.resources.templateSrv, lgn) ); Promise.all( - legacyLogGroupNameValues.map((lg) => datasource.api.getLogGroups({ region: region, logGroupNamePrefix: lg })) + legacyLogGroupNameValues.map((lg) => + datasource.resources.getLogGroups({ region: region, logGroupNamePrefix: lg }) + ) ) .then((results) => { const logGroups = results.flatMap((r) => @@ -70,7 +73,7 @@ export const LogGroupsField = ({
) => - datasource?.api.getLogGroups({ region: region, ...params }) ?? [] + datasource?.resources.getLogGroups({ region: region, ...params }) ?? [] } onChange={onChange} accountOptions={accountState.value} diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx index f65f4551d4a..62b22c6a361 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx @@ -5,7 +5,7 @@ import lodash from 'lodash'; import React from 'react'; import selectEvent from 'react-select-event'; -import { ResourceResponse, LogGroupResponse } from '../../types'; +import { ResourceResponse, LogGroupResponse } from '../../resources/types'; import { LogGroupsSelector } from './LogGroupsSelector'; diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx index 03b471beeca..a9949551ea4 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx @@ -5,7 +5,8 @@ import { EditorField, Space } from '@grafana/experimental'; import { Button, Checkbox, Icon, Label, LoadingPlaceholder, Modal, Select, useStyles2 } from '@grafana/ui'; import Search from '../../Search'; -import { DescribeLogGroupsRequest, LogGroup, LogGroupResponse, ResourceResponse } from '../../types'; +import { DescribeLogGroupsRequest, ResourceResponse, LogGroupResponse } from '../../resources/types'; +import { LogGroup } from '../../types'; import { Account, ALL_ACCOUNTS_OPTION } from '../Account'; import getStyles from '../styles'; diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx index 815cbdda001..9393625583b 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx @@ -15,9 +15,9 @@ const ds = setupMockedDataSource({ variables: [], }); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); -ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); const metricStat: MetricStat = { region: 'us-east-2', @@ -121,8 +121,8 @@ describe('MetricStatEditor', () => { }; beforeEach(() => { - propsNamespaceMetrics.datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces); - propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics); + propsNamespaceMetrics.datasource.resources.getNamespaces = jest.fn().mockResolvedValue(namespaces); + propsNamespaceMetrics.datasource.resources.getMetrics = jest.fn().mockResolvedValue(metrics); onChange.mockClear(); }); @@ -146,7 +146,7 @@ describe('MetricStatEditor', () => { }); it('should remove metricName from metricStat if it does not exist in new namespace', async () => { - propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockImplementation(({ namespace, region }) => { + propsNamespaceMetrics.datasource.resources.getMetrics = jest.fn().mockImplementation(({ namespace, region }) => { let mockMetrics = namespace === 'n1' && region === props.metricStat.region ? metrics @@ -202,8 +202,8 @@ describe('MetricStatEditor', () => { it('should set value to "all" when its a monitoring account and no account id is defined in the query', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - props.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); - props.datasource.api.getAccounts = jest.fn().mockResolvedValue([ + props.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); + props.datasource.resources.getAccounts = jest.fn().mockResolvedValue([ { value: '123456789', label: 'test-account1', @@ -231,8 +231,8 @@ describe('MetricStatEditor', () => { it('should unset value when no accounts were found and an account id is defined in the query', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - props.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); - props.datasource.api.getAccounts = jest.fn().mockResolvedValue([]); + props.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); + props.datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); await act(async () => { render( { - datasource.api.isMonitoringAccount(metricStat.region).then((isMonitoringAccount) => { + datasource.resources.isMonitoringAccount(metricStat.region).then((isMonitoringAccount) => { if (isMonitoringAccount && !accountState.loading && accountState.value?.length && !metricStat.accountId) { onChange({ ...metricStat, accountId: 'all' }); } @@ -43,7 +43,7 @@ export function MetricStatEditor({ onChange({ ...metricStat, accountId: undefined }); } }); - }, [accountState, metricStat, onChange, datasource.api]); + }, [accountState, metricStat, onChange, datasource.resources]); const onNamespaceChange = async (metricStat: MetricStat) => { const validatedQuery = await validateMetricName(metricStat); @@ -55,7 +55,7 @@ export function MetricStatEditor({ if (!metricName) { return metricStat; } - await datasource.api.getMetrics({ namespace, region }).then((result: Array>) => { + await datasource.resources.getMetrics({ namespace, region }).then((result: Array>) => { if (!result.find((metric) => metric.value === metricName)) { metricName = ''; } diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index bac03da437f..42be2690b75 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -46,11 +46,11 @@ const setup = () => { const datasource = new CloudWatchDatasource(instanceSettings, templateSrv as any, {} as any); datasource.metricFindQuery = async () => [{ value: 'test', label: 'test', text: 'test' }]; - datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getRegions = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getRegions = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); const props: Props = { query: { @@ -93,7 +93,9 @@ describe('QueryEditor', () => { if (props.query.queryMode !== 'Metrics') { fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`); } - props.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + props.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); props.query.metricQueryType = MetricQueryType.Search; props.query.metricEditorMode = MetricEditorMode.Builder; props.query.dimensions = { instanceId: 'instance-123' }; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx index a7782c0f3a8..dd1ad95e8d6 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx @@ -165,9 +165,9 @@ describe('QueryEditor should render right editor', () => { let datasourceMock: ReturnType; beforeEach(() => { datasourceMock = setupMockedDataSource(); - datasourceMock.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); - datasourceMock.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasourceMock.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasourceMock.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); + datasourceMock.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasourceMock.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); originalValue = config.featureToggles.cloudWatchCrossAccountQuerying; }); afterEach(() => { diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx index 3466b9fcae0..2e782cc07a1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx @@ -13,7 +13,7 @@ const originalFeatureToggleValue = config.featureToggles.cloudWatchCrossAccountQ const ds = setupMockedDataSource({ variables: [], }); -ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([]); describe('QueryHeader', () => { afterEach(() => { @@ -22,14 +22,14 @@ describe('QueryHeader', () => { describe('when changing region', () => { const { datasource } = setupMockedDataSource(); - datasource.api.getRegions = jest.fn().mockResolvedValue([ + datasource.resources.getRegions = jest.fn().mockResolvedValue([ { value: 'us-east-2', label: 'us-east-2' }, { value: 'us-east-1', label: 'us-east-1' }, ]); it('should reset account id if new region is not monitoring account', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); render( { it('should not reset account id if new region is a monitoring account', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); render( { it('should not call isMonitoringAccount if its a logs query', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); render( { await act(async () => { await selectEvent.select(screen.getByLabelText(/Region/), 'us-east-2', { container: document.body }); }); - expect(datasource.api.isMonitoringAccount).not.toHaveBeenCalledWith('us-east-2'); + expect(datasource.resources.isMonitoringAccount).not.toHaveBeenCalledWith('us-east-2'); }); it('should not call isMonitoringAccount if feature toggle is not enabled', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = false; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn(); + datasource.resources.isMonitoringAccount = jest.fn(); render( { await act(async () => { await selectEvent.select(screen.getByLabelText(/Region/), 'us-east-2', { container: document.body }); }); - expect(datasource.api.isMonitoringAccount).not.toHaveBeenCalledWith(); + expect(datasource.resources.isMonitoringAccount).not.toHaveBeenCalledWith(); }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx index 77235f8a7bc..76f05ee0468 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx @@ -32,7 +32,7 @@ const QueryHeader: React.FC = ({ onRunQuery, }) => { const { queryMode, region } = query; - const isMonitoringAccount = useIsMonitoringAccount(datasource.api, query.region); + const isMonitoringAccount = useIsMonitoringAccount(datasource.resources, query.region); const [regions, regionIsLoading] = useRegions(datasource); const onQueryModeChange = ({ value }: SelectableValue) => { @@ -46,7 +46,7 @@ const QueryHeader: React.FC = ({ }; const onRegionChange = async (region: string) => { if (config.featureToggles.cloudWatchCrossAccountQuerying && isCloudWatchMetricsQuery(query)) { - const isMonitoringAccount = await datasource.api.isMonitoringAccount(region); + const isMonitoringAccount = await datasource.resources.isMonitoringAccount(region); onChange({ ...query, region, accountId: isMonitoringAccount ? query.accountId : undefined }); } else { onChange({ ...query, region }); diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx index bffc5440a59..c9bc216a967 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx @@ -22,10 +22,10 @@ export const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({ describe('Cloudwatch SQLBuilderEditor', () => { beforeEach(() => { - datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([]); }); const baseProps = { @@ -46,7 +46,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AWS/EC2')).toBeInTheDocument(); expect(screen.getByLabelText('With schema')).not.toBeChecked(); @@ -67,7 +67,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AWS/EC2')).toBeInTheDocument(); expect(screen.getByLabelText('With schema')).toBeChecked(); @@ -94,7 +94,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { render(); await waitFor(() => - expect(datasource.api.getDimensionKeys).toHaveBeenCalledWith({ + expect(datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'AWS/EC2', region: query.region, dimensionFilters: { InstanceId: null }, @@ -121,7 +121,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AVERAGE')).toBeInTheDocument(); expect(screen.getByText('CPUUtilization')).toBeInTheDocument(); @@ -137,7 +137,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AVG')).toBeInTheDocument(); const directionElement = screen.getByLabelText('Direction'); @@ -149,7 +149,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { const query = makeSQLQuery({}); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.queryByText('AVG')).toBeNull(); const directionElement = screen.getByLabelText('Direction'); diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx index 8f8c2762340..7e9f61025b7 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx @@ -60,15 +60,15 @@ const metrics = [ describe('Cloudwatch SQLBuilderSelectRow', () => { beforeEach(() => { - datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue(namespaces); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([]); onQueryChange.mockReset(); }); it('Should not reset metricName when selecting a namespace if metric exist in new namespace', async () => { - datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics); + datasource.resources.getMetrics = jest.fn().mockResolvedValue(metrics); await act(async () => { render(); @@ -103,7 +103,7 @@ describe('Cloudwatch SQLBuilderSelectRow', () => { }); it('Should reset metricName when selecting a namespace if metric does not exist in new namespace', async () => { - datasource.api.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => { + datasource.resources.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => { let mockMetrics = namespace === 'n1' && region === baseProps.query.region ? metrics diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx index ab1a8551dc2..350d36e5cb2 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx @@ -68,7 +68,7 @@ const SQLBuilderSelectRow: React.FC = ({ datasource, q const validateMetricName = async (query: CloudWatchMetricsQuery) => { let { region, sql, namespace } = query; - await datasource.api.getMetrics({ namespace, region }).then((result: Array>) => { + await datasource.resources.getMetrics({ namespace, region }).then((result: Array>) => { if (!result.some((metric) => metric.value === metricName)) { sql = removeMetricName(query).sql; } diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx index c5856f3632b..1bbea200ba5 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx @@ -109,7 +109,7 @@ const FilterItem: React.FC = (props) => { return []; } - return datasource.api + return datasource.resources .getDimensionValues({ region: query.region, namespace, metricName, dimensionKey: filter.property.name }) .then((result: Array>) => { return appendTemplateVariables(datasource, result); diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx index 2492bcb2b11..6ca453eef3f 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx @@ -22,7 +22,7 @@ const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({ sql: sql, }); -datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); describe('Cloudwatch SQLGroupBy', () => { const baseProps = { diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx index e8cc5db0a2a..b2a065e8050 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx @@ -4,7 +4,8 @@ import React from 'react'; import { select } from 'react-select-event'; import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; -import { GetDimensionKeysRequest, VariableQueryType } from '../../types'; +import { GetDimensionKeysRequest } from '../../resources/types'; +import { VariableQueryType } from '../../types'; import { VariableQueryEditor, Props } from './VariableQueryEditor'; @@ -22,22 +23,22 @@ const defaultQuery = { const ds = setupMockedDataSource(); -ds.datasource.api.getRegions = jest.fn().mockResolvedValue([ +ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([ { label: 'a1', value: 'a1' }, { label: 'b1', value: 'b1' }, { label: 'c1', value: 'c1' }, ]); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([ +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([ { label: 'x2', value: 'x2' }, { label: 'y2', value: 'y2' }, { label: 'z2', value: 'z2' }, ]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([ +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([ { label: 'h3', value: 'h3' }, { label: 'i3', value: 'i3' }, { label: 'j3', value: 'j3' }, ]); -ds.datasource.api.getDimensionKeys = jest +ds.datasource.resources.getDimensionKeys = jest .fn() .mockImplementation(({ namespace: region, dimensionFilters }: GetDimensionKeysRequest) => { if (!!dimensionFilters) { @@ -55,12 +56,12 @@ ds.datasource.api.getDimensionKeys = jest } return Promise.resolve([{ label: 't4', value: 't4' }]); }); -ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([ +ds.datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([ { label: 'foo', value: 'foo' }, { label: 'bar', value: 'bar' }, ]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); -ds.datasource.api.getEc2InstanceAttribute = jest.fn().mockReturnValue([]); +ds.datasource.resources.getEc2InstanceAttribute = jest.fn().mockReturnValue([]); const onChange = jest.fn(); const defaultProps: Props = { @@ -142,7 +143,7 @@ describe('VariableEditor', () => { await select(keySelect, 'v4', { container: document.body, }); - expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith({ + expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'a1', metricName: 'i3', @@ -232,8 +233,8 @@ describe('VariableEditor', () => { }) ); - expect(ds.datasource.api.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); - expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); + expect(ds.datasource.resources.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); + expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); expect(props.onChange).toHaveBeenCalledWith({ ...defaultQuery, refId: 'CloudWatchVariableQueryEditor-VariableQuery', diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx index 5e474a1cf51..02355d2566d 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx @@ -69,19 +69,21 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { const sanitizeQuery = async (query: VariableQuery) => { let { metricName, dimensionKey, dimensionFilters, namespace, region } = query; if (metricName) { - await datasource.api.getMetrics({ namespace, region }).then((result: Array>) => { + await datasource.resources.getMetrics({ namespace, region }).then((result: Array>) => { if (!result.find((metric) => metric.value === metricName)) { metricName = ''; } }); } if (dimensionKey) { - await datasource.api.getDimensionKeys({ namespace, region }).then((result: Array>) => { - if (!result.find((key) => key.value === dimensionKey)) { - dimensionKey = ''; - dimensionFilters = {}; - } - }); + await datasource.resources + .getDimensionKeys({ namespace, region }) + .then((result: Array>) => { + if (!result.find((key) => key.value === dimensionKey)) { + dimensionKey = ''; + dimensionFilters = {}; + } + }); } return { ...query, metricName, dimensionKey, dimensionFilters }; }; diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 2be3be9381b..92bf5f4f1e8 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -280,7 +280,7 @@ describe('datasource', () => { }, ]), }).datasource; - const allMetrics = await datasource.api.getAllMetrics({ region: 'us-east-2' }); + const allMetrics = await datasource.resources.getAllMetrics({ region: 'us-east-2' }); expect(allMetrics[0].metricName).toEqual('CPUUtilization'); expect(allMetrics[0].namespace).toEqual('AWS/EC2'); expect(allMetrics[1].metricName).toEqual('CPUPercentage'); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 835c57385db..9651a795da9 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -19,7 +19,6 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr import { RowContextOptions } from '../../../features/logs/components/LogRowContextProvider'; import { CloudWatchAnnotationSupport } from './annotationSupport'; -import { CloudWatchAPI } from './api'; import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries'; import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards'; @@ -28,6 +27,7 @@ import { MetricMathCompletionItemProvider } from './metric-math/completion/Compl import { CloudWatchAnnotationQueryRunner } from './query-runner/CloudWatchAnnotationQueryRunner'; import { CloudWatchLogsQueryRunner } from './query-runner/CloudWatchLogsQueryRunner'; import { CloudWatchMetricsQueryRunner } from './query-runner/CloudWatchMetricsQueryRunner'; +import { ResourcesAPI } from './resources/ResourcesAPI'; import { CloudWatchAnnotationQuery, CloudWatchJsonData, @@ -52,7 +52,7 @@ export class CloudWatchDatasource private metricsQueryRunner: CloudWatchMetricsQueryRunner; private annotationQueryRunner: CloudWatchAnnotationQueryRunner; logsQueryRunner: CloudWatchLogsQueryRunner; - api: CloudWatchAPI; + resources: ResourcesAPI; constructor( private instanceSettings: DataSourceInstanceSettings, @@ -61,14 +61,14 @@ export class CloudWatchDatasource ) { super(instanceSettings); this.defaultRegion = instanceSettings.jsonData.defaultRegion; - this.api = new CloudWatchAPI(instanceSettings, templateSrv); + this.resources = new ResourcesAPI(instanceSettings, templateSrv); this.languageProvider = new CloudWatchLanguageProvider(this); - this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.api, this.templateSrv); - this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.api, this.templateSrv); + this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.resources, this.templateSrv); + this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.resources, this.templateSrv); this.metricsQueryRunner = new CloudWatchMetricsQueryRunner(instanceSettings, templateSrv); this.logsQueryRunner = new CloudWatchLogsQueryRunner(instanceSettings, templateSrv, timeSrv); this.annotationQueryRunner = new CloudWatchAnnotationQueryRunner(instanceSettings, templateSrv); - this.variables = new CloudWatchVariableSupport(this.api); + this.variables = new CloudWatchVariableSupport(this.resources); this.annotations = CloudWatchAnnotationSupport; } @@ -167,7 +167,7 @@ export class CloudWatchDatasource // public getVariables() { - return this.api.getVariables(); + return this.resources.getVariables(); } getActualRegion(region?: string) { diff --git a/public/app/plugins/datasource/cloudwatch/hooks.test.ts b/public/app/plugins/datasource/cloudwatch/hooks.test.ts index 4512ee8723c..97349de75a5 100644 --- a/public/app/plugins/datasource/cloudwatch/hooks.test.ts +++ b/public/app/plugins/datasource/cloudwatch/hooks.test.ts @@ -2,7 +2,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { config } from '@grafana/runtime'; -import { setupMockedAPI } from './__mocks__/API'; import { accountIdVariable, dimensionVariable, @@ -11,6 +10,7 @@ import { regionVariable, setupMockedDataSource, } from './__mocks__/CloudWatchDataSource'; +import { setupMockedResourcesAPI } from './__mocks__/ResourcesAPI'; import { useAccountOptions, useDimensionKeys, useIsMonitoringAccount, useMetrics } from './hooks'; const WAIT_OPTIONS = { @@ -26,7 +26,7 @@ describe('hooks', () => { describe('useIsMonitoringAccount', () => { it('should interpolate variables before calling api', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const isMonitoringAccountMock = jest.fn().mockResolvedValue(true); @@ -44,7 +44,7 @@ describe('hooks', () => { variables: [regionVariable, namespaceVariable, accountIdVariable], }); const getMetricsMock = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = getMetricsMock; + datasource.resources.getMetrics = getMetricsMock; const { waitForNextUpdate } = renderHook(() => useMetrics(datasource, { @@ -70,7 +70,7 @@ describe('hooks', () => { variables: [regionVariable, namespaceVariable, accountIdVariable, metricVariable, dimensionVariable], }); const getDimensionKeysMock = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = getDimensionKeysMock; + datasource.resources.getDimensionKeys = getDimensionKeysMock; const { waitForNextUpdate } = renderHook(() => useDimensionKeys(datasource, { @@ -100,7 +100,7 @@ describe('hooks', () => { describe('useAccountOptions', () => { it('does not call the api if the feature toggle is off', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = false; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]); @@ -112,7 +112,7 @@ describe('hooks', () => { it('interpolates region variables before calling the api', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]); @@ -125,7 +125,7 @@ describe('hooks', () => { it('returns properly formatted account options, and template variables', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]); diff --git a/public/app/plugins/datasource/cloudwatch/hooks.ts b/public/app/plugins/datasource/cloudwatch/hooks.ts index 04fefb1b5d4..a4a6738580a 100644 --- a/public/app/plugins/datasource/cloudwatch/hooks.ts +++ b/public/app/plugins/datasource/cloudwatch/hooks.ts @@ -4,9 +4,9 @@ import { useAsyncFn, useDeepCompareEffect } from 'react-use'; import { SelectableValue, toOption } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { CloudWatchAPI } from './api'; import { CloudWatchDatasource } from './datasource'; -import { GetDimensionKeysRequest, GetMetricsRequest } from './types'; +import { ResourcesAPI } from './resources/ResourcesAPI'; +import { GetMetricsRequest, GetDimensionKeysRequest } from './resources/types'; import { appendTemplateVariables } from './utils/utils'; export const useRegions = (datasource: CloudWatchDatasource): [Array>, boolean] => { @@ -21,7 +21,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array>) => setRegions([...regions, variableOptionGroup])) .finally(() => setRegionsIsLoading(false)); @@ -33,7 +33,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array { const [namespaces, setNamespaces] = useState>>([]); useEffect(() => { - datasource.api.getNamespaces().then((namespaces) => { + datasource.resources.getNamespaces().then((namespaces) => { setNamespaces(appendTemplateVariables(datasource, namespaces)); }); }, [datasource]); @@ -56,7 +56,7 @@ export const useMetrics = (datasource: CloudWatchDatasource, { region, namespace accountId = datasource.templateSrv.replace(accountId, {}); } useEffect(() => { - datasource.api.getMetrics({ namespace, region, accountId }).then((result: Array>) => { + datasource.resources.getMetrics({ namespace, region, accountId }).then((result: Array>) => { setMetrics(appendTemplateVariables(datasource, result)); }); }, [datasource, region, namespace, accountId]); @@ -87,12 +87,12 @@ export const useDimensionKeys = ( } if (dimensionFilters) { - dimensionFilters = datasource.api.convertDimensionFormat(dimensionFilters, {}); + dimensionFilters = datasource.resources.convertDimensionFormat(dimensionFilters, {}); } // doing deep comparison to avoid making new api calls to list metrics unless dimension filter object props changes useDeepCompareEffect(() => { - datasource.api + datasource.resources .getDimensionKeys({ namespace, region, metricName, accountId, dimensionFilters }) .then((result: Array>) => { setDimensionKeys(appendTemplateVariables(datasource, result)); @@ -102,37 +102,37 @@ export const useDimensionKeys = ( return dimensionKeys; }; -export const useIsMonitoringAccount = (api: CloudWatchAPI, region: string) => { +export const useIsMonitoringAccount = (resources: ResourcesAPI, region: string) => { const [isMonitoringAccount, setIsMonitoringAccount] = useState(false); // we call this before the use effect to ensure dependency array below // receives the interpolated value so that the effect is triggered when a variable is changed if (region) { - region = api.templateSrv.replace(region, {}); + region = resources.templateSrv.replace(region, {}); } useEffect(() => { if (config.featureToggles.cloudWatchCrossAccountQuerying) { - api.isMonitoringAccount(region).then((result) => setIsMonitoringAccount(result)); + resources.isMonitoringAccount(region).then((result) => setIsMonitoringAccount(result)); } - }, [region, api]); + }, [region, resources]); return isMonitoringAccount; }; export const useAccountOptions = ( - api: Pick | undefined, + resources: Pick | undefined, region: string ) => { // we call this before the use effect to ensure dependency array below // receives the interpolated value so that the effect is triggered when a variable is changed if (region) { - region = api?.templateSrv.replace(region, {}) ?? ''; + region = resources?.templateSrv.replace(region, {}) ?? ''; } const fetchAccountOptions = async () => { if (!config.featureToggles.cloudWatchCrossAccountQuerying) { return Promise.resolve([]); } - const accounts = (await api?.getAccounts({ region })) ?? []; + const accounts = (await resources?.getAccounts({ region })) ?? []; if (accounts.length === 0) { return []; } @@ -143,7 +143,7 @@ export const useAccountOptions = ( description: a.id, })); - const variableOptions = api?.getVariables().map(toOption) || []; + const variableOptions = resources?.getVariables().map(toOption) || []; const variableOptionGroup: SelectableValue = { label: 'Template Variables', @@ -153,11 +153,11 @@ export const useAccountOptions = ( return [...options, variableOptionGroup]; }; - const [state, doFetch] = useAsyncFn(fetchAccountOptions, [api, region]); + const [state, doFetch] = useAsyncFn(fetchAccountOptions, [resources, region]); useEffect(() => { doFetch(); - }, [api, region, doFetch]); + }, [resources, region, doFetch]); return state; }; diff --git a/public/app/plugins/datasource/cloudwatch/language_provider.test.ts b/public/app/plugins/datasource/cloudwatch/language_provider.test.ts index 23ddde3b747..d72ab5ed262 100644 --- a/public/app/plugins/datasource/cloudwatch/language_provider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language_provider.test.ts @@ -5,6 +5,7 @@ import { TypeaheadOutput } from '@grafana/ui'; import { CloudWatchDatasource } from './datasource'; import { CloudWatchLanguageProvider } from './language_provider'; +import { ResourceResponse } from './resources/types'; import { AGGREGATION_FUNCTIONS_STATS, BOOLEAN_FUNCTIONS, @@ -15,7 +16,7 @@ import { STRING_FUNCTIONS, FIELD_AND_FILTER_FUNCTIONS, } from './syntax'; -import { LogGroupField, ResourceResponse } from './types'; +import { LogGroupField } from './types'; const fields = ['field1', '@message']; @@ -109,7 +110,7 @@ async function runSuggestionTest(query: string, expectedItems: string[][]) { function makeDatasource(): CloudWatchDatasource { return { - api: { + resources: { getLogGroupFields(): Promise>> { return Promise.resolve([{ value: { name: 'field1' } }, { value: { name: '@message' } }]); }, diff --git a/public/app/plugins/datasource/cloudwatch/language_provider.ts b/public/app/plugins/datasource/cloudwatch/language_provider.ts index 6203d27412a..eca53233a6b 100644 --- a/public/app/plugins/datasource/cloudwatch/language_provider.ts +++ b/public/app/plugins/datasource/cloudwatch/language_provider.ts @@ -136,7 +136,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider { ); const results = await Promise.all( interpolatedLogGroups.map((logGroupName) => - this.datasource.api + this.datasource.resources .getLogGroupFields({ logGroupName, region }) .then((fields) => fields.filter((f) => f).map((f) => f.value.name ?? '')) ) diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts index 0761ed05aa5..2fa961019f4 100644 --- a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts @@ -4,7 +4,7 @@ import { setupMockedTemplateService } from '../../__mocks__/CloudWatchDataSource import * as MetricMathTestData from '../../__mocks__/metric-math-test-data'; import MonacoMock from '../../__mocks__/monarch/Monaco'; import TextModel from '../../__mocks__/monarch/TextModel'; -import { CloudWatchAPI } from '../../api'; +import { ResourcesAPI } from '../../resources/ResourcesAPI'; import cloudWatchMetricMathLanguageDefinition from '../definition'; import { METRIC_MATH_FNS, @@ -20,7 +20,7 @@ const getSuggestions = async (value: string, position: monacoTypes.IPosition) => const setup = new MetricMathCompletionItemProvider( { getActualRegion: () => 'us-east-2', - } as CloudWatchAPI, + } as ResourcesAPI, setupMockedTemplateService([]) ); const monaco = MonacoMock as Monaco; diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts index 9ebfa2729a5..17c061ac534 100644 --- a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts @@ -1,11 +1,11 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import type { Monaco, monacoTypes } from '@grafana/ui'; -import { CloudWatchAPI } from '../../api'; import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; import { LinkedToken } from '../../monarch/LinkedToken'; import { TRIGGER_SUGGEST } from '../../monarch/commands'; import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types'; +import { ResourcesAPI } from '../../resources/ResourcesAPI'; import { METRIC_MATH_FNS, METRIC_MATH_KEYWORDS, @@ -21,8 +21,8 @@ import { MetricMathTokenTypes } from './types'; type CompletionItem = monacoTypes.languages.CompletionItem; export class MetricMathCompletionItemProvider extends CompletionItemProvider { - constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) { - super(api, templateSrv); + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + super(resources, templateSrv); this.getStatementPosition = getStatementPosition; this.getSuggestionKinds = getSuggestionKinds; this.tokenTypes = MetricMathTokenTypes; diff --git a/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts index e06b54dac1c..0b74cd46bb2 100644 --- a/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts @@ -1,7 +1,7 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import type { Monaco, monacoTypes } from '@grafana/ui'; -import { CloudWatchAPI } from '../api'; +import { ResourcesAPI } from '../resources/ResourcesAPI'; import { LinkedToken } from './LinkedToken'; import { linkedTokenBuilder } from './linkedTokenBuilder'; @@ -18,12 +18,12 @@ CompletionItemProvider is an extendable class which needs to implement : - getSuggestions */ export class CompletionItemProvider implements Completeable { - api: CloudWatchAPI; + resources: ResourcesAPI; templateSrv: TemplateSrv; tokenTypes: TokenTypes; - constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) { - this.api = api; + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + this.resources = resources; this.templateSrv = templateSrv; this.templateSrv = templateSrv; diff --git a/public/app/plugins/datasource/cloudwatch/api.test.ts b/public/app/plugins/datasource/cloudwatch/resources/ResourceAPI.test.ts similarity index 90% rename from public/app/plugins/datasource/cloudwatch/api.test.ts rename to public/app/plugins/datasource/cloudwatch/resources/ResourceAPI.test.ts index e786a3f0b4d..371228043ce 100644 --- a/public/app/plugins/datasource/cloudwatch/api.test.ts +++ b/public/app/plugins/datasource/cloudwatch/resources/ResourceAPI.test.ts @@ -1,9 +1,9 @@ -import { setupMockedAPI } from './__mocks__/API'; +import { setupMockedResourcesAPI } from '../__mocks__/ResourcesAPI'; -describe('api', () => { +describe('ResourcesAPI', () => { describe('describeLogGroup', () => { it('replaces region correctly in the query', async () => { - const { api, resourceRequestMock } = setupMockedAPI(); + const { api, resourceRequestMock } = setupMockedResourcesAPI(); await api.getLogGroups({ region: 'default' }); expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1'); @@ -30,7 +30,7 @@ describe('api', () => { }, ]; - const { api } = setupMockedAPI({ response }); + const { api } = setupMockedResourcesAPI({ response }); const expectedLogGroups = [ { text: '/aws/containerinsights/dev303-workshop/application', @@ -58,7 +58,7 @@ describe('api', () => { describe('memoization', () => { it('should not initiate new api request in case a previous request had same args', async () => { const getMock = jest.fn(); - const { api, resourceRequestMock } = setupMockedAPI({ getMock }); + const { api, resourceRequestMock } = setupMockedResourcesAPI({ getMock }); resourceRequestMock.mockResolvedValue([]); await Promise.all([ api.getMetrics({ namespace: 'AWS/EC2', region: 'us-east-1' }), @@ -87,7 +87,7 @@ describe('api', () => { }, }, ]); - const { api } = setupMockedAPI({ getMock }); + const { api } = setupMockedResourcesAPI({ getMock }); const allMetrics = await api.getAllMetrics({ region: 'us-east-2' }); expect(allMetrics).toEqual([ { metricName: 'CPUUtilization', namespace: 'AWS/EC2' }, @@ -110,7 +110,7 @@ describe('api', () => { }, }, ]); - const { api } = setupMockedAPI({ getMock }); + const { api } = setupMockedResourcesAPI({ getMock }); const allMetrics = await api.getMetrics({ region: 'us-east-2', namespace: 'AWS/EC2' }); expect(allMetrics).toEqual([ { label: 'CPUUtilization', value: 'CPUUtilization' }, diff --git a/public/app/plugins/datasource/cloudwatch/api.ts b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts similarity index 95% rename from public/app/plugins/datasource/cloudwatch/api.ts rename to public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts index 719d32c30b0..e90a65236c6 100644 --- a/public/app/plugins/datasource/cloudwatch/api.ts +++ b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts @@ -4,28 +4,24 @@ import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { CloudWatchRequest } from './query-runner/CloudWatchRequest'; +import { CloudWatchRequest } from '../query-runner/CloudWatchRequest'; +import { CloudWatchJsonData, LogGroupField, MultiFilters } from '../types'; + import { - CloudWatchJsonData, + ResourceRequest, + Account, + ResourceResponse, DescribeLogGroupsRequest, + LogGroupResponse, + GetLogGroupFieldsRequest, + GetMetricsRequest, GetDimensionKeysRequest, GetDimensionValuesRequest, - GetMetricsRequest, - LogGroupResponse, MetricResponse, - MultiFilters, - Account, - ResourceRequest, - ResourceResponse, - GetLogGroupFieldsRequest, - LogGroupField, + SelectableResourceValue, } from './types'; -export interface SelectableResourceValue extends SelectableValue { - text: string; -} - -export class CloudWatchAPI extends CloudWatchRequest { +export class ResourcesAPI extends CloudWatchRequest { private memoizedGetRequest; constructor(instanceSettings: DataSourceInstanceSettings, templateSrv: TemplateSrv) { diff --git a/public/app/plugins/datasource/cloudwatch/resources/types.ts b/public/app/plugins/datasource/cloudwatch/resources/types.ts new file mode 100644 index 00000000000..72dd51844d4 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/resources/types.ts @@ -0,0 +1,63 @@ +import { SelectableValue } from '@grafana/data'; + +import { Dimensions } from '../types'; + +export interface ResourceResponse { + accountId?: string; + value: T; +} + +export interface ResourceRequest { + region: string; + accountId?: string; +} + +export interface GetLogGroupFieldsRequest extends ResourceRequest { + arn?: string; + logGroupName: string; +} + +export interface GetDimensionKeysRequest extends ResourceRequest { + metricName?: string; + namespace?: string; + dimensionFilters?: Dimensions; +} + +export interface GetDimensionValuesRequest extends ResourceRequest { + dimensionKey: string; + namespace: string; + metricName?: string; + dimensionFilters?: Dimensions; +} + +export interface GetMetricsRequest extends ResourceRequest { + namespace?: string; +} + +export interface DescribeLogGroupsRequest extends ResourceRequest { + logGroupNamePrefix?: string; + logGroupPattern?: string; + limit?: number; + listAllLogGroups?: boolean; +} + +export interface Account { + arn: string; + id: string; + label: string; + isMonitoringAccount: boolean; +} + +export interface LogGroupResponse { + arn: string; + name: string; +} + +export interface MetricResponse { + name: string; + namespace: string; +} + +export interface SelectableResourceValue extends SelectableValue { + text: string; +} diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index bd8609cf5fc..ed445ecd9e5 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -336,73 +336,6 @@ export interface LegacyAnnotationQuery extends MetricStat, DataQuery { type: string; } -export interface MetricResponse { - name: string; - namespace: string; -} - -export interface ResourceRequest { - region: string; - accountId?: string; -} - -export interface GetLogGroupFieldsRequest extends ResourceRequest { - /** - * The log group identifier - */ - arn?: string; - /** - * The name of the log group to search. - */ - logGroupName: string; -} - -export interface GetDimensionKeysRequest extends ResourceRequest { - metricName?: string; - namespace?: string; - dimensionFilters?: Dimensions; -} - -export interface GetDimensionValuesRequest extends ResourceRequest { - dimensionKey: string; - namespace: string; - metricName?: string; - dimensionFilters?: Dimensions; -} - -export interface GetMetricsRequest extends ResourceRequest { - namespace?: string; -} - -export interface DescribeLogGroupsRequest extends ResourceRequest { - logGroupNamePrefix?: string; - logGroupPattern?: string; - limit?: number; - listAllLogGroups?: boolean; -} - -export interface Account { - arn: string; - id: string; - label: string; - isMonitoringAccount: boolean; -} - -export interface LogGroupResponse { - arn: string; - name: string; -} - -export interface MetricResponse { - name: string; - namespace: string; -} - -export interface ResourceResponse { - accountId?: string; - value: T; -} - export interface LogGroup { arn: string; name: string; diff --git a/public/app/plugins/datasource/cloudwatch/variables.test.ts b/public/app/plugins/datasource/cloudwatch/variables.test.ts index 2d9b3c456e2..d9b4496a3d6 100644 --- a/public/app/plugins/datasource/cloudwatch/variables.test.ts +++ b/public/app/plugins/datasource/cloudwatch/variables.test.ts @@ -1,5 +1,5 @@ -import { setupMockedAPI } from './__mocks__/API'; import { dimensionVariable, labelsVariable, setupMockedDataSource } from './__mocks__/CloudWatchDataSource'; +import { setupMockedResourcesAPI } from './__mocks__/ResourcesAPI'; import { VariableQuery, VariableQueryType } from './types'; import { CloudWatchVariableSupport } from './variables'; @@ -16,20 +16,20 @@ const defaultQuery: VariableQuery = { }; const mock = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable] }); -mock.datasource.api.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]); -mock.datasource.api.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]); -mock.datasource.api.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]); -mock.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]); -mock.datasource.api.getLogGroups = jest +mock.datasource.resources.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]); +mock.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]); +mock.datasource.resources.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]); +mock.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]); +mock.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'a', name: 'a' } }, { value: { arn: 'b', name: 'b' } }]); -mock.datasource.api.getAccounts = jest.fn().mockResolvedValue([]); +mock.datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); const getDimensionValues = jest.fn().mockResolvedValue([{ label: 'e', value: 'e' }]); const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]); const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]); const getResourceARNs = jest.fn().mockResolvedValue([{ label: 'h', value: 'h' }]); -const variables = new CloudWatchVariableSupport(mock.datasource.api); +const variables = new CloudWatchVariableSupport(mock.datasource.resources); describe('variables', () => { it('should run regions', async () => { @@ -54,7 +54,7 @@ describe('variables', () => { describe('accounts', () => { it('should run accounts', async () => { - const { api } = setupMockedAPI(); + const { api } = setupMockedResourcesAPI(); const getAccountMock = jest.fn().mockResolvedValue([]); api.getAccounts = getAccountMock; const variables = new CloudWatchVariableSupport(api); @@ -63,7 +63,7 @@ describe('variables', () => { }); it('should map accounts to metric find value and insert "all" option', async () => { - const { api } = setupMockedAPI(); + const { api } = setupMockedResourcesAPI(); api.getAccounts = jest.fn().mockResolvedValue([{ id: '123', label: 'Account1' }]); const variables = new CloudWatchVariableSupport(api); const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.Accounts }); @@ -83,7 +83,7 @@ describe('variables', () => { dimensionFilters: { a: 'b' }, }; beforeEach(() => { - mock.datasource.api.getDimensionValues = getDimensionValues; + mock.datasource.resources.getDimensionValues = getDimensionValues; getDimensionValues.mockClear(); }); @@ -113,7 +113,7 @@ describe('variables', () => { describe('EBS volume ids', () => { beforeEach(() => { - mock.datasource.api.getEbsVolumeIds = getEbsVolumeIds; + mock.datasource.resources.getEbsVolumeIds = getEbsVolumeIds; getEbsVolumeIds.mockClear(); }); @@ -142,7 +142,7 @@ describe('variables', () => { ec2Filters: { a: ['b'] }, }; beforeEach(() => { - mock.datasource.api.getEc2InstanceAttribute = getEc2InstanceAttribute; + mock.datasource.resources.getEc2InstanceAttribute = getEc2InstanceAttribute; getEc2InstanceAttribute.mockClear(); }); @@ -167,7 +167,7 @@ describe('variables', () => { tags: { a: ['b'] }, }; beforeEach(() => { - mock.datasource.api.getResourceARNs = getResourceARNs; + mock.datasource.resources.getResourceARNs = getResourceARNs; getResourceARNs.mockClear(); }); diff --git a/public/app/plugins/datasource/cloudwatch/variables.ts b/public/app/plugins/datasource/cloudwatch/variables.ts index 2b7fa2e6614..0089878a150 100644 --- a/public/app/plugins/datasource/cloudwatch/variables.ts +++ b/public/app/plugins/datasource/cloudwatch/variables.ts @@ -9,16 +9,16 @@ import { SelectableValue, } from '@grafana/data'; -import { CloudWatchAPI } from './api'; import { ALL_ACCOUNTS_OPTION } from './components/Account'; import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor'; import { CloudWatchDatasource } from './datasource'; import { migrateVariableQuery } from './migrations/variableQueryMigrations'; +import { ResourcesAPI } from './resources/ResourcesAPI'; import { standardStatistics } from './standardStatistics'; import { VariableQuery, VariableQueryType } from './types'; export class CloudWatchVariableSupport extends CustomVariableSupport { - constructor(private readonly api: CloudWatchAPI) { + constructor(private readonly resources: ResourcesAPI) { super(); this.query = this.query.bind(this); } @@ -62,7 +62,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport regions.map(selectableValueToMetricFindOption)); + return this.resources.getRegions().then((regions) => regions.map(selectableValueToMetricFindOption)); } async handleNamespacesQuery() { - return this.api.getNamespaces().then((namespaces) => namespaces.map(selectableValueToMetricFindOption)); + return this.resources.getNamespaces().then((namespaces) => namespaces.map(selectableValueToMetricFindOption)); } async handleMetricsQuery({ namespace, region }: VariableQuery) { - return this.api.getMetrics({ namespace, region }).then((metrics) => metrics.map(selectableValueToMetricFindOption)); + return this.resources + .getMetrics({ namespace, region }) + .then((metrics) => metrics.map(selectableValueToMetricFindOption)); } async handleDimensionKeysQuery({ namespace, region }: VariableQuery) { - return this.api.getDimensionKeys({ namespace, region }).then((keys) => keys.map(selectableValueToMetricFindOption)); + return this.resources + .getDimensionKeys({ namespace, region }) + .then((keys) => keys.map(selectableValueToMetricFindOption)); } async handleDimensionValuesQuery({ namespace, region, dimensionKey, metricName, dimensionFilters }: VariableQuery) { if (!dimensionKey || !metricName) { return []; } - return this.api + return this.resources .getDimensionValues({ region, namespace, @@ -114,14 +118,14 @@ export class CloudWatchVariableSupport extends CustomVariableSupport ids.map(selectableValueToMetricFindOption)); + return this.resources.getEbsVolumeIds(region, instanceID).then((ids) => ids.map(selectableValueToMetricFindOption)); } async handleEc2InstanceAttributeQuery({ region, attributeName, ec2Filters }: VariableQuery) { if (!attributeName) { return []; } - return this.api + return this.resources .getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {}) .then((values) => values.map(selectableValueToMetricFindOption)); } @@ -130,7 +134,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport { + return this.resources.getAccounts({ region }).then((accounts) => { const metricFindOptions = accounts.map((account) => ({ text: account.label, value: account.id, diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx index be770175068..bd7f5e76d43 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx @@ -17,7 +17,7 @@ const indexPatternTypes: Array> = [ const esVersions: SelectableValue[] = [ { label: '7.10+', value: '7.10.0' }, - { label: '8.0+', value: '8.0.0' }, + { label: '8.x', value: '8.0.0' }, ]; type Props = { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts index 2d08a83eb61..4b8d43f11a8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts @@ -17,7 +17,7 @@ export default function createMockQuery(overrides?: Partial): '//change this example to create your own time series query\n
Created on Requested by Expires +
{dateTimeFormat(b.createdAt * 1000)}{b.creator}{dateTimeFormat(b.expiresAt * 1000)}
{dateTimeFormat(bundle.createdAt * 1000)}{bundle.creator}{dateTimeFormat(bundle.expiresAt * 1000)}{bundle.state === 'pending' && } Download + removeBundle(bundle.uid)} name="trash-alt" variant="destructive" /> +
//the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by , bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc', resultFormat: 'time_series', workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2', - resource: 'test-resource', + resources: ['test-resource'], ...overrides?.azureLogAnalytics, }, @@ -36,8 +36,7 @@ export default function createMockQuery(overrides?: Partial): metricName: 'Metric A', metricNamespace: 'Microsoft.Compute/virtualMachines', customNamespace: '', - resourceGroup: 'grafanastaging', - resourceName: 'grafana', + resources: [{ resourceGroup: 'grafanastaging', resourceName: 'grafana' }], timeGrain: 'auto', alias: '', // timeGrains: [], diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 3d56a7be3b5..3b4a11b5bf3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -1,5 +1,3 @@ -import { get, set } from 'lodash'; - import { toUtc } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; @@ -237,7 +235,7 @@ describe('AzureLogAnalyticsDatasource', () => { const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); query.queryType = AzureQueryType.LogAnalytics; - query.azureLogAnalytics = { resource: `$${singleVariable.name}` }; + query.azureLogAnalytics = { resources: [`$${singleVariable.name}`] }; expect(ds.targetContainsTemplate(query)).toEqual(true); }); @@ -270,7 +268,7 @@ describe('AzureLogAnalyticsDatasource', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { - resource: '/sub/124/rg/cloud/vm/server', + resources: ['/sub/124/rg/cloud/vm/server'], query: 'perf | take 100', }, }; @@ -303,7 +301,7 @@ describe('AzureLogAnalyticsDatasource', () => { refId: 'A', hide: true, azureLogAnalytics: { - resource: '/sub/124/rg/cloud/vm/server', + resources: ['/sub/124/rg/cloud/vm/server'], query: 'perf | take 100', }, }; @@ -315,7 +313,7 @@ describe('AzureLogAnalyticsDatasource', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { - resource: '/sub/124/rg/cloud/vm/server', + resources: ['/sub/124/rg/cloud/vm/server'], }, }; @@ -352,9 +350,9 @@ describe('AzureLogAnalyticsDatasource', () => { templateSrv.init(Array.from(templateVariables.values()).map((item) => item.templateVariable)); const query = createMockQuery(); const azureLogAnalytics: { [index: string]: any } = {}; - for (const [path, templateVariable] of templateVariables.entries()) { - set(azureLogAnalytics, path, `$${templateVariable.variableName}`); - } + azureLogAnalytics.query = '$query'; + azureLogAnalytics.workspace = '$workspace'; + azureLogAnalytics.resources = ['$resource']; query.queryType = AzureQueryType.LogAnalytics; query.azureLogAnalytics = { ...query.azureLogAnalytics, @@ -362,9 +360,11 @@ describe('AzureLogAnalyticsDatasource', () => { }; const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {}); expect(templatedQuery[0]).toHaveProperty('datasource'); - for (const [path, templateVariable] of templateVariables.entries()) { - expect(get(templatedQuery[0].azureLogAnalytics, path)).toEqual(templateVariable.templateVariable.current.value); - } + expect(templatedQuery[0].azureLogAnalytics).toMatchObject({ + query: templateVariables.get('query')?.templateVariable.current.value, + workspace: templateVariables.get('workspace')?.templateVariable.current.value, + resources: [templateVariables.get('resource')?.templateVariable.current.value], + }); }); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 0fb89e1060c..42984e611b8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -66,7 +66,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< return ( item.hide !== true && !!item.azureLogAnalytics?.query && - (!!item.azureLogAnalytics.resource || !!item.azureLogAnalytics.workspace) + (!!item.azureLogAnalytics.resources?.length || !!item.azureLogAnalytics.workspace) ); } @@ -124,10 +124,10 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< } const templateSrv = getTemplateSrv(); - const resource = templateSrv.replace(item.resource, scopedVars); + const resources = item.resources?.map((r) => templateSrv.replace(r, scopedVars)); let workspace = templateSrv.replace(item.workspace, scopedVars); - if (!workspace && !resource && this.firstWorkspace) { + if (!workspace && !resources && this.firstWorkspace) { workspace = this.firstWorkspace; } @@ -140,7 +140,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< azureLogAnalytics: { resultFormat: item.resultFormat, query, - resource, + resources, // Workspace was removed in Grafana 8, but remains for backwards compat workspace, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 406e9d39891..98934049c87 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -43,12 +43,12 @@ describe('AzureMonitorDatasource', () => { }, { description: 'filter query with no resourceGroup', - query: createMockQuery({ azureMonitor: { resourceGroup: undefined } }), + query: createMockQuery({ azureMonitor: { resources: [{ resourceGroup: undefined }] } }), filtered: false, }, { description: 'filter query with no resourceName', - query: createMockQuery({ azureMonitor: { resourceName: undefined } }), + query: createMockQuery({ azureMonitor: { resources: [{ resourceName: undefined }] } }), filtered: false, }, { @@ -117,9 +117,8 @@ describe('AzureMonitorDatasource', () => { expect(templatedQuery).toMatchObject({ subscription, azureMonitor: { - resourceGroup, metricNamespace, - resourceName, + resources: [{ resourceGroup, resourceName }], }, }); }); @@ -346,8 +345,8 @@ describe('AzureMonitorDatasource', () => { it('should return a query with any template variables replaced', () => { const templateableProps = [ - 'resourceGroup', - 'resourceName', + 'resources[0].resourceGroup', + 'resources[0].resourceName', 'metricNamespace', 'timeGrain', 'aggregation', @@ -925,7 +924,7 @@ describe('AzureMonitorDatasource', () => { const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); query.queryType = AzureQueryType.AzureMonitor; - query.azureLogAnalytics = { resource: `$${singleVariable.name}` }; + query.azureLogAnalytics = { resources: [`$${singleVariable.name}`] }; expect(ds.targetContainsTemplate(query)).toEqual(false); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index ad4633dd32c..16c37b1b2e5 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -68,8 +68,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend 0 && + item.azureMonitor.resources.every((r) => hasValue(r.resourceGroup) && hasValue(r.resourceName)) && hasValue(item?.azureMonitor?.metricDefinition || item?.azureMonitor?.metricNamespace); const hasResourceUri = hasValue(item.azureMonitor?.resourceUri); @@ -97,8 +98,10 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend ({ + resourceGroup: templateSrv.replace(r.resourceGroup, scopedVars), + resourceName: templateSrv.replace(r.resourceName, scopedVars), + })); const metricNamespace = templateSrv.replace(item.metricNamespace, scopedVars); const customNamespace = templateSrv.replace(item.customNamespace, scopedVars); const timeGrain = templateSrv.replace((item.timeGrain || '').toString(), scopedVars); @@ -117,10 +120,10 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend = ({ ResourceRowType.Resource, ResourceRowType.Variable, ]} - resource={query.azureLogAnalytics?.resource ?? ''} + resource={ + query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length + ? query.azureLogAnalytics.resources[0] + : '' + } queryType="logs" /> diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx index 46fceccb2d9..c0ba93b623d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx @@ -33,12 +33,12 @@ const QueryField: React.FC = ({ query, datasource, o } useEffect(() => { - if (!query.azureLogAnalytics?.resource) { + if (!query.azureLogAnalytics?.resources || !query.azureLogAnalytics.resources.length) { return; } const promises = [ - datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resource), + datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]), getPromise(), ] as const; @@ -56,7 +56,7 @@ const QueryField: React.FC = ({ query, datasource, o worker?.setSchema(schema, 'https://help.kusto.windows.net', 'Samples'); }); }); - }, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resource]); + }, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resources]); const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => { monacoPromiseRef.current?.resolve?.({ editor, monaco }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts index 33ba224000e..9c5beb75421 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts @@ -9,7 +9,7 @@ async function migrateWorkspaceQueryToResourceQuery( query: AzureMonitorQuery, onChange: (newQuery: AzureMonitorQuery) => void ) { - if (query.azureLogAnalytics?.workspace !== undefined && !query.azureLogAnalytics.resource) { + if (query.azureLogAnalytics?.workspace !== undefined && !query.azureLogAnalytics.resources) { const isWorkspaceGUID = isGUIDish(query.azureLogAnalytics.workspace); let resource: string; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index 2022c0ac7ea..40ef8bf7025 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -81,8 +81,7 @@ describe('MetricsQueryEditor', () => { const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); const query = createMockQuery(); delete query?.subscription; - delete query?.azureMonitor?.resourceGroup; - delete query?.azureMonitor?.resourceName; + delete query?.azureMonitor?.resources; delete query?.azureMonitor?.metricNamespace; const onChange = jest.fn(); @@ -125,8 +124,12 @@ describe('MetricsQueryEditor', () => { subscription: 'def-456', azureMonitor: expect.objectContaining({ metricNamespace: 'microsoft.compute/virtualmachines', - resourceGroup: 'dev-3', - resourceName: 'web-server', + resources: [ + { + resourceGroup: 'dev-3', + resourceName: 'web-server', + }, + ], }), }) ); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx index b9e8251f358..1d1c8c89f4d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -39,9 +39,10 @@ const MetricsQueryEditor: React.FC = ({ const metricNames = useMetricNames(query, datasource, onChange, setError); const resource: AzureMetricResource = { subscription: query.subscription, - resourceGroup: query.azureMonitor?.resourceGroup, + resourceGroup: query.azureMonitor?.resources?.[0]?.resourceGroup, metricNamespace: query.azureMonitor?.metricNamespace, - resourceName: query.azureMonitor?.resourceName, + resourceName: query.azureMonitor?.resources?.[0]?.resourceName, + region: query.azureMonitor?.region, }; return ( diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts index 3ca999e8e25..20f7ab9cc88 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts @@ -45,13 +45,21 @@ describe('AzureMonitor: metrics dataHooks', () => { hook: useMetricNames, emptyQueryPartial: { metricNamespace: 'azure/vm', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], }, customProperties: { metricNamespace: 'azure/vm', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricName: 'metric-$ENVIRONMENT', }, expectedOptions: [ @@ -75,13 +83,21 @@ describe('AzureMonitor: metrics dataHooks', () => { hook: useMetricNamespaces, emptyQueryPartial: { metricNamespace: 'azure/vm', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], }, customProperties: { metricNamespace: 'azure/vm-$ENVIRONMENT', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricName: 'metric-name', }, expectedOptions: [ @@ -188,8 +204,12 @@ describe('AzureMonitor: metrics dataHooks', () => { name: 'useMetricMetadata', hook: useMetricMetadata, emptyQueryPartial: { - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricNamespace: 'azure/vm', metricName: 'Average CPU', }, @@ -240,8 +260,12 @@ describe('AzureMonitor: metrics dataHooks', () => { name: 'useMetricNamespaces', hook: useMetricNamespaces, emptyQueryPartial: { - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricNamespace: 'azure/vm', }, customProperties: {}, @@ -264,7 +288,11 @@ describe('AzureMonitor: metrics dataHooks', () => { expect(result.current).toEqual(metricNamespacesConfig.expectedOptions); expect(datasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith( - expect.objectContaining(metricNamespacesConfig.emptyQueryPartial), + expect.objectContaining({ + resourceGroup: 'rg', + resourceName: 'rn', + metricNamespace: 'azure/vm', + }), // Here, "global" should be false false ); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts index 19185023676..b6edf825d7c 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts @@ -4,7 +4,7 @@ import { rangeUtil } from '@grafana/data'; import Datasource from '../../datasource'; import TimegrainConverter from '../../time_grain_converter'; -import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; +import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, AzureMonitorResource } from '../../types'; import { toOption } from '../../utils/common'; import { useAsyncState } from '../../utils/useAsyncState'; @@ -38,9 +38,20 @@ export interface MetricMetadata { type OnChangeFn = (newQuery: AzureMonitorQuery) => void; +const getResourceGroupAndName = (resources?: AzureMonitorResource[]) => { + if (!resources || !resources.length) { + return { resourceGroup: '', resourceName: '' }; + } + return { + resourceGroup: resources[0].resourceGroup ?? '', + resourceName: resources[0].resourceName ?? '', + }; +}; + export const useMetricNamespaces: DataHook = (query, datasource, onChange, setError) => { const { subscription } = query; - const { metricNamespace, resourceGroup, resourceName } = query.azureMonitor ?? {}; + const { metricNamespace, resources } = query.azureMonitor ?? {}; + const { resourceGroup, resourceName } = getResourceGroupAndName(resources); const metricNamespaces = useAsyncState( async () => { @@ -75,7 +86,8 @@ export const useMetricNamespaces: DataHook = (query, datasource, onChange, setEr export const useMetricNames: DataHook = (query, datasource, onChange, setError) => { const { subscription } = query; - const { metricNamespace, metricName, resourceGroup, resourceName, customNamespace } = query.azureMonitor ?? {}; + const { metricNamespace, metricName, resources, customNamespace } = query.azureMonitor ?? {}; + const { resourceGroup, resourceName } = getResourceGroupAndName(resources); return useAsyncState( async () => { @@ -110,8 +122,8 @@ const defaultMetricMetadata: MetricMetadata = { export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasource, onChange: OnChangeFn) => { const [metricMetadata, setMetricMetadata] = useState(defaultMetricMetadata); const { subscription } = query; - const { resourceGroup, resourceName, metricNamespace, metricName, aggregation, timeGrain, customNamespace } = - query.azureMonitor ?? {}; + const { resources, metricNamespace, metricName, aggregation, timeGrain, customNamespace } = query.azureMonitor ?? {}; + const { resourceGroup, resourceName } = getResourceGroupAndName(resources); // Fetch new metric metadata when the fields change useEffect(() => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx index 18e5c3f800d..4098d27b9ac 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx @@ -63,8 +63,9 @@ const ResourceField: React.FC> > resources && handleApply(resources[0])} onCancel={closePicker} selectableEntryTypes={selectableEntryTypes} queryType={queryType} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx index e9efb20c08d..f4f55d70b0d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx @@ -7,29 +7,38 @@ import Advanced from './Advanced'; describe('AzureMonitor ResourcePicker', () => { it('should set a parameter as an object', async () => { const onChange = jest.fn(); - const { rerender } = render(); + const { rerender } = render(); const advancedSection = screen.getByText('Advanced'); advancedSection.click(); const subsInput = await screen.findByLabelText('Subscription'); await userEvent.type(subsInput, 'd'); - expect(onChange).toHaveBeenCalledWith({ subscription: 'd' }); + expect(onChange).toHaveBeenCalledWith([{ subscription: 'd' }]); - rerender(); + rerender(); expect(screen.getByLabelText('Subscription').outerHTML).toMatch('value="def-123"'); }); it('should set a parameter as uri', async () => { const onChange = jest.fn(); - const { rerender } = render(); + const { rerender } = render(); const advancedSection = screen.getByText('Advanced'); advancedSection.click(); const subsInput = await screen.findByLabelText('Resource URI'); await userEvent.type(subsInput, '/'); - expect(onChange).toHaveBeenCalledWith('/'); + expect(onChange).toHaveBeenCalledWith(['/']); - rerender(); + rerender(); expect(screen.getByLabelText('Resource URI').outerHTML).toMatch('value="/subscriptions/sub"'); }); + + it('should render multiple resources', async () => { + render(); + const advancedSection = screen.getByText('Advanced'); + advancedSection.click(); + + expect(screen.getByDisplayValue('/subscriptions/sub1')).toBeInTheDocument(); + expect(screen.getByDisplayValue('/subscriptions/sub2')).toBeInTheDocument(); + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx index 2a6958a50b7..15c20289dd7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx @@ -7,12 +7,18 @@ import { AzureMetricResource } from '../../types'; import { Space } from '../Space'; interface ResourcePickerProps { - resource: T; - onChange: (resource: T) => void; + resources: T[]; + onChange: (resources: T[]) => void; } -const Advanced = ({ resource, onChange }: ResourcePickerProps) => { - const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resource && JSON.stringify(resource).includes('$')); +const Advanced = ({ resources, onChange }: ResourcePickerProps) => { + const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resources.length && JSON.stringify(resources).includes('$')); + + const onResourceChange = (resource: string | AzureMetricResource, index: number) => { + const newResources = [...resources]; + newResources[index] = resource; + onChange(newResources); + }; return (
@@ -22,104 +28,115 @@ const Advanced = ({ resource, onChange }: ResourcePickerProps setIsAdvancedOpen(!isAdvancedOpen)} > - {typeof resource === 'string' ? ( - <> - {' '} - - onChange(event.currentTarget.value)} - placeholder="ex: /subscriptions/$subId" - /> - - ) : ( - <> - - onChange({ ...resource, subscription: event.currentTarget.value })} - placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee" - /> - - - onChange({ ...resource, resourceGroup: event.currentTarget.value })} - placeholder="resource-group" - /> - - - onChange({ ...resource, metricNamespace: event.currentTarget.value })} - placeholder="Microsoft.Insights/metricNamespaces" - /> - - - onChange({ ...resource, resourceName: event.currentTarget.value })} - placeholder="name" - /> - - - )} + + onResourceChange({ ...resource, subscription: event.currentTarget.value }, index) + } + placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee" + /> + + + + onResourceChange({ ...resource, resourceGroup: event.currentTarget.value }, index) + } + placeholder="resource-group" + /> + + + + onResourceChange({ ...resource, metricNamespace: event.currentTarget.value }, index) + } + placeholder="Microsoft.Insights/metricNamespaces" + /> + + + + onResourceChange({ ...resource, resourceName: event.currentTarget.value }, index) + } + placeholder="name" + /> + + + )} +
+ ))} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx index 15d11998a84..eea2a9cfbb3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx @@ -76,7 +76,7 @@ const NestedRow: React.FC = ({
- + {isOpen && diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx index 4e2922b4568..6537802765d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx @@ -17,6 +17,15 @@ import { ResourceRowType } from './types'; import ResourcePicker from '.'; +jest.mock('@grafana/runtime', () => ({ + ...(jest.requireActual('@grafana/runtime') as unknown as object), + getTemplateSrv: () => ({ + replace: (val: string) => { + return val; + }, + }), +})); + const noResourceURI = ''; const singleSubscriptionSelectionURI = '/subscriptions/def-456'; const singleResourceGroupSelectionURI = '/subscriptions/def-456/resourceGroups/dev-3'; @@ -50,7 +59,7 @@ const queryType: ResourcePickerQueryType = 'logs'; const defaultProps = { templateVariables: [], - resource: noResourceURI, + resources: [noResourceURI], resourcePickerData: createMockResourcePickerData(), onCancel: noop, onApply: noop, @@ -68,7 +77,7 @@ describe('AzureMonitor ResourcePicker', () => { window.HTMLElement.prototype.scrollIntoView = jest.fn(); }); it('should pre-load subscriptions when there is no existing selection', async () => { - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -77,7 +86,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('should show a subscription as selected if there is one saved', async () => { - render(); + render(); const subscriptionCheckboxes = await screen.findAllByLabelText('Dev Subscription'); expect(subscriptionCheckboxes.length).toBe(2); expect(subscriptionCheckboxes[0]).toBeChecked(); @@ -85,7 +94,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('should show a resourceGroup as selected if there is one saved', async () => { - render(); + render(); const resourceGroupCheckboxes = await screen.findAllByLabelText('A Great Resource Group'); expect(resourceGroupCheckboxes.length).toBe(2); expect(resourceGroupCheckboxes[0]).toBeChecked(); @@ -93,7 +102,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('should show scroll down to a resource and mark it as selected if there is one saved', async () => { - render(); + render(); const resourceCheckboxes = await screen.findAllByLabelText('db-server'); expect(resourceCheckboxes.length).toBe(2); expect(resourceCheckboxes[0]).toBeChecked(); @@ -101,7 +110,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('opens the selected nested resources', async () => { - render(); + render(); const collapseSubscriptionBtn = await screen.findByLabelText('Collapse Dev Subscription'); expect(collapseSubscriptionBtn).toBeInTheDocument(); const collapseResourceGroupBtn = await screen.findByLabelText('Collapse A Great Resource Group'); @@ -109,7 +118,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('scrolls down to the selected resource', async () => { - render(); + render(); await screen.findByLabelText('Collapse A Great Resource Group'); expect(window.HTMLElement.prototype.scrollIntoView).toBeCalledTimes(1); }); @@ -133,12 +142,40 @@ describe('AzureMonitor ResourcePicker', () => { const applyButton = screen.getByRole('button', { name: 'Apply' }); applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith('/subscriptions/def-123'); + expect(onApply).toBeCalledWith(['/subscriptions/def-123']); + }); + + it('should call onApply removing an element', async () => { + const onApply = jest.fn(); + render(); + const subscriptionCheckbox = await screen.findAllByLabelText('Primary Subscription'); + expect(subscriptionCheckbox).toHaveLength(2); + expect(subscriptionCheckbox.at(0)).toBeChecked(); + subscriptionCheckbox.at(0)?.click(); + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + expect(onApply).toBeCalledTimes(1); + expect(onApply).toBeCalledWith([]); + }); + + it('should call onApply removing an element ignoring the case', async () => { + const onApply = jest.fn(); + render( + + ); + const subscriptionCheckbox = await screen.findAllByLabelText('A Great Resource Group'); + expect(subscriptionCheckbox).toHaveLength(2); + expect(subscriptionCheckbox.at(0)).toBeChecked(); + subscriptionCheckbox.at(0)?.click(); + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + expect(onApply).toBeCalledTimes(1); + expect(onApply).toBeCalledWith([]); }); it('should call onApply with a new subscription when a user clicks on the checkbox in the row', async () => { const onApply = jest.fn(); - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -146,7 +183,20 @@ describe('AzureMonitor ResourcePicker', () => { const applyButton = screen.getByRole('button', { name: 'Apply' }); applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith({ subscription: 'def-123' }); + expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]); + }); + + it('should call onApply removing a resource element', async () => { + const onApply = jest.fn(); + render(); + const subscriptionCheckbox = await screen.findAllByLabelText('Primary Subscription'); + expect(subscriptionCheckbox).toHaveLength(2); + expect(subscriptionCheckbox.at(0)).toBeChecked(); + subscriptionCheckbox.at(0)?.click(); + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + expect(onApply).toBeCalledTimes(1); + expect(onApply).toBeCalledWith([]); }); it('should call onApply with a new subscription uri when a user types it in the selection box', async () => { @@ -166,12 +216,12 @@ describe('AzureMonitor ResourcePicker', () => { applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith('/subscriptions/def-123'); + expect(onApply).toBeCalledWith(['/subscriptions/def-123']); }); it('should call onApply with a new subscription when a user types it in the selection box', async () => { const onApply = jest.fn(); - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -186,11 +236,11 @@ describe('AzureMonitor ResourcePicker', () => { applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith({ subscription: 'def-123' }); + expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]); }); it('should show unselect a subscription if the value is manually edited', async () => { - render(); + render(); const subscriptionCheckboxes = await screen.findAllByLabelText('Dev Subscription'); expect(subscriptionCheckboxes.length).toBe(2); expect(subscriptionCheckboxes[0]).toBeChecked(); @@ -264,7 +314,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('resets result when the user clears their search', async () => { - render(); + render(); const subscriptionCheckboxBeforeSearch = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckboxBeforeSearch).toBeInTheDocument(); @@ -295,7 +345,7 @@ describe('AzureMonitor ResourcePicker', () => { {...defaultProps} queryType={'metrics'} resourcePickerData={resourcePickerData} - resource={noResourceURI} + resources={[noResourceURI]} /> ); const subscriptionExpand = await screen.findByLabelText('Expand Primary Subscription'); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx index 4b9b1fe4f19..aa9a5ffbb61 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx @@ -15,21 +15,21 @@ import NestedRow from './NestedRow'; import Search from './Search'; import getStyles from './styles'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types'; -import { findRow, parseResourceDetails, resourceToString } from './utils'; +import { findRows, parseMultipleResourceDetails, resourcesToStrings, matchURI, resourceToString } from './utils'; interface ResourcePickerProps { resourcePickerData: ResourcePickerData; - resource: T; + resources: T[]; selectableEntryTypes: ResourceRowType[]; queryType: ResourcePickerQueryType; - onApply: (resource?: T) => void; + onApply: (resources: T[]) => void; onCancel: () => void; } const ResourcePicker = ({ resourcePickerData, - resource, + resources, onApply, onCancel, selectableEntryTypes, @@ -40,14 +40,14 @@ const ResourcePicker = ({ const [isLoading, setIsLoading] = useState(false); const [rows, setRows] = useState([]); const [selectedRows, setSelectedRows] = useState([]); - const [internalSelected, setInternalSelected] = useState(resource); + const [internalSelected, setInternalSelected] = useState(resources); const [errorMessage, setErrorMessage] = useState(undefined); const [shouldShowLimitFlag, setShouldShowLimitFlag] = useState(false); // Sync the resourceURI prop to internal state useEffect(() => { - setInternalSelected(resource); - }, [resource]); + setInternalSelected(resources); + }, [resources]); const loadInitialData = useCallback(async () => { if (!isLoading) { @@ -55,7 +55,7 @@ const ResourcePicker = ({ setIsLoading(true); const resources = await resourcePickerData.fetchInitialRows( queryType, - parseResourceDetails(internalSelected ?? {}) + parseMultipleResourceDetails(internalSelected ?? {}) ); setRows(resources); } catch (error) { @@ -75,14 +75,9 @@ const ResourcePicker = ({ setSelectedRows([]); } - const found = internalSelected && findRow(rows, resourceToString(internalSelected)); - if (found) { - return setSelectedRows([ - { - ...found, - children: undefined, - }, - ]); + const found = internalSelected && findRows(rows, resourcesToStrings(internalSelected)); + if (found && found.length) { + return setSelectedRows(found); } return setSelectedRows([]); }, [internalSelected, rows]); @@ -109,19 +104,29 @@ const ResourcePicker = ({ [resourcePickerData, rows, queryType] ); - const resourceIsString = typeof resource === 'string'; + const resourceIsString = resources?.length && typeof resources[0] === 'string'; const handleSelectionChanged = useCallback( (row: ResourceRow, isSelected: boolean) => { - isSelected - ? setInternalSelected(resourceIsString ? row.uri : parseResourceDetails(row.uri)) - : setInternalSelected(resourceIsString ? '' : {}); + if (isSelected) { + const newRes = resourceIsString ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0]; + const newSelected = (internalSelected ? internalSelected.concat(newRes) : [newRes]).filter((r) => { + // avoid setting empty resources + return typeof r === 'string' ? r !== '' : r.subscription; + }); + setInternalSelected(newSelected); + } else { + const newInternalSelected = internalSelected?.filter((r) => { + return !matchURI(resourceToString(r), row.uri); + }); + setInternalSelected(newInternalSelected); + } }, - [resourceIsString] + [resourceIsString, internalSelected, setInternalSelected] ); const handleApply = useCallback(() => { if (internalSelected) { - onApply(resourceIsString ? internalSelected : parseResourceDetails(internalSelected)); + onApply(resourceIsString ? internalSelected : parseMultipleResourceDetails(internalSelected)); } }, [resourceIsString, internalSelected, onApply]); @@ -230,7 +235,7 @@ const ResourcePicker = ({ )} - setInternalSelected(r)} /> + setInternalSelected(r)} />
{row.typeLabel}{row.location ?? '-'}{row.locationDisplayName ?? '-'}