Merge branch 'main' into leeoniya/timeseries-exemplars-rewrite

This commit is contained in:
Leon Sorokin
2023-01-17 14:57:22 -06:00
439 changed files with 8497 additions and 5637 deletions
+9 -44
View File
@@ -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
View File
@@ -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
+3
View File
@@ -33,3 +33,6 @@ public/openapi3.json
# Generated Kinds report
kinds/report.json
# Generated schema docs
docs/sources/developers/kinds/
+2
View File
@@ -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
View File
@@ -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)
@@ -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`. |
@@ -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. |
@@ -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
+10
View File
@@ -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) |
@@ -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
+10 -8
View File
@@ -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
+20 -14
View File
@@ -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=
+1
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -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,
+22 -8
View File
@@ -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 [];
@@ -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)
@@ -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
View File
@@ -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
View File
@@ -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
+9 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+1 -1
View File
@@ -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")
+5 -5
View File
@@ -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)
+10 -10
View File
@@ -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
View File
@@ -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
}
+5 -5
View File
@@ -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()
+1
View File
@@ -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{}{
+4 -1
View File
@@ -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")
+30
View File
@@ -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)
+5 -3
View File
@@ -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,
}
+4 -3
View File
@@ -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{}{
-18
View File
@@ -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)
}
}
+6 -5
View File
@@ -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
View File
@@ -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)
+5 -4
View File
@@ -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
View File
@@ -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
+6 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+5 -5
View File
@@ -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)
}
+1 -1
View File
@@ -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)
+490
View File
@@ -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, "|", "&#124;")
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)
}
+2 -26
View File
@@ -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
}
+13
View File
@@ -0,0 +1,13 @@
---
keywords:
- grafana
- schema
title: {{ .KindName }} kind
---
# {{ .KindName }} kind
### Maturity: {{ .KindMaturity }}
### Version: {{ .KindVersion }}
{{ .Markdown }}
+36 -29
View File
@@ -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 != "" {
+7 -6
View File
@@ -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 -3
View File
@@ -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
+144
View File
@@ -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)
})
}
}
+1
View File
@@ -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,
}
}
+3 -2
View File
@@ -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
+45 -1
View File
@@ -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))
-10
View File
@@ -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"`
}
+9 -3
View File
@@ -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"
}
-16
View File
@@ -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
-8
View File
@@ -1,8 +0,0 @@
package models
import "github.com/grafana/grafana/pkg/services/user"
type SetUserHelpFlagCommand struct {
HelpFlags1 user.HelpFlags1
UserId int64
}
-8
View File
@@ -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
+5 -4
View File
@@ -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 {
+4 -4
View File
@@ -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) {
+2 -2
View File
@@ -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
}
+5 -5
View File
@@ -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.
+2 -2
View File
@@ -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