Compare commits

...

16 Commits

Author SHA1 Message Date
dependabot[bot] 80137abe3f deps(go): bump the aws-sdk-go group across 1 directory with 11 updates
Bumps the aws-sdk-go group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) | `1.55.7` | `1.55.8` |
| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.40.0` | `1.41.0` |
| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `1.18.21` | `1.19.6` |
| [github.com/aws/aws-sdk-go-v2/service/cloudwatch](https://github.com/aws/aws-sdk-go-v2) | `1.45.3` | `1.53.0` |
| [github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs](https://github.com/aws/aws-sdk-go-v2) | `1.51.0` | `1.63.0` |
| [github.com/aws/aws-sdk-go-v2/service/ec2](https://github.com/aws/aws-sdk-go-v2) | `1.225.2` | `1.279.0` |
| [github.com/aws/aws-sdk-go-v2/service/oam](https://github.com/aws/aws-sdk-go-v2) | `1.18.3` | `1.23.10` |
| [github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi](https://github.com/aws/aws-sdk-go-v2) | `1.26.6` | `1.31.5` |
| [github.com/aws/aws-sdk-go-v2/service/secretsmanager](https://github.com/aws/aws-sdk-go-v2) | `1.40.1` | `1.41.0` |



Updates `github.com/aws/aws-sdk-go` from 1.55.7 to 1.55.8
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Changelog](https://github.com/aws/aws-sdk-go/blob/main/CHANGELOG_PENDING.md)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.55.7...v1.55.8)

Updates `github.com/aws/aws-sdk-go-v2` from 1.40.0 to 1.41.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.40.0...v1.41.0)

Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.18.21 to 1.19.6
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.18.21...service/m2/v1.19.6)

Updates `github.com/aws/aws-sdk-go-v2/service/cloudwatch` from 1.45.3 to 1.53.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/rds/v1.45.3...service/s3/v1.53.0)

Updates `github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs` from 1.51.0 to 1.63.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.51.0...service/s3/v1.63.0)

Updates `github.com/aws/aws-sdk-go-v2/service/ec2` from 1.225.2 to 1.279.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/ec2/v1.225.2...service/ec2/v1.279.0)

Updates `github.com/aws/aws-sdk-go-v2/service/oam` from 1.18.3 to 1.23.10
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.18.3...service/ebs/v1.23.10)

Updates `github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi` from 1.26.6 to 1.31.5
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.26.6...config/v1.31.5)

Updates `github.com/aws/aws-sdk-go-v2/service/secretsmanager` from 1.40.1 to 1.41.0
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.40.1...v1.41.0)

Updates `github.com/aws/aws-sdk-go-v2/service/sts` from 1.39.1 to 1.41.5
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.39.1...service/sts/v1.41.5)

Updates `github.com/aws/smithy-go` from 1.23.2 to 1.24.0
- [Release notes](https://github.com/aws/smithy-go/releases)
- [Changelog](https://github.com/aws/smithy-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws/smithy-go/compare/v1.23.2...v1.24.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go
  dependency-version: 1.55.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/credentials
  dependency-version: 1.19.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudwatch
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs
  dependency-version: 1.63.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/service/ec2
  dependency-version: 1.279.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/service/oam
  dependency-version: 1.23.10
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi
  dependency-version: 1.31.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/service/secretsmanager
  dependency-version: 1.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/aws-sdk-go-v2/service/sts
  dependency-version: 1.41.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
- dependency-name: github.com/aws/smithy-go
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws-sdk-go
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-08 02:44:01 +00:00
grafana-pr-automation[bot] 97af86efb2 I18n: Download translations from Crowdin (#115968)
New Crowdin translations by GitHub Action

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-08 00:43:13 +00:00
Paul Marbach f58ab2a6a1 Gauge: Fix endpoint rendering for non-gradient cases (#115910)
* Gauge: Fix endpoint rendering for non-gradient cases

* break out the endpoint markers to its own component with tests
2026-01-07 17:17:35 -05:00
Charandas b96a1ae722 Custom Routes: use existing server's mux container instead of gorilla.Mux (#115605) 2026-01-07 12:46:27 -08:00
Kim Nylander a53875e621 [DOC] Changed so max_spans_per_span_set can't be changed in Cloud Traces (#115914)
Changed so max_spans_per_span_set can't be changed in Cloud Traces
2026-01-07 15:46:02 -05:00
Cory Forseth 9598ae6434 Datasources: extract data source read methods from service (#115834)
* extra data source read methods

* update tests

* more tests

* fix more tests; actually initialize retriever instead of sending nil

* moving GetAllDataSources isn't strictly required, so keep to minimal changes

* better name for retriever logger

Co-authored-by: Dafydd <72009875+dafydd-t@users.noreply.github.com>

* add compile-time check for DS retriever impl

---------

Co-authored-by: Dafydd <72009875+dafydd-t@users.noreply.github.com>
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
2026-01-07 14:29:59 -06:00
owensmallwood ab0b05550f Unified Storag: Fix readme (#115957)
* fix readme

* spelling
2026-01-07 19:35:33 +00:00
beejeebus 4518add556 Use a different metric name for new config CRUD APIs
Also, make sure to register the metrics with the same prometheus registerer
as the http server, so that metrics will show up.
2026-01-07 14:28:31 -05:00
Kristina Demeshchik 00b89b0d29 Dashboards: Fix liveNow not working for panels with time shift (#115902)
* relative time for timeshifts

* remove extra assertion

* absolute time range
2026-01-07 14:24:20 -05:00
Todd Treece a3eedfeb73 Plugins: Move fixed role registration behind toggle (#115940) 2026-01-07 13:52:01 -05:00
Renato Costa 1e8f1f74ea unified-storage: apply backwards compatibility changes outside sqlkv (#115954) 2026-01-07 13:51:15 -05:00
owensmallwood 66b05914e2 Tracing: Use service name from config (#115955)
use service name from config
2026-01-07 12:50:11 -06:00
Yunwen Zheng 0c60d356d1 RecentlyViewedDashboards: Hide entire section when there is no recently view item (#115905)
* RecentlyViewedDashboards: Hide entire section when there is no recently view item
2026-01-07 13:31:48 -05:00
Ezequiel Victorero 41d7213d7e Docs: Update dualwrite ini config (#115934) 2026-01-07 17:58:58 +01:00
Todd Treece efad6c7be0 Chore: Update enterprise imports (#115947) 2026-01-07 16:55:59 +00:00
Paulo Dias e116254f32 Alerting: Update createdBy field when silence is being Recreated (#115543) 2026-01-07 16:05:53 +00:00
66 changed files with 1326 additions and 661 deletions
@@ -135,9 +135,12 @@ You can use the **Span Limit** field in **Options** section of the TraceQL query
This field sets the maximum number of spans to return for each span set. This field sets the maximum number of spans to return for each span set.
By default, the maximum value that you can set for the **Span Limit** value (or the spss query) is 100. By default, the maximum value that you can set for the **Span Limit** value (or the spss query) is 100.
In Tempo configuration, this value is controlled by the `max_spans_per_span_set` parameter and can be modified by your Tempo administrator. In Tempo configuration, this value is controlled by the `max_spans_per_span_set` parameter and can be modified by your Tempo administrator.
Grafana Cloud users can contact Grafana Support to request a change.
Entering a value higher than the default results in an error. Entering a value higher than the default results in an error.
{{< admonition type="note" >}}
Changing the value of `max_spans_per_span_set` isn't supported in Grafana Cloud.
{{< /admonition >}}
### Focus on traces or spans ### Focus on traces or spans
Under **Options**, you can choose to display the table as **Traces** or **Spans** focused. Under **Options**, you can choose to display the table as **Traces** or **Spans** focused.
+19 -19
View File
@@ -31,15 +31,17 @@ require (
github.com/andybalholm/brotli v1.2.0 // @grafana/partner-datasources github.com/andybalholm/brotli v1.2.0 // @grafana/partner-datasources
github.com/apache/arrow-go/v18 v18.4.1 // @grafana/plugins-platform-backend github.com/apache/arrow-go/v18 v18.4.1 // @grafana/plugins-platform-backend
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources github.com/aws/aws-sdk-go v1.55.8 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2 v1.41.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.53.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.63.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/ec2 v1.279.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/oam v1.23.10 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.31.5 // @grafana/aws-datasources
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.0 // @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // @grafana/grafana-operator-experience-squad
github.com/aws/smithy-go v1.24.0 // @grafana/aws-datasources
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad
@@ -341,24 +343,22 @@ require (
github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/at-wat/mqtt-go v0.19.6 // indirect github.com/at-wat/mqtt-go v0.19.6 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
+38 -38
View File
@@ -848,60 +848,60 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
github.com/aws/aws-sdk-go v1.22.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.22.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.53.0 h1:XY6wKzfriEF+V8bFYFi1S3i8ly+Zetq/RuPyaGdMMzE=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3/go.mod h1:aqsLGsPs+rJfwDBwWHLcIV8F7AFcikFTPLwUD4RwORQ= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.53.0/go.mod h1:zUms+kt0awoSYh/MwI9d3AV5xMHIDRf7I736b1Drw/k=
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFieZUfVdINOiCTvChOMPfdLnmiLzs= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.63.0 h1:vEc1y56GbepIC0/NsYfFn4splRMNXgJTTG3G1B/6Ov0=
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0/go.mod h1:UseIHRfrm7PqeZo6fcTb6FUCXzCnh1KJbQbmOfxArGM= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.63.0/go.mod h1:ESQxVIp7hs1MdsdEF4KITf65SfM3fh/EEiYi+s0S/pE=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJdQgRP5mKCTDt4GNk= github.com/aws/aws-sdk-go-v2/service/ec2 v1.279.0 h1:o7eJKe6VYAnqERPlLAvDW5VKXV6eTKv1oxTpMoDP378=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54= github.com/aws/aws-sdk-go-v2/service/ec2 v1.279.0/go.mod h1:Wg68QRgy2gEGGdmTPU/UbVpdv8sM14bUZmF64KFwAsY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo= github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo=
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2/go.mod h1:Pqd9k4TuespkireN206cK2QBsaBTL6X+VPAez5Qcijk= github.com/aws/aws-sdk-go-v2/service/kms v1.41.2/go.mod h1:Pqd9k4TuespkireN206cK2QBsaBTL6X+VPAez5Qcijk=
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 h1:teOWtElLARLOhpYWwupjLbY9j5I/yZ/H1I8jg41An78= github.com/aws/aws-sdk-go-v2/service/oam v1.23.10 h1:bgLzHGeE6HCvd+DmyyBQS4duiyUz+g6hmKl1kunaeP4=
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3/go.mod h1:wGhpdyftHX6/1U4egowHkYdypwBMjpb+KjAAprv6z20= github.com/aws/aws-sdk-go-v2/service/oam v1.23.10/go.mod h1:jEr64iJ6XD0MkzzW3VqqPX7Cws9MLHhLo+sMCoBC7B8=
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:PwbxovpcJvb25k019bkibvJfCpCmIANOFrXZIFPmRzk= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.31.5 h1:0jwTqyyPsbn4UysC6ltj/AuntNBWBeU++kNJQtShtg0=
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.31.5/go.mod h1:ydy76wx7I+HsqhlEo0vhVTl785TDNbpgtEXhd3i4ZTc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.0 h1:vL6rQXcGtFv9q/9eRPdI+lL+dvTm7xKGZYSHEvmrpDk=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.0/go.mod h1:QwEDLD+7EukuEUnbWtiNE8LhgvvmhjZoi4XAppYPtyc=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
@@ -165,9 +165,17 @@ describe('DateMath', () => {
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 3]).valueOf()); expect(date!.valueOf()).toEqual(dateTime([2014, 1, 3]).valueOf());
}); });
it('should handle multiple math expressions', () => { it.each([
const date = dateMath.parseDateMath('-2d-6h', dateTime([2014, 1, 5])); ['-2d-6h', [2014, 1, 5], [2014, 1, 2, 18]],
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 2, 18]).valueOf()); ['-30m-2d', [2014, 1, 5], [2014, 1, 2, 23, 30]],
['-2d-1d', [2014, 1, 5], [2014, 1, 2]],
['-1h-30m', [2014, 1, 5, 12, 0], [2014, 1, 5, 10, 30]],
['-1d-1h-30m', [2014, 1, 5, 12, 0], [2014, 1, 4, 10, 30]],
['+1d-6h', [2014, 1, 5], [2014, 1, 5, 18]],
['-1w-1d', [2014, 1, 14], [2014, 1, 6]],
])('should handle multiple math expressions: %s', (expression, inputDate, expectedDate) => {
const date = dateMath.parseDateMath(expression, dateTime(inputDate));
expect(date!.valueOf()).toEqual(dateTime(expectedDate).valueOf());
}); });
it('should return false when invalid expression', () => { it('should return false when invalid expression', () => {
@@ -1,8 +1,9 @@
import { useId, memo, HTMLAttributes, ReactNode, SVGProps } from 'react'; import { useId, memo, HTMLAttributes, SVGProps } from 'react';
import { FieldDisplay } from '@grafana/data'; import { FieldDisplay } from '@grafana/data';
import { getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors'; import { RadialArcPathEndpointMarks } from './RadialArcPathEndpointMarks';
import { getBarEndcapColors, getGradientCss } from './colors';
import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; import { RadialShape, RadialGaugeDimensions, GradientStop } from './types';
import { drawRadialArcPath, toRad } from './utils'; import { drawRadialArcPath, toRad } from './utils';
@@ -29,11 +30,6 @@ interface RadialArcPathPropsWithGradient extends RadialArcPathPropsBase {
type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient; type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient;
const ENDPOINT_MARKER_MIN_ANGLE = 10;
const DOT_OPACITY = 0.5;
const DOT_RADIUS_FACTOR = 0.4;
const MAX_DOT_RADIUS = 8;
export const RadialArcPath = memo( export const RadialArcPath = memo(
({ ({
arcLengthDeg, arcLengthDeg,
@@ -68,67 +64,25 @@ export const RadialArcPath = memo(
const xEnd = centerX + radius * Math.cos(endRadians); const xEnd = centerX + radius * Math.cos(endRadians);
const yEnd = centerY + radius * Math.sin(endRadians); const yEnd = centerY + radius * Math.sin(endRadians);
const dotRadius =
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX }; const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX };
const pathProps: SVGProps<SVGPathElement> = {}; const pathProps: SVGProps<SVGPathElement> = {};
let barEndcapColors: [string, string] | undefined;
let endpointMarks: ReactNode = null;
if (isGradient) { if (isGradient) {
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape); bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
if (endpointMarker && (rest.gradient?.length ?? 0) > 0) {
switch (endpointMarker) {
case 'point':
const [pointColorStart, pointColorEnd] = getEndpointMarkerColors(
rest.gradient!,
fieldDisplay.display.percent
);
endpointMarks = (
<>
{arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && (
<circle cx={xStart} cy={yStart} r={dotRadius} fill={pointColorStart} opacity={DOT_OPACITY} />
)}
<circle cx={xEnd} cy={yEnd} r={dotRadius} fill={pointColorEnd} opacity={DOT_OPACITY} />
</>
);
break;
case 'glow':
const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE);
const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle);
const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle);
endpointMarks =
arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? (
<path
d={['M', xStartMark, yStartMark, 'A', radius, radius, 0, 0, 1, xEnd, yEnd].join(' ')}
fill="none"
strokeWidth={barWidth}
stroke={endpointMarkerGlowFilter}
strokeLinecap={roundedBars ? 'round' : 'butt'}
filter={glowFilter}
/>
) : null;
break;
default:
break;
}
}
if (barEndcaps) {
barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent);
}
pathProps.fill = 'none'; pathProps.fill = 'none';
pathProps.stroke = 'white'; pathProps.stroke = 'white';
} else { } else {
bgDivStyle.backgroundColor = rest.color; bgDivStyle.backgroundColor = rest.color;
pathProps.fill = 'none'; pathProps.fill = 'none';
pathProps.stroke = rest.color; pathProps.stroke = rest.color;
} }
let barEndcapColors: [string, string] | undefined;
if (barEndcaps) {
barEndcapColors = isGradient
? getBarEndcapColors(rest.gradient, fieldDisplay.display.percent)
: [rest.color, rest.color];
}
const pathEl = ( const pathEl = (
<path d={path} strokeWidth={barWidth} strokeLinecap={roundedBars ? 'round' : 'butt'} {...pathProps} /> <path d={path} strokeWidth={barWidth} strokeLinecap={roundedBars ? 'round' : 'butt'} {...pathProps} />
); );
@@ -158,7 +112,23 @@ export const RadialArcPath = memo(
)} )}
</g> </g>
{endpointMarks} {endpointMarker && (
<RadialArcPathEndpointMarks
startAngle={angle}
arcLengthDeg={arcLengthDeg}
dimensions={dimensions}
endpointMarker={endpointMarker}
fieldDisplay={fieldDisplay}
xStart={xStart}
xEnd={xEnd}
yStart={yStart}
yEnd={yEnd}
roundedBars={roundedBars}
endpointMarkerGlowFilter={endpointMarkerGlowFilter}
glowFilter={glowFilter}
{...rest}
/>
)}
</> </>
); );
} }
@@ -0,0 +1,143 @@
import { render, RenderResult } from '@testing-library/react';
import { FieldDisplay } from '@grafana/data';
import { RadialArcPathEndpointMarks, RadialArcPathEndpointMarksProps } from './RadialArcPathEndpointMarks';
import { RadialGaugeDimensions } from './types';
const ser = new XMLSerializer();
const expectHTML = (result: RenderResult, expected: string) => {
let actual = ser.serializeToString(result.asFragment()).replace(/xmlns=".*?" /g, '');
expect(actual).toEqual(expected.replace(/^\s*|\n/gm, ''));
};
describe('RadialArcPathEndpointMarks', () => {
const defaultDimensions = Object.freeze({
centerX: 100,
centerY: 100,
radius: 80,
barWidth: 20,
vizWidth: 200,
vizHeight: 200,
margin: 10,
barIndex: 0,
thresholdsBarRadius: 0,
thresholdsBarWidth: 0,
thresholdsBarSpacing: 0,
scaleLabelsFontSize: 0,
scaleLabelsSpacing: 0,
scaleLabelsRadius: 0,
gaugeBottomY: 0,
}) satisfies RadialGaugeDimensions;
const defaultFieldDisplay = Object.freeze({
name: 'Test',
field: {},
display: { text: '50', numeric: 50, color: '#FF0000' },
hasLinks: false,
}) satisfies FieldDisplay;
const defaultProps = Object.freeze({
arcLengthDeg: 90,
dimensions: defaultDimensions,
fieldDisplay: defaultFieldDisplay,
startAngle: 0,
xStart: 100,
xEnd: 150,
yStart: 100,
yEnd: 50,
}) satisfies Omit<RadialArcPathEndpointMarksProps, 'color' | 'gradient' | 'endpointMarker'>;
it('renders the expected marks when endpointMarker is "point" w/ a static color', () => {
expectHTML(
render(
<svg role="img">
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="point" color="#FF0000" />
</svg>
),
'<svg role=\"img\"><circle cx=\"100\" cy=\"100\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/></svg>'
);
});
it('renders the expected marks when endpointMarker is "point" w/ a gradient color', () => {
expectHTML(
render(
<svg role="img">
<RadialArcPathEndpointMarks
{...defaultProps}
endpointMarker="point"
gradient={[
{ color: '#00FF00', percent: 0 },
{ color: '#0000FF', percent: 1 },
]}
/>
</svg>
),
'<svg role=\"img\"><circle cx=\"100\" cy=\"100\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#fbfbfb\" opacity=\"0.5\"/></svg>'
);
});
it('renders the expected marks when endpointMarker is "glow" w/ a static color', () => {
expectHTML(
render(
<svg role="img">
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="glow" color="#FF0000" />
</svg>
),
'<svg role=\"img\"><path d=\"M 113.89185421335443 21.215379759023364 A 80 80 0 0 1 150 50\" fill=\"none\" stroke-width=\"20\" stroke-linecap=\"butt\"/></svg>'
);
});
it('renders the expected marks when endpointMarker is "glow" w/ a gradient color', () => {
expectHTML(
render(
<svg role="img">
<RadialArcPathEndpointMarks
{...defaultProps}
endpointMarker="glow"
gradient={[
{ color: '#00FF00', percent: 0 },
{ color: '#0000FF', percent: 1 },
]}
/>
</svg>
),
'<svg role=\"img\"><path d=\"M 113.89185421335443 21.215379759023364 A 80 80 0 0 1 150 50\" fill=\"none\" stroke-width=\"20\" stroke-linecap=\"butt\"/></svg>'
);
});
it('does not render the start mark when arcLengthDeg is less than the minimum angle for "point" endpointMarker', () => {
expectHTML(
render(
<svg role="img">
<RadialArcPathEndpointMarks {...defaultProps} arcLengthDeg={5} endpointMarker="point" color="#FF0000" />
</svg>
),
'<svg role=\"img\"><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/></svg>'
);
});
it('does not render anything when arcLengthDeg is less than the minimum angle for "glow" endpointMarker', () => {
expectHTML(
render(
<svg role="img">
<RadialArcPathEndpointMarks {...defaultProps} arcLengthDeg={5} endpointMarker="glow" color="#FF0000" />
</svg>
),
'<svg role=\"img\"/>'
);
});
it('does not render anything if endpointMarker is some other value', () => {
expectHTML(
render(
<svg role="img">
{/* @ts-ignore: confirming the component doesn't throw */}
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="foo" />
</svg>
),
'<svg role=\"img\"/>'
);
});
});
@@ -0,0 +1,98 @@
import { FieldDisplay } from '@grafana/data';
import { getEndpointMarkerColors, getGuideDotColor } from './colors';
import { GradientStop, RadialGaugeDimensions } from './types';
import { toRad } from './utils';
interface RadialArcPathEndpointMarksPropsBase {
arcLengthDeg: number;
dimensions: RadialGaugeDimensions;
fieldDisplay: FieldDisplay;
endpointMarker: 'point' | 'glow';
roundedBars?: boolean;
startAngle: number;
glowFilter?: string;
endpointMarkerGlowFilter?: string;
xStart: number;
xEnd: number;
yStart: number;
yEnd: number;
}
interface RadialArcPathEndpointMarksPropsWithColor extends RadialArcPathEndpointMarksPropsBase {
color: string;
}
interface RadialArcPathEndpointMarksPropsWithGradient extends RadialArcPathEndpointMarksPropsBase {
gradient: GradientStop[];
}
export type RadialArcPathEndpointMarksProps =
| RadialArcPathEndpointMarksPropsWithColor
| RadialArcPathEndpointMarksPropsWithGradient;
const ENDPOINT_MARKER_MIN_ANGLE = 10;
const DOT_OPACITY = 0.5;
const DOT_RADIUS_FACTOR = 0.4;
const MAX_DOT_RADIUS = 8;
export function RadialArcPathEndpointMarks({
startAngle: angle,
arcLengthDeg,
dimensions,
endpointMarker,
fieldDisplay,
xStart,
xEnd,
yStart,
yEnd,
roundedBars,
endpointMarkerGlowFilter,
glowFilter,
...rest
}: RadialArcPathEndpointMarksProps) {
const isGradient = 'gradient' in rest;
const { radius, centerX, centerY, barWidth } = dimensions;
const endRadians = toRad(angle + arcLengthDeg);
switch (endpointMarker) {
case 'point': {
const [pointColorStart, pointColorEnd] = isGradient
? getEndpointMarkerColors(rest.gradient, fieldDisplay.display.percent)
: [getGuideDotColor(rest.color), getGuideDotColor(rest.color)];
const dotRadius =
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
return (
<>
{arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && (
<circle cx={xStart} cy={yStart} r={dotRadius} fill={pointColorStart} opacity={DOT_OPACITY} />
)}
<circle cx={xEnd} cy={yEnd} r={dotRadius} fill={pointColorEnd} opacity={DOT_OPACITY} />
</>
);
}
case 'glow':
const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE);
const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle);
const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle);
if (arcLengthDeg <= ENDPOINT_MARKER_MIN_ANGLE) {
break;
}
return (
<path
d={['M', xStartMark, yStartMark, 'A', radius, radius, 0, 0, 1, xEnd, yEnd].join(' ')}
fill="none"
strokeWidth={barWidth}
stroke={endpointMarkerGlowFilter}
strokeLinecap={roundedBars ? 'round' : 'butt'}
filter={glowFilter}
/>
);
default:
break;
}
return null;
}
@@ -175,7 +175,7 @@ export function getGradientCss(gradientStops: GradientStop[], shape: RadialShape
const GRAY_05 = '#111217'; const GRAY_05 = '#111217';
const GRAY_90 = '#fbfbfb'; const GRAY_90 = '#fbfbfb';
const CONTRAST_THRESHOLD_MAX = 4.5; const CONTRAST_THRESHOLD_MAX = 4.5;
const getGuideDotColor = (color: string): string => { export const getGuideDotColor = (color: string): string => {
const darkColor = GRAY_05; const darkColor = GRAY_05;
const lightColor = GRAY_90; const lightColor = GRAY_90;
return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor;
+4 -4
View File
@@ -204,7 +204,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response.
func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response { func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "GetDataSourceByUID"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("GetDataSourceByUID"), time.Since(start).Seconds())
}() }()
ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID()) ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID())
@@ -240,7 +240,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re
func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response { func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "DeleteDataSourceByUID"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("DeleteDataSourceByUID"), time.Since(start).Seconds())
}() }()
uid := web.Params(c.Req)[":uid"] uid := web.Params(c.Req)[":uid"]
@@ -375,7 +375,7 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error {
func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response { func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "AddDataSource"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("AddDataSource"), time.Since(start).Seconds())
}() }()
cmd := datasources.AddDataSourceCommand{} cmd := datasources.AddDataSourceCommand{}
@@ -497,7 +497,7 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response { func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "UpdateDataSourceByUID"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("UpdateDataSourceByUID"), time.Since(start).Seconds())
}() }()
cmd := datasources.UpdateDataSourceCommand{} cmd := datasources.UpdateDataSourceCommand{}
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
+1 -1
View File
@@ -91,7 +91,7 @@ func setupDsConfigHandlerMetrics() (prometheus.Registerer, *prometheus.Histogram
Namespace: "grafana", Namespace: "grafana",
Name: "ds_config_handler_requests_duration_seconds", Name: "ds_config_handler_requests_duration_seconds",
Help: "Duration of requests handled by datasource configuration handlers", Help: "Duration of requests handled by datasource configuration handlers",
}, []string{"code_path", "handler"}) }, []string{"handler"})
promRegister.MustRegister(dsConfigHandlerRequestsDuration) promRegister.MustRegister(dsConfigHandlerRequestsDuration)
return promRegister, dsConfigHandlerRequestsDuration return promRegister, dsConfigHandlerRequestsDuration
} }
+1 -1
View File
@@ -387,7 +387,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
Namespace: "grafana", Namespace: "grafana",
Name: "ds_config_handler_requests_duration_seconds", Name: "ds_config_handler_requests_duration_seconds",
Help: "Duration of requests handled by datasource configuration handlers", Help: "Duration of requests handled by datasource configuration handlers",
}, []string{"code_path", "handler"}), }, []string{"handler"}),
} }
promRegister.MustRegister(hs.htmlHandlerRequestsDuration) promRegister.MustRegister(hs.htmlHandlerRequestsDuration)
+10 -5
View File
@@ -928,9 +928,10 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
features := featuremgmt.WithFeatures() features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsRetriever := datasourceservice.ProvideDataSourceRetriever(sqlStore, features)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features), dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, &actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
require.NoError(t, err) require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
require.NoError(t, err) require.NoError(t, err)
@@ -1050,9 +1051,11 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
var routes []*plugins.Route var routes []*plugins.Route
features := featuremgmt.WithFeatures() features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features), var sqlStore db.DB = nil
dsRetriever := datasourceservice.ProvideDataSourceRetriever(sqlStore, features)
dsService, err := datasourceservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, &actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
require.NoError(t, err) require.NoError(t, err)
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features) proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
require.NoError(t, err) require.NoError(t, err)
@@ -1106,9 +1109,11 @@ func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasource
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger()) secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
features := featuremgmt.WithFeatures() features := featuremgmt.WithFeatures()
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features), var sqlStore db.DB = nil
dsRetriever := datasourceservice.ProvideDataSourceRetriever(sqlStore, features)
dsService, err := datasourceservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, &actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
require.NoError(t, err) require.NoError(t, err)
tracer := tracing.InitializeTracerForTest() tracer := tracing.InitializeTracerForTest()
+3 -1
View File
@@ -11,6 +11,9 @@ import (
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" _ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
_ "github.com/Azure/go-autorest/autorest" _ "github.com/Azure/go-autorest/autorest"
_ "github.com/Azure/go-autorest/autorest/adal" _ "github.com/Azure/go-autorest/autorest/adal"
_ "github.com/aws/aws-sdk-go-v2/credentials"
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
_ "github.com/aws/aws-sdk-go-v2/service/sts"
_ "github.com/beevik/etree" _ "github.com/beevik/etree"
_ "github.com/blugelabs/bluge" _ "github.com/blugelabs/bluge"
_ "github.com/blugelabs/bluge_segment_api" _ "github.com/blugelabs/bluge_segment_api"
@@ -46,7 +49,6 @@ import (
_ "sigs.k8s.io/randfill" _ "sigs.k8s.io/randfill"
_ "xorm.io/builder" _ "xorm.io/builder"
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
_ "github.com/grafana/authlib/authn" _ "github.com/grafana/authlib/authn"
_ "github.com/grafana/authlib/authz" _ "github.com/grafana/authlib/authz"
_ "github.com/grafana/authlib/cache" _ "github.com/grafana/authlib/cache"
+1 -1
View File
@@ -209,7 +209,7 @@ func (ots *TracingService) initSampler() (tracesdk.Sampler, error) {
case "rateLimiting": case "rateLimiting":
return newRateLimiter(ots.cfg.SamplerParam), nil return newRateLimiter(ots.cfg.SamplerParam), nil
case "remote": case "remote":
return jaegerremote.New("grafana", return jaegerremote.New(ots.cfg.ServiceName,
jaegerremote.WithSamplingServerURL(ots.cfg.SamplerRemoteURL), jaegerremote.WithSamplingServerURL(ots.cfg.SamplerRemoteURL),
jaegerremote.WithInitialSampler(tracesdk.TraceIDRatioBased(ots.cfg.SamplerParam)), jaegerremote.WithInitialSampler(tracesdk.TraceIDRatioBased(ots.cfg.SamplerParam)),
), nil ), nil
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect github.com/apache/arrow-go/v18 v18.4.1 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect github.com/aws/aws-sdk-go v1.55.8 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
+2 -2
View File
@@ -23,8 +23,8 @@ github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
+17 -4
View File
@@ -57,6 +57,12 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
} }
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
if s.dsConfigHandlerRequestsDuration != nil {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.List"), time.Since(start).Seconds())
}()
}
return s.datasources.ListDataSources(ctx) return s.datasources.ListDataSources(ctx)
} }
@@ -64,7 +70,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
if s.dsConfigHandlerRequestsDuration != nil { if s.dsConfigHandlerRequestsDuration != nil {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Get"), time.Since(start).Seconds())
}() }()
} }
@@ -76,7 +82,7 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa
if s.dsConfigHandlerRequestsDuration != nil { if s.dsConfigHandlerRequestsDuration != nil {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Create"), time.Since(start).Seconds())
}() }()
} }
@@ -92,7 +98,7 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
if s.dsConfigHandlerRequestsDuration != nil { if s.dsConfigHandlerRequestsDuration != nil {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Update"), time.Since(start).Seconds())
}() }()
} }
@@ -135,7 +141,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
if s.dsConfigHandlerRequestsDuration != nil { if s.dsConfigHandlerRequestsDuration != nil {
start := time.Now() start := time.Now()
defer func() { defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Delete"), time.Since(start).Seconds())
}() }()
} }
@@ -145,6 +151,13 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
// DeleteCollection implements rest.CollectionDeleter. // DeleteCollection implements rest.CollectionDeleter.
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
if s.dsConfigHandlerRequestsDuration != nil {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.DeleteCollection"), time.Since(start).Seconds())
}()
}
dss, err := s.datasources.ListDataSources(ctx) dss, err := s.datasources.ListDataSources(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
+5 -4
View File
@@ -21,6 +21,7 @@ import (
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1" queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/sources"
@@ -69,10 +70,10 @@ func RegisterAPIService(
dataSourceCRUDMetric := metricutil.NewHistogramVec(prometheus.HistogramOpts{ dataSourceCRUDMetric := metricutil.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "grafana", Namespace: "grafana",
Name: "ds_config_handler_requests_duration_seconds", Name: "ds_config_handler_apis_requests_duration_seconds",
Help: "Duration of requests handled by datasource configuration handlers", Help: "Duration of requests handled by new k8s style APIs datasource configuration handlers",
}, []string{"code_path", "handler"}) }, []string{"handler"})
regErr := reg.Register(dataSourceCRUDMetric) regErr := metrics.ProvideRegisterer().Register(dataSourceCRUDMetric)
if regErr != nil && !errors.As(regErr, &prometheus.AlreadyRegisteredError{}) { if regErr != nil && !errors.As(regErr, &prometheus.AlreadyRegisteredError{}) {
return nil, regErr return nil, regErr
} }
+7 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
) )
@@ -36,9 +37,13 @@ func ProvideAppInstaller(
pluginStore pluginstore.Store, pluginStore pluginstore.Store,
pluginAssetsService *pluginassets.Service, pluginAssetsService *pluginassets.Service,
accessControlService accesscontrol.Service, accessClient authlib.AccessClient, accessControlService accesscontrol.Service, accessClient authlib.AccessClient,
features featuremgmt.FeatureToggles,
) (*AppInstaller, error) { ) (*AppInstaller, error) {
if err := registerAccessControlRoles(accessControlService); err != nil { //nolint:staticcheck // not yet migrated to OpenFeature
return nil, fmt.Errorf("registering access control roles: %w", err) if features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) {
if err := registerAccessControlRoles(accessControlService); err != nil {
return nil, fmt.Errorf("registering access control roles: %w", err)
}
} }
localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService) localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService)
+1
View File
@@ -330,6 +330,7 @@ var wireBasicSet = wire.NewSet(
dashsnapstore.ProvideStore, dashsnapstore.ProvideStore,
wire.Bind(new(dashboardsnapshots.Service), new(*dashsnapsvc.ServiceImpl)), wire.Bind(new(dashboardsnapshots.Service), new(*dashsnapsvc.ServiceImpl)),
dashsnapsvc.ProvideService, dashsnapsvc.ProvideService,
datasourceservice.ProvideDataSourceRetriever,
datasourceservice.ProvideService, datasourceservice.ProvideService,
wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)), wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)),
datasourceservice.ProvideLegacyDataSourceLookup, datasourceservice.ProvideLegacyDataSourceLookup,
+7 -5
View File
File diff suppressed because one or more lines are too long
@@ -3,7 +3,6 @@ package authorizer
import ( import (
"context" "context"
"github.com/grafana/grafana/pkg/setting"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
k8suser "k8s.io/apiserver/pkg/authentication/user" k8suser "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
@@ -29,9 +28,9 @@ type GrafanaAuthorizer struct {
// 4. We check authorizer that is configured speficially for an api. // 4. We check authorizer that is configured speficially for an api.
// 5. As a last fallback we check Role, this will only happen if an api have not configured // 5. As a last fallback we check Role, this will only happen if an api have not configured
// an authorizer or return authorizer.DecisionNoOpinion // an authorizer or return authorizer.DecisionNoOpinion
func NewGrafanaBuiltInSTAuthorizer(cfg *setting.Cfg) *GrafanaAuthorizer { func NewGrafanaBuiltInSTAuthorizer() *GrafanaAuthorizer {
authorizers := []authorizer.Authorizer{ authorizers := []authorizer.Authorizer{
newImpersonationAuthorizer(), NewImpersonationAuthorizer(),
authorizerfactory.NewPrivilegedGroups(k8suser.SystemPrivilegedGroup), authorizerfactory.NewPrivilegedGroups(k8suser.SystemPrivilegedGroup),
newNamespaceAuthorizer(), newNamespaceAuthorizer(),
} }
@@ -8,7 +8,7 @@ import (
var _ authorizer.Authorizer = (*impersonationAuthorizer)(nil) var _ authorizer.Authorizer = (*impersonationAuthorizer)(nil)
func newImpersonationAuthorizer() *impersonationAuthorizer { func NewImpersonationAuthorizer() *impersonationAuthorizer {
return &impersonationAuthorizer{} return &impersonationAuthorizer{}
} }
+1 -13
View File
@@ -76,19 +76,7 @@ var PathRewriters = []filters.PathRewriter{
func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc { func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc {
return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler {
requestHandler, err := GetCustomRoutesHandler( handler := filters.WithTracingHTTPLoggingAttributes(delegateHandler)
delegateHandler,
c.LoopbackClientConfig,
builders,
reg,
c.MergedResourceConfig,
)
if err != nil {
panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error()))
}
// Needs to run last in request chain to function as expected, hence we register it first.
handler := filters.WithTracingHTTPLoggingAttributes(requestHandler)
// filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context // filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context
handler = filters.WithRequester(handler) handler = filters.WithRequester(handler)
+257 -97
View File
@@ -3,146 +3,306 @@ package builder
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/emicklei/go-restful/v3"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
serverstorage "k8s.io/apiserver/pkg/server/storage" serverstorage "k8s.io/apiserver/pkg/server/storage"
restclient "k8s.io/client-go/rest"
klog "k8s.io/klog/v2" klog "k8s.io/klog/v2"
"k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/spec3"
) )
type requestHandler struct { // convertHandlerToRouteFunction converts an http.HandlerFunc to a restful.RouteFunction
router *mux.Router // It extracts path parameters from restful.Request and populates them in the request context
// so that mux.Vars can read them (for backward compatibility with handlers that use mux.Vars)
func convertHandlerToRouteFunction(handler http.HandlerFunc) restful.RouteFunction {
return func(req *restful.Request, resp *restful.Response) {
// Extract path parameters from restful.Request and populate mux.Vars
// This is needed for backward compatibility with handlers that use mux.Vars(r)
vars := make(map[string]string)
// Get all path parameters from the restful.Request
// The restful.Request has PathParameters() method that returns a map
pathParams := req.PathParameters()
for key, value := range pathParams {
vars[key] = value
}
// Set the vars in the request context using mux.SetURLVars
// This makes mux.Vars(r) work correctly
if len(vars) > 0 {
req.Request = mux.SetURLVars(req.Request, vars)
}
handler(resp.ResponseWriter, req.Request)
}
} }
func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) { // AugmentWebServicesWithCustomRoutes adds custom routes from builders to existing WebServices
useful := false // only true if any routes exist anywhere // in the container.
router := mux.NewRouter() func AugmentWebServicesWithCustomRoutes(
container *restful.Container,
builders []APIGroupBuilder,
metricsRegistry prometheus.Registerer,
apiResourceConfig *serverstorage.ResourceConfig,
) error {
if container == nil {
return fmt.Errorf("container cannot be nil")
}
metrics := NewCustomRouteMetrics(metricsRegistry) metrics := NewCustomRouteMetrics(metricsRegistry)
for _, builder := range builders { // Build a map of existing WebServices by root path
provider, ok := builder.(APIGroupRouteProvider) existingWebServices := make(map[string]*restful.WebService)
for _, ws := range container.RegisteredWebServices() {
existingWebServices[ws.RootPath()] = ws
}
for _, b := range builders {
provider, ok := b.(APIGroupRouteProvider)
if !ok || provider == nil { if !ok || provider == nil {
continue continue
} }
for _, gv := range GetGroupVersions(builder) { for _, gv := range GetGroupVersions(b) {
// filter out api groups that are disabled in APIEnablementOptions // Filter out disabled API groups
gvr := gv.WithResource("") gvr := gv.WithResource("")
if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) {
klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String()) klog.InfoS("Skipping custom routes for disabled group version", "gv", gv.String())
continue continue
} }
routes := provider.GetAPIRoutes(gv) routes := provider.GetAPIRoutes(gv)
if routes == nil { if routes == nil {
continue continue
} }
prefix := "/apis/" + gv.String() // Find or create WebService for this group version
rootPath := "/apis/" + gv.String()
// Root handlers ws, exists := existingWebServices[rootPath]
var sub *mux.Router if !exists {
for _, route := range routes.Root { // Create a new WebService if one doesn't exist
if sub == nil { ws = new(restful.WebService)
sub = router.PathPrefix(prefix).Subrouter() ws.Path(rootPath)
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{} container.Add(ws)
} existingWebServices[rootPath] = ws
useful = true
methods, err := methodsFromSpec(route.Path, route.Spec)
if err != nil {
return nil, err
}
instrumentedHandler := metrics.InstrumentHandler(
gv.Group,
gv.Version,
route.Path, // Use path as resource identifier
route.Handler,
)
sub.HandleFunc("/"+route.Path, instrumentedHandler).
Methods(methods...)
} }
// Namespace handlers // Add root handlers using OpenAPI specs
sub = nil for _, route := range routes.Root {
prefix += "/namespaces/{namespace}"
for _, route := range routes.Namespace {
if sub == nil {
sub = router.PathPrefix(prefix).Subrouter()
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{}
}
useful = true
methods, err := methodsFromSpec(route.Path, route.Spec)
if err != nil {
return nil, err
}
instrumentedHandler := metrics.InstrumentHandler( instrumentedHandler := metrics.InstrumentHandler(
gv.Group, gv.Group,
gv.Version, gv.Version,
route.Path, // Use path as resource identifier route.Path,
route.Handler, route.Handler,
) )
routeFunction := convertHandlerToRouteFunction(instrumentedHandler)
sub.HandleFunc("/"+route.Path, instrumentedHandler). // Use OpenAPI spec to configure routes properly
Methods(methods...) if err := addRouteFromSpec(ws, route.Path, route.Spec, routeFunction, false); err != nil {
return fmt.Errorf("failed to add root route %s: %w", route.Path, err)
}
}
// Add namespace handlers using OpenAPI specs
for _, route := range routes.Namespace {
instrumentedHandler := metrics.InstrumentHandler(
gv.Group,
gv.Version,
route.Path,
route.Handler,
)
routeFunction := convertHandlerToRouteFunction(instrumentedHandler)
// Use OpenAPI spec to configure routes properly
if err := addRouteFromSpec(ws, route.Path, route.Spec, routeFunction, true); err != nil {
return fmt.Errorf("failed to add namespace route %s: %w", route.Path, err)
}
} }
} }
} }
if !useful { return nil
return delegateHandler, nil
}
// Per Gorilla Mux issue here: https://github.com/gorilla/mux/issues/616#issuecomment-798807509
// default handler must come last
router.PathPrefix("/").Handler(delegateHandler)
return &requestHandler{
router: router,
}, nil
} }
func (h *requestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // addRouteFromSpec adds routes to a WebService using OpenAPI specs
h.router.ServeHTTP(w, req) func addRouteFromSpec(ws *restful.WebService, routePath string, pathProps *spec3.PathProps, handler restful.RouteFunction, isNamespaced bool) error {
if pathProps == nil {
return fmt.Errorf("pathProps cannot be nil for route %s", routePath)
}
// Build the full path (relative to WebService root)
var fullPath string
if isNamespaced {
fullPath = "/namespaces/{namespace}/" + routePath
} else {
fullPath = "/" + routePath
}
// Add routes for each HTTP method defined in the OpenAPI spec
operations := map[string]*spec3.Operation{
"GET": pathProps.Get,
"POST": pathProps.Post,
"PUT": pathProps.Put,
"PATCH": pathProps.Patch,
"DELETE": pathProps.Delete,
}
for method, operation := range operations {
if operation == nil {
continue
}
// Create route builder for this method
var routeBuilder *restful.RouteBuilder
switch method {
case "GET":
routeBuilder = ws.GET(fullPath)
case "POST":
routeBuilder = ws.POST(fullPath)
case "PUT":
routeBuilder = ws.PUT(fullPath)
case "PATCH":
routeBuilder = ws.PATCH(fullPath)
case "DELETE":
routeBuilder = ws.DELETE(fullPath)
}
// Set operation ID from OpenAPI spec (with K8s verb prefix if needed)
operationID := operation.OperationId
if operationID == "" {
// Generate from path if not specified
operationID = generateOperationNameFromPath(routePath)
}
operationID = prefixRouteIDWithK8sVerbIfNotPresent(operationID, method)
routeBuilder = routeBuilder.Operation(operationID)
// Add description from OpenAPI spec
if operation.Description != "" {
routeBuilder = routeBuilder.Doc(operation.Description)
}
// Check if namespace parameter is already in the OpenAPI spec
hasNamespaceParam := false
if operation.Parameters != nil {
for _, param := range operation.Parameters {
if param.Name == "namespace" && param.In == "path" {
hasNamespaceParam = true
break
}
}
}
// Add namespace parameter for namespaced routes if not already in spec
if isNamespaced && !hasNamespaceParam {
routeBuilder = routeBuilder.Param(restful.PathParameter("namespace", "object name and auth scope, such as for teams and projects"))
}
// Add parameters from OpenAPI spec
if operation.Parameters != nil {
for _, param := range operation.Parameters {
switch param.In {
case "path":
routeBuilder = routeBuilder.Param(restful.PathParameter(param.Name, param.Description))
case "query":
routeBuilder = routeBuilder.Param(restful.QueryParameter(param.Name, param.Description))
case "header":
routeBuilder = routeBuilder.Param(restful.HeaderParameter(param.Name, param.Description))
}
}
}
// Note: Request/response schemas are already defined in the OpenAPI spec from builders
// and will be added to the OpenAPI document via addBuilderRoutes in openapi.go.
// We don't duplicate that information here since restful uses the route metadata
// for OpenAPI generation, which is handled separately in this codebase.
// Register the route with handler
ws.Route(routeBuilder.To(handler))
}
return nil
} }
func methodsFromSpec(slug string, props *spec3.PathProps) ([]string, error) { func prefixRouteIDWithK8sVerbIfNotPresent(operationID string, method string) string {
if props == nil { for _, verb := range allowedK8sVerbs {
return []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, nil if len(operationID) > len(verb) && operationID[:len(verb)] == verb {
return operationID
}
} }
return fmt.Sprintf("%s%s", httpMethodToK8sVerb[strings.ToUpper(method)], operationID)
methods := make([]string, 0)
if props.Get != nil {
methods = append(methods, "GET")
}
if props.Post != nil {
methods = append(methods, "POST")
}
if props.Put != nil {
methods = append(methods, "PUT")
}
if props.Patch != nil {
methods = append(methods, "PATCH")
}
if props.Delete != nil {
methods = append(methods, "DELETE")
}
if len(methods) == 0 {
return nil, fmt.Errorf("invalid OpenAPI Spec for slug=%s without any methods in PathProps", slug)
}
return methods, nil
} }
type methodNotAllowedHandler struct{} var allowedK8sVerbs = []string{
"get", "log", "read", "replace", "patch", "delete", "deletecollection", "watch", "connect", "proxy", "list", "create", "patch",
func (h *methodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { }
w.WriteHeader(405) // method not allowed
var httpMethodToK8sVerb = map[string]string{
http.MethodGet: "get",
http.MethodPost: "create",
http.MethodPut: "replace",
http.MethodPatch: "patch",
http.MethodDelete: "delete",
http.MethodConnect: "connect",
http.MethodOptions: "connect", // No real equivalent to options and head
http.MethodHead: "connect",
}
// generateOperationNameFromPath creates an operation name from a route path.
// The operation name is used by the OpenAPI generator and should be descriptive.
// It uses meaningful path segments to create readable yet unique operation names.
// Examples:
// - "/search" -> "Search"
// - "/snapshots/create" -> "SnapshotsCreate"
// - "ofrep/v1/evaluate/flags" -> "OfrepEvaluateFlags"
// - "ofrep/v1/evaluate/flags/{flagKey}" -> "OfrepEvaluateFlagsFlagKey"
func generateOperationNameFromPath(routePath string) string {
// Remove leading slash and split by path segments
parts := strings.Split(strings.TrimPrefix(routePath, "/"), "/")
// Filter to keep meaningful segments and path parameters
var nameParts []string
skipPrefixes := map[string]bool{
"namespaces": true,
"apis": true,
}
for _, part := range parts {
if part == "" {
continue
}
// Extract parameter name from {paramName} format
if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
paramName := part[1 : len(part)-1]
// Skip generic parameters like {namespace}, but keep specific ones like {flagKey}
if paramName != "namespace" && paramName != "name" {
nameParts = append(nameParts, strings.ToUpper(paramName[:1])+paramName[1:])
}
continue
}
// Skip common prefixes
if skipPrefixes[strings.ToLower(part)] {
continue
}
// Skip version segments like v1, v0alpha1, v2beta1, etc.
if strings.HasPrefix(strings.ToLower(part), "v") &&
(len(part) <= 3 || strings.Contains(strings.ToLower(part), "alpha") || strings.Contains(strings.ToLower(part), "beta")) {
continue
}
// Capitalize first letter and add to parts
if len(part) > 0 {
nameParts = append(nameParts, strings.ToUpper(part[:1])+part[1:])
}
}
if len(nameParts) == 0 {
return "Route"
}
return strings.Join(nameParts, "")
} }
-10
View File
@@ -5,7 +5,6 @@ import (
"net" "net"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"github.com/grafana/grafana/pkg/services/apiserver/options" "github.com/grafana/grafana/pkg/services/apiserver/options"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -41,15 +40,6 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver") apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
runtimeConfig := apiserverCfg.Key("runtime_config").String() runtimeConfig := apiserverCfg.Key("runtime_config").String()
runtimeConfigSplit := strings.Split(runtimeConfig, ",")
// TODO: temporary fix to allow disabling local features service and still being able to use its authz handler
if !cfg.OpenFeature.APIEnabled {
runtimeConfigSplit = append(runtimeConfigSplit, "features.grafana.app/v0alpha1=false")
}
runtimeConfig = strings.Join(runtimeConfigSplit, ",")
if runtimeConfig != "" { if runtimeConfig != "" {
if err := o.APIEnablementOptions.RuntimeConfig.Set(runtimeConfig); err != nil { if err := o.APIEnablementOptions.RuntimeConfig.Set(runtimeConfig); err != nil {
return fmt.Errorf("failed to set runtime config: %w", err) return fmt.Errorf("failed to set runtime config: %w", err)
+14 -1
View File
@@ -155,7 +155,7 @@ func ProvideService(
features: features, features: features,
rr: rr, rr: rr,
builders: []builder.APIGroupBuilder{}, builders: []builder.APIGroupBuilder{},
authorizer: authorizer.NewGrafanaBuiltInSTAuthorizer(cfg), authorizer: authorizer.NewGrafanaBuiltInSTAuthorizer(),
tracing: tracing, tracing: tracing,
db: db, // For Unified storage db: db, // For Unified storage
metrics: reg, metrics: reg,
@@ -443,6 +443,19 @@ func (s *service) start(ctx context.Context) error {
return err return err
} }
// Augment existing WebServices with custom routes from builders
// This directly adds routes to existing WebServices using the OpenAPI specs from builders
if server.Handler != nil && server.Handler.GoRestfulContainer != nil {
if err := builder.AugmentWebServicesWithCustomRoutes(
server.Handler.GoRestfulContainer,
builders,
s.metrics,
serverConfig.MergedResourceConfig,
); err != nil {
return fmt.Errorf("failed to augment web services with custom routes: %w", err)
}
}
// stash the options for later use // stash the options for later use
s.options = o s.options = o
@@ -51,6 +51,7 @@ type Service struct {
pluginStore pluginstore.Store pluginStore pluginstore.Store
pluginClient plugins.Client pluginClient plugins.Client
basePluginContextProvider plugincontext.BasePluginContextProvider basePluginContextProvider plugincontext.BasePluginContextProvider
retriever DataSourceRetriever
ptc proxyTransportCache ptc proxyTransportCache
} }
@@ -70,6 +71,7 @@ func ProvideService(
features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, datasourcePermissionsService accesscontrol.DatasourcePermissionsService, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, datasourcePermissionsService accesscontrol.DatasourcePermissionsService,
quotaService quota.Service, pluginStore pluginstore.Store, pluginClient plugins.Client, quotaService quota.Service, pluginStore pluginstore.Store, pluginClient plugins.Client,
basePluginContextProvider plugincontext.BasePluginContextProvider, basePluginContextProvider plugincontext.BasePluginContextProvider,
retriever DataSourceRetriever,
) (*Service, error) { ) (*Service, error) {
dslogger := log.New("datasources") dslogger := log.New("datasources")
store := &SqlStore{db: db, logger: dslogger, features: features} store := &SqlStore{db: db, logger: dslogger, features: features}
@@ -89,6 +91,7 @@ func ProvideService(
pluginStore: pluginStore, pluginStore: pluginStore,
pluginClient: pluginClient, pluginClient: pluginClient,
basePluginContextProvider: basePluginContextProvider, basePluginContextProvider: basePluginContextProvider,
retriever: retriever,
} }
ac.RegisterScopeAttributeResolver(NewNameScopeResolver(store)) ac.RegisterScopeAttributeResolver(NewNameScopeResolver(store))
@@ -175,11 +178,11 @@ func NewIDScopeResolver(db DataSourceRetriever) (string, accesscontrol.ScopeAttr
} }
func (s *Service) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) { func (s *Service) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
return s.SQLStore.GetDataSource(ctx, query) return s.retriever.GetDataSource(ctx, query)
} }
func (s *Service) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) { func (s *Service) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) {
return s.SQLStore.GetDataSourceInNamespace(ctx, namespace, name, group) return s.retriever.GetDataSourceInNamespace(ctx, namespace, name, group)
} }
func (s *Service) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) { func (s *Service) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) {
@@ -832,8 +832,9 @@ func TestIntegrationService_DeleteDataSource(t *testing.T) {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
permissionSvc := acmock.NewMockedPermissionsService() permissionSvc := acmock.NewMockedPermissionsService()
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
features := featuremgmt.WithFeatures()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, features, acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
cmd := &datasources.DeleteDataSourceCommand{ cmd := &datasources.DeleteDataSourceCommand{
@@ -857,7 +858,9 @@ func TestIntegrationService_DeleteDataSource(t *testing.T) {
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
cfg := &setting.Cfg{} cfg := &setting.Cfg{}
enableRBACManagedPermissions(t, cfg) enableRBACManagedPermissions(t, cfg)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
// First add the datasource // First add the datasource
@@ -1124,7 +1127,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider) rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
@@ -1161,7 +1166,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
ds := datasources.DataSource{ ds := datasources.DataSource{
@@ -1212,7 +1219,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
ds := datasources.DataSource{ ds := datasources.DataSource{
@@ -1260,7 +1269,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
ds := datasources.DataSource{ ds := datasources.DataSource{
@@ -1316,7 +1327,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
ds := datasources.DataSource{ ds := datasources.DataSource{
@@ -1351,7 +1364,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
ds := datasources.DataSource{ ds := datasources.DataSource{
@@ -1420,7 +1435,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
ds := datasources.DataSource{ ds := datasources.DataSource{
@@ -1499,7 +1516,9 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
ds := datasources.DataSource{ ds := datasources.DataSource{
@@ -1522,7 +1541,9 @@ func TestIntegrationService_getProxySettings(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
t.Run("Should default to disabled", func(t *testing.T) { t.Run("Should default to disabled", func(t *testing.T) {
@@ -1620,7 +1641,9 @@ func TestIntegrationService_getTimeout(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
for _, tc := range testCases { for _, tc := range testCases {
@@ -1645,7 +1668,9 @@ func TestIntegrationService_GetDecryptedValues(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
jsonData := map[string]string{ jsonData := map[string]string{
@@ -1673,7 +1698,9 @@ func TestIntegrationService_GetDecryptedValues(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
jsonData := map[string]string{ jsonData := map[string]string{
@@ -1699,7 +1726,9 @@ func TestIntegrationDataSource_CustomHeaders(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
require.NoError(t, err) require.NoError(t, err)
dsService.cfg = setting.NewCfg() dsService.cfg = setting.NewCfg()
@@ -1788,7 +1817,9 @@ func initDSService(t *testing.T) *Service {
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService() mockPermission := acmock.NewMockedPermissionsService()
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{ features := featuremgmt.WithFeatures()
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{{ PluginList: []pluginstore.Plugin{{
JSONData: plugins.JSONData{ JSONData: plugins.JSONData{
ID: "test", ID: "test",
@@ -1808,7 +1839,7 @@ func initDSService(t *testing.T) *Service {
ObjectBytes: req.ObjectBytes, ObjectBytes: req.ObjectBytes,
}, nil }, nil
}, },
}, plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) }, plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
require.NoError(t, err) require.NoError(t, err)
return dsService return dsService
@@ -0,0 +1,34 @@
package service
import (
"context"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
// DataSourceRetrieverImpl implements DataSourceRetriever by delegating to a Store.
type DataSourceRetrieverImpl struct {
store Store
}
var _ DataSourceRetriever = (*DataSourceRetrieverImpl)(nil)
// ProvideDataSourceRetriever creates a DataSourceRetriever for wire injection.
func ProvideDataSourceRetriever(db db.DB, features featuremgmt.FeatureToggles) DataSourceRetriever {
dslogger := log.New("datasources-retriever")
store := &SqlStore{db: db, logger: dslogger, features: features}
return &DataSourceRetrieverImpl{store: store}
}
// GetDataSource gets a datasource.
func (r *DataSourceRetrieverImpl) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
return r.store.GetDataSource(ctx, query)
}
// GetDataSourceInNamespace gets a datasource by namespace, name (datasource uid), and group (datasource type).
func (r *DataSourceRetrieverImpl) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) {
return r.store.GetDataSourceInNamespace(ctx, namespace, name, group)
}
+2 -1
View File
@@ -542,9 +542,10 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService()) dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService())
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsRetriever := dsservice.ProvideDataSourceRetriever(sqlStore, featuremgmt.WithFeatures())
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), _, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(),
quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, plugincontext. quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, plugincontext.
ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
require.NoError(t, err) require.NoError(t, err)
m := metrics.NewNGAlert(prometheus.NewRegistry()) m := metrics.NewNGAlert(prometheus.NewRegistry())
@@ -37,9 +37,10 @@ func SetupTestDataSourceSecretMigrationService(t *testing.T, sqlStore db.DB, kvS
features := featuremgmt.WithFeatures() features := featuremgmt.WithFeatures()
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
quotaService := quotatest.New(false, nil) quotaService := quotatest.New(false, nil)
dsRetriever := dsservice.ProvideDataSourceRetriever(sqlStore, features)
dsService, err := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), dsService, err := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(),
acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
require.NoError(t, err) require.NoError(t, err)
migService := ProvideDataSourceMigrationService(dsService, kvStore, features) migService := ProvideDataSourceMigrationService(dsService, kvStore, features)
return migService return migService
+7 -6
View File
@@ -293,15 +293,15 @@ overrides_path = overrides.yaml
overrides_reload_period = 5s overrides_reload_period = 5s
``` ```
To overrides the default quota for a tenant, add the following to the overrides.yaml file: To override the default quota for a tenant, add the following to the `overrides.yaml` file:
```yaml ```yaml
overrides: overrides:
<NAMESPACE>: <NAMESPACE>:
quotas: quotas:
<GROUP>.<RESOURCE>: <GROUP>/<RESOURCE>:
limit: 10 limit: 10
``` ```
Unless otherwise set, the NAMESPACE when running locally is `default`. Unless otherwise set, the `NAMESPACE` when running locally is `default`.
To access quotas, use the following API endpoint: To access quotas, use the following API endpoint:
``` ```
@@ -806,8 +806,10 @@ flowchart TD
#### Setting Dual Writer Mode #### Setting Dual Writer Mode
```ini ```ini
[unified_storage.{resource}.{kind}.{group}] ; [unified_storage.{resource}.{group}]
dualWriterMode = {0-5} [unified_storage.dashboards.dashboard.grafana.app]
; modes {0-5}
dualWriterMode = 0
``` ```
#### Background Sync Configuration #### Background Sync Configuration
@@ -1376,4 +1378,3 @@ disable_data_migrations = false
### Documentation ### Documentation
For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md). For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md).
@@ -11,7 +11,7 @@ INSERT INTO {{ .Ident "resource" }}
{{ .Ident "previous_resource_version" }} {{ .Ident "previous_resource_version" }}
) )
VALUES ( VALUES (
COALESCE({{ .Arg .Value }}, ""), (SELECT {{ .Ident "value" }} FROM {{ .Ident "resource_history" }} WHERE {{ .Ident "guid" }} = {{ .Arg .GUID }}),
{{ .Arg .GUID }}, {{ .Arg .GUID }},
{{ .Arg .Group }}, {{ .Arg .Group }},
{{ .Arg .Resource }}, {{ .Arg .Resource }},
@@ -19,13 +19,5 @@ VALUES (
{{ .Arg .Name }}, {{ .Arg .Name }},
{{ .Arg .Action }}, {{ .Arg .Action }},
{{ .Arg .Folder }}, {{ .Arg .Folder }},
CASE WHEN {{ .Arg .Action }} = 1 THEN 0 ELSE ( {{ .Arg .PreviousRV }}
SELECT {{ .Ident "resource_version" }}
FROM {{ .Ident "resource" }}
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
AND {{ .Ident "name" }} = {{ .Arg .Name }}
ORDER BY {{ .Ident "resource_version" }} DESC LIMIT 1
) END
); );
@@ -7,9 +7,7 @@ INSERT INTO {{ .Ident "resource_history" }}
{{ .Ident "namespace" }}, {{ .Ident "namespace" }},
{{ .Ident "name" }}, {{ .Ident "name" }},
{{ .Ident "action" }}, {{ .Ident "action" }},
{{ .Ident "folder" }}, {{ .Ident "folder" }}
{{ .Ident "previous_resource_version" }},
{{ .Ident "generation" }}
) )
VALUES ( VALUES (
COALESCE({{ .Arg .Value }}, ""), COALESCE({{ .Arg .Value }}, ""),
@@ -19,26 +17,5 @@ VALUES (
{{ .Arg .Namespace }}, {{ .Arg .Namespace }},
{{ .Arg .Name }}, {{ .Arg .Name }},
{{ .Arg .Action }}, {{ .Arg .Action }},
{{ .Arg .Folder }}, {{ .Arg .Folder }}
CASE WHEN {{ .Arg .Action }} = 1 THEN 0 ELSE (
SELECT {{ .Ident "resource_version" }}
FROM {{ .Ident "resource_history" }}
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
AND {{ .Ident "name" }} = {{ .Arg .Name }}
ORDER BY {{ .Ident "resource_version" }} DESC LIMIT 1
) END,
CASE
WHEN {{ .Arg .Action }} = 1 THEN 1
WHEN {{ .Arg .Action }} = 3 THEN 0
ELSE 1 + (
SELECT COUNT(1)
FROM {{ .Ident "resource_history" }}
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
AND {{ .Ident "name" }} = {{ .Arg .Name }}
)
END
); );
@@ -1,8 +1,10 @@
UPDATE {{ .Ident "resource" }} UPDATE {{ .Ident "resource" }}
SET SET
{{ .Ident "value" }} = {{ .Arg .Value }}, {{ .Ident "guid" }} = {{ .Arg .GUID }},
{{ .Ident "value" }} = (SELECT {{ .Ident "value" }} FROM {{ .Ident "resource_history" }} WHERE {{ .Ident "guid" }} = {{ .Arg .GUID }}),
{{ .Ident "action" }} = {{ .Arg .Action }}, {{ .Ident "action" }} = {{ .Arg .Action }},
{{ .Ident "folder" }} = {{ .Arg .Folder }} {{ .Ident "folder" }} = {{ .Arg .Folder }},
{{ .Ident "previous_resource_version" }} = {{ .Arg .PreviousRV }}
WHERE {{ .Ident "group" }} = {{ .Arg .Group }} WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
AND {{ .Ident "resource" }} = {{ .Arg .Resource }} AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
@@ -0,0 +1,5 @@
UPDATE {{ .Ident "resource_history" }}
SET
{{ .Ident "previous_resource_version" }} = {{ .Arg .PreviousRV }},
{{ .Ident "generation" }} = {{ .Arg .Generation }}
WHERE {{ .Ident "guid" }} = {{ .Arg .GUID }};
+122 -5
View File
@@ -12,6 +12,9 @@ import (
"time" "time"
"github.com/grafana/grafana/pkg/apimachinery/validation" "github.com/grafana/grafana/pkg/apimachinery/validation"
"github.com/grafana/grafana/pkg/storage/unified/sql/db"
"github.com/grafana/grafana/pkg/storage/unified/sql/dbutil"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
gocache "github.com/patrickmn/go-cache" gocache "github.com/patrickmn/go-cache"
) )
@@ -306,10 +309,6 @@ func (d *dataStore) GetResourceKeyAtRevision(ctx context.Context, key GetRequest
return DataKey{}, fmt.Errorf("invalid get request key: %w", err) return DataKey{}, fmt.Errorf("invalid get request key: %w", err)
} }
if rv == 0 {
rv = math.MaxInt64
}
listKey := ListRequestKey(key) listKey := ListRequestKey(key)
iter := d.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv}) iter := d.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv})
@@ -598,7 +597,7 @@ func ParseKey(key string) (DataKey, error) {
}, nil }, nil
} }
// Temporary while we need to support unified/sql/backend compatibility // Temporary while we need to support unified/sql/backend compatibility.
// Remove once we stop using RvManager in storage_backend.go // Remove once we stop using RvManager in storage_backend.go
func ParseKeyWithGUID(key string) (DataKey, error) { func ParseKeyWithGUID(key string) (DataKey, error) {
parts := strings.Split(key, "/") parts := strings.Split(key, "/")
@@ -815,3 +814,121 @@ func (d *dataStore) getGroupResources(ctx context.Context) ([]GroupResource, err
return results, nil return results, nil
} }
// TODO: remove when backwards compatibility is no longer needed.
var (
sqlKVUpdateLegacyResourceHistory = mustTemplate("sqlkv_update_legacy_resource_history.sql")
sqlKVInsertLegacyResource = mustTemplate("sqlkv_insert_legacy_resource.sql")
sqlKVUpdateLegacyResource = mustTemplate("sqlkv_update_legacy_resource.sql")
)
// TODO: remove when backwards compatibility is no longer needed.
type sqlKVLegacySaveRequest struct {
sqltemplate.SQLTemplate
GUID string
Group string
Resource string
Namespace string
Name string
Action int64
Folder string
PreviousRV int64
}
func (req sqlKVLegacySaveRequest) Validate() error {
return nil
}
// TODO: remove when backwards compatibility is no longer needed.
type sqlKVLegacyUpdateHistoryRequest struct {
sqltemplate.SQLTemplate
GUID string
PreviousRV int64
Generation int64
}
func (req sqlKVLegacyUpdateHistoryRequest) Validate() error {
return nil
}
// applyBackwardsCompatibleChanges updates the `resource` and `resource_history` tables
// to make sure the sqlkv implementation is backwards-compatible with the existing sql backend.
// Specifically, it will update the `resource_history` table to include the previous resource version
// and generation, which come from the `WriteEvent`, and also make the corresponding change on the
// `resource` table, no longer used in the storage backend.
//
// TODO: remove when backwards compatibility is no longer needed.
func (d *dataStore) applyBackwardsCompatibleChanges(ctx context.Context, tx db.Tx, event WriteEvent, key DataKey) error {
kv, isSQLKV := d.kv.(*sqlKV)
if !isSQLKV {
return nil
}
_, err := dbutil.Exec(ctx, tx, sqlKVUpdateLegacyResourceHistory, sqlKVLegacyUpdateHistoryRequest{
SQLTemplate: sqltemplate.New(kv.dialect),
GUID: key.GUID,
PreviousRV: event.PreviousRV,
Generation: event.Object.GetGeneration(),
})
if err != nil {
return fmt.Errorf("compatibility layer: failed to insert to resource: %w", err)
}
var action int64
switch key.Action {
case DataActionCreated:
action = 1
case DataActionUpdated:
action = 2
case DataActionDeleted:
action = 3
}
switch key.Action {
case DataActionCreated:
_, err := dbutil.Exec(ctx, tx, sqlKVInsertLegacyResource, sqlKVLegacySaveRequest{
SQLTemplate: sqltemplate.New(kv.dialect),
GUID: key.GUID,
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
Action: action,
Folder: key.Folder,
PreviousRV: event.PreviousRV,
})
if err != nil {
return fmt.Errorf("compatibility layer: failed to insert to resource: %w", err)
}
case DataActionUpdated:
_, err := dbutil.Exec(ctx, tx, sqlKVUpdateLegacyResource, sqlKVLegacySaveRequest{
SQLTemplate: sqltemplate.New(kv.dialect),
GUID: key.GUID,
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
Folder: key.Folder,
PreviousRV: event.PreviousRV,
})
if err != nil {
return fmt.Errorf("compatibility layer: failed to update resource: %w", err)
}
case DataActionDeleted:
_, err := dbutil.Exec(ctx, tx, sqlKVDeleteLegacyResource, sqlKVLegacySaveRequest{
SQLTemplate: sqltemplate.New(kv.dialect),
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
})
if err != nil {
return fmt.Errorf("compatibility layer: failed to delete from resource: %w", err)
}
}
return nil
}
+8 -75
View File
@@ -44,8 +44,6 @@ var (
sqlKVInsertData = mustTemplate("sqlkv_insert_datastore.sql") sqlKVInsertData = mustTemplate("sqlkv_insert_datastore.sql")
sqlKVUpdateData = mustTemplate("sqlkv_update_datastore.sql") sqlKVUpdateData = mustTemplate("sqlkv_update_datastore.sql")
sqlKVInsertLegacyResourceHistory = mustTemplate("sqlkv_insert_legacy_resource_history.sql") sqlKVInsertLegacyResourceHistory = mustTemplate("sqlkv_insert_legacy_resource_history.sql")
sqlKVInsertLegacyResource = mustTemplate("sqlkv_insert_legacy_resource.sql")
sqlKVUpdateLegacyResource = mustTemplate("sqlkv_update_legacy_resource.sql")
sqlKVDeleteLegacyResource = mustTemplate("sqlkv_delete_legacy_resource.sql") sqlKVDeleteLegacyResource = mustTemplate("sqlkv_delete_legacy_resource.sql")
sqlKVDelete = mustTemplate("sqlkv_delete.sql") sqlKVDelete = mustTemplate("sqlkv_delete.sql")
sqlKVBatchDelete = mustTemplate("sqlkv_batch_delete.sql") sqlKVBatchDelete = mustTemplate("sqlkv_batch_delete.sql")
@@ -157,26 +155,6 @@ func (req sqlKVSaveRequest) Validate() error {
return req.sqlKVSectionKey.Validate() return req.sqlKVSectionKey.Validate()
} }
type sqlKVLegacySaveRequest struct {
sqltemplate.SQLTemplate
Value []byte
GUID string
Group string
Resource string
Namespace string
Name string
Action int64
Folder string
}
func (req sqlKVLegacySaveRequest) Validate() error {
return nil
}
func (req sqlKVLegacySaveRequest) Results() ([]byte, error) {
return req.Value, nil
}
type sqlKVKeysRequest struct { type sqlKVKeysRequest struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
sqlKVSection sqlKVSection
@@ -392,7 +370,7 @@ func (w *sqlWriteCloser) Close() error {
// used to keep backwards compatibility between sql-based kvstore and unified/sql/backend // used to keep backwards compatibility between sql-based kvstore and unified/sql/backend
tx, ok := rvmanager.TxFromCtx(w.ctx) tx, ok := rvmanager.TxFromCtx(w.ctx)
if !ok { if !ok {
// temporary save for dataStore without rvmanager // temporary save for dataStore without rvmanager (non backwards-compatible)
// we can use the same template as the event one after we: // we can use the same template as the event one after we:
// - move PK from GUID to key_path // - move PK from GUID to key_path
// - remove all unnecessary columns (or at least their NOT NULL constraints) // - remove all unnecessary columns (or at least their NOT NULL constraints)
@@ -429,11 +407,12 @@ func (w *sqlWriteCloser) Close() error {
return nil return nil
} }
// special, temporary save that includes all the fields in resource_history that are not relevant for the kvstore, // special, temporary backwards-compatible save that includes all the fields in resource_history that are not relevant
// as well as the resource table. This is only called if an RvManager was passed to storage_backend, as that // for the kvstore, as well as the resource table. This is only called if an RvManager was passed to storage_backend, as that
// component will be responsible for populating the resource_version and key_path columns // component will be responsible for populating the resource_version and key_path columns.
// note that we are not touching resource_version table, neither the resource_version columns or the key_path column // For full backwards-compatibility, the `Save` function needs to be called within a callback that updates the resource_history
// as the RvManager will be responsible for this // table with `previous_resource_version` and `generation` and updates the `resource` table accordingly. See the
// storage_backend for the full implementation.
dataKey, err := ParseKeyWithGUID(w.sectionKey.Key) dataKey, err := ParseKeyWithGUID(w.sectionKey.Key)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse key: %w", err) return fmt.Errorf("failed to parse key: %w", err)
@@ -448,7 +427,7 @@ func (w *sqlWriteCloser) Close() error {
case DataActionDeleted: case DataActionDeleted:
action = 3 action = 3
default: default:
return fmt.Errorf("failed to parse key: %w", err) return fmt.Errorf("failed to parse key: invalid action")
} }
_, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResourceHistory, sqlKVSaveRequest{ _, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResourceHistory, sqlKVSaveRequest{
@@ -468,52 +447,6 @@ func (w *sqlWriteCloser) Close() error {
return fmt.Errorf("failed to save to resource_history: %w", err) return fmt.Errorf("failed to save to resource_history: %w", err)
} }
switch dataKey.Action {
case DataActionCreated:
_, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResource, sqlKVLegacySaveRequest{
SQLTemplate: sqltemplate.New(w.kv.dialect),
Value: w.buf.Bytes(),
GUID: dataKey.GUID,
Group: dataKey.Group,
Resource: dataKey.Resource,
Namespace: dataKey.Namespace,
Name: dataKey.Name,
Action: action,
Folder: dataKey.Folder,
})
if err != nil {
return fmt.Errorf("failed to insert to resource: %w", err)
}
case DataActionUpdated:
_, err = dbutil.Exec(w.ctx, tx, sqlKVUpdateLegacyResource, sqlKVLegacySaveRequest{
SQLTemplate: sqltemplate.New(w.kv.dialect),
Value: w.buf.Bytes(),
Group: dataKey.Group,
Resource: dataKey.Resource,
Namespace: dataKey.Namespace,
Name: dataKey.Name,
Action: action,
Folder: dataKey.Folder,
})
if err != nil {
return fmt.Errorf("failed to update resource: %w", err)
}
case DataActionDeleted:
_, err = dbutil.Exec(w.ctx, tx, sqlKVDeleteLegacyResource, sqlKVLegacySaveRequest{
SQLTemplate: sqltemplate.New(w.kv.dialect),
Group: dataKey.Group,
Resource: dataKey.Resource,
Namespace: dataKey.Namespace,
Name: dataKey.Name,
})
if err != nil {
return fmt.Errorf("failed to delete from resource: %w", err)
}
}
return nil return nil
} }
@@ -332,11 +332,14 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in
dataKey.GUID = uuid.New().String() dataKey.GUID = uuid.New().String()
var err error var err error
rv, err = k.rvManager.ExecWithRV(ctx, event.Key, func(tx db.Tx) (string, error) { rv, err = k.rvManager.ExecWithRV(ctx, event.Key, func(tx db.Tx) (string, error) {
err := k.dataStore.Save(rvmanager.ContextWithTx(ctx, tx), dataKey, bytes.NewReader(event.Value)) if err := k.dataStore.Save(rvmanager.ContextWithTx(ctx, tx), dataKey, bytes.NewReader(event.Value)); err != nil {
if err != nil {
return "", fmt.Errorf("failed to write data: %w", err) return "", fmt.Errorf("failed to write data: %w", err)
} }
if err := k.dataStore.applyBackwardsCompatibleChanges(ctx, tx, event, dataKey); err != nil {
return "", fmt.Errorf("failed to apply backwards compatible updates: %w", err)
}
return dataKey.GUID, nil return dataKey.GUID, nil
}) })
if err != nil { if err != nil {
-144
View File
@@ -1,144 +0,0 @@
package apis
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util/testutil"
)
const pluginsDiscoveryJSON = `[
{
"version": "v0alpha1",
"freshness": "Current",
"resources": [
{
"resource": "metas",
"responseKind": {
"group": "",
"kind": "Meta",
"version": ""
},
"scope": "Namespaced",
"singularResource": "meta",
"subresources": [
{
"responseKind": {
"group": "",
"kind": "Meta",
"version": ""
},
"subresource": "status",
"verbs": [
"get",
"patch",
"update"
]
}
],
"verbs": [
"get",
"list"
]
},
{
"resource": "plugins",
"responseKind": {
"group": "",
"kind": "Plugin",
"version": ""
},
"scope": "Namespaced",
"singularResource": "plugin",
"subresources": [
{
"responseKind": {
"group": "",
"kind": "Plugin",
"version": ""
},
"subresource": "status",
"verbs": [
"get",
"patch",
"update"
]
}
],
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
}
]
}
]`
func setupHelper(t *testing.T, openFeatureAPIEnabled bool) *K8sTestHelper {
t.Helper()
helper := NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: true,
DisableAnonymous: true,
APIServerRuntimeConfig: "plugins.grafana.app/v0alpha1=true",
OpenFeatureAPIEnabled: openFeatureAPIEnabled,
})
t.Cleanup(func() { helper.Shutdown() })
return helper
}
func TestIntegrationAPIServerRuntimeConfig(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
t.Run("discovery with openfeature api enabled", func(t *testing.T) {
helper := setupHelper(t, true)
disco, err := helper.GetGroupVersionInfoJSON("features.grafana.app")
require.NoError(t, err)
require.JSONEq(t, `[
{
"freshness": "Current",
"resources": [
{
"resource": "noop",
"responseKind": {
"group": "",
"kind": "Status",
"version": ""
},
"scope": "Namespaced",
"singularResource": "noop",
"verbs": [
"get"
]
}
],
"version": "v0alpha1"
}
]`, disco)
// plugins should still be discoverable
disco, err = helper.GetGroupVersionInfoJSON("plugins.grafana.app")
require.NoError(t, err)
require.JSONEq(t, pluginsDiscoveryJSON, disco)
require.NoError(t, err)
})
t.Run("discovery with openfeature api false", func(t *testing.T) {
helper := setupHelper(t, false)
_, err := helper.GetGroupVersionInfoJSON("features.grafana.app")
require.Error(t, err, "expected error when openfeature api is disabled")
// plugins should still be discoverable
disco, err := helper.GetGroupVersionInfoJSON("plugins.grafana.app")
require.NoError(t, err)
require.JSONEq(t, pluginsDiscoveryJSON, disco)
require.NoError(t, err)
})
}
+4
View File
@@ -10,6 +10,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/tests/testsuite"
@@ -177,6 +178,9 @@ func setupHelper(t *testing.T) *apis.K8sTestHelper {
AppModeProduction: true, AppModeProduction: true,
DisableAnonymous: true, DisableAnonymous: true,
APIServerRuntimeConfig: "plugins.grafana.app/v0alpha1=true", APIServerRuntimeConfig: "plugins.grafana.app/v0alpha1=true",
EnableFeatureToggles: []string{
featuremgmt.FlagPluginStoreServiceLoading,
},
}) })
t.Cleanup(func() { helper.Shutdown() }) t.Cleanup(func() { helper.Shutdown() })
return helper return helper
+3 -2
View File
@@ -320,8 +320,9 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) {
require.NoError(t, err) require.NoError(t, err)
_, err = openFeatureSect.NewKey("enable_api", strconv.FormatBool(opts.OpenFeatureAPIEnabled)) _, err = openFeatureSect.NewKey("enable_api", strconv.FormatBool(opts.OpenFeatureAPIEnabled))
require.NoError(t, err) require.NoError(t, err)
if !opts.OpenFeatureAPIEnabled {
_, err = openFeatureSect.NewKey("provider", "static") // in practice, APIEnabled being false goes with features-service type, but trying to make tests work if opts.OpenFeatureAPIEnabled {
_, err = openFeatureSect.NewKey("provider", "static")
require.NoError(t, err) require.NoError(t, err)
_, err = openFeatureSect.NewKey("targetingKey", "grafana") _, err = openFeatureSect.NewKey("targetingKey", "grafana")
require.NoError(t, err) require.NoError(t, err)
@@ -47,7 +47,7 @@ export const getFormFieldsForSilence = (silence: Silence): SilenceFormFields =>
startsAt: interval.start.toISOString(), startsAt: interval.start.toISOString(),
endsAt: interval.end.toISOString(), endsAt: interval.end.toISOString(),
comment: silence.comment, comment: silence.comment,
createdBy: silence.createdBy, createdBy: isExpired ? contextSrv.user.name : silence.createdBy,
duration: intervalToAbbreviatedDurationString(interval), duration: intervalToAbbreviatedDurationString(interval),
isRegex: false, isRegex: false,
matchers: silence.matchers?.map(matcherToMatcherField) || [], matchers: silence.matchers?.map(matcherToMatcherField) || [],
@@ -39,7 +39,7 @@ export function RecentlyViewedDashboards() {
retry(); retry();
}; };
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) { if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) {
return null; return null;
} }
@@ -76,10 +76,6 @@ export function RecentlyViewedDashboards() {
</> </>
)} )}
{loading && <Spinner />} {loading && <Spinner />}
{/* TODO: Better empty state https://github.com/grafana/grafana/issues/114804 */}
{!loading && recentDashboards.length === 0 && (
<Text>{t('browse-dashboards.recently-viewed.empty', 'Nothing viewed yet')}</Text>
)}
{!loading && recentDashboards.length > 0 && ( {!loading && recentDashboards.length > 0 && (
<ul className={styles.list}> <ul className={styles.list}>
@@ -128,7 +128,7 @@ describe('PanelTimeRange', () => {
expect(panelTime.state.value.to.format('Z')).toBe('+00:00'); // UTC expect(panelTime.state.value.to.format('Z')).toBe('+00:00'); // UTC
}); });
it('should handle invalid time reference in timeShift', () => { it('should handle invalid time reference in timeShift with relative time range', () => {
const panelTime = new PanelTimeRange({ timeShift: 'now-1d' }); const panelTime = new PanelTimeRange({ timeShift: 'now-1d' });
buildAndActivateSceneFor(panelTime); buildAndActivateSceneFor(panelTime);
@@ -139,6 +139,22 @@ describe('PanelTimeRange', () => {
expect(panelTime.state.to).toBe('now'); expect(panelTime.state.to).toBe('now');
}); });
it('should handle invalid time reference in timeShift with absolute time range', () => {
const panelTime = new PanelTimeRange({ timeShift: 'now-1d' });
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
const absoluteFrom = '2019-02-11T10:00:00.000Z';
const absoluteTo = '2019-02-11T16:00:00.000Z';
const scene = new SceneFlexLayout({
$timeRange: new SceneTimeRange({ from: absoluteFrom, to: absoluteTo }),
children: [new SceneFlexItem({ body: panel })],
});
activateFullSceneTree(scene);
expect(panelTime.state.timeInfo).toBe('invalid timeshift');
expect(panelTime.state.from).toBe(absoluteFrom);
expect(panelTime.state.to).toBe(absoluteTo);
});
it('should handle invalid time reference in timeShift combined with timeFrom', () => { it('should handle invalid time reference in timeShift combined with timeFrom', () => {
const panelTime = new PanelTimeRange({ const panelTime = new PanelTimeRange({
timeFrom: 'now-2h', timeFrom: 'now-2h',
@@ -153,6 +169,66 @@ describe('PanelTimeRange', () => {
expect(panelTime.state.to).toBe('now'); expect(panelTime.state.to).toBe('now');
}); });
describe('from/to state format for liveNow compatibility', () => {
it('should store relative strings in from/to when timeShift is applied to relative time range', () => {
const panelTime = new PanelTimeRange({ timeShift: '2h' });
buildAndActivateSceneFor(panelTime);
expect(panelTime.state.from).toBe('now-6h-2h');
expect(panelTime.state.to).toBe('now-2h');
expect(panelTime.state.value.raw.from).toBe('now-6h-2h');
expect(panelTime.state.value.raw.to).toBe('now-2h');
});
it('should store relative strings when both timeFrom and timeShift are applied', () => {
const panelTime = new PanelTimeRange({ timeFrom: '2h', timeShift: '1h' });
buildAndActivateSceneFor(panelTime);
expect(panelTime.state.from).toBe('now-2h-1h');
expect(panelTime.state.to).toBe('now-1h');
});
it('should store ISO strings when timeShift is applied to absolute time range', () => {
const panelTime = new PanelTimeRange({ timeShift: '1h' });
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
const absoluteFrom = '2019-02-11T10:00:00.000Z';
const absoluteTo = '2019-02-11T16:00:00.000Z';
const scene = new SceneFlexLayout({
$timeRange: new SceneTimeRange({ from: absoluteFrom, to: absoluteTo }),
children: [new SceneFlexItem({ body: panel })],
});
activateFullSceneTree(scene);
expect(panelTime.state.from).toBe('2019-02-11T09:00:00.000Z');
expect(panelTime.state.to).toBe('2019-02-11T15:00:00.000Z');
});
it('should update from/to when ancestor time range changes', () => {
const panelTime = new PanelTimeRange({ timeShift: '1h' });
const sceneTimeRange = new SceneTimeRange({ from: 'now-6h', to: 'now' });
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
const scene = new SceneFlexLayout({
$timeRange: sceneTimeRange,
children: [new SceneFlexItem({ body: panel })],
});
activateFullSceneTree(scene);
expect(panelTime.state.from).toBe('now-6h-1h');
expect(panelTime.state.to).toBe('now-1h');
sceneTimeRange.onTimeRangeChange({
from: dateTime('2019-02-11T12:00:00.000Z'),
to: dateTime('2019-02-11T18:00:00.000Z'),
raw: { from: 'now-12h', to: 'now' },
});
expect(panelTime.state.from).toBe('now-12h-1h');
expect(panelTime.state.to).toBe('now-1h');
});
});
describe('onTimeRangeChange', () => { describe('onTimeRangeChange', () => {
it('should reverse timeShift when updating time range', () => { it('should reverse timeShift when updating time range', () => {
const oneHourShift = '1h'; const oneHourShift = '1h';
@@ -81,7 +81,19 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
} }
const overrideResult = this.getTimeOverride(timeRange.value); const overrideResult = this.getTimeOverride(timeRange.value);
this.setState({ value: overrideResult.timeRange, timeInfo: overrideResult.timeInfo }); const { timeRange: overrideTimeRange } = overrideResult;
this.setState({
value: overrideTimeRange,
timeInfo: overrideResult.timeInfo,
from:
typeof overrideTimeRange.raw.from === 'string'
? overrideTimeRange.raw.from
: overrideTimeRange.raw.from.toISOString(),
to:
typeof overrideTimeRange.raw.to === 'string'
? overrideTimeRange.raw.to
: overrideTimeRange.raw.to.toISOString(),
});
} }
// Get a time shifted request to compare with the primary request. // Get a time shifted request to compare with the primary request.
@@ -153,10 +165,10 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
// Only evaluate if the timeFrom if parent time is relative // Only evaluate if the timeFrom if parent time is relative
if (rangeUtil.isRelativeTimeRange(parentTimeRange.raw)) { if (rangeUtil.isRelativeTimeRange(parentTimeRange.raw)) {
const timeZone = this.getTimeZone(); const timezone = this.getTimeZone();
newTimeData.timeRange = { newTimeData.timeRange = {
from: dateMath.parse(timeFromInfo.from, undefined, timeZone)!, from: dateMath.toDateTime(timeFromInfo.from, { timezone })!,
to: dateMath.parse(timeFromInfo.to, undefined, timeZone)!, to: dateMath.toDateTime(timeFromInfo.to, { timezone })!,
raw: { from: timeFromInfo.from, to: timeFromInfo.to }, raw: { from: timeFromInfo.from, to: timeFromInfo.to },
}; };
infoBlocks.push(timeFromInfo.display); infoBlocks.push(timeFromInfo.display);
@@ -172,18 +184,39 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
return newTimeData; return newTimeData;
} }
const timeShift = '-' + timeShiftInterpolated; const shift = '-' + timeShiftInterpolated;
infoBlocks.push('timeshift ' + timeShift); infoBlocks.push('timeshift ' + shift);
const from = dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false)!; if (rangeUtil.isRelativeTimeRange(newTimeData.timeRange.raw)) {
const to = dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true)!; const timezone = this.getTimeZone();
if (!from || !to) { const rawFromShifted = `${newTimeData.timeRange.raw.from}${shift}`;
newTimeData.timeInfo = 'invalid timeshift'; const rawToShifted = `${newTimeData.timeRange.raw.to}${shift}`;
return newTimeData;
const from = dateMath.toDateTime(rawFromShifted, { timezone });
const to = dateMath.toDateTime(rawToShifted, { timezone });
if (!from || !to) {
newTimeData.timeInfo = 'invalid timeshift';
return newTimeData;
}
newTimeData.timeRange = {
from,
to,
raw: { from: rawFromShifted, to: rawToShifted },
};
} else {
const from = dateMath.parseDateMath(shift, newTimeData.timeRange.from, false);
const to = dateMath.parseDateMath(shift, newTimeData.timeRange.to, true);
if (!from || !to) {
newTimeData.timeInfo = 'invalid timeshift';
return newTimeData;
}
newTimeData.timeRange = { from, to, raw: { from, to } };
} }
newTimeData.timeRange = { from, to, raw: { from, to } };
} }
if (compareWith) { if (compareWith) {
+17 -4
View File
@@ -3791,7 +3791,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4454,6 +4453,7 @@
}, },
"no-properties-changed": "Žádné relevantní vlastnosti se nezměnily", "no-properties-changed": "Žádné relevantní vlastnosti se nezměnily",
"table": { "table": {
"notes": "",
"updated": "Datum", "updated": "Datum",
"updatedBy": "Aktualizoval uživatel", "updatedBy": "Aktualizoval uživatel",
"version": "Verze" "version": "Verze"
@@ -4912,7 +4912,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Hodnoty oddělené čárkou"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Filtr názvu", "name-filter": "Filtr názvu",
@@ -6010,6 +6011,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Vlastní možnosti", "custom-options": "Vlastní možnosti",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Hodnoty oddělené čárkou", "name-values-separated-comma": "Hodnoty oddělené čárkou",
"selection-options": "Možnosti výběru" "selection-options": "Možnosti výběru"
}, },
@@ -6601,6 +6605,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Nástěnka byla uložena" "message-dashboard-saved": "Nástěnka byla uložena"
}, },
@@ -6624,6 +6633,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6683,8 +6693,11 @@
"tooltip-show-usages": "Zobrazit použití" "tooltip-show-usages": "Zobrazit použití"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Náhled hodnot", "show-more": "Zobrazit více",
"show-more": "Zobrazit více" "preview-of-values_one": "",
"preview-of-values_few": "",
"preview-of-values_many": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Keine relevanten Eigenschaften geändert", "no-properties-changed": "Keine relevanten Eigenschaften geändert",
"table": { "table": {
"notes": "",
"updated": "Datum", "updated": "Datum",
"updatedBy": "Aktualisiert von", "updatedBy": "Aktualisiert von",
"version": "Version" "version": "Version"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Werte werden durch Komma getrennt"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Namensfilter", "name-filter": "Namensfilter",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Benutzerdefinierte Optionen", "custom-options": "Benutzerdefinierte Optionen",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Werte werden durch Komma getrennt", "name-values-separated-comma": "Werte werden durch Komma getrennt",
"selection-options": "Auswahloptionen" "selection-options": "Auswahloptionen"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Dashboard gespeichert" "message-dashboard-saved": "Dashboard gespeichert"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Nutzungen anzeigen" "tooltip-show-usages": "Nutzungen anzeigen"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Vorschau der Werte", "show-more": "Mehr anzeigen",
"show-more": "Mehr anzeigen" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
-1
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "Clear history", "clear": "Clear history",
"empty": "Nothing viewed yet",
"error": "Recently viewed dashboards couldnt be loaded.", "error": "Recently viewed dashboards couldnt be loaded.",
"retry": "Retry", "retry": "Retry",
"title": "Recently viewed" "title": "Recently viewed"
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "No se ha cambiado ninguna propiedad relevante", "no-properties-changed": "No se ha cambiado ninguna propiedad relevante",
"table": { "table": {
"notes": "",
"updated": "Fecha", "updated": "Fecha",
"updatedBy": "Actualizada por", "updatedBy": "Actualizada por",
"version": "Versión" "version": "Versión"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Valores separados por coma"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Nombrar filtro", "name-filter": "Nombrar filtro",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Opciones personalizadas", "custom-options": "Opciones personalizadas",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Valores separados por comas", "name-values-separated-comma": "Valores separados por comas",
"selection-options": "Opciones de selección" "selection-options": "Opciones de selección"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Dashboard guardado" "message-dashboard-saved": "Dashboard guardado"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Mostrar usos" "tooltip-show-usages": "Mostrar usos"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Vista previa de los valores", "show-more": "Mostrar más",
"show-more": "Mostrar más" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Aucune propriété pertinente na été modifiée", "no-properties-changed": "Aucune propriété pertinente na été modifiée",
"table": { "table": {
"notes": "",
"updated": "Date", "updated": "Date",
"updatedBy": "Mis à jour par", "updatedBy": "Mis à jour par",
"version": "Version" "version": "Version"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Valeurs séparées par une virgule"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Nom du filtre", "name-filter": "Nom du filtre",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Personnaliser les options", "custom-options": "Personnaliser les options",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Valeurs séparées par des virgules", "name-values-separated-comma": "Valeurs séparées par des virgules",
"selection-options": "Options de sélection" "selection-options": "Options de sélection"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Tableau de bord enregistré" "message-dashboard-saved": "Tableau de bord enregistré"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Afficher les usages" "tooltip-show-usages": "Afficher les usages"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Aperçu des valeurs", "show-more": "Afficher plus",
"show-more": "Afficher plus" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Nem változtak meg a releváns tulajdonságok", "no-properties-changed": "Nem változtak meg a releváns tulajdonságok",
"table": { "table": {
"notes": "",
"updated": "Dátum", "updated": "Dátum",
"updatedBy": "Frissítette:", "updatedBy": "Frissítette:",
"version": "Verzió" "version": "Verzió"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Értékek vesszővel elválasztva"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Névszűrő", "name-filter": "Névszűrő",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Egyéni opciók", "custom-options": "Egyéni opciók",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Értékek vesszővel elválasztva", "name-values-separated-comma": "Értékek vesszővel elválasztva",
"selection-options": "Kijelölés beállításai" "selection-options": "Kijelölés beállításai"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Irányítópult elmentve" "message-dashboard-saved": "Irányítópult elmentve"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Használatok megjelenítése" "tooltip-show-usages": "Használatok megjelenítése"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Értékek előnézete", "show-more": "Több megjelenítése",
"show-more": "Több megjelenítése" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+14 -4
View File
@@ -3743,7 +3743,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4397,6 +4396,7 @@
}, },
"no-properties-changed": "Tidak ada properti yang relevan yang diubah", "no-properties-changed": "Tidak ada properti yang relevan yang diubah",
"table": { "table": {
"notes": "",
"updated": "Tanggal", "updated": "Tanggal",
"updatedBy": "Diperbarui Oleh", "updatedBy": "Diperbarui Oleh",
"version": "Versi" "version": "Versi"
@@ -4855,7 +4855,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Nilai dipisahkan dengan koma"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Filter nama", "name-filter": "Filter nama",
@@ -5947,6 +5948,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Opsi kustom", "custom-options": "Opsi kustom",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Nilai dipisahkan dengan koma", "name-values-separated-comma": "Nilai dipisahkan dengan koma",
"selection-options": "Opsi pemilihan" "selection-options": "Opsi pemilihan"
}, },
@@ -6532,6 +6536,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Dasbor disimpan" "message-dashboard-saved": "Dasbor disimpan"
}, },
@@ -6555,6 +6564,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6614,8 +6624,8 @@
"tooltip-show-usages": "Tampilkan penggunaan" "tooltip-show-usages": "Tampilkan penggunaan"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Pratinjau nilai", "show-more": "Tampilkan lebih banyak",
"show-more": "Tampilkan lebih banyak" "preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Nessuna proprietà rilevante modificata", "no-properties-changed": "Nessuna proprietà rilevante modificata",
"table": { "table": {
"notes": "",
"updated": "Data", "updated": "Data",
"updatedBy": "Aggiornato da", "updatedBy": "Aggiornato da",
"version": "Versione" "version": "Versione"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Valori separati da virgola"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Filtro nome", "name-filter": "Filtro nome",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Opzioni personalizzate", "custom-options": "Opzioni personalizzate",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Valori separati da virgola", "name-values-separated-comma": "Valori separati da virgola",
"selection-options": "Seleziona opzioni" "selection-options": "Seleziona opzioni"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Dashboard salvata" "message-dashboard-saved": "Dashboard salvata"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Mostra utilizzi" "tooltip-show-usages": "Mostra utilizzi"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Anteprima dei valori", "show-more": "Mostra di più",
"show-more": "Mostra di più" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+14 -4
View File
@@ -3743,7 +3743,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4397,6 +4396,7 @@
}, },
"no-properties-changed": "関連するプロパティは変更されていません", "no-properties-changed": "関連するプロパティは変更されていません",
"table": { "table": {
"notes": "",
"updated": "日付", "updated": "日付",
"updatedBy": "更新者", "updatedBy": "更新者",
"version": "バージョン" "version": "バージョン"
@@ -4855,7 +4855,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "カンマで区切った値"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "名前フィルター", "name-filter": "名前フィルター",
@@ -5947,6 +5948,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "カスタムオプション", "custom-options": "カスタムオプション",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "カンマ区切りの値", "name-values-separated-comma": "カンマ区切りの値",
"selection-options": "選択オプション" "selection-options": "選択オプション"
}, },
@@ -6532,6 +6536,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "ダッシュボードが保存されました" "message-dashboard-saved": "ダッシュボードが保存されました"
}, },
@@ -6555,6 +6564,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6614,8 +6624,8 @@
"tooltip-show-usages": "使用状況を表示" "tooltip-show-usages": "使用状況を表示"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "値のプレビュー", "show-more": "さらに表示",
"show-more": "さらに表示" "preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+14 -4
View File
@@ -3743,7 +3743,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4397,6 +4396,7 @@
}, },
"no-properties-changed": "변경된 관련 속성 없음", "no-properties-changed": "변경된 관련 속성 없음",
"table": { "table": {
"notes": "",
"updated": "날짜", "updated": "날짜",
"updatedBy": "업데이트한 사용자", "updatedBy": "업데이트한 사용자",
"version": "버전" "version": "버전"
@@ -4855,7 +4855,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "쉼표로 구분된 값"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "이름 필터", "name-filter": "이름 필터",
@@ -5947,6 +5948,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "사용자 지정 옵션", "custom-options": "사용자 지정 옵션",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "쉼표로 구분된 값", "name-values-separated-comma": "쉼표로 구분된 값",
"selection-options": "선택 옵션" "selection-options": "선택 옵션"
}, },
@@ -6532,6 +6536,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "대시보드가 저장되었습니다" "message-dashboard-saved": "대시보드가 저장되었습니다"
}, },
@@ -6555,6 +6564,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6614,8 +6624,8 @@
"tooltip-show-usages": "사용처 표시" "tooltip-show-usages": "사용처 표시"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "값 미리 보기", "show-more": " 보기",
"show-more": "더 보기" "preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Geen relevante eigenschappen gewijzigd", "no-properties-changed": "Geen relevante eigenschappen gewijzigd",
"table": { "table": {
"notes": "",
"updated": "Datum", "updated": "Datum",
"updatedBy": "Bijgewerkt door", "updatedBy": "Bijgewerkt door",
"version": "Versie" "version": "Versie"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Waarden gescheiden door komma"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Filter een naam geven", "name-filter": "Filter een naam geven",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Aangepaste opties", "custom-options": "Aangepaste opties",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Waarden gescheiden door komma", "name-values-separated-comma": "Waarden gescheiden door komma",
"selection-options": "Selectiemogelijkheden" "selection-options": "Selectiemogelijkheden"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Dashboard opgeslagen" "message-dashboard-saved": "Dashboard opgeslagen"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Gebruik weergeven" "tooltip-show-usages": "Gebruik weergeven"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Voorbeeldweergave van waarden", "show-more": "Meer weergeven",
"show-more": "Meer weergeven" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+17 -4
View File
@@ -3791,7 +3791,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4454,6 +4453,7 @@
}, },
"no-properties-changed": "Nie zmieniono istotnych właściwości", "no-properties-changed": "Nie zmieniono istotnych właściwości",
"table": { "table": {
"notes": "",
"updated": "Data", "updated": "Data",
"updatedBy": "Zaktualizowane przez", "updatedBy": "Zaktualizowane przez",
"version": "Wersja" "version": "Wersja"
@@ -4912,7 +4912,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Wartości rozdzielone przecinkami"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Filtr nazwy", "name-filter": "Filtr nazwy",
@@ -6010,6 +6011,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Opcje niestandardowe", "custom-options": "Opcje niestandardowe",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Wartości rozdzielone przecinkami", "name-values-separated-comma": "Wartości rozdzielone przecinkami",
"selection-options": "Opcje wyboru" "selection-options": "Opcje wyboru"
}, },
@@ -6601,6 +6605,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Pulpit został zapisany" "message-dashboard-saved": "Pulpit został zapisany"
}, },
@@ -6624,6 +6633,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6683,8 +6693,11 @@
"tooltip-show-usages": "Wyświetl użycie" "tooltip-show-usages": "Wyświetl użycie"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Podgląd wartości", "show-more": "Pokaż więcej",
"show-more": "Pokaż więcej" "preview-of-values_one": "",
"preview-of-values_few": "",
"preview-of-values_many": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Nenhuma propriedade relevante alterada", "no-properties-changed": "Nenhuma propriedade relevante alterada",
"table": { "table": {
"notes": "",
"updated": "Data", "updated": "Data",
"updatedBy": "Atualizada por", "updatedBy": "Atualizada por",
"version": "Versão" "version": "Versão"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Valores separados por vírgula"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Filtro de nome", "name-filter": "Filtro de nome",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Opções personalizadas", "custom-options": "Opções personalizadas",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Valores separados por vírgula", "name-values-separated-comma": "Valores separados por vírgula",
"selection-options": "Opções de seleção" "selection-options": "Opções de seleção"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Painel de controle salvo" "message-dashboard-saved": "Painel de controle salvo"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Exibir usos" "tooltip-show-usages": "Exibir usos"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Pré-visualização de valores", "show-more": "Exibir mais",
"show-more": "Exibir mais" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Nenhuma propriedade relevante alterada", "no-properties-changed": "Nenhuma propriedade relevante alterada",
"table": { "table": {
"notes": "",
"updated": "Data", "updated": "Data",
"updatedBy": "Atualizado por", "updatedBy": "Atualizado por",
"version": "Versão" "version": "Versão"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Valores separados por vírgulas"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Filtro de nome", "name-filter": "Filtro de nome",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Opções personalizadas", "custom-options": "Opções personalizadas",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Valores separados por vírgulas", "name-values-separated-comma": "Valores separados por vírgulas",
"selection-options": "Opções de seleção" "selection-options": "Opções de seleção"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Painel de controlo guardado" "message-dashboard-saved": "Painel de controlo guardado"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Mostrar utilizações" "tooltip-show-usages": "Mostrar utilizações"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Pré-visualização de valores", "show-more": "Mostrar mais",
"show-more": "Mostrar mais" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+17 -4
View File
@@ -3791,7 +3791,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4454,6 +4453,7 @@
}, },
"no-properties-changed": "Нет изменений соответствующих свойств", "no-properties-changed": "Нет изменений соответствующих свойств",
"table": { "table": {
"notes": "",
"updated": "Дата", "updated": "Дата",
"updatedBy": "Обновлено", "updatedBy": "Обновлено",
"version": "Версия" "version": "Версия"
@@ -4912,7 +4912,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Значения, разделенные запятыми"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Фильтр по названию", "name-filter": "Фильтр по названию",
@@ -6010,6 +6011,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Пользовательские параметры", "custom-options": "Пользовательские параметры",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Значения, разделенные запятыми", "name-values-separated-comma": "Значения, разделенные запятыми",
"selection-options": "Параметры выбора" "selection-options": "Параметры выбора"
}, },
@@ -6601,6 +6605,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Дашборд сохранен" "message-dashboard-saved": "Дашборд сохранен"
}, },
@@ -6624,6 +6633,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6683,8 +6693,11 @@
"tooltip-show-usages": "Показать варианты использования" "tooltip-show-usages": "Показать варианты использования"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Просмотр значений", "show-more": "Показать еще",
"show-more": "Показать еще" "preview-of-values_one": "",
"preview-of-values_few": "",
"preview-of-values_many": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "Inga relevanta egenskaper har ändrats", "no-properties-changed": "Inga relevanta egenskaper har ändrats",
"table": { "table": {
"notes": "",
"updated": "Datum", "updated": "Datum",
"updatedBy": "Uppdaterad per", "updatedBy": "Uppdaterad per",
"version": "Version" "version": "Version"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Värden åtskilda med kommatecken"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Namnfilter", "name-filter": "Namnfilter",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Anpassade alternativ", "custom-options": "Anpassade alternativ",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Värden åtskilda med kommatecken", "name-values-separated-comma": "Värden åtskilda med kommatecken",
"selection-options": "Urvalsalternativ" "selection-options": "Urvalsalternativ"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Kontrollpanelen sparades" "message-dashboard-saved": "Kontrollpanelen sparades"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Visa användningar" "tooltip-show-usages": "Visa användningar"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Förhandsgranska värden", "show-more": "Visa mer",
"show-more": "Visa mer" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+15 -4
View File
@@ -3759,7 +3759,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4416,6 +4415,7 @@
}, },
"no-properties-changed": "İlgili hiçbir özellik değiştirilmedi", "no-properties-changed": "İlgili hiçbir özellik değiştirilmedi",
"table": { "table": {
"notes": "",
"updated": "Tarih", "updated": "Tarih",
"updatedBy": "Güncelleyen:", "updatedBy": "Güncelleyen:",
"version": "Sürüm" "version": "Sürüm"
@@ -4874,7 +4874,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "Virgülle ayrılmış değerler"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "Ad filtresi", "name-filter": "Ad filtresi",
@@ -5968,6 +5969,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "Özel seçenekler", "custom-options": "Özel seçenekler",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "Virgülle ayrılmış değerler", "name-values-separated-comma": "Virgülle ayrılmış değerler",
"selection-options": "Seçim ayarları" "selection-options": "Seçim ayarları"
}, },
@@ -6555,6 +6559,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "Pano kaydedildi" "message-dashboard-saved": "Pano kaydedildi"
}, },
@@ -6578,6 +6587,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6637,8 +6647,9 @@
"tooltip-show-usages": "Kullanımları göster" "tooltip-show-usages": "Kullanımları göster"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "Değerlerin ön izlemesi", "show-more": "Daha fazla göster",
"show-more": "Daha fazla göster" "preview-of-values_one": "",
"preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+14 -4
View File
@@ -3743,7 +3743,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4397,6 +4396,7 @@
}, },
"no-properties-changed": "没有相关属性更改", "no-properties-changed": "没有相关属性更改",
"table": { "table": {
"notes": "",
"updated": "日期", "updated": "日期",
"updatedBy": "更新者", "updatedBy": "更新者",
"version": "版本" "version": "版本"
@@ -4855,7 +4855,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "以逗号分隔的值"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "名称筛选器", "name-filter": "名称筛选器",
@@ -5947,6 +5948,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "自定义选项", "custom-options": "自定义选项",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "以逗号分隔的值", "name-values-separated-comma": "以逗号分隔的值",
"selection-options": "选择内容选项" "selection-options": "选择内容选项"
}, },
@@ -6532,6 +6536,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "数据面板已保存" "message-dashboard-saved": "数据面板已保存"
}, },
@@ -6555,6 +6564,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6614,8 +6624,8 @@
"tooltip-show-usages": "显示使用情况" "tooltip-show-usages": "显示使用情况"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "值预览", "show-more": "显示更多",
"show-more": "显示更多" "preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {
+14 -4
View File
@@ -3743,7 +3743,6 @@
}, },
"recently-viewed": { "recently-viewed": {
"clear": "", "clear": "",
"empty": "",
"error": "", "error": "",
"retry": "", "retry": "",
"title": "" "title": ""
@@ -4397,6 +4396,7 @@
}, },
"no-properties-changed": "沒有相關的屬性變更", "no-properties-changed": "沒有相關的屬性變更",
"table": { "table": {
"notes": "",
"updated": "日期", "updated": "日期",
"updatedBy": "更新者", "updatedBy": "更新者",
"version": "版本" "version": "版本"
@@ -4855,7 +4855,8 @@
"apply": "", "apply": "",
"change-value": "", "change-value": "",
"discard": "", "discard": "",
"modal-title": "" "modal-title": "",
"values": "以逗號分隔的值"
}, },
"datasource-options": { "datasource-options": {
"name-filter": "名稱篩選", "name-filter": "名稱篩選",
@@ -5947,6 +5948,9 @@
}, },
"custom-variable-form": { "custom-variable-form": {
"custom-options": "自訂選項", "custom-options": "自訂選項",
"json-values-tooltip": "",
"name-csv-values": "",
"name-json-values": "",
"name-values-separated-comma": "以逗號分隔的值", "name-values-separated-comma": "以逗號分隔的值",
"selection-options": "選擇選項" "selection-options": "選擇選項"
}, },
@@ -6532,6 +6536,11 @@
} }
} }
}, },
"use-modal-editor": {
"description": {
"change-variable-query": ""
}
},
"use-save-dashboard": { "use-save-dashboard": {
"message-dashboard-saved": "儀表板已儲存" "message-dashboard-saved": "儀表板已儲存"
}, },
@@ -6555,6 +6564,7 @@
"label": "" "label": ""
}, },
"hidden": { "hidden": {
"description": "",
"label": "" "label": ""
}, },
"hidden-label": { "hidden-label": {
@@ -6614,8 +6624,8 @@
"tooltip-show-usages": "顯示使用情況" "tooltip-show-usages": "顯示使用情況"
}, },
"variable-values-preview": { "variable-values-preview": {
"preview-of-values": "數值預覽", "show-more": "顯示更多",
"show-more": "顯示更多" "preview-of-values_other": ""
}, },
"version-history": { "version-history": {
"comparison": { "comparison": {