Merge branch 'main' into leeoniya/timeseries-exemplars-rewrite
This commit is contained in:
+9
-44
@@ -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"],
|
||||
|
||||
+8
-8
@@ -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
|
||||
|
||||
@@ -33,3 +33,6 @@ public/openapi3.json
|
||||
|
||||
# Generated Kinds report
|
||||
kinds/report.json
|
||||
|
||||
# Generated schema docs
|
||||
docs/sources/developers/kinds/
|
||||
|
||||
@@ -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]
|
||||
|
||||
+10
-6
@@ -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)
|
||||
|
||||
+3
@@ -95,6 +95,8 @@ The following list contains role-based access control actions.
|
||||
| `orgs:read` | `orgs:*` <br> `orgs:id:*` | Read one or more organizations. |
|
||||
| `orgs:write` | `orgs:*` <br> `orgs:id:*` | Update one or more organizations. |
|
||||
| `plugins.app:access` | `plugins:*` <br> `plugins:id:*` | Access one or more application plugins (still enforcing the organization role) |
|
||||
| `plugins:install` | n/a | Install and uninstall plugins. |
|
||||
| `plugins:write` | `plugins:*` <br> `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:*` <br> `reports:id:*` | Update reports. |
|
||||
@@ -196,6 +198,7 @@ The following list contains role-based access control scopes.
|
||||
| `orgs:*` <br> `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:*` <br> `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:*` <br> `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:*` <br> `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`. |
|
||||
|
||||
+8
-6
@@ -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`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixed:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader`<br>`fixed:plugins.app:reader`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Basic role | Associated fixed roles | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| Grafana Admin | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:plugins:maintainer` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixed:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning:writer`<br>`fixed:datasources.caching:reader`<br>`fixed:datasources.caching:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:plugins:writer` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:writer`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. |
|
||||
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader`<br>`fixed:plugins.app:reader`<br>`fixed:dashboards.insights:reader`<br>`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`<br>`orgs.quotas:read` | Read an organization and its quotas. |
|
||||
| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs.preferences:read`<br>`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`<br>`reports:send`<br>`reports.settings:read` | Read all reports and shared report settings. |
|
||||
| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and <br>`reports:create`<br>`reports:write`<br>`reports:delete`<br>`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. |
|
||||
|
||||
+3
-3
@@ -524,9 +524,9 @@ policies:
|
||||
- severity =~ "warning|critical"
|
||||
# <list> 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']
|
||||
# <list> 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Grafana schema
|
||||
weight: 200
|
||||
_build:
|
||||
list: false
|
||||
---
|
||||
|
||||
# Grafana schema
|
||||
|
||||
{{< section >}}
|
||||
@@ -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 >}}
|
||||
@@ -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).<br/>1 for shared crosshair.<br/>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<br/>changes to said schema.<br/>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.<br/>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<br/>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<br/>TODO docs<br/>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]`. |
|
||||
|
||||
|
||||
@@ -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.<br/>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<br/>creator of the playlist of by the application. |
|
||||
| `items` | [PlaylistItem](#playlistitem)[] | No | The ordered list of items that the playlist will iterate over.<br/>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.<br/><br/> - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This<br/> is not portable as the numerical identifier is non-deterministic between different instances.<br/> Will be replaced by dashboard_by_uid in the future. (deprecated)<br/> - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All<br/> dashboards behind the tag will be added to the playlist.<br/> - dashboard_by_uid: The value is the dashboard UID |
|
||||
| `title` | string | No | Title is an unused property -- it will be removed in the future |
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|----------|------|----------|-------------|
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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) |
|
||||
|
||||
+28
-16
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"))
|
||||
|
||||
+3
-8
@@ -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"
|
||||
}
|
||||
|
||||
@@ -224,5 +224,6 @@ export interface AuthSettings {
|
||||
SAMLSkipOrgRoleSync?: boolean;
|
||||
LDAPSkipOrgRoleSync?: boolean;
|
||||
GrafanaComSkipOrgRoleSync?: boolean;
|
||||
AzureADSkipOrgRoleSync?: boolean;
|
||||
DisableSyncLock?: boolean;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export interface FeatureToggles {
|
||||
panelTitleSearch?: boolean;
|
||||
tempoApmTable?: boolean;
|
||||
prometheusAzureOverrideAudience?: boolean;
|
||||
influxdbBackendMigration?: boolean;
|
||||
showFeatureFlagsInUI?: boolean;
|
||||
publicDashboards?: boolean;
|
||||
publicDashboardsEmailSharing?: boolean;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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({})}
|
||||
|
||||
@@ -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-')) {
|
||||
|
||||
@@ -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<typeof PanelChrome> = {
|
||||
title: 'Visualizations/PanelChrome',
|
||||
component: PanelChrome,
|
||||
@@ -95,34 +93,33 @@ export const Examples = () => {
|
||||
<DashboardStoryCanvas>
|
||||
<HorizontalGroup spacing="md" align="flex-start">
|
||||
<VerticalGroup spacing="md">
|
||||
{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,
|
||||
})}
|
||||
</VerticalGroup>
|
||||
<VerticalGroup spacing="md">
|
||||
{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;
|
||||
|
||||
@@ -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: [<div key="title-item-test"> This should be a self-contained node </div>],
|
||||
});
|
||||
|
||||
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: [<div key="title-item-test"> This should be a self-contained node </div>],
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('title-items-container')).toBeInTheDocument();
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.errorContainer}>
|
||||
<PanelStatus message={status?.message} onClick={status?.onClick} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={styles.container} style={containerStyles}>
|
||||
<div className={styles.container} style={containerStyles} aria-label={ariaLabel}>
|
||||
<div className={styles.loadingBarContainer}>
|
||||
{showLoading ? <LoadingBar width={'28%'} height={'2px'} /> : null}
|
||||
{loadingState === LoadingState.Loading ? <LoadingBar width={'28%'} height={'2px'} /> : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.headerContainer} style={headerStyles} data-testid="header-container">
|
||||
@@ -127,29 +114,19 @@ export function PanelChrome({
|
||||
</h6>
|
||||
)}
|
||||
|
||||
{showStreaming && (
|
||||
<div className={styles.item} style={itemStyles}>
|
||||
<Tooltip content="Streaming">
|
||||
<Icon name="circle" type="mono" size="sm" className={styles.streaming} />
|
||||
</Tooltip>
|
||||
<PanelDescription description={description} />
|
||||
|
||||
{titleItems.length > 0 && (
|
||||
<div className={styles.titleItems} data-testid="title-items-container">
|
||||
{titleItems.map((item) => item)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{titleItems.length > 0 && (
|
||||
<div className={styles.items} data-testid="title-items-container">
|
||||
{titleItems
|
||||
.filter((item) => isIconName(item.icon))
|
||||
.map((item, i) => (
|
||||
<div key={`${item.icon}-${i}`} className={styles.item} style={itemStyles}>
|
||||
{item.onClick ? (
|
||||
<IconButton tooltip={item.tooltip} name={item.icon} size="sm" onClick={item.onClick} />
|
||||
) : (
|
||||
<Tooltip content={item.tooltip ?? ''}>
|
||||
<Icon name={item.icon} size="sm" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{loadingState === LoadingState.Streaming && (
|
||||
<div className={styles.item} style={itemStyles}>
|
||||
<Tooltip content="Streaming">
|
||||
<Icon name="circle-mono" size="sm" className={styles.streaming} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -167,10 +144,14 @@ export function PanelChrome({
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{isUsingDeprecatedLeftItems && <div className={styles.items}>{itemsRenderer(leftItems, (item) => item)}</div>}
|
||||
{leftItems && <div className={styles.items}>{itemsRenderer(leftItems, (item) => item)}</div>}
|
||||
</div>
|
||||
|
||||
{renderStatus()}
|
||||
{statusMessage && (
|
||||
<div className={styles.errorContainer}>
|
||||
<PanelStatus message={statusMessage} onClick={statusMessageOnClick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.content} style={contentStyle}>
|
||||
@@ -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),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<div className="panel-info-content markdown-html">
|
||||
<div dangerouslySetInnerHTML={{ __html: panelDescription }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return description !== '' ? (
|
||||
<Tooltip interactive content={getDescriptionContent}>
|
||||
<span className={styles.description}>
|
||||
<Icon name="info-circle" size="lg" aria-label="description" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : 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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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<TableCellProps> = (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 = () => {
|
||||
|
||||
@@ -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<TableCellProps> = (props) => {
|
||||
const { field, cell, tableStyles, row, cellProps } = props;
|
||||
@@ -27,7 +28,8 @@ export const DefaultCell: FC<TableCellProps> = (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<TableCellProps> = (props) => {
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
|
||||
{(api) => {
|
||||
return (
|
||||
<div onClick={api.openMenu} className={getLinkStyle(tableStyles, field, api.targetClassName)}>
|
||||
<div onClick={api.openMenu} className={getLinkStyle(tableStyles, cellOptions, api.targetClassName)}>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
@@ -53,49 +55,24 @@ export const DefaultCell: FC<TableCellProps> = (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);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ImageCell: FC<TableCellProps> = (props) => {
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
{!hasLinks && <img src={displayValue.text} className={tableStyles.imageCell} alt="" />}
|
||||
{hasLinks && (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
|
||||
<DataLinksContextMenu style={{ height: '100%' }} links={() => getCellLinks(field, row) || []}>
|
||||
{(api) => {
|
||||
return (
|
||||
<div onClick={api.openMenu} className={cx(tableStyles.imageCellLink, api.targetClassName)}>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<Portal>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const PlotLegend: React.FC<PlotLegendProps> = 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 [];
|
||||
|
||||
+59
-293
@@ -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('<SpanGraph>', () => {
|
||||
describe('<UnthemedViewingLayer>', () => {
|
||||
polyfillAnimationFrame(window);
|
||||
|
||||
let props: ViewingLayerProps;
|
||||
let wrapper: ShallowWrapper<ViewingLayerProps, {}, UnthemedViewingLayer>;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
@@ -47,302 +41,74 @@ describe('<SpanGraph>', () => {
|
||||
updateViewRangeTime: jest.fn(),
|
||||
viewRange: getViewRange(0, 1),
|
||||
} as unknown as ViewingLayerProps;
|
||||
|
||||
wrapper = shallow(<ViewingLayer {...props} />)
|
||||
.dive()
|
||||
.dive();
|
||||
});
|
||||
|
||||
describe('_getDraggingBounds()', () => {
|
||||
beforeEach(() => {
|
||||
props = { ...props, viewRange: getViewRange(0.1, 0.9) };
|
||||
wrapper = shallow(<ViewingLayer {...props} />)
|
||||
.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(<ViewingLayer {...props} />);
|
||||
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(<ViewingLayer {...props} />)
|
||||
.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(<ViewingLayer {...props} />)
|
||||
.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(<ViewingLayer {...props} />)
|
||||
.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(<ViewingLayer {...props} />);
|
||||
expect(screen.getByTestId('ViewingLayerCursorGuide')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders a <GraphTicks />', () => {
|
||||
expect(wrapper.find(GraphTicks).length).toBe(1);
|
||||
it('renders <GraphTicks />', () => {
|
||||
render(<ViewingLayer {...props} />);
|
||||
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(<ViewingLayer {...props} />);
|
||||
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(<ViewingLayer {...props} />);
|
||||
expect(screen.getAllByTestId('scrubber-component')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders a filtering box if leftBound exists', () => {
|
||||
const _props = { ...props, viewRange: getViewRange(0.2, 1) };
|
||||
wrapper = shallow(<ViewingLayer {..._props} />)
|
||||
.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(<ViewingLayer {...props} />);
|
||||
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(<ViewingLayer {..._props} />)
|
||||
.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(<ViewingLayer {...props} />);
|
||||
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 = <Scrubber {...({ position: viewStart } as ScrubberProps)} />;
|
||||
expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy();
|
||||
scrubber = <Scrubber {...({ position: viewEnd } as ScrubberProps)} />;
|
||||
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(<ViewingLayer {...props} />);
|
||||
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(<ViewingLayer {...props} />);
|
||||
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(<ViewingLayer {...props} />);
|
||||
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(<ViewingLayer {...props} />);
|
||||
const button = screen.queryByRole('button', { hidden: true })!;
|
||||
await userEvent.click(button);
|
||||
expect(props.updateViewRangeTime).toHaveBeenCalledWith(0, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,7 +364,14 @@ export class UnthemedViewingLayer extends React.PureComponent<ViewingLayerProps,
|
||||
onMouseMove={this._draggerReframe.handleMouseMove}
|
||||
>
|
||||
{leftInactive > 0 && (
|
||||
<rect x={0} y={0} height="100%" width={`${leftInactive}%`} className={styles.ViewingLayerInactive} />
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
height="100%"
|
||||
width={`${leftInactive}%`}
|
||||
className={styles.ViewingLayerInactive}
|
||||
data-testid="left-ViewingLayerInactive"
|
||||
/>
|
||||
)}
|
||||
{rightInactive > 0 && (
|
||||
<rect
|
||||
@@ -373,6 +380,7 @@ export class UnthemedViewingLayer extends React.PureComponent<ViewingLayerProps,
|
||||
height="100%"
|
||||
width={`${rightInactive}%`}
|
||||
className={styles.ViewingLayerInactive}
|
||||
data-testid="right-ViewingLayerInactive"
|
||||
/>
|
||||
)}
|
||||
<GraphTicks numTicks={numTicks} />
|
||||
@@ -384,6 +392,7 @@ export class UnthemedViewingLayer extends React.PureComponent<ViewingLayerProps,
|
||||
x2={cursorPosition}
|
||||
y2={height - 2}
|
||||
strokeWidth="1"
|
||||
data-testid="ViewingLayerCursorGuide"
|
||||
/>
|
||||
)}
|
||||
{shiftStart != null && this._getMarkers(viewStart, shiftStart)}
|
||||
|
||||
@@ -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<SetStateAction<string>>;
|
||||
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
|
||||
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)
|
||||
|
||||
+42
-193
@@ -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-<id>), 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('<TimelineViewingLayer>', () => {
|
||||
let wrapper: ReactWrapper<TimelineViewingLayerProps, {}, TimelineViewingLayer>;
|
||||
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('<TimelineViewingLayer>', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props.updateNextViewRangeTime.mockReset();
|
||||
props.updateViewRangeTime.mockReset();
|
||||
wrapper = mount(<TimelineViewingLayer {...(props as unknown as TimelineViewingLayerProps)} />);
|
||||
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(<TimelineViewingLayer {...props} />);
|
||||
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(<TimelineViewingLayer {...props} />);
|
||||
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(<TimelineViewingLayer {...props} />);
|
||||
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(<TimelineViewingLayer {...props} />);
|
||||
expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument();
|
||||
|
||||
viewRangeTime = { ...baseViewRangeTime, shiftEnd: cursor };
|
||||
props = { ...props, viewRangeTime };
|
||||
render(<TimelineViewingLayer {...props} />);
|
||||
expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument();
|
||||
|
||||
viewRangeTime = { ...baseViewRangeTime, reframe: { anchor: cursor, shift: cursor } };
|
||||
props = { ...props, viewRangeTime };
|
||||
render(<TimelineViewingLayer {...props} />);
|
||||
expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<VirtualizedTra
|
||||
}
|
||||
|
||||
scrollToTop = () => {
|
||||
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() {
|
||||
|
||||
@@ -216,6 +216,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
|
||||
currentViewRangeTime={viewRange.time.current}
|
||||
topOfViewRef={topOfViewRef}
|
||||
focusedSpanIdForSearch={focusedSpanIdForSearch}
|
||||
datasourceType={this.props.datasourceType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+3
-2
@@ -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"`
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-17
@@ -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)
|
||||
|
||||
|
||||
+54
-54
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+48
-47
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+123
-78
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
+7
-6
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.<types> 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", "<br/>")
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
keywords:
|
||||
- grafana
|
||||
- schema
|
||||
title: {{ .KindName }} kind
|
||||
---
|
||||
|
||||
# {{ .KindName }} kind
|
||||
|
||||
### Maturity: {{ .KindMaturity }}
|
||||
### Version: {{ .KindVersion }}
|
||||
|
||||
{{ .Markdown }}
|
||||
+36
-29
@@ -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 != "" {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/user"
|
||||
|
||||
type SetUserHelpFlagCommand struct {
|
||||
HelpFlags1 user.HelpFlags1
|
||||
UserId int64
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user