mirror of
https://github.com/grafana/grafana.git
synced 2026-01-13 07:24:10 +08:00
Compare commits
104 Commits
sriram/SQL
...
v8.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2306d2765 | ||
|
|
229c45ccda | ||
|
|
1126ea6539 | ||
|
|
06015fbdaf | ||
|
|
dc363a05ac | ||
|
|
70a4fd8a21 | ||
|
|
91db8910a7 | ||
|
|
e69544b6ea | ||
|
|
c6fb06721f | ||
|
|
900c94fd8a | ||
|
|
d6b00710cb | ||
|
|
4fe38b169c | ||
|
|
bd12923c59 | ||
|
|
9570495af5 | ||
|
|
368742ab04 | ||
|
|
d07ed05918 | ||
|
|
4699620e23 | ||
|
|
09461d03ea | ||
|
|
c7e9b1685c | ||
|
|
20212aaf97 | ||
|
|
832321b405 | ||
|
|
f4bd6d4f84 | ||
|
|
805cbba81d | ||
|
|
f793c0d2fc | ||
|
|
b82bc90180 | ||
|
|
b37d4152d8 | ||
|
|
2cd112b9da | ||
|
|
e426039d6c | ||
|
|
f28d869db2 | ||
|
|
618a590a05 | ||
|
|
ce5fe730b8 | ||
|
|
6648f8c5a1 | ||
|
|
d38782bc94 | ||
|
|
2e86425ed9 | ||
|
|
5aaef25a33 | ||
|
|
35dad9c267 | ||
|
|
c2d95d4d01 | ||
|
|
c89e1236fe | ||
|
|
3d76c772be | ||
|
|
1feb7ce022 | ||
|
|
2089b58def | ||
|
|
f3479aad2b | ||
|
|
6c92beb7fe | ||
|
|
06cb288848 | ||
|
|
cac03cc7fe | ||
|
|
ea57ae7470 | ||
|
|
bbfb211408 | ||
|
|
07de5e5a5d | ||
|
|
7d9308113a | ||
|
|
5ce3c48c8c | ||
|
|
e90558d3b3 | ||
|
|
e7ca31214b | ||
|
|
fee11227da | ||
|
|
258f3eae32 | ||
|
|
ac1ca2e4b5 | ||
|
|
1fc22f6e4d | ||
|
|
fa6c44b12a | ||
|
|
53a11de774 | ||
|
|
4ffa29d959 | ||
|
|
22563454b7 | ||
|
|
345fb89699 | ||
|
|
5bf35bd49e | ||
|
|
aa2dbd63f3 | ||
|
|
4d1969c9c4 | ||
|
|
5003b911e2 | ||
|
|
ef7a2bda39 | ||
|
|
1b8255e317 | ||
|
|
d343d2242d | ||
|
|
a0fe59adef | ||
|
|
04900c63f0 | ||
|
|
e4862a64f7 | ||
|
|
036db0f9d0 | ||
|
|
9b9b7e5a45 | ||
|
|
793cd39af3 | ||
|
|
f5be918e49 | ||
|
|
698c13482a | ||
|
|
88ea42ab9c | ||
|
|
9ae43a7aaa | ||
|
|
ee7c12592d | ||
|
|
17a5142901 | ||
|
|
9e825719f6 | ||
|
|
10c44b4f8d | ||
|
|
395730e2f7 | ||
|
|
c418e75fcf | ||
|
|
56db5cc071 | ||
|
|
e272e239d0 | ||
|
|
972ec9cf85 | ||
|
|
635e489a59 | ||
|
|
333c1da5c7 | ||
|
|
cea17c7e49 | ||
|
|
704ba385aa | ||
|
|
b21ba77c4f | ||
|
|
3a002da406 | ||
|
|
6f52226c66 | ||
|
|
ab5ec6e838 | ||
|
|
71b0ae9094 | ||
|
|
3b1abd4bd3 | ||
|
|
13b8b7721b | ||
|
|
8a369feb63 | ||
|
|
499b1f4ff2 | ||
|
|
8fcfc4d87e | ||
|
|
c05f195de5 | ||
|
|
49bec6ce4f | ||
|
|
d8b03490c3 |
@@ -156,6 +156,7 @@ steps:
|
||||
from_secret: grafana_misc_stats_api_key
|
||||
HOST: end-to-end-tests-server
|
||||
PORT: 3001
|
||||
failure: ignore
|
||||
depends_on:
|
||||
- end-to-end-tests-server
|
||||
|
||||
@@ -3491,6 +3492,6 @@ get:
|
||||
|
||||
---
|
||||
kind: signature
|
||||
hmac: 974d6cc07d690099a1e1ac4581c6ccafc6c6c2998289c09655c70932088dce40
|
||||
hmac: 9211e316cd24660667ba57b860129d2f06b0a4a669aa4d5c1f13156667ae027f
|
||||
|
||||
...
|
||||
|
||||
@@ -16,7 +16,7 @@ var config = {
|
||||
"click element button[aria-label='Login button']",
|
||||
"wait for element [aria-label='Skip change password button'] to be visible",
|
||||
],
|
||||
threshold: 2,
|
||||
threshold: 3,
|
||||
},
|
||||
{
|
||||
url: '${HOST}/?orgId=1',
|
||||
|
||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -1,3 +1,104 @@
|
||||
<!-- 8.2.0-beta1 START -->
|
||||
|
||||
# 8.2.0-beta1 (2021-09-16)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **AccessControl:** Introduce new permissions to restrict access for reloading provisioning configuration. [#38906](https://github.com/grafana/grafana/pull/38906), [@vtorosyan](https://github.com/vtorosyan)
|
||||
- **Alerting:** Add UI to edit Cortex/Loki namespace, group names, and group evaluation interval. [#38543](https://github.com/grafana/grafana/pull/38543), [@domasx2](https://github.com/domasx2)
|
||||
- **Alerting:** Add a Test button to test contact point. [#37475](https://github.com/grafana/grafana/pull/37475), [@domasx2](https://github.com/domasx2)
|
||||
- **Alerting:** Allow creating/editing recording rules for Loki and Cortex. [#38064](https://github.com/grafana/grafana/pull/38064), [@domasx2](https://github.com/domasx2)
|
||||
- **Alerting:** Sort notification channels by name to make them easier to locate. [#37426](https://github.com/grafana/grafana/pull/37426), [@jstangroome](https://github.com/jstangroome)
|
||||
- **AzureMonitor:** Add data links to deep link to Azure Portal Azure Resource Graph. [#35591](https://github.com/grafana/grafana/pull/35591), [@shuotli](https://github.com/shuotli)
|
||||
- **AzureMonitor:** Add support for annotations from Azure Monitor Metrics and Azure Resource Graph services. [#37633](https://github.com/grafana/grafana/pull/37633), [@joshhunt](https://github.com/joshhunt)
|
||||
- **AzureMonitor:** Show error message when subscriptions request fails in ConfigEditor. [#37837](https://github.com/grafana/grafana/pull/37837), [@joshhunt](https://github.com/joshhunt)
|
||||
- **Chore:** Update to Golang 1.16.7. [#38604](https://github.com/grafana/grafana/pull/38604), [@dsotirakis](https://github.com/dsotirakis)
|
||||
- **CloudWatch Logs:** Add link to X-Ray data source for trace IDs in logs. [#39135](https://github.com/grafana/grafana/pull/39135), [@aocenas](https://github.com/aocenas)
|
||||
- **CloudWatch Logs:** Disable query path using websockets (Live) feature. [#39231](https://github.com/grafana/grafana/pull/39231), [@aocenas](https://github.com/aocenas)
|
||||
- **CloudWatch/Logs:** Don't group dataframes for non time series queries. [#37998](https://github.com/grafana/grafana/pull/37998), [@aocenas](https://github.com/aocenas)
|
||||
- **Cloudwatch:** Migrate queries that use multiple stats to one query per stat. [#36925](https://github.com/grafana/grafana/pull/36925), [@sunker](https://github.com/sunker)
|
||||
- **Dashboard:** Keep live timeseries moving left (v2). [#37769](https://github.com/grafana/grafana/pull/37769), [@ryantxu](https://github.com/ryantxu)
|
||||
- **Datasources:** Introduce `response_limit` for datasource responses. [#38962](https://github.com/grafana/grafana/pull/38962), [@dsotirakis](https://github.com/dsotirakis)
|
||||
- **Explore:** Add filter by trace or span ID to `trace to logs` feature. [#38943](https://github.com/grafana/grafana/pull/38943), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Explore:** Download traces as JSON in Explore Inspector. [#38614](https://github.com/grafana/grafana/pull/38614), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Explore:** Reuse Dashboard's QueryRows component. [#38942](https://github.com/grafana/grafana/pull/38942), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Explore:** Support custom display label for derived fields buttons for Loki datasource. [#37273](https://github.com/grafana/grafana/pull/37273), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Grafana UI:** Update monaco-related dependencies. [#39027](https://github.com/grafana/grafana/pull/39027), [@gabor](https://github.com/gabor)
|
||||
- **Graphite:** Deprecate browser access mode. [#38783](https://github.com/grafana/grafana/pull/38783), [@ifrost](https://github.com/ifrost)
|
||||
- **InfluxDB:** Improve handling of intervals in alerting. [#37588](https://github.com/grafana/grafana/pull/37588), [@gabor](https://github.com/gabor)
|
||||
- **InfluxDB:** InfluxQL query editor: Handle unusual characters in tag values better. [#39170](https://github.com/grafana/grafana/pull/39170), [@gabor](https://github.com/gabor)
|
||||
- **Jaeger:** Add ability to upload JSON file for trace data. [#37205](https://github.com/grafana/grafana/pull/37205), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **LibraryElements:** Enable specifying UID for new and existing library elements. [#39019](https://github.com/grafana/grafana/pull/39019), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **LibraryPanels:** Remove library panel icon from the panel header so you can no longer tell that a panel is a library panel from the dashboard view. [#38749](https://github.com/grafana/grafana/pull/38749), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **Logs panel:** Scroll to the bottom on page refresh when sorting in ascending order. [#37634](https://github.com/grafana/grafana/pull/37634), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Loki:** Add fuzzy search to label browser. [#36864](https://github.com/grafana/grafana/pull/36864), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Navigation:** Implement active state for items in the Sidemenu. [#39030](https://github.com/grafana/grafana/pull/39030), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Packaging:** Add stricter systemd unit options. [#38109](https://github.com/grafana/grafana/pull/38109), [@erdnaxe](https://github.com/erdnaxe)
|
||||
- **Packaging:** Update PID file location from `/var/run` to `/run`. [#35739](https://github.com/grafana/grafana/pull/35739), [@MichaIng](https://github.com/MichaIng)
|
||||
- **Plugins:** Add Hide OAuth Forward config option. [#36306](https://github.com/grafana/grafana/pull/36306), [@wbrowne](https://github.com/wbrowne)
|
||||
- **Postgres/MySQL/MSSQL:** Add setting to limit the maximum number of rows processed. [#38986](https://github.com/grafana/grafana/pull/38986), [@marefr](https://github.com/marefr)
|
||||
- **Prometheus:** Add browser access mode deprecation warning. [#37578](https://github.com/grafana/grafana/pull/37578), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Add interpolation for built-in-time variables to backend. [#39051](https://github.com/grafana/grafana/pull/39051), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Tempo:** Add ability to upload trace data in JSON format. [#37407](https://github.com/grafana/grafana/pull/37407), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **TimeSeries/XYChart:** Allow grid lines visibility control in XYChart and TimeSeries panels. [#38502](https://github.com/grafana/grafana/pull/38502), [@dprokop](https://github.com/dprokop)
|
||||
- **Transformations:** Convert field types to time string number or boolean. [#38517](https://github.com/grafana/grafana/pull/38517), [@nikki-kiga](https://github.com/nikki-kiga)
|
||||
- **Value mappings:** Add regular-expression based value mapping. [#38931](https://github.com/grafana/grafana/pull/38931), [@mcdee](https://github.com/mcdee)
|
||||
- **Zipkin:** Add ability to upload trace JSON. [#37483](https://github.com/grafana/grafana/pull/37483), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Admin:** Prevent user from deleting user's current/active organization. [#38056](https://github.com/grafana/grafana/pull/38056), [@idafurjes](https://github.com/idafurjes)
|
||||
- **LibraryPanels:** Fix library panel getting saved in the dashboard's folder. [#38978](https://github.com/grafana/grafana/pull/38978), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **OAuth:** Make generic teams URL and JMES path configurable. [#37233](https://github.com/grafana/grafana/pull/37233), [@djairhogeuens](https://github.com/djairhogeuens)
|
||||
- **QueryEditor:** Fix broken copy-paste for mouse middle-click (#39117). [#39117](https://github.com/grafana/grafana/pull/39117), [@glintik](https://github.com/glintik)
|
||||
- **Thresholds:** Fix undefined color in "Add threshold". [#39113](https://github.com/grafana/grafana/pull/39113), [@glintik](https://github.com/glintik)
|
||||
- **Timeseries:** Add wide-to-long, and fix multi-frame output. [#38670](https://github.com/grafana/grafana/pull/38670), [@ryantxu](https://github.com/ryantxu)
|
||||
- **TooltipPlugin:** Fix behavior of Shared Crosshair when Tooltip is set to All. [#37285](https://github.com/grafana/grafana/pull/37285), [@nikki-kiga](https://github.com/nikki-kiga)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
The `monaco-editor` dependency in `grafana-ui` has been updated to a newer version (`0.27.0`), which is not completely backward compatible with the old version (`0.21.2`). The backward incompatible changes are fairly small, but they do exist, so if your code accesses the raw monaco-objects through the `grafana-ui` package, please check the [monaco-editor changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md) and apply any necessary changes. Issue [#39027](https://github.com/grafana/grafana/issues/39027)
|
||||
|
||||
The mandatory `css` prop in `grafana/ui` components has been removed.
|
||||
|
||||
Previous versions of `grafana/ui` components were typed incorrectly due to a dependency mismatch between emotion 10 and 11 causing a `css` prop to be added to components that extended react types.
|
||||
Issue [#38078](https://github.com/grafana/grafana/issues/38078)
|
||||
|
||||
Panel queries and/or annotation queries that used more than one statistic will be converted into one query/annotation per statistic. In case an alerting rule was based on a query row that had more than one statistic, it would now be based only on the first statistic for that query row. New alerting rules will not be created for migrated queries. Please note that in most cases it would not make sense to have an alerting rule that is based on multiple statistics anyway. Issue [#36925](https://github.com/grafana/grafana/issues/36925)
|
||||
|
||||
### Deprecations
|
||||
|
||||
`getHighlighterExpressions` in datasource APIs ( used to highlight logs while editing queries) has been deprecated and will be removed in a future release.
|
||||
|
||||
# Deprecation notice
|
||||
|
||||
`ExploreQueryFieldProps` interface for query editors has been deprecated and will be removed in a future release. Use `QueryEditorProps` instead. Issue [#38942](https://github.com/grafana/grafana/issues/38942)
|
||||
|
||||
### Plugin development fixes & changes
|
||||
|
||||
- **Grafana UI:** Fix TS error property `css` is missing in type. [#38078](https://github.com/grafana/grafana/pull/38078), [@jackw](https://github.com/jackw)
|
||||
|
||||
<!-- 8.2.0-beta1 END -->
|
||||
<!-- 8.1.4 START -->
|
||||
|
||||
# 8.1.4 (2021-09-16)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Explore:** Ensure logs volume bar colors match legend colors. [#39072](https://github.com/grafana/grafana/pull/39072), [@ifrost](https://github.com/ifrost)
|
||||
- **LDAP:** Search all DNs for users. [#38891](https://github.com/grafana/grafana/pull/38891), [@sakjur](https://github.com/sakjur)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Alerting:** Fix notification channel migration. [#38983](https://github.com/grafana/grafana/pull/38983), [@papagian](https://github.com/papagian)
|
||||
- **Annotations:** Fix blank panels for queries with unknown data sources. [#39017](https://github.com/grafana/grafana/pull/39017), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **BarChart:** Fix stale values and x axis labels. [#39188](https://github.com/grafana/grafana/pull/39188), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Graph:** Make old graph panel thresholds work even if ngalert is enabled. [#38918](https://github.com/grafana/grafana/pull/38918), [@domasx2](https://github.com/domasx2)
|
||||
- **InfluxDB:** Fix regex to identify `/` as separator. [#39185](https://github.com/grafana/grafana/pull/39185), [@dsotirakis](https://github.com/dsotirakis)
|
||||
- **LibraryPanels:** Fix update issues related to library panels in rows. [#38963](https://github.com/grafana/grafana/pull/38963), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **Variables:** Fix variables not updating inside a Panel when the preceding Row uses "Repeat For". [#38935](https://github.com/grafana/grafana/pull/38935), [@axelavargas](https://github.com/axelavargas)
|
||||
|
||||
<!-- 8.1.4 END -->
|
||||
<!-- 8.1.3 START -->
|
||||
|
||||
# 8.1.3 (2021-09-08)
|
||||
|
||||
@@ -211,7 +211,7 @@ rudderstack_data_plane_url =
|
||||
# Application Insights connection string. Specify an URL string to enable this feature.
|
||||
application_insights_connection_string =
|
||||
|
||||
# Optional. Specifies an Application Insights endpoint URL where the endpoint string is wrapped in backticks ``.
|
||||
# Optional. Specifies an Application Insights endpoint URL where the endpoint string is wrapped in backticks ``.
|
||||
application_insights_endpoint_url =
|
||||
|
||||
#################################### Security ############################
|
||||
@@ -731,14 +731,65 @@ global_alert_rule = -1
|
||||
|
||||
#################################### Unified Alerting ####################
|
||||
[unified_alerting]
|
||||
# Enable the Unified Alerting sub-system and interface. When enabled we'll migrate all of your alert rules and notification channels to the new system. New alert rules will be created and your notification channels will be converted into an Alertmanager configuration. Previous data is preserved to enable backwards compatibility but new data is removed.
|
||||
enabled = false
|
||||
|
||||
# Comma-separated list of organization IDs for which to disable unified alerting. Only supported if unified alerting is enabled.
|
||||
disabled_orgs =
|
||||
|
||||
# Specify the frequency of polling for admin config changes.
|
||||
admin_config_poll_interval_seconds = 60
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
admin_config_poll_interval = 60s
|
||||
|
||||
# Specify the frequency of polling for Alertmanager config changes.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
alertmanager_config_poll_interval = 60s
|
||||
|
||||
# Listen address/hostname and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port.
|
||||
ha_listen_address = "0.0.0.0:9094"
|
||||
|
||||
# Explicit address/hostname and port to advertise other Grafana instances. The port is used for both TCP and UDP.
|
||||
ha_advertise_address = ""
|
||||
|
||||
# Comma-separated list of initial instances (in a format of host:port) that will form the HA cluster. Configuring this setting will enable High Availability mode for alerting.
|
||||
ha_peers = ""
|
||||
|
||||
# Time to wait for an instance to send a notification via the Alertmanager. In HA, each Grafana instance will
|
||||
# be assigned a position (e.g. 0, 1). We then multiply this position with the timeout to indicate how long should
|
||||
# each instance wait before sending the notification to take into account replication lag.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
ha_peer_timeout = 15s
|
||||
|
||||
# The interval between sending gossip messages. By lowering this value (more frequent) gossip messages are propagated
|
||||
# across cluster more quickly at the expense of increased bandwidth usage.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
ha_gossip_interval = 200ms
|
||||
|
||||
# The interval between gossip full state syncs. Setting this interval lower (more frequent) will increase convergence speeds
|
||||
# across larger clusters at the expense of increased bandwidth usage.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
ha_push_pull_interval = 60s
|
||||
|
||||
# Enable or disable alerting rule execution. The alerting UI remains visible. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
execute_alerts = true
|
||||
|
||||
# Alert evaluation timeout when fetching data from the datasource. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
# The timeout string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
evaluation_timeout = 30s
|
||||
|
||||
# Number of times we'll attempt to evaluate an alert rule before giving up on that evaluation. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
max_attempts = 3
|
||||
|
||||
# Minimum interval to enforce between rule evaluations. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
min_interval = 10s
|
||||
|
||||
#################################### Alerting ############################
|
||||
[alerting]
|
||||
# Disable alerting engine & UI features
|
||||
# Disable legacy alerting engine & UI features
|
||||
enabled = true
|
||||
# Makes it possible to turn off alert rule execution but alerting UI is visible
|
||||
|
||||
# Makes it possible to turn off alert execution but alerting UI is visible
|
||||
execute_alerts = true
|
||||
|
||||
# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
|
||||
@@ -914,7 +965,7 @@ app_tls_skip_verify_insecure = false
|
||||
# Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded.
|
||||
allow_loading_unsigned_plugins =
|
||||
# Enable or disable installing plugins directly from within Grafana.
|
||||
plugin_admin_enabled = false
|
||||
plugin_admin_enabled = true
|
||||
plugin_admin_external_manage_enabled = false
|
||||
plugin_catalog_url = https://grafana.com/grafana/plugins/
|
||||
|
||||
|
||||
@@ -708,14 +708,65 @@
|
||||
|
||||
#################################### Unified Alerting ####################
|
||||
[unified_alerting]
|
||||
#Enable the Unified Alerting sub-system and interface. When enabled we'll migrate all of your alert rules and notification channels to the new system. New alert rules will be created and your notification channels will be converted into an Alertmanager configuration. Previous data is preserved to enable backwards compatibility but new data is removed.```
|
||||
;enabled = false
|
||||
|
||||
# Comma-separated list of organization IDs for which to disable unified alerting. Only supported if unified alerting is enabled.
|
||||
;disabled_orgs =
|
||||
|
||||
# Specify the frequency of polling for admin config changes.
|
||||
;admin_config_poll_interval_seconds = 60
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;admin_config_poll_interval = 60s
|
||||
|
||||
# Specify the frequency of polling for Alertmanager config changes.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;alertmanager_config_poll_interval = 60s
|
||||
|
||||
# Listen address/hostname and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port. The default value is `0.0.0.0:9094`.
|
||||
;ha_listen_address = "0.0.0.0:9094"
|
||||
|
||||
# Listen address/hostname and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port. The default value is `0.0.0.0:9094`.
|
||||
;ha_advertise_address = ""
|
||||
|
||||
# Comma-separated list of initial instances (in a format of host:port) that will form the HA cluster. Configuring this setting will enable High Availability mode for alerting.
|
||||
;ha_peers = ""
|
||||
|
||||
# Time to wait for an instance to send a notification via the Alertmanager. In HA, each Grafana instance will
|
||||
# be assigned a position (e.g. 0, 1). We then multiply this position with the timeout to indicate how long should
|
||||
# each instance wait before sending the notification to take into account replication lag.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;ha_peer_timeout = "15s"
|
||||
|
||||
# The interval between sending gossip messages. By lowering this value (more frequent) gossip messages are propagated
|
||||
# across cluster more quickly at the expense of increased bandwidth usage.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;ha_gossip_interval = "200ms"
|
||||
|
||||
# The interval between gossip full state syncs. Setting this interval lower (more frequent) will increase convergence speeds
|
||||
# across larger clusters at the expense of increased bandwidth usage.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;ha_push_pull_interval = "60s"
|
||||
|
||||
# Enable or disable alerting rule execution. The alerting UI remains visible. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
;execute_alerts = true
|
||||
|
||||
# Alert evaluation timeout when fetching data from the datasource. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
# The timeout string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;evaluation_timeout = 30s
|
||||
|
||||
# Number of times we'll attempt to evaluate an alert rule before giving up on that evaluation. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
;max_attempts = 3
|
||||
|
||||
# Minimum interval to enforce between rule evaluations. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. This option has a legacy version in the `[alerting]` section that takes precedence.
|
||||
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
;min_interval = 10s
|
||||
|
||||
#################################### Alerting ############################
|
||||
[alerting]
|
||||
# Disable alerting engine & UI features
|
||||
# Disable legacy alerting engine & UI features
|
||||
;enabled = true
|
||||
# Makes it possible to turn off alert rule execution but alerting UI is visible
|
||||
|
||||
# Makes it possible to turn off alert execution but alerting UI is visible
|
||||
;execute_alerts = true
|
||||
|
||||
# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
|
||||
@@ -728,7 +779,6 @@
|
||||
# This limit will protect the server from render overloading and make sure notifications are sent out quickly
|
||||
;concurrent_render_limit = 5
|
||||
|
||||
|
||||
# Default setting for alert calculation timeout. Default value is 30
|
||||
;evaluation_timeout_seconds = 30
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
@@ -83,7 +84,7 @@
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10",
|
||||
"csvContent": "Time,Name,Stat1,Stat2\n2020-01-01T00:00:00Z,Stockholm, 10, 15\n2020-01-01T00:00:00Z,New York, 19, 5\n2020-01-01T00:00:00Z,London, 10, 1\n2020-01-01T00:00:00Z,Negative, 15, -5\n2020-01-01T00:00:00Z,Long value, 15,10",
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
@@ -147,6 +148,7 @@
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
@@ -216,6 +218,7 @@
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
@@ -285,6 +288,7 @@
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "always",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
@@ -353,6 +357,7 @@
|
||||
},
|
||||
"orientation": "auto",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {
|
||||
"titleSize": 10,
|
||||
"valueSize": 25
|
||||
@@ -425,6 +430,7 @@
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
@@ -495,6 +501,7 @@
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
|
||||
1
devenv/docker/ha-test-unified-alerting/.gitignore
vendored
Normal file
1
devenv/docker/ha-test-unified-alerting/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
grafana/provisioning/dashboards/alerts/alert-*
|
||||
66
devenv/docker/ha-test-unified-alerting/README.md
Normal file
66
devenv/docker/ha-test-unified-alerting/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Grafana Unified Alerting High Availability (HA) test setup
|
||||
|
||||
A set of docker compose services which together creates a Grafana HA test setup for unified alerting.
|
||||
|
||||
Included services
|
||||
|
||||
- Grafana
|
||||
- Mysql - Grafana configuration database, exporter for metrics and session storage
|
||||
- Prometheus - Monitoring of Grafana and used as data source
|
||||
- Nginx - Reverse proxy for Grafana and Prometheus. Enables browsing Grafana/Prometheus UI using a hostname
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Build grafana docker container
|
||||
|
||||
Build a Grafana docker container from current branch and commit and tag it as grafana/grafana:dev.
|
||||
|
||||
```bash
|
||||
$ cd <grafana repo>
|
||||
$ make build-docker-full
|
||||
```
|
||||
|
||||
### Virtual host names
|
||||
|
||||
#### Alternative 1 - Use dnsmasq
|
||||
|
||||
```bash
|
||||
$ sudo apt-get install dnsmasq
|
||||
$ echo 'address=/loc/127.0.0.1' | sudo tee /etc/dnsmasq.d/dnsmasq-loc.conf > /dev/null
|
||||
$ sudo /etc/init.d/dnsmasq restart
|
||||
$ ping whatever.loc
|
||||
PING whatever.loc (127.0.0.1) 56(84) bytes of data.
|
||||
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.076 ms
|
||||
--- whatever.loc ping statistics ---
|
||||
1 packet transmitted, 1 received, 0% packet loss, time 1998ms
|
||||
```
|
||||
|
||||
#### Alternative 2 - Manually update /etc/hosts
|
||||
|
||||
Update your `/etc/hosts` to be able to access Grafana and/or Prometheus UI using a hostname.
|
||||
|
||||
```bash
|
||||
$ cat /etc/hosts
|
||||
127.0.0.1 grafana.loc
|
||||
127.0.0.1 prometheus.loc
|
||||
```
|
||||
|
||||
## Start services
|
||||
|
||||
```bash
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
Browse
|
||||
- http://grafana.loc/
|
||||
- http://prometheus.loc/
|
||||
|
||||
|
||||
## Test alerting
|
||||
|
||||
### Create contact points
|
||||
TBD
|
||||
### Create alerts
|
||||
TBD
|
||||
### Create silences
|
||||
TBD
|
||||
90
devenv/docker/ha-test-unified-alerting/docker-compose.yaml
Normal file
90
devenv/docker/ha-test-unified-alerting/docker-compose.yaml
Normal file
@@ -0,0 +1,90 @@
|
||||
version: "2.1"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mysql:5.6
|
||||
platform: linux/x86_64
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_DATABASE: grafana
|
||||
MYSQL_USER: grafana
|
||||
MYSQL_PASSWORD: password
|
||||
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all, --max-connections=1001]
|
||||
ports:
|
||||
- 3306
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
mysqld-exporter:
|
||||
image: prom/mysqld-exporter
|
||||
environment:
|
||||
- DATA_SOURCE_NAME=root:rootpass@(db:3306)/
|
||||
ports:
|
||||
- 9104
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.4.2
|
||||
volumes:
|
||||
- ./prometheus/:/etc/prometheus/
|
||||
environment:
|
||||
- VIRTUAL_HOST=prometheus.loc
|
||||
ports:
|
||||
- 909
|
||||
nginx-proxy:
|
||||
image: jwilder/nginx-proxy
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
grafana1:
|
||||
image: grafana/grafana:dev
|
||||
volumes:
|
||||
- ./grafana/provisioning/:/etc/grafana/provisioning/
|
||||
environment:
|
||||
- VIRTUAL_HOST=grafana.loc
|
||||
- GF_FEATURE_TOGGLES_ENABLE=ngalert
|
||||
- GF_UNIFIED_ALERTING_HA_PEERS=ha-test-unified-alerting_grafana2_1:9094,ha-test-unified-alerting_grafana1_1:9094
|
||||
- GF_SERVER_ROOT_URL=http://grafana.loc
|
||||
- GF_DATABASE_NAME=grafana
|
||||
- GF_DATABASE_USER=grafana
|
||||
- GF_DATABASE_PASSWORD=password
|
||||
- GF_DATABASE_TYPE=mysql
|
||||
- GF_DATABASE_HOST=db:3306
|
||||
- GF_DATABASE_MAX_OPEN_CONN=300
|
||||
- GF_SESSION_PROVIDER=mysql
|
||||
- GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true
|
||||
ports:
|
||||
- 3010:3000
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
grafana2:
|
||||
image: grafana/grafana:dev
|
||||
volumes:
|
||||
- ./grafana/provisioning/:/etc/grafana/provisioning/
|
||||
environment:
|
||||
- VIRTUAL_HOST=grafana.loc
|
||||
- GF_FEATURE_TOGGLES_ENABLE=ngalert
|
||||
- GF_UNIFIED_ALERTING_HA_PEERS=ha-test-unified-alerting_grafana2_1:9094,ha-test-unified-alerting_grafana1_1:9094
|
||||
- GF_SERVER_ROOT_URL=http://grafana.loc
|
||||
- GF_DATABASE_NAME=grafana
|
||||
- GF_DATABASE_USER=grafana
|
||||
- GF_DATABASE_PASSWORD=password
|
||||
- GF_DATABASE_TYPE=mysql
|
||||
- GF_DATABASE_HOST=db:3306
|
||||
- GF_DATABASE_MAX_OPEN_CONN=300
|
||||
- GF_SESSION_PROVIDER=mysql
|
||||
- GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true
|
||||
ports:
|
||||
- 3020:3000
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
local numAlerts = std.extVar('alerts');
|
||||
local condition = std.extVar('condition');
|
||||
local arr = std.range(1, numAlerts);
|
||||
|
||||
local alertDashboardTemplate = {
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
65
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"for": "1m",
|
||||
"name": "bulk alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": [
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"$$hashKey": "object:117",
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "New dashboard",
|
||||
"uid": null,
|
||||
"version": 0
|
||||
};
|
||||
|
||||
|
||||
{
|
||||
['alert-' + std.toString(x) + '.json']:
|
||||
alertDashboardTemplate + {
|
||||
panels: [
|
||||
alertDashboardTemplate.panels[0] +
|
||||
{
|
||||
alert+: {
|
||||
name: 'Alert rule ' + x,
|
||||
conditions: [
|
||||
alertDashboardTemplate.panels[0].alert.conditions[0] +
|
||||
{
|
||||
evaluator+: {
|
||||
params: [condition]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
uid: 'alert-' + x,
|
||||
title: 'Alert ' + x
|
||||
},
|
||||
for x in arr
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {
|
||||
"Active alerts": "#bf1b00"
|
||||
},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"interval": "",
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": false,
|
||||
"current": true,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "Active grafana instances",
|
||||
"dashes": true,
|
||||
"fill": 0
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(grafana_alerting_notification_sent_total[1m])) by(job)",
|
||||
"format": "time_series",
|
||||
"instant": false,
|
||||
"interval": "1m",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Notifications sent",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "min(grafana_alerting_active_alerts) without(instance)",
|
||||
"format": "time_series",
|
||||
"interval": "1m",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Active alerts",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "count(up{job=\"grafana\"})",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Active grafana instances",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Notifications sent vs active alerts",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Overview",
|
||||
"uid": "xHy7-hAik",
|
||||
"version": 6
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Alerts'
|
||||
folder: 'Alerts'
|
||||
type: file
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards/alerts
|
||||
|
||||
- name: 'MySQL'
|
||||
folder: 'MySQL'
|
||||
type: file
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards/mysql
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
jsonData:
|
||||
timeInterval: 10s
|
||||
queryTimeout: 30s
|
||||
httpMethod: POST
|
||||
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
@@ -0,0 +1,47 @@
|
||||
# my global config
|
||||
global:
|
||||
scrape_interval: 10s # By default, scrape targets every 15 seconds.
|
||||
evaluation_interval: 10s # By default, scrape targets every 15 seconds.
|
||||
# scrape_timeout is set to the global default (10s).
|
||||
|
||||
# Load and evaluate rules in this file every 'evaluation_interval' seconds.
|
||||
#rule_files:
|
||||
# - "alert.rules"
|
||||
# - "first.rules"
|
||||
# - "second.rules"
|
||||
|
||||
# alerting:
|
||||
# alertmanagers:
|
||||
# - scheme: http
|
||||
# static_configs:
|
||||
# - targets:
|
||||
# - "127.0.0.1:9093"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'grafana'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'grafana'
|
||||
type: 'A'
|
||||
port: 3000
|
||||
refresh_interval: 10s
|
||||
|
||||
- job_name: 'mysql'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'mysqld-exporter'
|
||||
type: 'A'
|
||||
port: 9104
|
||||
refresh_interval: 10s
|
||||
|
||||
- job_name: 'loki'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'loki'
|
||||
type: 'A'
|
||||
port: 3100
|
||||
refresh_interval: 10s
|
||||
@@ -16,6 +16,8 @@ To see all settings currently applied to the Grafana server, refer to [View serv
|
||||
|
||||
## Config file locations
|
||||
|
||||
The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change the location in this file.
|
||||
|
||||
_Do not_ change `defaults.ini`! Grafana defaults are stored in this file. Depending on your OS, make all configuration changes in either `custom.ini` or `grafana.ini`.
|
||||
|
||||
- Default configuration from `$WORKING_DIR/conf/defaults.ini`
|
||||
@@ -1119,9 +1121,83 @@ Sets a global limit on number of alert rules that can be created. Default is -1
|
||||
|
||||
For more information about the Grafana 8 alerts, refer to [Unified Alerting]({{< relref "../alerting/unified-alerting/_index.md" >}}).
|
||||
|
||||
### admin_config_poll_interval_seconds
|
||||
### enabled
|
||||
|
||||
Specify the frequency of polling for admin config changes. The default value is `60`.
|
||||
Enable the Unified Alerting sub-system and interface. When enabled we'll migrate all of your alert rules and notification channels to the new system. New alert rules will be created and your notification channels will be converted into an Alertmanager configuration. Previous data is preserved to enable backwards compatibility but new data is removed. The default value is `false`.
|
||||
|
||||
Alerting Rules migrated from dashboards and panels will include a link back via the `annotations`.
|
||||
|
||||
### disabled_orgs
|
||||
|
||||
Comma-separated list of organization IDs for which to disable Grafana 8 Unified Alerting.
|
||||
|
||||
### admin_config_poll_interval
|
||||
|
||||
Specify the frequency of polling for admin config changes. The default value is `60s`.
|
||||
|
||||
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
### alertmanager_config_poll_interval
|
||||
|
||||
Specify the frequency of polling for Alertmanager config changes. The default value is `60s`.
|
||||
|
||||
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
### ha_listen_address
|
||||
|
||||
Listen address/hostname and port to receive unified alerting messages for other Grafana instances. The port is used for both TCP and UDP. It is assumed other Grafana instances are also running on the same port. The default value is `0.0.0.0:9094`.
|
||||
|
||||
### ha_advertise_address
|
||||
|
||||
Explicit address/hostname and port to advertise other Grafana instances. The port is used for both TCP and UDP.
|
||||
|
||||
### ha_peers
|
||||
|
||||
Comma-separated list of initial instances (in a format of host:port) that will form the HA cluster. Configuring this setting will enable High Availability mode for alerting.
|
||||
|
||||
### ha_peer_timeout
|
||||
|
||||
Time to wait for an instance to send a notification via the Alertmanager. In HA, each Grafana instance will
|
||||
be assigned a position (e.g. 0, 1). We then multiply this position with the timeout to indicate how long should
|
||||
each instance wait before sending the notification to take into account replication lag. The default value is `15s`.
|
||||
|
||||
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
### ha_gossip_interval
|
||||
|
||||
The interval between sending gossip messages. By lowering this value (more frequent) gossip messages are propagated
|
||||
across cluster more quickly at the expense of increased bandwidth usage. The default value is `200ms`.
|
||||
|
||||
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
### ha_push_pull_interval
|
||||
|
||||
The interval between gossip full state syncs. Setting this interval lower (more frequent) will increase convergence speeds
|
||||
across larger clusters at the expense of increased bandwidth usage. The default value is `60s`.
|
||||
|
||||
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
### execute_alerts
|
||||
|
||||
Enable or disable alerting rule execution. The default value is `true`. The alerting UI remains visible. This option has a [legacy version in the alerting section]({{< relref "#execute_alerts-1">}}) that takes precedence.
|
||||
|
||||
### evaluation_timeout
|
||||
|
||||
Sets the alert evaluation timeout when fetching data from the datasource. The default value is `30s`. This option has a [legacy version in the alerting section]({{< relref "#evaluation_timeout_seconds">}}) that takes precedence.
|
||||
|
||||
The timeout string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
### max_attempts
|
||||
|
||||
Sets a maximum number of times we'll attempt to evaluate an alert rule before giving up on that evaluation. The default value is `3`. This option has a [legacy version in the alerting section]({{< relref "#max_attempts-1">}}) that takes precedence.
|
||||
|
||||
### min_interval
|
||||
|
||||
Sets the minimum interval to enforce between rule evaluations. The default value is `10s` which equals the scheduler interval. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. This option has [a legacy version in the alerting section]({{< relref "#min_interval_seconds">}}) that takes precedence.
|
||||
|
||||
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
|
||||
|
||||
> **Note.** This setting has precedence over each individual rule frequency. If a rule frequency is lower than this value, then this value is enforced.
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -1131,7 +1207,7 @@ For more information about the Alerting feature in Grafana, refer to [Alerts ove
|
||||
|
||||
### enabled
|
||||
|
||||
Set to `false` to disable alerting engine and hide Alerting in the Grafana UI. Default is `true`.
|
||||
Set to `false` to [enable Grafana 8 alerting]({{<relref "#unified_alerting">}}) and to disable legacy alerting engine. Default is `true`.
|
||||
|
||||
### execute_alerts
|
||||
|
||||
@@ -1677,7 +1753,7 @@ For more information about Grafana Enterprise, refer to [Grafana Enterprise]({{<
|
||||
|
||||
### enable
|
||||
|
||||
Keys of alpha features to enable, separated by space. Available alpha features are: `ngalert`
|
||||
Keys of alpha features to enable, separated by space.
|
||||
|
||||
## [date_formats]
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ Since not all datasources have the same configuration settings we only have the
|
||||
| maxSeries | number | Influxdb | Max number of series/tables that Grafana processes |
|
||||
| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to POST |
|
||||
| customQueryParameters | string | Prometheus | Query parameters to add, as a URL-encoded string. |
|
||||
| manageAlerts | boolean | Prometheus and Loki | Manage alerts via Alerting UI |
|
||||
| manageAlerts | boolean | Prometheus and Loki | Manage alerts via Alerting UI |
|
||||
| esVersion | string | Elasticsearch | Elasticsearch version (E.g. `7.0.0`, `7.6.1`) |
|
||||
| timeField | string | Elasticsearch | Which field that should be used as timestamp |
|
||||
| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
|
||||
|
||||
@@ -7,9 +7,9 @@ weight = 110
|
||||
|
||||
Alerts allow you to know about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services.
|
||||
|
||||
Grafana 8.0 has new and improved alerts. The new alerting system is an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources in one UI and API.
|
||||
Grafana 8.0 has new and improved alerts that centralizes alerting information for Grafana managed alerts as well as alerts from Prometheus-compatible data sources into one user interface and API.
|
||||
|
||||
> **Note:** Out of the box, Grafana still supports old [legacy dashboard alerts]({{< relref "./old-alerting/_index.md" >}}). We encourage you to create issues in the Grafana GitHub repository for bugs found while testing Grafana 8 alerts.
|
||||
> **Note:** Grafana 8 alerts is an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature. Out of the box, Grafana still supports old [legacy dashboard alerts]({{< relref "./old-alerting/_index.md" >}}). We encourage you to create issues in the Grafana GitHub repository for bugs found while testing Grafana 8 alerts.
|
||||
|
||||
Alerts have four main components:
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
+++
|
||||
title = "What's New with Grafana 8 Alerts"
|
||||
title = "What's New with Grafana 8 alerts"
|
||||
description = "What's New with Grafana 8 Alerts"
|
||||
keywords = ["grafana", "alerting", "guide"]
|
||||
weight = 112
|
||||
+++
|
||||
|
||||
# What's New with Grafana 8 Alerts
|
||||
# What's New with Grafana 8 alerts
|
||||
|
||||
The Alerts released with Grafana 8.0 are an opt-in feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible datasources in one UI and API. You are able to create and edit alerting rules for Grafana managed alerts, Cortex alerts, and Loki alerts as well as see alerting information from prometheus-compatible datasources in a single, searchable view.
|
||||
The alerts released with Grafana 8.0 centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible datasources in one UI and API. You can create and edit alerting rules for Grafana managed alerts, Cortex alerts, and Loki alerts as well as see alerting information from prometheus-compatible datasources in a single, searchable view.
|
||||
|
||||
## Multi-dimensional alerting
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ weight = 113
|
||||
|
||||
# Overview of Grafana 8 alerts
|
||||
|
||||
Alerts allow you to know about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services.
|
||||
Grafana 8.0 has a new and improved alerting sub-system that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources into one user interface and API.
|
||||
|
||||
> **Note:** Grafana 8 alerts (beta) is an [opt-in]({{< relref"./opt-in.md" >}}) feature. Out of the box, Grafana still supports old [legacy dashboard alerts]({{< relref "../old-alerting/_index.md" >}}). We encourage you to create issues in the Grafana GitHub repository for bugs found while testing this new feature.
|
||||
> **Note:** Grafana 8 alerts is an [opt-in]({{< relref "../unified-alerting/opt-in.md" >}}) feature. Out of the box, Grafana still supports old [legacy dashboard alerts]({{< relref "./old-alerting/_index.md" >}}). We encourage you to create issues in the Grafana GitHub repository for bugs found while testing Grafana 8 alerts.
|
||||
|
||||
Grafana 8 alerts have four main components:
|
||||
|
||||
@@ -45,7 +45,7 @@ Alerting rules can only query backend data sources with alerting enabled:
|
||||
|
||||
## Metrics from the alerting engine
|
||||
|
||||
The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../administration/view-server/internal-metrics.md" >}}).
|
||||
The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../administration/view-server/internal-metrics.md" >}}). See also, [View alert rules and their current state]({{< relref "alerting-rules/rule-list.md" >}}).
|
||||
|
||||
| Metric Name | Type | Description |
|
||||
| ------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------- |
|
||||
@@ -57,4 +57,6 @@ The alerting engine publishes some internal metrics about itself. You can read m
|
||||
| `grafana_alerting_rule_evaluation_duration` | summary | The duration for a rule to execute |
|
||||
| `grafana_alerting_rule_group_rules` | gauge | The number of rules |
|
||||
|
||||
- [View alert rules and their current state]({{< relref "alerting-rules/rule-list.md" >}})
|
||||
## Limitation
|
||||
|
||||
Grafana 8 alerting system can retrieve rules from all available Prometheus, Loki, and Alertmanager data sources. It might not be able to fetch rules from all other supported data sources at this time.
|
||||
|
||||
@@ -4,7 +4,7 @@ aliases = ["/docs/grafana/latest/alerting/rules/"]
|
||||
weight = 130
|
||||
+++
|
||||
|
||||
# Create and manage alerting Rules
|
||||
# Create and manage alerting rules
|
||||
|
||||
One or more queries and/or expressions, a condition, the frequency of evaluation, and the (optional) duration that a condition must be met before creating an alert. Alerting rules are how you express the criteria for creating an alert. Queries and expressions select and can operate on the data you wish to alert on. A condition sets the threshold that an alert must meet or exceed to create an alert. The interval specifies how frequently the rule should be evaluated. The duration, when configured, sets a period that a condition must be met or exceeded before an alert is created. Alerting rules also can contain settings for what to do when your query does not return any data, or there is an error attempting to execute the query.
|
||||
|
||||
|
||||
@@ -4,33 +4,53 @@ description = "Enable Grafana 8 Alerts"
|
||||
weight = 128
|
||||
+++
|
||||
|
||||
# Enable Grafana 8 Alerts
|
||||
# Opt-in to Grafana 8 alerts
|
||||
|
||||
Setting the `ngalert` feature toggle enables the new Grafana 8 alerting system.
|
||||
This topic describes how to enable Grafana 8 alerts as well as the rules and restrictions that govern the migration of existing dashboard alerts to this new alerting system. You can also [disable Grafana 8 alerts]({{< relref "./opt-in.md#disable-grafana-8-alerts" >}}) if needed.
|
||||
|
||||
> **Note:** We recommend that you backup Grafana's database before enabling this feature. If you are using PostgreSQL as the backend data source, then the minimum required version is 9.5.
|
||||
Before you begin, we recommend that you backup Grafana's database. If you are using PostgreSQL as the backend data source, then the minimum required version is 9.5.
|
||||
|
||||
At startup, when [the feature toggle is enabled]({{< relref "../../administration/configuration.md">}}#feature_toggles), the legacy Grafana dashboard alerting is disabled and existing dashboard alerts are migrated into a format that is compatible with the Grafana 8 alerting system. You can view these migrated rules, alongside any new alerts you create after the migration, from the Alerting page of your Grafana instance.
|
||||
## Enable Grafana 8 alerts
|
||||
|
||||
> **Note - v8.2 or earlier:** Since the new system stores the notification log and silences on disk, we require the use of persistent disks for using Grafana 8 alerts. Otherwise, the silences and notification log will get lost on a restart, and you might get unwanted or duplicate notifications.
|
||||
To enable Grafana 8 alerts:
|
||||
|
||||
> **Note - v8.3+**: We have removed the need of persistent disk. The notification log and silences are now stored in the database. If you used the file-based approach, we'll read those files and eventually (every 15 minutes) persist them to the database.
|
||||
1. Go to your custom configuration file located in $WORKING_DIR/conf/custom.ini.
|
||||
1. In the [unified alerts]({{< relref "../../administration/configuration.md#unified_alerting" >}}) section, set the `enabled` property to `true`.
|
||||
1. Next, in the [alerting]({{< relref "../../administration/configuration.md#alerting" >}}) section of the configuration file, update the configuration for the legacy dashboard alerts by setting the `enabled` property to `false`.
|
||||
1. Restart Grafana for the configuration changes to take effect.
|
||||
|
||||
Read and write access to dashboard alerts in Grafana versions 7 and earlier were governed by the dashboard and folder permissions under which the alerts were stored. In Grafana 8, alerts are stored in folders and inherit the permissions of those folders. During the migration, dashboard alert permissions are matched to the new rules permissions as follows:
|
||||
> **Note:** Before Grafana v8.2, to enable or disable Grafana 8 alerts, users configured the `ngalert` feature toggle. This toggle option is no longer available.
|
||||
|
||||
Moreover, before v8.2, notification logs and silences were stored on a disk. If you did not use persistent disks, any configured silences and logs would get lost on a restart, resulting in unwanted or duplicate notifications.
|
||||
|
||||
As of Grafana 8.2, we no longer require the use of a persistent disk. Instead, the notification logs and silences are stored regularly (every 15 minutes), and a clean shutdown to the database. If you used the file-based approach, Grafana will read the existing file and persisting it eventually.
|
||||
|
||||
## Migrating legacy alerts to Grafana 8 alerting system
|
||||
|
||||
When Grafana 8 alerting is enabled, existing legacy dashboard alerts migrate in a format compatible with the Grafana 8 alerting system. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside new alerts.
|
||||
|
||||
Read and write access to legacy dashboard alerts was governed by the dashboard and folder permissions storing them. In Grafana 8, alerts inherit the permissions of the folders they are stored in. During migration, legacy dashboard alert permissions are matched to the new rules permissions as follows:
|
||||
|
||||
- If alert's dashboard has permissions, it will create a folder named like `Migrated {"dashboardUid": "UID", "panelId": 1, "alertId": 1}` to match permissions of the dashboard (including the inherited permissions from the folder).
|
||||
- If there are no dashboard permissions and the dashboard is under a folder, then the rule is linked to this folder and inherits its permissions.
|
||||
- If there are no dashboard permissions and the dashboard is under the General folder, then the rule is linked to the `General Alerting` folder and the rule inherits the default permissions.
|
||||
- If there are no dashboard permissions and the dashboard is under the General folder, then the rule is linked to the `General Alerting` folder, and the rule inherits the default permissions.
|
||||
|
||||
During beta, Grafana 8 alerting system can retrieve rules from all available Prometheus, Loki, and Alertmanager data sources. It might not be able to fetch rules from all other supported data sources at this time.
|
||||
Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route.
|
||||
|
||||
Also notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route.
|
||||
Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana 8 alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts.
|
||||
Silences (expiring after one year) are created for all paused dashboard alerts.
|
||||
|
||||
Since `Hipchat` and `Sensu` are discontinued, they are not migrated to the new alerting. If you have dashboard alerts associated with those types of channels and you want to migrate to the new alerting, make sure you assign another supported notification channel, so that you continue to receive notifications for those alerts.
|
||||
Finally, silences (expiring after one year) are created for all paused dashboard alerts.
|
||||
### Limitation
|
||||
|
||||
## Disabling Grafana 8 Alerting after migration
|
||||
Grafana 8 alerting system can retrieve rules from all available Prometheus, Loki, and Alertmanager data sources. It might not be able to fetch rules from all other supported data sources at this time.
|
||||
|
||||
To disable Grafana 8 Alerting, remove or disable the `ngalert` feature toggle. Dashboard alerts will be re-enabled and any alerts created during or after the migration are deleted.
|
||||
## Disable Grafana 8 alerts
|
||||
|
||||
> **Note:** Any alerting rules created in the Grafana 8 Alerting system will be lost when migrating back to dashboard alerts
|
||||
To disable Grafana 8 alerts and enable legacy dashboard alerts:
|
||||
|
||||
1. Go to your custom configuration file located in $WORKING_DIR/conf/custom.ini.
|
||||
1. In the [unified alerts]({{< relref "../../administration/configuration.md#unified_alerting" >}}) section, set the `enabled` property to `false`.
|
||||
1. Next, in the [alerting]({{< relref "../../administration/configuration.md#alerting" >}}) section of the configuration file, update the configuration for the legacy dashboard alerts by setting the `enabled` property to `true`.
|
||||
1. Restart Grafana for the configuration changes to take effect.
|
||||
|
||||
> **Note:** If you choose to migrate from Grafana 8 alerts to legacy dashboard alerts, you will lose any new alerts that you created in the Grafana 8 alerting system.
|
||||
|
||||
@@ -109,10 +109,12 @@ allowed_groups =
|
||||
```
|
||||
|
||||
You can also use these environment variables to configure **client_id** and **client_secret**:
|
||||
|
||||
```
|
||||
GF_AUTH_AZUREAD_CLIENT_ID
|
||||
GF_AUTH_AZUREAD_CLIENT_SECRET
|
||||
```
|
||||
|
||||
**Note:** Ensure that the [root_url]({{< relref "../administration/configuration/#root-url" >}}) in Grafana is set in your Azure Application Reply URLs (**App** -> **Settings** -> **Reply URLs**)
|
||||
|
||||
### Configure allowed groups
|
||||
|
||||
@@ -31,8 +31,8 @@ The reference information that follows complements conceptual information about
|
||||
|
||||
## Default built-in role assignments
|
||||
|
||||
| Built-in role | Associated role | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Built-in role | Associated role | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Grafana Admin | `fixed:permissions:admin:edit`<br>`fixed:permissions:admin:read`<br>`fixed:provisioning:admin`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read`<br>`fixed:users:admin:edit`<br>`fixed:users:admin:read`<br>`fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:ldap:admin:edit`<br>`fixed:ldap:admin:read`<br>`fixed:server:admin:read`<br>`fixed:settings:admin:read`<br>`fixed:settings:admin:edit` | Allow access to the same resources and permissions the [Grafana server administrator]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has by default. |
|
||||
| Admin | `fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read` | Allow access to the same resources and permissions that the [Grafana organization administrator]({{< relref "../../permissions/organization_roles.md" >}}) has by default. |
|
||||
| Admin | `fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read` | Allow access to the same resources and permissions that the [Grafana organization administrator]({{< relref "../../permissions/organization_roles.md" >}}) has by default. |
|
||||
| Editor | `fixed:datasource:editor:read` |
|
||||
|
||||
@@ -401,6 +401,12 @@ The full Redis URL of your Redis server. Example: `redis://localhost:6739/0`.
|
||||
|
||||
The default is `"redis://localhost:6379"`.
|
||||
|
||||
### cluster
|
||||
|
||||
A comma-separated list of Redis cluster members in `host:port` format. For example, `localhost:7000, localhost: 7001, localhost:7002`.
|
||||
|
||||
> **Note:** If you have specify `cluster`, the value for `url` is ignored.
|
||||
|
||||
### prefix
|
||||
|
||||
A string that prefixes all Redis keys. This value must be set if using a shared database in Redis. If `prefix` is empty, then one will not be used.
|
||||
|
||||
@@ -67,7 +67,6 @@ You can use the unit dropdown to also specify custom units, custom prefix or suf
|
||||
|
||||
To select a custom unit enter the unit and select the last `Custom: xxx` option in the dropdown.
|
||||
|
||||
- If y u want a space -> If you want a space
|
||||
- `suffix:<suffix>` for custom unit that should go after value.
|
||||
- `time:<format>` For custom date time formats type for example `time:YYYY-MM-DD`. See [formats](https://momentjs.com/docs/#/displaying/) for the format syntax and options.
|
||||
- `si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the
|
||||
|
||||
@@ -8,6 +8,8 @@ weight = 10000
|
||||
Here you can find detailed release notes that list everything that is included in every release as well as notices
|
||||
about deprecations, breaking changes as well as changes that relate to plugin development.
|
||||
|
||||
- [Release notes for 8.2.0-beta1]({{< relref "release-notes-8-2-0-beta1" >}})
|
||||
- [Release notes for 8.1.4]({{< relref "release-notes-8-1-4" >}})
|
||||
- [Release notes for 8.1.3]({{< relref "release-notes-8-1-3" >}})
|
||||
- [Release notes for 8.1.2]({{< relref "release-notes-8-1-2" >}})
|
||||
- [Release notes for 8.1.1]({{< relref "release-notes-8-1-1" >}})
|
||||
|
||||
89
docs/sources/release-notes/release-notes-8-2-0-beta1.md
Normal file
89
docs/sources/release-notes/release-notes-8-2-0-beta1.md
Normal file
@@ -0,0 +1,89 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.2.0-beta1"
|
||||
[_build]
|
||||
list = false
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.2.0-beta1
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **AccessControl:** Introduce new permissions to restrict access for reloading provisioning configuration. [#38906](https://github.com/grafana/grafana/pull/38906), [@vtorosyan](https://github.com/vtorosyan)
|
||||
- **Alerting:** Add UI to edit Cortex/Loki namespace, group names, and group evaluation interval. [#38543](https://github.com/grafana/grafana/pull/38543), [@domasx2](https://github.com/domasx2)
|
||||
- **Alerting:** Add a Test button to test contact point. [#37475](https://github.com/grafana/grafana/pull/37475), [@domasx2](https://github.com/domasx2)
|
||||
- **Alerting:** Allow creating/editing recording rules for Loki and Cortex. [#38064](https://github.com/grafana/grafana/pull/38064), [@domasx2](https://github.com/domasx2)
|
||||
- **Alerting:** Sort notification channels by name to make them easier to locate. [#37426](https://github.com/grafana/grafana/pull/37426), [@jstangroome](https://github.com/jstangroome)
|
||||
- **AzureMonitor:** Add data links to deep link to Azure Portal Azure Resource Graph. [#35591](https://github.com/grafana/grafana/pull/35591), [@shuotli](https://github.com/shuotli)
|
||||
- **AzureMonitor:** Add support for annotations from Azure Monitor Metrics and Azure Resource Graph services. [#37633](https://github.com/grafana/grafana/pull/37633), [@joshhunt](https://github.com/joshhunt)
|
||||
- **AzureMonitor:** Show error message when subscriptions request fails in ConfigEditor. [#37837](https://github.com/grafana/grafana/pull/37837), [@joshhunt](https://github.com/joshhunt)
|
||||
- **Chore:** Update to Golang 1.16.7. [#38604](https://github.com/grafana/grafana/pull/38604), [@dsotirakis](https://github.com/dsotirakis)
|
||||
- **CloudWatch Logs:** Add link to X-Ray data source for trace IDs in logs. [#39135](https://github.com/grafana/grafana/pull/39135), [@aocenas](https://github.com/aocenas)
|
||||
- **CloudWatch Logs:** Disable query path using websockets (Live) feature. [#39231](https://github.com/grafana/grafana/pull/39231), [@aocenas](https://github.com/aocenas)
|
||||
- **CloudWatch/Logs:** Don't group dataframes for non time series queries. [#37998](https://github.com/grafana/grafana/pull/37998), [@aocenas](https://github.com/aocenas)
|
||||
- **Cloudwatch:** Migrate queries that use multiple stats to one query per stat. [#36925](https://github.com/grafana/grafana/pull/36925), [@sunker](https://github.com/sunker)
|
||||
- **Dashboard:** Keep live timeseries moving left (v2). [#37769](https://github.com/grafana/grafana/pull/37769), [@ryantxu](https://github.com/ryantxu)
|
||||
- **Datasources:** Introduce `response_limit` for datasource responses. [#38962](https://github.com/grafana/grafana/pull/38962), [@dsotirakis](https://github.com/dsotirakis)
|
||||
- **Explore:** Add filter by trace or span ID to `trace to logs` feature. [#38943](https://github.com/grafana/grafana/pull/38943), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Explore:** Download traces as JSON in Explore Inspector. [#38614](https://github.com/grafana/grafana/pull/38614), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Explore:** Reuse Dashboard's QueryRows component. [#38942](https://github.com/grafana/grafana/pull/38942), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Explore:** Support custom display label for derived fields buttons for Loki datasource. [#37273](https://github.com/grafana/grafana/pull/37273), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Grafana UI:** Update monaco-related dependencies. [#39027](https://github.com/grafana/grafana/pull/39027), [@gabor](https://github.com/gabor)
|
||||
- **Graphite:** Deprecate browser access mode. [#38783](https://github.com/grafana/grafana/pull/38783), [@ifrost](https://github.com/ifrost)
|
||||
- **InfluxDB:** Improve handling of intervals in alerting. [#37588](https://github.com/grafana/grafana/pull/37588), [@gabor](https://github.com/gabor)
|
||||
- **InfluxDB:** InfluxQL query editor: Handle unusual characters in tag values better. [#39170](https://github.com/grafana/grafana/pull/39170), [@gabor](https://github.com/gabor)
|
||||
- **Jaeger:** Add ability to upload JSON file for trace data. [#37205](https://github.com/grafana/grafana/pull/37205), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **LibraryElements:** Enable specifying UID for new and existing library elements. [#39019](https://github.com/grafana/grafana/pull/39019), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **LibraryPanels:** Remove library panel icon from the panel header so you can no longer tell that a panel is a library panel from the dashboard view. [#38749](https://github.com/grafana/grafana/pull/38749), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **Logs panel:** Scroll to the bottom on page refresh when sorting in ascending order. [#37634](https://github.com/grafana/grafana/pull/37634), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Loki:** Add fuzzy search to label browser. [#36864](https://github.com/grafana/grafana/pull/36864), [@connorlindsey](https://github.com/connorlindsey)
|
||||
- **Navigation:** Implement active state for items in the Sidemenu. [#39030](https://github.com/grafana/grafana/pull/39030), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Packaging:** Add stricter systemd unit options. [#38109](https://github.com/grafana/grafana/pull/38109), [@erdnaxe](https://github.com/erdnaxe)
|
||||
- **Packaging:** Update PID file location from `/var/run` to `/run`. [#35739](https://github.com/grafana/grafana/pull/35739), [@MichaIng](https://github.com/MichaIng)
|
||||
- **Plugins:** Add Hide OAuth Forward config option. [#36306](https://github.com/grafana/grafana/pull/36306), [@wbrowne](https://github.com/wbrowne)
|
||||
- **Postgres/MySQL/MSSQL:** Add setting to limit the maximum number of rows processed. [#38986](https://github.com/grafana/grafana/pull/38986), [@marefr](https://github.com/marefr)
|
||||
- **Prometheus:** Add browser access mode deprecation warning. [#37578](https://github.com/grafana/grafana/pull/37578), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Add interpolation for built-in-time variables to backend. [#39051](https://github.com/grafana/grafana/pull/39051), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Tempo:** Add ability to upload trace data in JSON format. [#37407](https://github.com/grafana/grafana/pull/37407), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **TimeSeries/XYChart:** Allow grid lines visibility control in XYChart and TimeSeries panels. [#38502](https://github.com/grafana/grafana/pull/38502), [@dprokop](https://github.com/dprokop)
|
||||
- **Transformations:** Convert field types to time string number or boolean. [#38517](https://github.com/grafana/grafana/pull/38517), [@nikki-kiga](https://github.com/nikki-kiga)
|
||||
- **Value mappings:** Add regular-expression based value mapping. [#38931](https://github.com/grafana/grafana/pull/38931), [@mcdee](https://github.com/mcdee)
|
||||
- **Zipkin:** Add ability to upload trace JSON. [#37483](https://github.com/grafana/grafana/pull/37483), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Admin:** Prevent user from deleting user's current/active organization. [#38056](https://github.com/grafana/grafana/pull/38056), [@idafurjes](https://github.com/idafurjes)
|
||||
- **LibraryPanels:** Fix library panel getting saved in the dashboard's folder. [#38978](https://github.com/grafana/grafana/pull/38978), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
- **OAuth:** Make generic teams URL and JMES path configurable. [#37233](https://github.com/grafana/grafana/pull/37233), [@djairhogeuens](https://github.com/djairhogeuens)
|
||||
- **QueryEditor:** Fix broken copy-paste for mouse middle-click (#39117). [#39117](https://github.com/grafana/grafana/pull/39117), [@glintik](https://github.com/glintik)
|
||||
- **Thresholds:** Fix undefined color in "Add threshold". [#39113](https://github.com/grafana/grafana/pull/39113), [@glintik](https://github.com/glintik)
|
||||
- **Timeseries:** Add wide-to-long, and fix multi-frame output. [#38670](https://github.com/grafana/grafana/pull/38670), [@ryantxu](https://github.com/ryantxu)
|
||||
- **TooltipPlugin:** Fix behavior of Shared Crosshair when Tooltip is set to All. [#37285](https://github.com/grafana/grafana/pull/37285), [@nikki-kiga](https://github.com/nikki-kiga)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
The `monaco-editor` dependency in `grafana-ui` has been updated to a newer version (`0.27.0`), which is not completely backward compatible with the old version (`0.21.2`). The backward incompatible changes are fairly small, but they do exist, so if your code accesses the raw monaco-objects through the `grafana-ui` package, please check the [monaco-editor changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md) and apply any necessary changes. Issue [#39027](https://github.com/grafana/grafana/issues/39027)
|
||||
|
||||
The mandatory `css` prop in `grafana/ui` components has been removed.
|
||||
|
||||
Previous versions of `grafana/ui` components were typed incorrectly due to a dependency mismatch between emotion 10 and 11 causing a `css` prop to be added to components that extended react types.
|
||||
Issue [#38078](https://github.com/grafana/grafana/issues/38078)
|
||||
|
||||
Panel queries and/or annotation queries that used more than one statistic will be converted into one query/annotation per statistic. In case an alerting rule was based on a query row that had more than one statistic, it would now be based only on the first statistic for that query row. New alerting rules will not be created for migrated queries. Please note that in most cases it would not make sense to have an alerting rule that is based on multiple statistics anyway. Issue [#36925](https://github.com/grafana/grafana/issues/36925)
|
||||
|
||||
### Deprecations
|
||||
|
||||
`getHighlighterExpressions` in datasource APIs ( used to highlight logs while editing queries) has been deprecated and will be removed in a future release.
|
||||
|
||||
# Deprecation notice
|
||||
|
||||
`ExploreQueryFieldProps` interface for query editors has been deprecated and will be removed in a future release. Use `QueryEditorProps` instead. Issue [#38942](https://github.com/grafana/grafana/issues/38942)
|
||||
|
||||
### Plugin development fixes & changes
|
||||
|
||||
- **Grafana UI:** Fix TS error property `css` is missing in type. [#38078](https://github.com/grafana/grafana/pull/38078), [@jackw](https://github.com/jackw)
|
||||
|
||||
### Grafana 8 alerts fixes
|
||||
|
||||
- **Organisation level isolation:** The fix for organization level isolation introduced a [new migration flow](https://github.com/grafana/grafana/pull/37414) that deletes all Grafana 8 alerting data and migrates the dashboard alerts to the new system again so that an Alertmanager configuration is created properly for each organization. It removes any manually created (or modified) Grafana 8 alerts or Alertmanager configuration after upgrading.
|
||||
210
e2e/suite1/dashboards/TestDashboard.json
Normal file
210
e2e/suite1/dashboards/TestDashboard.json
Normal file
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 321,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"text": {}
|
||||
},
|
||||
"pluginVersion": "8.3.0-pre",
|
||||
"title": "Gauge Example",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"text": {},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "8.3.0-pre",
|
||||
"title": "Stat",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
}
|
||||
},
|
||||
"title": "Time series example",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 31,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "2021-09-01T04:00:00.000Z",
|
||||
"to": "2021-09-15T04:00:00.000Z"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "E2E Test - Import Dashboard",
|
||||
"uid": "kquZN5H7k",
|
||||
"version": 4
|
||||
}
|
||||
@@ -12,7 +12,7 @@ const addDataSource = () => {
|
||||
e2e.components.DataSourcePicker.container()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input().should('be.visible').click({ force: true });
|
||||
e2e.components.DataSourcePicker.input().should('be.visible').click({ force: true });
|
||||
});
|
||||
|
||||
e2e().contains('gdev-tempo').scrollIntoView().should('be.visible').click();
|
||||
@@ -53,7 +53,7 @@ describe('Exemplars', () => {
|
||||
e2e.components.DataSourcePicker.container()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input().should('be.visible').click();
|
||||
e2e.components.DataSourcePicker.input().should('be.visible').click();
|
||||
});
|
||||
e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click();
|
||||
e2e.components.TimePicker.openButton().click();
|
||||
|
||||
@@ -1,122 +1,12 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Import Dashboard Test',
|
||||
itName: 'Ensure you can import a dashboard',
|
||||
describeName: 'Import Dashboards Test',
|
||||
itName: 'Ensure you can import a number of json test dashboards from a specific test directory',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.importDashboard(TEST_DASHBOARD);
|
||||
e2e.flows.importDashboards('/dashboards', 1000);
|
||||
},
|
||||
});
|
||||
|
||||
const TEST_DASHBOARD = {
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
builtIn: 1,
|
||||
datasource: '-- Grafana --',
|
||||
enable: true,
|
||||
hide: true,
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
editable: true,
|
||||
gnetId: null,
|
||||
graphTooltip: 0,
|
||||
id: 74,
|
||||
links: [],
|
||||
panels: [
|
||||
{
|
||||
datasource: null,
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
color: {
|
||||
mode: 'palette-classic',
|
||||
},
|
||||
custom: {
|
||||
axisLabel: '',
|
||||
axisPlacement: 'auto',
|
||||
barAlignment: 0,
|
||||
drawStyle: 'line',
|
||||
fillOpacity: 0,
|
||||
gradientMode: 'none',
|
||||
hideFrom: {
|
||||
legend: false,
|
||||
tooltip: false,
|
||||
viz: false,
|
||||
},
|
||||
lineInterpolation: 'linear',
|
||||
lineWidth: 1,
|
||||
pointSize: 5,
|
||||
scaleDistribution: {
|
||||
type: 'linear',
|
||||
},
|
||||
showPoints: 'auto',
|
||||
spanNulls: false,
|
||||
stacking: {
|
||||
group: 'A',
|
||||
mode: 'none',
|
||||
},
|
||||
thresholdsStyle: {
|
||||
mode: 'off',
|
||||
},
|
||||
},
|
||||
mappings: [],
|
||||
thresholds: {
|
||||
mode: 'absolute',
|
||||
steps: [
|
||||
{
|
||||
color: 'green',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
value: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
gridPos: {
|
||||
h: 9,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
id: 2,
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'single',
|
||||
},
|
||||
},
|
||||
title: 'Panel Title',
|
||||
type: 'timeseries',
|
||||
},
|
||||
],
|
||||
schemaVersion: 30,
|
||||
style: 'dark',
|
||||
tags: [],
|
||||
templating: {
|
||||
list: [],
|
||||
},
|
||||
time: {
|
||||
from: '2021-06-30T04:00:00.000Z',
|
||||
to: '2021-07-02T03:59:59.000Z',
|
||||
},
|
||||
timepicker: {},
|
||||
timezone: '',
|
||||
title: 'An imported dashboard for e2e tests',
|
||||
uid: '6V0Nzyz7k',
|
||||
version: 1,
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ e2e.scenario({
|
||||
e2e.components.QueryTab.content().should('be.visible');
|
||||
e2e.components.TransformTab.content().should('not.exist');
|
||||
e2e.components.AlertTab.content().should('not.exist');
|
||||
e2e.components.PanelAlertTabContent.content().should('not.exist');
|
||||
|
||||
// Bottom pane tabs
|
||||
// Can change to Transform tab
|
||||
@@ -38,6 +39,7 @@ e2e.scenario({
|
||||
e2e.components.Transforms.card('Merge').scrollIntoView().should('be.visible');
|
||||
e2e.components.QueryTab.content().should('not.exist');
|
||||
e2e.components.AlertTab.content().should('not.exist');
|
||||
e2e.components.PanelAlertTabContent.content().should('not.exist');
|
||||
|
||||
// Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered)
|
||||
e2e.components.Tab.title('Alert').should('be.visible').click();
|
||||
@@ -47,6 +49,7 @@ e2e.scenario({
|
||||
e2e.components.AlertTab.content().should('be.visible');
|
||||
e2e.components.QueryTab.content().should('not.exist');
|
||||
e2e.components.TransformTab.content().should('not.exist');
|
||||
e2e.components.PanelAlertTabContent.content().should('not.exist');
|
||||
|
||||
e2e.components.Tab.title('Query').should('be.visible').click();
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ e2e.scenario({
|
||||
e2e.components.DataSourcePicker.container()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input().should('be.visible').click();
|
||||
e2e.components.DataSourcePicker.input().should('be.visible').click();
|
||||
});
|
||||
|
||||
cy.contains('gdev-prometheus').scrollIntoView().should('be.visible').click();
|
||||
|
||||
@@ -50,8 +50,11 @@ e2e.scenario({
|
||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2').should('be.visible').click();
|
||||
|
||||
e2e.components.PageToolbar.container().click();
|
||||
|
||||
e2e.components.DashboardLinks.dropDown().should('be.visible').click().wait('@tagsTemplatingSearch');
|
||||
e2e.components.DashboardLinks.dropDown()
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.wait('@tagsTemplatingSearch');
|
||||
|
||||
// verify all links, should have p2 value
|
||||
verifyLinks('p2');
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('Trace view', () => {
|
||||
e2e.components.DataSourcePicker.container()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input().should('be.visible').click();
|
||||
e2e.components.DataSourcePicker.input().should('be.visible').click();
|
||||
});
|
||||
|
||||
e2e().contains('gdev-jaeger').scrollIntoView().should('be.visible').click();
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Variables - Add variable', () => {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input().should('be.visible').type('gdev-testdata').type('{enter}');
|
||||
e2e.components.DataSourcePicker.input().should('be.visible').type('gdev-testdata').type('{enter}');
|
||||
});
|
||||
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
|
||||
@@ -139,7 +139,7 @@ describe('Variables - Add variable', () => {
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input().should('be.visible').type('gdev-testdata').type('{enter}');
|
||||
e2e.components.DataSourcePicker.input().should('be.visible').type('gdev-testdata').type('{enter}');
|
||||
});
|
||||
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*.ts", "../../packages/grafana-e2e/cypress/support/index.d.ts"]
|
||||
"include": ["**/*.ts", "../../packages/grafana-e2e/cypress/support/index.d.ts"],
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
|
||||
11
embed.go
11
embed.go
@@ -2,16 +2,15 @@ package grafana
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
// CoreSchema embeds all core CUE files, which live in packages/grafana-schema/src
|
||||
//
|
||||
//go:embed cue.mod cue packages/grafana-schema/src/schema/*.cue packages/grafana-schema/src/scuemata/*/*.cue
|
||||
//go:embed cue.mod cue packages/grafana-schema/src/schema/*.cue packages/grafana-schema/src/scuemata/*/*.cue packages/grafana-schema/src/scuemata/*/*/*.cue
|
||||
var CoreSchema embed.FS
|
||||
|
||||
// PluginSchema embeds all expected plugin CUE files and plugin metadata from
|
||||
// within the public/app/plugins subdirectory.
|
||||
//
|
||||
//go:embed public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json
|
||||
var base embed.FS
|
||||
|
||||
// PluginSchema embeds all CUE files within the public/ subdirectory.
|
||||
var PluginSchema, _ = fs.Sub(base, "public/app/plugins")
|
||||
var PluginSchema embed.FS
|
||||
|
||||
5
go.mod
5
go.mod
@@ -35,6 +35,7 @@ require (
|
||||
github.com/go-kit/kit v0.11.0
|
||||
github.com/go-macaron/binding v0.0.0-20190806013118-0b4f37bab25b
|
||||
github.com/go-openapi/strfmt v0.20.1
|
||||
github.com/go-redis/redis/v8 v8.11.3
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-stack/stack v1.8.0
|
||||
@@ -49,7 +50,7 @@ require (
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/gosimple/slug v1.9.0
|
||||
github.com/grafana/grafana-aws-sdk v0.7.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.113.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.114.0
|
||||
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/hashicorp/go-hclog v0.16.1
|
||||
@@ -112,7 +113,6 @@ require (
|
||||
gopkg.in/ldap.v3 v3.1.0
|
||||
gopkg.in/macaron.v1 v1.4.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/redis.v5 v5.2.9
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
@@ -141,6 +141,7 @@ require (
|
||||
github.com/cockroachdb/apd/v2 v2.0.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.3.13 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
|
||||
23
go.sum
23
go.sum
@@ -461,6 +461,7 @@ github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/
|
||||
github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
@@ -759,9 +760,12 @@ github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3h
|
||||
github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-redis/redis/v8 v8.0.0-beta.10.0.20200905143926-df7fe4e2ce72/go.mod h1:CJP1ZIHwhosNYwIdaHPZK9vHsM3+roNBaZ7U9Of1DXc=
|
||||
github.com/go-redis/redis/v8 v8.2.3/go.mod h1:ysgGY09J/QeDYbu3HikWEIPCwaeOkuNoTgKayTEaEOw=
|
||||
github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8=
|
||||
github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc=
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY=
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
@@ -773,6 +777,7 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
|
||||
@@ -1020,8 +1025,8 @@ github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0r
|
||||
github.com/grafana/grafana-aws-sdk v0.7.0 h1:D+Lhxi3P/7vpyDHUK/fdX9bL2mRz8hLG04ucNf1E02o=
|
||||
github.com/grafana/grafana-aws-sdk v0.7.0/go.mod h1:+pPo5U+pX0zWimR7YBc7ASeSQfbRkcTyQYqMiAj7G5U=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.113.0 h1:X46np4UNgM0YLhxC0oLa2q7WOHdU5T/oppZ+XlYusMk=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.113.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.114.0 h1:9I55IXw7mOT71tZ/pdqCaWGz8vxfz31CXjaDtBV9ZBo=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 h1:qCmofFVwQR9QnsinstVqI1NPLMVl33jNCnOCXEAVn6E=
|
||||
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103/go.mod h1:GHIsn+EohCChsdu5YouNZewqLeV9L2FNw4DEJU3P9qE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
@@ -1567,8 +1572,9 @@ github.com/newrelic/newrelic-telemetry-sdk-go v0.2.0/go.mod h1:G9MqE/cHGv3Hx3qpY
|
||||
github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2/go.mod h1:TLb2Sg7HQcgGdloNxkrmtgDNR9uVYF3lfdFIN4Ro6Sk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nsqio/go-nsq v1.0.7/go.mod h1:XP5zaUs3pqf+Q71EqUJs3HYfBIqfK6G83WQMdNN+Ito=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
@@ -1594,8 +1600,9 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
|
||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4=
|
||||
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
@@ -1604,8 +1611,9 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
|
||||
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/openconfig/gnmi v0.0.0-20180912164834-33a1865c3029/go.mod h1:t+O9It+LKzfOAhKTT5O0ehDix+MTqbtT0T9t+7zzOvc=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
@@ -2356,6 +2364,7 @@ golang.org/x/net v0.0.0-20210324051636-2c4c8ecb7826/go.mod h1:RBQZq4jEuRlivfhVLd
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@@ -2500,6 +2509,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -2652,6 +2662,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20201119054027-25dc3e1ccc3c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201226215659-b1c90890d22a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
@@ -2898,8 +2909,6 @@ gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/olivere/elastic.v5 v5.0.70/go.mod h1:FylZT6jQWtfHsicejzOm3jIMVPOAksa80i3o+6qtQRk=
|
||||
gopkg.in/redis.v5 v5.2.9 h1:MNZYOLPomQzZMfpN3ZtD1uyJ2IDonTTlxYiV/pEApiw=
|
||||
gopkg.in/redis.v5 v5.2.9/go.mod h1:6gtv0/+A4iM08kdRfocWYB3bLX2tebpNtfKlFT6H4mY=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "8.2.0-pre"
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "8.2.0-beta.2"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"repository": "github:grafana/grafana",
|
||||
"scripts": {
|
||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||
@@ -290,7 +290,7 @@
|
||||
"ol": "6.7.0",
|
||||
"papaparse": "5.3.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"prismjs": "1.24.0",
|
||||
"prismjs": "1.25.0",
|
||||
"prop-types": "15.7.2",
|
||||
"rc-cascader": "1.5.0",
|
||||
"re-resizable": "^6.2.0",
|
||||
@@ -329,7 +329,6 @@
|
||||
"whatwg-fetch": "3.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"caniuse-db": "1.0.30000772",
|
||||
"underscore": "1.12.1"
|
||||
},
|
||||
"workspaces": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "5.0.2",
|
||||
"@grafana/schema": "8.2.0-pre",
|
||||
"@grafana/schema": "8.2.0-beta.2",
|
||||
"@types/d3-interpolate": "^1.3.1",
|
||||
"date-fns": "2.21.3",
|
||||
"eventemitter3": "4.0.7",
|
||||
|
||||
@@ -135,4 +135,71 @@ describe('DateMath', () => {
|
||||
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 3]).valueOf());
|
||||
});
|
||||
});
|
||||
|
||||
describe('Round to fiscal start/end', () => {
|
||||
it('Should round to start of fiscal year when datetime is the same year as the start of the fiscal year', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2021, 3, 5]), 'y', false);
|
||||
let expected = dateTime([2021, 1, 1]);
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to start of fiscal year when datetime is the next year from the start of the fiscal year', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 2]), 'y', false);
|
||||
let expected = dateTime([2021, 1, 1]);
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to start of fiscal year when datetime is on a leap day', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2020, 1, 29]), 'y', false);
|
||||
let expected = dateTime([2020, 1, 1]);
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to end of fiscal year when datetime is the same year as the start of the fiscal year', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2021, 5, 2]), 'y', true);
|
||||
let expected = dateTime([2022, 0, 1]).endOf('M');
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to end of fiscal year when datetime is the next year from the start of the fiscal year', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 1]), 'y', true);
|
||||
let expected = dateTime([2022, 0, 1]).endOf('M');
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to end of fiscal year when datetime is on a leap day', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2020, 1, 29]), 'y', true);
|
||||
let expected = dateTime([2021, 0, 1]).endOf('M');
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
//fq1 = 2021-02-01 - 2021-04-30
|
||||
//fq2 = 2021-05-01 - 2021-07-31
|
||||
//fq4 = 2021-08-01 - 2021-10-31
|
||||
//fq5 = 2021-11-01 - 2022-01-31
|
||||
|
||||
it('Should round to start of q2 when one month into q2', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2021, 6, 1]), 'Q', false);
|
||||
let expected = dateTime([2021, 4, 1]);
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to start of q4 when datetime is in next year from fiscal year start', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 1]), 'Q', false);
|
||||
let expected = dateTime([2021, 10, 1]);
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to end of q2 when one month into q2', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2021, 6, 1]), 'Q', true);
|
||||
let expected = dateTime([2021, 6, 1]).endOf('M');
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
|
||||
it('Should round to end of q4 when datetime is in next year from fiscal year start', () => {
|
||||
let date = dateMath.roundToFiscal(1, dateTime([2022, 0, 1]), 'Q', true);
|
||||
let expected = dateTime([2022, 0, 31]).endOf('M');
|
||||
expect(date!.valueOf()).toEqual(expected.valueOf());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { includes, isDate } from 'lodash';
|
||||
import { DateTime, dateTime, dateTimeForTimeZone, ISO_8601, isDateTime, DurationUnit } from './moment_wrapper';
|
||||
import { TimeZone } from '../types/index';
|
||||
|
||||
const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
||||
const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's', 'Q'];
|
||||
|
||||
export function isMathString(text: string | DateTime | Date): boolean {
|
||||
if (!text) {
|
||||
@@ -26,7 +26,8 @@ export function isMathString(text: string | DateTime | Date): boolean {
|
||||
export function parse(
|
||||
text?: string | DateTime | Date | null,
|
||||
roundUp?: boolean,
|
||||
timezone?: TimeZone
|
||||
timezone?: TimeZone,
|
||||
fiscalYearStartMonth?: number
|
||||
): DateTime | undefined {
|
||||
if (!text) {
|
||||
return undefined;
|
||||
@@ -67,7 +68,7 @@ export function parse(
|
||||
return time;
|
||||
}
|
||||
|
||||
return parseDateMath(mathString, time, roundUp);
|
||||
return parseDateMath(mathString, time, roundUp, fiscalYearStartMonth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +97,12 @@ export function isValid(text: string | DateTime): boolean {
|
||||
* @param roundUp If true it will round the time to endOf time unit, otherwise to startOf time unit.
|
||||
*/
|
||||
// TODO: Had to revert Andrejs `time: moment.Moment` to `time: any`
|
||||
export function parseDateMath(mathString: string, time: any, roundUp?: boolean): DateTime | undefined {
|
||||
export function parseDateMath(
|
||||
mathString: string,
|
||||
time: any,
|
||||
roundUp?: boolean,
|
||||
fiscalYearStartMonth = 0
|
||||
): DateTime | undefined {
|
||||
const strippedMathString = mathString.replace(/\s/g, '');
|
||||
const dateTime = time;
|
||||
let i = 0;
|
||||
@@ -107,6 +113,7 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
||||
let type;
|
||||
let num;
|
||||
let unit;
|
||||
let isFiscal = false;
|
||||
|
||||
if (c === '/') {
|
||||
type = 0;
|
||||
@@ -121,7 +128,7 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
||||
if (isNaN(parseInt(strippedMathString.charAt(i), 10))) {
|
||||
num = 1;
|
||||
} else if (strippedMathString.length === 2) {
|
||||
num = strippedMathString.charAt(i);
|
||||
num = parseInt(strippedMathString.charAt(i), 10);
|
||||
} else {
|
||||
const numFrom = i;
|
||||
while (!isNaN(parseInt(strippedMathString.charAt(i), 10))) {
|
||||
@@ -141,14 +148,27 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
||||
}
|
||||
unit = strippedMathString.charAt(i++);
|
||||
|
||||
if (unit === 'f') {
|
||||
unit = strippedMathString.charAt(i++);
|
||||
isFiscal = true;
|
||||
}
|
||||
|
||||
if (!includes(units, unit)) {
|
||||
return undefined;
|
||||
} else {
|
||||
if (type === 0) {
|
||||
if (roundUp) {
|
||||
dateTime.endOf(unit);
|
||||
if (isFiscal) {
|
||||
roundToFiscal(fiscalYearStartMonth, dateTime, unit, roundUp);
|
||||
} else {
|
||||
dateTime.endOf(unit);
|
||||
}
|
||||
} else {
|
||||
dateTime.startOf(unit);
|
||||
if (isFiscal) {
|
||||
roundToFiscal(fiscalYearStartMonth, dateTime, unit, roundUp);
|
||||
} else {
|
||||
dateTime.startOf(unit);
|
||||
}
|
||||
}
|
||||
} else if (type === 1) {
|
||||
dateTime.add(num, unit);
|
||||
@@ -159,3 +179,24 @@ export function parseDateMath(mathString: string, time: any, roundUp?: boolean):
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
export function roundToFiscal(fyStartMonth: number, dateTime: any, unit: string, roundUp: boolean | undefined) {
|
||||
switch (unit) {
|
||||
case 'y':
|
||||
if (roundUp) {
|
||||
roundToFiscal(fyStartMonth, dateTime, unit, false).add(11, 'M').endOf('M');
|
||||
} else {
|
||||
dateTime.subtract((dateTime.month() - fyStartMonth + 12) % 12, 'M').startOf('M');
|
||||
}
|
||||
return dateTime;
|
||||
case 'Q':
|
||||
if (roundUp) {
|
||||
roundToFiscal(fyStartMonth, dateTime, unit, false).add(2, 'M').endOf('M');
|
||||
} else {
|
||||
dateTime.subtract((dateTime.month() - fyStartMonth + 3) % 3, 'M').startOf('M');
|
||||
}
|
||||
return dateTime;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { intervalToAbbreviatedDurationString, addDurationToDate, parseDuration } from './durationutil';
|
||||
import {
|
||||
intervalToAbbreviatedDurationString,
|
||||
addDurationToDate,
|
||||
parseDuration,
|
||||
isValidDuration,
|
||||
isValidGoDuration,
|
||||
} from './durationutil';
|
||||
|
||||
describe('Duration util', () => {
|
||||
describe('intervalToAbbreviatedDurationString', () => {
|
||||
@@ -20,4 +26,28 @@ describe('Duration util', () => {
|
||||
expect(parseDuration(durationString)).toEqual({ months: '3', minutes: '4' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidDuration', () => {
|
||||
it('valid duration string returns true', () => {
|
||||
const durationString = '3M 5d 20m';
|
||||
expect(isValidDuration(durationString)).toEqual(true);
|
||||
});
|
||||
|
||||
it('invalid duration string returns false', () => {
|
||||
const durationString = '3M 6v 5b 4m';
|
||||
expect(isValidDuration(durationString)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidGoDuration', () => {
|
||||
it('valid duration string returns true', () => {
|
||||
const durationString = '3h 4m 1s 2ms 3us 5ns';
|
||||
expect(isValidGoDuration(durationString)).toEqual(true);
|
||||
});
|
||||
|
||||
it('invalid duration string returns false', () => {
|
||||
const durationString = '3M 6v 5b 4m';
|
||||
expect(isValidGoDuration(durationString)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ const durationMap: { [key in Required<keyof Duration>]: string[] } = {
|
||||
};
|
||||
|
||||
/**
|
||||
* intervalToAbbreviatedDurationString convers interval to readable duration string
|
||||
* intervalToAbbreviatedDurationString converts interval to readable duration string
|
||||
*
|
||||
* @param interval - interval to convert
|
||||
* @param includeSeconds - optional, default true. If false, will not include seconds unless interval is less than 1 minute
|
||||
@@ -85,3 +85,55 @@ export function durationToMilliseconds(duration: Duration): number {
|
||||
export function isValidDate(dateString: string): boolean {
|
||||
return !isNaN(Date.parse(dateString));
|
||||
}
|
||||
|
||||
/**
|
||||
* isValidDuration returns true if the given string can be parsed into a valid Duration object, false otherwise
|
||||
*
|
||||
* @param durationString - string representation of a duration
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function isValidDuration(durationString: string): boolean {
|
||||
for (const value of durationString.trim().split(' ')) {
|
||||
const match = value.match(/(\d+)(.+)/);
|
||||
if (match === null || match.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = Object.entries(durationMap).find(([_, abbreviations]) => abbreviations?.includes(match[2]))?.[0];
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* isValidGoDuration returns true if the given string can be parsed into a valid Duration object based on
|
||||
* Go's time.parseDuration, false otherwise.
|
||||
*
|
||||
* Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
*
|
||||
* Go docs: https://pkg.go.dev/time#ParseDuration
|
||||
*
|
||||
* @param durationString - string representation of a duration
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function isValidGoDuration(durationString: string): boolean {
|
||||
const timeUnits = ['h', 'm', 's', 'ms', 'us', 'µs', 'ns'];
|
||||
for (const value of durationString.trim().split(' ')) {
|
||||
const match = value.match(/(\d+)(.+)/);
|
||||
if (match === null || match.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isValidUnit = timeUnits.includes(match[2]);
|
||||
if (!isValidUnit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface DateTimeOptionsWhenParsing extends DateTimeOptions {
|
||||
* the returned DateTime value will be 06:00:00.
|
||||
*/
|
||||
roundUp?: boolean;
|
||||
fiscalYearStartMonth?: number;
|
||||
}
|
||||
|
||||
type DateTimeParser<T extends DateTimeOptions = DateTimeOptions> = (value: DateTimeInput, options?: T) => DateTime;
|
||||
@@ -56,7 +57,7 @@ const parseString = (value: string, options?: DateTimeOptionsWhenParsing): DateT
|
||||
return moment() as DateTime;
|
||||
}
|
||||
|
||||
const parsed = parse(value, options?.roundUp, options?.timeZone);
|
||||
const parsed = parse(value, options?.roundUp, options?.timeZone, options?.fiscalYearStartMonth);
|
||||
return parsed || (moment() as DateTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ const rangeOptions: TimeOption[] = [
|
||||
},
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' },
|
||||
{ from: 'now-1Q/fQ', to: 'now-1Q/fQ', display: 'Previous fiscal quarter' },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' },
|
||||
{ from: 'now-1y/fy', to: 'now-1y/fy', display: 'Previous fiscal year' },
|
||||
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
|
||||
@@ -58,6 +60,10 @@ const rangeOptions: TimeOption[] = [
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year' },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years' },
|
||||
{ from: 'now/fQ', to: 'now', display: 'This fiscal quarter so far' },
|
||||
{ from: 'now/fQ', to: 'now/fQ', display: 'This fiscal quarter' },
|
||||
{ from: 'now/fy', to: 'now', display: 'This fiscal year so far' },
|
||||
{ from: 'now/fy', to: 'now/fy', display: 'This fiscal year' },
|
||||
];
|
||||
|
||||
const hiddenRangeOptions: TimeOption[] = [
|
||||
@@ -192,9 +198,9 @@ export const describeTimeRangeAbbreviation = (range: TimeRange, timeZone?: TimeZ
|
||||
return parsed ? timeZoneAbbrevation(parsed, { timeZone }) : '';
|
||||
};
|
||||
|
||||
export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => {
|
||||
const from = dateTimeParse(raw.from, { roundUp: false, timeZone });
|
||||
const to = dateTimeParse(raw.to, { roundUp: true, timeZone });
|
||||
export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone, fiscalYearStartMonth?: number): TimeRange => {
|
||||
const from = dateTimeParse(raw.from, { roundUp: false, timeZone, fiscalYearStartMonth });
|
||||
const to = dateTimeParse(raw.to, { roundUp: true, timeZone, fiscalYearStartMonth });
|
||||
|
||||
if (dateMath.isMathString(raw.from) || dateMath.isMathString(raw.to)) {
|
||||
return { from, to, raw };
|
||||
@@ -210,6 +216,15 @@ function isRelativeTime(v: DateTime | string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isFiscal(timeRange: TimeRange) {
|
||||
if (typeof timeRange.raw.from === 'string' && timeRange.raw.from.indexOf('f') > 0) {
|
||||
return true;
|
||||
} else if (typeof timeRange.raw.to === 'string' && timeRange.raw.to.indexOf('f') > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isRelativeTimeRange(raw: RawTimeRange): boolean {
|
||||
return isRelativeTime(raw.from) || isRelativeTime(raw.to);
|
||||
}
|
||||
|
||||
@@ -157,6 +157,18 @@ describe('Format value', () => {
|
||||
expect(result.text).toEqual('elva');
|
||||
});
|
||||
|
||||
it('should return mapped color but use value format if no value mapping text specified', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ type: MappingType.RangeToText, options: { from: 1, to: 9, result: { color: '#FFF' } } },
|
||||
];
|
||||
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 2, mappings: valueMappings });
|
||||
const result = instance(5);
|
||||
|
||||
expect(result.color).toEqual('#FFF');
|
||||
expect(result.text).toEqual('5.00');
|
||||
});
|
||||
|
||||
it('should replace a matching regex', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ type: MappingType.RegexToText, options: { pattern: '([^.]*).example.com', result: { text: '$1' } } },
|
||||
|
||||
@@ -79,14 +79,12 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
||||
value = dateTime(value).valueOf();
|
||||
}
|
||||
|
||||
let text = toString(value);
|
||||
let numeric = isStringUnit ? NaN : anyToNumber(value);
|
||||
let prefix: string | undefined = undefined;
|
||||
let suffix: string | undefined = undefined;
|
||||
let color: string | undefined = undefined;
|
||||
let percent: number | undefined = undefined;
|
||||
|
||||
let shouldFormat = true;
|
||||
let text: string | undefined;
|
||||
let prefix: string | undefined;
|
||||
let suffix: string | undefined;
|
||||
let color: string | undefined;
|
||||
let percent: number | undefined;
|
||||
|
||||
if (mappings && mappings.length > 0) {
|
||||
const mappingResult = getValueMappingResult(mappings, value);
|
||||
@@ -99,13 +97,11 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
||||
if (mappingResult.color != null) {
|
||||
color = options.theme.visualization.getColorByName(mappingResult.color);
|
||||
}
|
||||
|
||||
shouldFormat = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNaN(numeric)) {
|
||||
if (shouldFormat && !isBoolean(value)) {
|
||||
if (text == null && !isBoolean(value)) {
|
||||
const v = formatFunc(numeric, config.decimals, null, options.timeZone, showMs);
|
||||
text = v.text;
|
||||
suffix = v.suffix;
|
||||
@@ -113,18 +109,21 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
||||
}
|
||||
|
||||
// Return the value along with scale info
|
||||
if (color === undefined) {
|
||||
if (color == null) {
|
||||
const scaleResult = scaleFunc(numeric);
|
||||
color = scaleResult.color;
|
||||
percent = scaleResult.percent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
if (config.noValue) {
|
||||
text = config.noValue;
|
||||
} else {
|
||||
text = ''; // No data?
|
||||
if (text == null) {
|
||||
text = toString(value);
|
||||
if (!text) {
|
||||
if (config.noValue) {
|
||||
text = config.noValue;
|
||||
} else {
|
||||
text = ''; // No data?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,27 @@ describe('PanelPlugin', () => {
|
||||
expect(panel.fieldConfigDefaults.defaults.custom).toEqual(expectedDefaults);
|
||||
});
|
||||
|
||||
test('throw error with array fieldConfigs', () => {
|
||||
const panel = new PanelPlugin(() => {
|
||||
return <div>Panel</div>;
|
||||
});
|
||||
|
||||
panel.useFieldConfig({
|
||||
useCustomConfig: (builder) => {
|
||||
builder.addCustomEditor({
|
||||
id: 'somethingUnique',
|
||||
path: 'numericOption[0]',
|
||||
name: 'Option editor',
|
||||
description: 'Option editor description',
|
||||
defaultValue: 10,
|
||||
} as any);
|
||||
},
|
||||
});
|
||||
expect(() => panel.fieldConfigRegistry).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[undefined] Field config paths do not support arrays: custom.somethingUnique"`
|
||||
);
|
||||
});
|
||||
|
||||
test('default values for nested paths', () => {
|
||||
const panel = new PanelPlugin(() => {
|
||||
return <div>Panel</div>;
|
||||
|
||||
@@ -75,6 +75,13 @@ export function createFieldConfigRegistry<TFieldConfigOptions>(
|
||||
}
|
||||
}
|
||||
|
||||
// assert that field configs do not use array path syntax
|
||||
for (const item of registry.list()) {
|
||||
if (item.path.indexOf('[') > 0) {
|
||||
throw new Error(`[${pluginName}] Field config paths do not support arrays: ${item.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class DarkColors implements ThemeColorsBase<Partial<ThemeRichColor>> {
|
||||
text = {
|
||||
primary: `rgb(${this.whiteBase})`,
|
||||
secondary: `rgba(${this.whiteBase}, 0.65)`,
|
||||
disabled: `rgba(${this.whiteBase}, 0.40)`,
|
||||
disabled: `rgba(${this.whiteBase}, 0.57)`,
|
||||
link: palette.blueDarkText,
|
||||
maxContrast: palette.white,
|
||||
};
|
||||
|
||||
@@ -45,7 +45,6 @@ export enum GrafanaEdition {
|
||||
export interface FeatureToggles {
|
||||
[name: string]: boolean;
|
||||
|
||||
ngalert: boolean;
|
||||
trimDefaults: boolean;
|
||||
accesscontrol: boolean;
|
||||
tempoServiceGraph: boolean;
|
||||
@@ -132,4 +131,5 @@ export interface GrafanaConfig {
|
||||
customTheme?: any;
|
||||
geomapDefaultBaseLayer?: MapLayerOptions;
|
||||
geomapDisableCustomBaseLayer?: boolean;
|
||||
unifiedAlertingEnabled: boolean;
|
||||
}
|
||||
|
||||
2
packages/grafana-data/src/types/constants.ts
Normal file
2
packages/grafana-data/src/types/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const GAUGE_DEFAULT_MINIMUM = 0;
|
||||
export const GAUGE_DEFAULT_MAXIMUM = 100;
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './constants';
|
||||
export * from './data';
|
||||
export * from './dataFrame';
|
||||
export * from './dataFrameTypes';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e-selectors",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"description": "Grafana End-to-End Test Selectors Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -202,6 +202,7 @@ export const Components = {
|
||||
},
|
||||
DataSourcePicker: {
|
||||
container: 'Data source picker select container',
|
||||
input: () => 'input[id="data-source-picker"]',
|
||||
},
|
||||
TimeZonePicker: {
|
||||
container: 'Time zone picker select container',
|
||||
@@ -243,4 +244,7 @@ export const Components = {
|
||||
name: 'data-testid-import-dashboard-title',
|
||||
submit: 'data-testid-import-dashboard-submit',
|
||||
},
|
||||
PanelAlertTabContent: {
|
||||
content: 'Unified alert editor tab content',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -159,6 +159,7 @@ export const Pages = {
|
||||
PluginPage: {
|
||||
page: 'Plugin page',
|
||||
signatureInfo: 'Plugin signature info',
|
||||
disabledInfo: 'Plugin disabled info',
|
||||
},
|
||||
PlaylistForm: {
|
||||
name: 'Playlist name',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const compareScreenshots = require('./compareScreenshots');
|
||||
const extendConfig = require('./extendConfig');
|
||||
const readProvisions = require('./readProvisions');
|
||||
@@ -12,6 +15,18 @@ module.exports = (on, config) => {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
on('task', {
|
||||
getJSONFilesFromDir: async ({ projectPath, relativePath }) => {
|
||||
const directoryPath = path.join(projectPath, relativePath);
|
||||
const jsonFiles = fs.readdirSync(directoryPath);
|
||||
return jsonFiles
|
||||
.filter((fileName) => /.json$/i.test(fileName))
|
||||
.map((fileName) => {
|
||||
const fileBuffer = fs.readFileSync(path.join(directoryPath, fileName));
|
||||
return JSON.parse(fileBuffer);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Always extend with this library's config and return for diffing
|
||||
// @todo remove this when possible: https://github.com/cypress-io/cypress/issues/5674
|
||||
|
||||
@@ -23,3 +23,10 @@ Cypress.Commands.add('readProvisions', (filePaths: string[]) => {
|
||||
filePaths,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getJSONFilesFromDir', (dirPath: string) => {
|
||||
return cy.task('getJSONFilesFromDir', {
|
||||
projectPath: Cypress.config().parentTestsFolder,
|
||||
relativePath: dirPath,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,5 +5,6 @@ declare namespace Cypress {
|
||||
compareScreenshots(config: CompareScreenshotsConfig | string): Chainable;
|
||||
logToConsole(message: string, optional?: any): void;
|
||||
readProvisions(filePaths: string[]): Chainable;
|
||||
getJSONFilesFromDir(dirPath: string): Chainable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -45,7 +45,7 @@
|
||||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"@cypress/webpack-preprocessor": "5.9.0",
|
||||
"@grafana/e2e-selectors": "8.2.0-pre",
|
||||
"@grafana/e2e-selectors": "8.2.0-beta.2",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"blink-diff": "1.0.13",
|
||||
|
||||
@@ -7,13 +7,14 @@ type Panel = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type Dashboard = { title: string; panels: Panel[]; uid: string; [key: string]: unknown };
|
||||
export type Dashboard = { title: string; panels: Panel[]; uid: string; [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* Smoke test a datasource by quickly importing a test dashboard for it
|
||||
* Smoke test a particular dashboard by quickly importing a json file and validate that all the panels finish loading
|
||||
* @param dashboardToImport a sample dashboard
|
||||
* @param queryTimeout a number of ms to wait for the imported dashboard to finish loading
|
||||
*/
|
||||
export const importDashboard = (dashboardToImport: Dashboard) => {
|
||||
export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: number) => {
|
||||
e2e().visit(fromBaseUrl('/dashboard/import'));
|
||||
|
||||
// Note: normally we'd use 'click' and then 'type' here, but the json object is so big that using 'val' is much faster
|
||||
@@ -24,7 +25,9 @@ export const importDashboard = (dashboardToImport: Dashboard) => {
|
||||
e2e.components.DashboardImportPage.submit().should('be.visible').click();
|
||||
e2e.components.ImportDashboardForm.name().should('be.visible').click().clear().type(dashboardToImport.title);
|
||||
e2e.components.ImportDashboardForm.submit().should('be.visible').click();
|
||||
e2e().wait(3000);
|
||||
|
||||
// wait for dashboard to load
|
||||
e2e().wait(queryTimeout || 6000);
|
||||
|
||||
// save the newly imported dashboard to context so it'll get properly deleted later
|
||||
e2e()
|
||||
@@ -42,19 +45,21 @@ export const importDashboard = (dashboardToImport: Dashboard) => {
|
||||
expect(dashboardToImport.uid).to.equal(uid);
|
||||
});
|
||||
|
||||
// inspect first panel and verify data has been processed for it
|
||||
e2e.components.Panels.Panel.title(dashboardToImport.panels[0].title).should('be.visible').click();
|
||||
e2e.components.Panels.Panel.headerItems('Inspect').should('be.visible').click();
|
||||
e2e.components.Tab.title('JSON').should('be.visible').click();
|
||||
e2e().wait(3000);
|
||||
e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click();
|
||||
e2e().wait(3000);
|
||||
e2e.components.Select.option().should('be.visible').contains('Data').click();
|
||||
e2e().wait(3000);
|
||||
dashboardToImport.panels.forEach((panel) => {
|
||||
// Look at the json data
|
||||
e2e.components.Panels.Panel.title(panel.title).should('be.visible').click();
|
||||
e2e.components.Panels.Panel.headerItems('Inspect').should('be.visible').click();
|
||||
e2e.components.Tab.title('JSON').should('be.visible').click();
|
||||
e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click();
|
||||
e2e.components.Select.option().should('be.visible').contains('Data').click();
|
||||
|
||||
// ensures that panel has loaded without knowingly hitting an error
|
||||
// note: this does not prove that data came back as we expected it,
|
||||
// it could get `state: Done` for no data for example
|
||||
// but it ensures we didn't hit a 401 or 500 or something like that
|
||||
e2e.components.CodeEditor.container().should('be.visible').contains('"state": "Done"');
|
||||
// ensures that panel has loaded without knowingly hitting an error
|
||||
// note: this does not prove that data came back as we expected it,
|
||||
// it could get `state: Done` for no data for example
|
||||
// but it ensures we didn't hit a 401 or 500 or something like that
|
||||
e2e.components.CodeEditor.container().should('be.visible').contains('"state": "Done"');
|
||||
|
||||
// need to close panel
|
||||
e2e.components.Drawer.General.close().click();
|
||||
});
|
||||
};
|
||||
|
||||
19
packages/grafana-e2e/src/flows/importDashboards.ts
Normal file
19
packages/grafana-e2e/src/flows/importDashboards.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { importDashboard, Dashboard } from './importDashboard';
|
||||
import { e2e } from '../index';
|
||||
|
||||
/**
|
||||
* Smoke test several dashboard json files from a test directory
|
||||
* and validate that all the panels in each import finish loading their queries
|
||||
* @param dirPath the relative path to a directory which contains json files representing dashboards,
|
||||
* for example if your dashboards live in `cypress/testDashboards` you can pass `/testDashboards`
|
||||
* @param queryTimeout a number of ms to wait for the imported dashboard to finish loading
|
||||
*/
|
||||
export const importDashboards = async (dirPath: string, queryTimeout?: number) => {
|
||||
e2e()
|
||||
.getJSONFilesFromDir(dirPath)
|
||||
.then((jsonFiles: Dashboard[]) => {
|
||||
jsonFiles.forEach((file) => {
|
||||
importDashboard(file, queryTimeout || 6000);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export * from './revertAllChanges';
|
||||
export * from './saveDashboard';
|
||||
export * from './selectOption';
|
||||
export * from './importDashboard';
|
||||
export * from './importDashboards';
|
||||
|
||||
export {
|
||||
VISUALIZATION_ALERT_LIST,
|
||||
|
||||
@@ -11,10 +11,10 @@ export const setTimeRange = ({ from, to, zone }: TimeRangeConfig) => {
|
||||
e2e.components.TimePicker.openButton().click();
|
||||
|
||||
if (zone) {
|
||||
e2e().contains('button', 'Change time zone').click();
|
||||
e2e().contains('button', 'Change time settings').click();
|
||||
|
||||
selectOption({
|
||||
clickToOpen: false,
|
||||
clickToOpen: true,
|
||||
container: e2e.components.TimeZonePicker.container(),
|
||||
optionText: zone,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -22,9 +22,9 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "8.2.0-pre",
|
||||
"@grafana/e2e-selectors": "8.2.0-pre",
|
||||
"@grafana/ui": "8.2.0-pre",
|
||||
"@grafana/data": "8.2.0-beta.2",
|
||||
"@grafana/e2e-selectors": "8.2.0-beta.2",
|
||||
"@grafana/ui": "8.2.0-beta.2",
|
||||
"history": "4.10.1",
|
||||
"systemjs": "0.20.19",
|
||||
"systemjs-plugin-css": "0.1.37"
|
||||
|
||||
@@ -145,6 +145,7 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
|
||||
return (
|
||||
<div aria-label={selectors.components.DataSourcePicker.container}>
|
||||
<Select
|
||||
inputId="data-source-picker"
|
||||
menuShouldPortal
|
||||
className={styles.select}
|
||||
isMulti={false}
|
||||
|
||||
@@ -60,7 +60,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
theme2: GrafanaTheme2;
|
||||
pluginsToPreload: string[] = [];
|
||||
featureToggles: FeatureToggles = {
|
||||
ngalert: false,
|
||||
accesscontrol: false,
|
||||
trimDefaults: false,
|
||||
tempoServiceGraph: false,
|
||||
@@ -80,7 +79,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
sampleRate: 1,
|
||||
};
|
||||
pluginCatalogURL = 'https://grafana.com/grafana/plugins/';
|
||||
pluginAdminEnabled = false;
|
||||
pluginAdminEnabled = true;
|
||||
pluginAdminExternalManageEnabled = false;
|
||||
expressionsEnabled = false;
|
||||
customTheme?: any;
|
||||
@@ -94,6 +93,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
};
|
||||
geomapDefaultBaseLayerConfig?: MapLayerOptions;
|
||||
geomapDisableCustomBaseLayer?: boolean;
|
||||
unifiedAlertingEnabled = false;
|
||||
applicationInsightsConnectionString?: string;
|
||||
applicationInsightsEndpointUrl?: string;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/schema",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"description": "Grafana Schema Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package schema
|
||||
|
||||
AxisPlacement: "auto" | "top" | "right" | "bottom" | "left" | "hidden" @cuetsy(kind="enum")
|
||||
PointVisibility: "auto" | "never" | "always" @cuetsy(kind="enum")
|
||||
VisibilityMode: "auto" | "never" | "always" @cuetsy(kind="enum")
|
||||
DrawStyle: "line" | "bars" | "points" @cuetsy(kind="enum")
|
||||
LineInterpolation: "linear" | "smooth" | "stepBefore" | "stepAfter" @cuetsy(kind="enum")
|
||||
ScaleDistribution: "linear" | "log" @cuetsy(kind="enum")
|
||||
GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(kind="enum")
|
||||
StackingMode: "none" | "normal" | "percent" @cuetsy(kind="enum")
|
||||
BarValueVisibility: "auto" | "never" | "always" @cuetsy(kind="enum")
|
||||
BarAlignment: -1 | 0 | 1 @cuetsy(kind="enum",memberNames="Before|Center|After")
|
||||
ScaleOrientation: 0 | 1 @cuetsy(kind="enum",memberNames="Horizontal|Vertical")
|
||||
ScaleDirection: 1 | 1 | -1 | -1 @cuetsy(kind="enum",memberNames="Up|Right|Down|Left")
|
||||
@@ -33,7 +32,7 @@ FillConfig: {
|
||||
fillBelowTo?: string
|
||||
} @cuetsy(kind="interface")
|
||||
PointsConfig: {
|
||||
showPoints?: PointVisibility
|
||||
showPoints?: VisibilityMode
|
||||
pointSize?: number
|
||||
pointColor?: string
|
||||
pointSymbol?: string
|
||||
|
||||
@@ -11,9 +11,9 @@ export enum AxisPlacement {
|
||||
Right = 'right',
|
||||
Top = 'top',
|
||||
}
|
||||
export enum PointVisibility {
|
||||
Always = 'always',
|
||||
export enum VisibilityMode {
|
||||
Auto = 'auto',
|
||||
Always = 'always',
|
||||
Never = 'never',
|
||||
}
|
||||
export enum GraphDrawStyle {
|
||||
@@ -45,12 +45,11 @@ export interface LineStyle {
|
||||
dash?: number[];
|
||||
fill?: 'solid' | 'dash' | 'dot' | 'square';
|
||||
}
|
||||
|
||||
export interface PointsConfig {
|
||||
pointColor?: string;
|
||||
pointSize?: number;
|
||||
pointSymbol?: string;
|
||||
showPoints?: PointVisibility;
|
||||
showPoints?: VisibilityMode;
|
||||
}
|
||||
export interface ScaleDistributionConfig {
|
||||
log?: number;
|
||||
@@ -71,15 +70,6 @@ export enum BarAlignment {
|
||||
After = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export enum BarValueVisibility {
|
||||
Auto = 'auto',
|
||||
Never = 'never',
|
||||
Always = 'always',
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
|
||||
42
packages/grafana-schema/src/scuemata/dashboard/dist/family.cue
vendored
Normal file
42
packages/grafana-schema/src/scuemata/dashboard/dist/family.cue
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
package dist
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/packages/grafana-schema/src/scuemata/dashboard"
|
||||
pbarchart "github.com/grafana/grafana/public/app/plugins/panel/barchart:grafanaschema"
|
||||
pbargauge "github.com/grafana/grafana/public/app/plugins/panel/bargauge:grafanaschema"
|
||||
pcanvas "github.com/grafana/grafana/public/app/plugins/panel/canvas:grafanaschema"
|
||||
pdashlist "github.com/grafana/grafana/public/app/plugins/panel/dashlist:grafanaschema"
|
||||
pgauge "github.com/grafana/grafana/public/app/plugins/panel/gauge:grafanaschema"
|
||||
phistogram "github.com/grafana/grafana/public/app/plugins/panel/histogram:grafanaschema"
|
||||
pnews "github.com/grafana/grafana/public/app/plugins/panel/news:grafanaschema"
|
||||
pstat "github.com/grafana/grafana/public/app/plugins/panel/stat:grafanaschema"
|
||||
st "github.com/grafana/grafana/public/app/plugins/panel/state-timeline:grafanaschema"
|
||||
sh "github.com/grafana/grafana/public/app/plugins/panel/status-history:grafanaschema"
|
||||
ptable "github.com/grafana/grafana/public/app/plugins/panel/table:grafanaschema"
|
||||
ptext "github.com/grafana/grafana/public/app/plugins/panel/text:grafanaschema"
|
||||
ptimeseries "github.com/grafana/grafana/public/app/plugins/panel/timeseries:grafanaschema"
|
||||
)
|
||||
|
||||
// Family composes the base dashboard scuemata family with all Grafana core plugins -
|
||||
// the plugins that are dist[ributed] with Grafana. The resulting composed scuemata is
|
||||
// exactly equivalent to what's produced by the DistDashboardFamily() Go function.
|
||||
//
|
||||
// CUE programs should default to importing this dist variant over the base variant.
|
||||
Family: dashboard.Family & {
|
||||
compose: Panel: {
|
||||
// TODO do this with a loop once we include the panel type/plugin id in the model
|
||||
barchart: pbarchart.Panel
|
||||
bargauge: pbargauge.Panel
|
||||
canvas: pcanvas.Panel
|
||||
dashlist: pdashlist.Panel
|
||||
gauge: pgauge.Panel
|
||||
histogram: phistogram.Panel
|
||||
news: pnews.Panel
|
||||
stat: pstat.Panel
|
||||
"state-timeline": st.Panel
|
||||
"status-history": sh.Panel
|
||||
text: ptext.Panel
|
||||
table: ptable.Panel
|
||||
timeseries: ptimeseries.Panel
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -28,10 +28,10 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.13.14",
|
||||
"@babel/preset-env": "7.13.12",
|
||||
"@grafana/data": "8.2.0-pre",
|
||||
"@grafana/data": "8.2.0-beta.2",
|
||||
"@grafana/eslint-config": "2.5.0",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@grafana/ui": "8.2.0-pre",
|
||||
"@grafana/ui": "8.2.0-beta.2",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/expect-puppeteer": "3.3.1",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/ui",
|
||||
"version": "8.2.0-pre",
|
||||
"version": "8.2.0-beta.2",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -33,9 +33,9 @@
|
||||
"@emotion/css": "11.1.3",
|
||||
"@emotion/react": "11.1.5",
|
||||
"@grafana/aws-sdk": "0.0.3",
|
||||
"@grafana/data": "8.2.0-pre",
|
||||
"@grafana/e2e-selectors": "8.2.0-pre",
|
||||
"@grafana/schema": "8.2.0-pre",
|
||||
"@grafana/data": "8.2.0-beta.2",
|
||||
"@grafana/e2e-selectors": "8.2.0-beta.2",
|
||||
"@grafana/schema": "8.2.0-beta.2",
|
||||
"@grafana/slate-react": "0.22.10-grafana",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@monaco-editor/react": "4.2.2",
|
||||
@@ -73,7 +73,7 @@
|
||||
"react-transition-group": "4.4.1",
|
||||
"slate": "0.47.8",
|
||||
"tinycolor2": "1.4.1",
|
||||
"uplot": "1.6.15"
|
||||
"uplot": "1.6.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "16.0.0",
|
||||
@@ -83,10 +83,10 @@
|
||||
"@storybook/addon-essentials": "6.3.7",
|
||||
"@storybook/addon-knobs": "6.3.0",
|
||||
"@storybook/addon-storysource": "6.3.7",
|
||||
"@storybook/react": "6.3.7",
|
||||
"@storybook/theming": "6.3.7",
|
||||
"@storybook/builder-webpack5": "6.3.7",
|
||||
"@storybook/manager-webpack5": "6.3.7",
|
||||
"@storybook/react": "6.3.7",
|
||||
"@storybook/theming": "6.3.7",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@types/classnames": "2.2.7",
|
||||
"@types/common-tags": "^1.8.0",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DisplayValue,
|
||||
VizOrientation,
|
||||
ThresholdsMode,
|
||||
FALLBACK_COLOR,
|
||||
Field,
|
||||
FieldType,
|
||||
getDisplayProcessor,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
import {
|
||||
BarGauge,
|
||||
Props,
|
||||
getCellColor,
|
||||
getValueColor,
|
||||
getBasicAndGradientStyles,
|
||||
getBarGradient,
|
||||
@@ -74,6 +76,69 @@ function getValue(value: number, title?: string): DisplayValue {
|
||||
}
|
||||
|
||||
describe('BarGauge', () => {
|
||||
describe('getCellColor', () => {
|
||||
it('returns a fallback if the positionValue is null', () => {
|
||||
const props = getProps();
|
||||
expect(getCellColor(null, props.value, props.display)).toEqual({
|
||||
background: FALLBACK_COLOR,
|
||||
border: FALLBACK_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show as lit if the value is null (somehow)', () => {
|
||||
const props = getProps();
|
||||
expect(getCellColor(1, (null as unknown) as DisplayValue, props.display)).toEqual(
|
||||
expect.objectContaining({
|
||||
isLit: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not show as lit if the numeric value is NaN', () => {
|
||||
const props = getProps();
|
||||
expect(
|
||||
getCellColor(
|
||||
1,
|
||||
{
|
||||
numeric: NaN,
|
||||
text: '0',
|
||||
},
|
||||
props.display
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
isLit: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not show as lit if the positionValue is greater than the numeric value', () => {
|
||||
const props = getProps();
|
||||
expect(getCellColor(75, props.value, props.display)).toEqual(
|
||||
expect.objectContaining({
|
||||
isLit: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('shows as lit otherwise', () => {
|
||||
const props = getProps();
|
||||
expect(getCellColor(1, props.value, props.display)).toEqual(
|
||||
expect.objectContaining({
|
||||
isLit: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a fallback if there is no display processor', () => {
|
||||
const props = getProps();
|
||||
expect(getCellColor(null, props.value, undefined)).toEqual({
|
||||
background: FALLBACK_COLOR,
|
||||
border: FALLBACK_COLOR,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get value color', () => {
|
||||
it('should get the threshold color if value is same as a threshold', () => {
|
||||
const props = getProps();
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
DisplayValue,
|
||||
formattedValueToString,
|
||||
FormattedValue,
|
||||
GAUGE_DEFAULT_MAXIMUM,
|
||||
GAUGE_DEFAULT_MINIMUM,
|
||||
DisplayValueAlignmentFactors,
|
||||
ThresholdsMode,
|
||||
DisplayProcessor,
|
||||
@@ -122,43 +124,8 @@ export class BarGauge extends PureComponent<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
getCellColor(positionValue: TimeSeriesValue): CellColors {
|
||||
const { value, display } = this.props;
|
||||
if (positionValue === null) {
|
||||
return {
|
||||
background: FALLBACK_COLOR,
|
||||
border: FALLBACK_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
const color = display ? display(positionValue).color : null;
|
||||
|
||||
if (color) {
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
||||
return {
|
||||
background: tinycolor(color).setAlpha(0.18).toRgbString(),
|
||||
border: 'transparent',
|
||||
isLit: false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
background: tinycolor(color).setAlpha(0.95).toRgbString(),
|
||||
backgroundShade: tinycolor(color).setAlpha(0.55).toRgbString(),
|
||||
border: tinycolor(color).setAlpha(0.9).toRgbString(),
|
||||
isLit: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
background: FALLBACK_COLOR,
|
||||
border: FALLBACK_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
renderRetroBars(): ReactNode {
|
||||
const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props;
|
||||
const { display, field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props;
|
||||
const {
|
||||
valueHeight,
|
||||
valueWidth,
|
||||
@@ -167,8 +134,8 @@ export class BarGauge extends PureComponent<Props> {
|
||||
wrapperWidth,
|
||||
wrapperHeight,
|
||||
} = calculateBarAndValueDimensions(this.props);
|
||||
const minValue = field.min!;
|
||||
const maxValue = field.max!;
|
||||
const minValue = field.min ?? GAUGE_DEFAULT_MINIMUM;
|
||||
const maxValue = field.max ?? GAUGE_DEFAULT_MAXIMUM;
|
||||
|
||||
const isVert = isVertical(orientation);
|
||||
const valueRange = maxValue - minValue;
|
||||
@@ -200,7 +167,7 @@ export class BarGauge extends PureComponent<Props> {
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const currentValue = minValue + (valueRange / cellCount) * i;
|
||||
const cellColor = this.getCellColor(currentValue);
|
||||
const cellColor = getCellColor(currentValue, value, display);
|
||||
const cellStyles: CSSProperties = {
|
||||
borderRadius: '2px',
|
||||
};
|
||||
@@ -425,6 +392,44 @@ export function calculateBarAndValueDimensions(props: Props): BarAndValueDimensi
|
||||
};
|
||||
}
|
||||
|
||||
export function getCellColor(
|
||||
positionValue: TimeSeriesValue,
|
||||
value: Props['value'],
|
||||
display: Props['display']
|
||||
): CellColors {
|
||||
if (positionValue === null) {
|
||||
return {
|
||||
background: FALLBACK_COLOR,
|
||||
border: FALLBACK_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
const color = display ? display(positionValue).color : null;
|
||||
|
||||
if (color) {
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || isNaN(value.numeric) || (positionValue !== null && positionValue > value.numeric)) {
|
||||
return {
|
||||
background: tinycolor(color).setAlpha(0.18).toRgbString(),
|
||||
border: 'transparent',
|
||||
isLit: false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
background: tinycolor(color).setAlpha(0.95).toRgbString(),
|
||||
backgroundShade: tinycolor(color).setAlpha(0.55).toRgbString(),
|
||||
border: tinycolor(color).setAlpha(0.9).toRgbString(),
|
||||
isLit: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
background: FALLBACK_COLOR,
|
||||
border: FALLBACK_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
export function getValuePercent(value: number, minValue: number, maxValue: number): number {
|
||||
return Math.min((value - minValue) / (maxValue - minValue), 1);
|
||||
}
|
||||
@@ -436,7 +441,9 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
|
||||
const { displayMode, field, value, alignmentFactors, orientation, theme, text } = props;
|
||||
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
|
||||
|
||||
const valuePercent = getValuePercent(value.numeric, field.min!, field.max!);
|
||||
const minValue = field.min ?? GAUGE_DEFAULT_MINIMUM;
|
||||
const maxValue = field.max ?? GAUGE_DEFAULT_MAXIMUM;
|
||||
const valuePercent = getValuePercent(value.numeric, minValue, maxValue);
|
||||
const valueColor = getValueColor(props);
|
||||
|
||||
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
|
||||
|
||||
@@ -69,9 +69,7 @@ export const Card: CardInterface = ({ heading, description, disabled, href, onCl
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.info}>
|
||||
<div>
|
||||
<div className={styles.heading} role="heading">
|
||||
{heading}
|
||||
</div>
|
||||
<h2 className={styles.heading}>{heading}</h2>
|
||||
{meta}
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
</div>
|
||||
@@ -115,6 +113,7 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
font-size: ${theme.typography.size.md};
|
||||
letter-spacing: inherit;
|
||||
line-height: ${theme.typography.body.lineHeight};
|
||||
color: ${theme.colors.text.primary};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
import { IconName } from '../../types/icon';
|
||||
@@ -51,10 +51,16 @@ export const ConfirmModal = ({
|
||||
}: ConfirmModalProps): JSX.Element => {
|
||||
const [disabled, setDisabled] = useState(Boolean(confirmationText));
|
||||
const styles = useStyles2(getStyles);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const onConfirmationTextChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
setDisabled(confirmationText?.localeCompare(event.currentTarget.value) !== 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// for some reason autoFocus property did no work on this button, but this does
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
|
||||
<div className={styles.modalText}>
|
||||
@@ -76,7 +82,7 @@ export const ConfirmModal = ({
|
||||
variant="destructive"
|
||||
onClick={onConfirm}
|
||||
disabled={disabled}
|
||||
autoFocus
|
||||
ref={buttonRef}
|
||||
aria-label={selectors.pages.ConfirmModal.delete}
|
||||
>
|
||||
{confirmText}
|
||||
|
||||
@@ -24,15 +24,18 @@ const menuItems = [
|
||||
items: [
|
||||
{ label: 'First', ariaLabel: 'First' },
|
||||
{ label: 'Second', ariaLabel: 'Second' },
|
||||
{ label: 'Third', ariaLabel: 'Third' },
|
||||
{ label: 'Fourth', ariaLabel: 'Fourth' },
|
||||
{ label: 'Fifth', ariaLabel: 'Fifth' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const renderMenuItems = () => {
|
||||
return menuItems?.map((group, index) => (
|
||||
<MenuGroup key={`${group.label}${index}`} label={group.label} ariaLabel={group.label}>
|
||||
{(group.items || []).map((item) => (
|
||||
<MenuItem key={item.label} label={item.label} ariaLabel={item.label} />
|
||||
return menuItems.map((group, index) => (
|
||||
<MenuGroup key={`${group.label}${index}`} label={group.label}>
|
||||
{group.items.map((item) => (
|
||||
<MenuItem key={item.label} label={item.label} />
|
||||
))}
|
||||
</MenuGroup>
|
||||
));
|
||||
|
||||
@@ -41,12 +41,20 @@ export const ContextMenu: React.FC<ContextMenuProps> = React.memo(
|
||||
}, [x, y]);
|
||||
|
||||
useClickAway(menuRef, () => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
onClose?.();
|
||||
});
|
||||
const header = renderHeader && renderHeader();
|
||||
const menuItems = renderMenuItems && renderMenuItems();
|
||||
const header = renderHeader?.();
|
||||
const menuItems = renderMenuItems?.();
|
||||
const onOpen = (setFocusedItem: (a: number) => void) => {
|
||||
setFocusedItem(0);
|
||||
};
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
@@ -55,7 +63,9 @@ export const ContextMenu: React.FC<ContextMenuProps> = React.memo(
|
||||
ref={menuRef}
|
||||
style={positionStyles}
|
||||
ariaLabel={selectors.components.Menu.MenuComponent('Context')}
|
||||
onOpen={onOpen}
|
||||
onClick={onClose}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{menuItems}
|
||||
</Menu>
|
||||
|
||||
@@ -23,13 +23,12 @@ export const DataLinksContextMenu: React.FC<DataLinksContextMenuProps> = ({ chil
|
||||
const itemsGroup: MenuItemsGroup[] = [{ items: linkModelToContextMenuItems(links), label: 'Data links' }];
|
||||
const renderMenuGroupItems = () => {
|
||||
return itemsGroup.map((group, index) => (
|
||||
<MenuGroup key={`${group.label}${index}`} label={group.label} ariaLabel={group.label}>
|
||||
<MenuGroup key={`${group.label}${index}`} label={group.label}>
|
||||
{(group.items || []).map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
url={item.url}
|
||||
label={item.label}
|
||||
ariaLabel={item.label}
|
||||
target={item.target}
|
||||
icon={item.icon}
|
||||
active={item.active}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStyles2 } from '../../../themes';
|
||||
import { Button } from '../../Button';
|
||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
import { TimeRangeList } from '../TimeRangePicker/TimeRangeList';
|
||||
import { quickOptions } from '../rangeOptions';
|
||||
import { quickOptions } from '../options';
|
||||
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
|
||||
import { TimePickerTitle } from '../TimeRangePicker/TimePickerTitle';
|
||||
import {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Icon } from '../Icon/Icon';
|
||||
import { getInputStyles } from '../Input/Input';
|
||||
import { TimePickerButtonLabel } from './TimeRangePicker';
|
||||
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
||||
import { otherOptions, quickOptions } from './rangeOptions';
|
||||
import { quickOptions } from './options';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
@@ -100,7 +100,6 @@ export const TimeRangeInput: FC<TimeRangeInputProps> = ({
|
||||
timeZone={timeZone}
|
||||
value={isValidTimeRange(value) ? (value as TimeRange) : getDefaultTimeRange()}
|
||||
onChange={onRangeChange}
|
||||
otherOptions={otherOptions}
|
||||
quickOptions={quickOptions}
|
||||
onChangeTimeZone={onChangeTimeZone}
|
||||
className={styles.content}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
dateMath,
|
||||
} from '@grafana/data';
|
||||
import { Themeable } from '../../types';
|
||||
import { otherOptions, quickOptions } from './rangeOptions';
|
||||
import { quickOptions } from './options';
|
||||
import { ButtonGroup, ToolbarButton } from '../Button';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
@@ -32,10 +32,12 @@ export interface TimeRangePickerProps extends Themeable {
|
||||
hideText?: boolean;
|
||||
value: TimeRange;
|
||||
timeZone?: TimeZone;
|
||||
fiscalYearStartMonth?: number;
|
||||
timeSyncButton?: JSX.Element;
|
||||
isSynced?: boolean;
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||
onChangeFiscalYearStartMonth?: (month: number) => void;
|
||||
onMoveBackward: () => void;
|
||||
onMoveForward: () => void;
|
||||
onZoom: () => void;
|
||||
@@ -75,11 +77,13 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
|
||||
onMoveForward,
|
||||
onZoom,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
timeSyncButton,
|
||||
isSynced,
|
||||
theme,
|
||||
history,
|
||||
onChangeTimeZone,
|
||||
onChangeFiscalYearStartMonth,
|
||||
hideQuickRanges,
|
||||
} = this.props;
|
||||
|
||||
@@ -117,13 +121,14 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
|
||||
<ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}>
|
||||
<TimePickerContent
|
||||
timeZone={timeZone}
|
||||
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
otherOptions={otherOptions}
|
||||
quickOptions={quickOptions}
|
||||
history={history}
|
||||
showHistory
|
||||
onChangeTimeZone={onChangeTimeZone}
|
||||
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||
hideQuickRanges={hideQuickRanges}
|
||||
/>
|
||||
</ClickOutsideWrapper>
|
||||
|
||||
@@ -38,14 +38,12 @@ describe('TimePickerContent', () => {
|
||||
|
||||
it('renders with relative picker', () => {
|
||||
renderComponent({ value: absoluteValue });
|
||||
expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without relative picker', () => {
|
||||
renderComponent({ value: absoluteValue, hideQuickRanges: true });
|
||||
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Last 5 minutes/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with timezone picker', () => {
|
||||
@@ -86,14 +84,12 @@ describe('TimePickerContent', () => {
|
||||
|
||||
it('renders with relative picker', () => {
|
||||
renderComponent({ value: absoluteValue, isFullscreen: false });
|
||||
expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without relative picker', () => {
|
||||
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
|
||||
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Last 5 minutes/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with absolute picker when absolute value and quick ranges are visible', () => {
|
||||
@@ -139,6 +135,10 @@ function renderComponent({
|
||||
<TimePickerContentWithScreenSize
|
||||
onChangeTimeZone={noop}
|
||||
onChange={noop}
|
||||
quickOptions={[
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
|
||||
]}
|
||||
timeZone="utc"
|
||||
value={value}
|
||||
isFullscreen={isFullscreen}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TimeRangeList } from './TimeRangeList';
|
||||
import { TimePickerFooter } from './TimePickerFooter';
|
||||
import { getFocusStyles } from '../../../themes/mixins';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { FilterInput } from '../..';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRanges, isContainerTall) => {
|
||||
return {
|
||||
@@ -46,11 +47,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, isReversed, hideQuickRang
|
||||
rightSide: css`
|
||||
width: 40% !important;
|
||||
border-right: ${isReversed ? `1px solid ${theme.colors.border.weak}` : 'none'};
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media only screen and (max-width: ${theme.breakpoints.values.lg}px) {
|
||||
width: 100% !important;
|
||||
}
|
||||
`,
|
||||
timeRangeFilter: css`
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
spacing: css`
|
||||
margin-top: 16px;
|
||||
`,
|
||||
@@ -127,9 +132,10 @@ interface Props {
|
||||
value: TimeRange;
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||
onChangeFiscalYearStartMonth?: (month: number) => void;
|
||||
timeZone?: TimeZone;
|
||||
fiscalYearStartMonth?: number;
|
||||
quickOptions?: TimeOption[];
|
||||
otherOptions?: TimeOption[];
|
||||
history?: TimeRange[];
|
||||
showHistory?: boolean;
|
||||
className?: string;
|
||||
@@ -150,11 +156,11 @@ interface FormProps extends Omit<Props, 'history'> {
|
||||
export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (props) => {
|
||||
const {
|
||||
quickOptions = [],
|
||||
otherOptions = [],
|
||||
isReversed,
|
||||
isFullscreen,
|
||||
hideQuickRanges,
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
value,
|
||||
onChange,
|
||||
history,
|
||||
@@ -162,6 +168,7 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
||||
className,
|
||||
hideTimeZone,
|
||||
onChangeTimeZone,
|
||||
onChangeFiscalYearStartMonth,
|
||||
} = props;
|
||||
const isHistoryEmpty = !history?.length;
|
||||
const isContainerTall =
|
||||
@@ -169,7 +176,10 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, isReversed, hideQuickRanges, isContainerTall);
|
||||
const historyOptions = mapToHistoryOptions(history, timeZone);
|
||||
const timeOption = useTimeOption(value.raw, otherOptions, quickOptions);
|
||||
const timeOption = useTimeOption(value.raw, quickOptions);
|
||||
const [searchTerm, setSearchQuery] = useState('');
|
||||
|
||||
const filteredQuickOptions = quickOptions.filter((o) => o.display.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
const onChangeTimeOption = (timeOption: TimeOption) => {
|
||||
return onChange(mapOptionToTimeRange(timeOption));
|
||||
@@ -179,26 +189,23 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
||||
<div id="TimePickerContent" className={cx(styles.container, className)}>
|
||||
<div className={styles.body}>
|
||||
{(!isFullscreen || !hideQuickRanges) && (
|
||||
<CustomScrollbar className={styles.rightSide}>
|
||||
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
|
||||
{!hideQuickRanges && (
|
||||
<>
|
||||
<TimeRangeList
|
||||
title="Relative time ranges"
|
||||
options={quickOptions}
|
||||
onChange={onChangeTimeOption}
|
||||
value={timeOption}
|
||||
/>
|
||||
<div className={styles.spacing} />
|
||||
<TimeRangeList
|
||||
title="Other quick ranges"
|
||||
options={otherOptions}
|
||||
onChange={onChangeTimeOption}
|
||||
value={timeOption}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CustomScrollbar>
|
||||
<div className={styles.rightSide}>
|
||||
<div className={styles.timeRangeFilter}>
|
||||
<FilterInput
|
||||
width={0}
|
||||
autoFocus={true}
|
||||
value={searchTerm}
|
||||
onChange={setSearchQuery}
|
||||
placeholder={'Search quick ranges'}
|
||||
/>
|
||||
</div>
|
||||
<CustomScrollbar>
|
||||
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
|
||||
{!hideQuickRanges && (
|
||||
<TimeRangeList options={filteredQuickOptions} onChange={onChangeTimeOption} value={timeOption} />
|
||||
)}
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
)}
|
||||
{isFullscreen && (
|
||||
<div className={styles.leftSide}>
|
||||
@@ -206,7 +213,14 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (p
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!hideTimeZone && isFullscreen && <TimePickerFooter timeZone={timeZone} onChangeTimeZone={onChangeTimeZone} />}
|
||||
{!hideTimeZone && isFullscreen && (
|
||||
<TimePickerFooter
|
||||
timeZone={timeZone}
|
||||
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||
onChangeTimeZone={onChangeTimeZone}
|
||||
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -268,7 +282,7 @@ const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
||||
};
|
||||
|
||||
const FullScreenForm: React.FC<FormProps> = (props) => {
|
||||
const { onChange } = props;
|
||||
const { onChange, value, timeZone, fiscalYearStartMonth, isReversed, historyOptions } = props;
|
||||
const theme = useTheme2();
|
||||
const styles = getFullScreenStyles(theme, props.hideQuickRanges);
|
||||
const onChangeTimeOption = (timeOption: TimeOption) => {
|
||||
@@ -282,18 +296,19 @@ const FullScreenForm: React.FC<FormProps> = (props) => {
|
||||
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
||||
</div>
|
||||
<TimeRangeForm
|
||||
value={props.value}
|
||||
timeZone={props.timeZone}
|
||||
onApply={props.onChange}
|
||||
value={value}
|
||||
timeZone={timeZone}
|
||||
fiscalYearStartMonth={fiscalYearStartMonth}
|
||||
onApply={onChange}
|
||||
isFullscreen={true}
|
||||
isReversed={props.isReversed}
|
||||
isReversed={isReversed}
|
||||
/>
|
||||
</div>
|
||||
{props.showHistory && (
|
||||
<div className={styles.recent}>
|
||||
<TimeRangeList
|
||||
title="Recently used absolute ranges"
|
||||
options={props.historyOptions || []}
|
||||
options={historyOptions || []}
|
||||
onChange={onChangeTimeOption}
|
||||
placeholderEmpty={<EmptyRecentList />}
|
||||
/>
|
||||
@@ -338,23 +353,13 @@ function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOpt
|
||||
|
||||
EmptyRecentList.displayName = 'EmptyRecentList';
|
||||
|
||||
const useTimeOption = (
|
||||
raw: RawTimeRange,
|
||||
quickOptions: TimeOption[],
|
||||
otherOptions: TimeOption[]
|
||||
): TimeOption | undefined => {
|
||||
const useTimeOption = (raw: RawTimeRange, quickOptions: TimeOption[]): TimeOption | undefined => {
|
||||
return useMemo(() => {
|
||||
if (!rangeUtil.isRelativeTimeRange(raw)) {
|
||||
return;
|
||||
}
|
||||
const quickOption = quickOptions.find((option) => {
|
||||
return quickOptions.find((option) => {
|
||||
return option.from === raw.from && option.to === raw.to;
|
||||
});
|
||||
if (quickOption) {
|
||||
return quickOption;
|
||||
}
|
||||
return otherOptions.find((option) => {
|
||||
return option.from === raw.from && option.to === raw.to;
|
||||
});
|
||||
}, [raw, otherOptions, quickOptions]);
|
||||
}, [raw, quickOptions]);
|
||||
};
|
||||
|
||||
@@ -9,18 +9,29 @@ import { Button } from '../../Button';
|
||||
import { TimeZonePicker } from '../TimeZonePicker';
|
||||
import { isString } from 'lodash';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Field, RadioButtonGroup, Select } from '../..';
|
||||
import { monthOptions } from '../options';
|
||||
|
||||
interface Props {
|
||||
timeZone?: TimeZone;
|
||||
fiscalYearStartMonth?: number;
|
||||
timestamp?: number;
|
||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||
onChangeFiscalYearStartMonth?: (month: number) => void;
|
||||
}
|
||||
|
||||
export const TimePickerFooter: FC<Props> = (props) => {
|
||||
const { timeZone, timestamp = Date.now(), onChangeTimeZone } = props;
|
||||
const {
|
||||
timeZone,
|
||||
fiscalYearStartMonth,
|
||||
timestamp = Date.now(),
|
||||
onChangeTimeZone,
|
||||
onChangeFiscalYearStartMonth,
|
||||
} = props;
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
const [editMode, setEditMode] = useState('tz');
|
||||
|
||||
const onToggleChangeTz = useCallback(
|
||||
const onToggleChangeTimeSettings = useCallback(
|
||||
(event?: React.MouseEvent) => {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
@@ -43,42 +54,72 @@ export const TimePickerFooter: FC<Props> = (props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className={cx(style.container, style.editContainer)}>
|
||||
<section aria-label={selectors.components.TimeZonePicker.container} className={style.timeZoneContainer}>
|
||||
<TimeZonePicker
|
||||
includeInternal={true}
|
||||
onChange={(timeZone) => {
|
||||
onToggleChangeTz();
|
||||
|
||||
if (isString(timeZone)) {
|
||||
onChangeTimeZone(timeZone);
|
||||
}
|
||||
}}
|
||||
autoFocus={true}
|
||||
onBlur={onToggleChangeTz}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-label="Time zone selection" className={style.container}>
|
||||
<div className={style.timeZoneContainer}>
|
||||
<div className={style.timeZone}>
|
||||
<TimeZoneTitle title={info.name} />
|
||||
<div className={style.spacer} />
|
||||
<TimeZoneDescription info={info} />
|
||||
<div>
|
||||
<section aria-label="Time zone selection" className={style.container}>
|
||||
<div className={style.timeZoneContainer}>
|
||||
<div className={style.timeZone}>
|
||||
<TimeZoneTitle title={info.name} />
|
||||
<div className={style.spacer} />
|
||||
<TimeZoneDescription info={info} />
|
||||
</div>
|
||||
<TimeZoneOffset timeZone={timeZone} timestamp={timestamp} />
|
||||
</div>
|
||||
<TimeZoneOffset timeZone={timeZone} timestamp={timestamp} />
|
||||
</div>
|
||||
<div className={style.spacer} />
|
||||
<Button variant="secondary" onClick={onToggleChangeTz} size="sm">
|
||||
Change time zone
|
||||
</Button>
|
||||
</section>
|
||||
<div className={style.spacer} />
|
||||
<Button variant="secondary" onClick={onToggleChangeTimeSettings} size="sm">
|
||||
Change time settings
|
||||
</Button>
|
||||
</section>
|
||||
{isEditing ? (
|
||||
<div className={style.editContainer}>
|
||||
<div>
|
||||
<RadioButtonGroup
|
||||
value={editMode}
|
||||
options={[
|
||||
{ label: 'Time Zone', value: 'tz' },
|
||||
{ label: 'Fiscal year', value: 'fy' },
|
||||
]}
|
||||
onChange={setEditMode}
|
||||
></RadioButtonGroup>
|
||||
</div>
|
||||
{editMode === 'tz' ? (
|
||||
<section
|
||||
aria-label={selectors.components.TimeZonePicker.container}
|
||||
className={cx(style.timeZoneContainer, style.timeSettingContainer)}
|
||||
>
|
||||
<TimeZonePicker
|
||||
includeInternal={true}
|
||||
onChange={(timeZone) => {
|
||||
onToggleChangeTimeSettings();
|
||||
|
||||
if (isString(timeZone)) {
|
||||
onChangeTimeZone(timeZone);
|
||||
}
|
||||
}}
|
||||
onBlur={onToggleChangeTimeSettings}
|
||||
/>
|
||||
</section>
|
||||
) : (
|
||||
<section
|
||||
aria-label={selectors.components.TimeZonePicker.container}
|
||||
className={cx(style.timeZoneContainer, style.timeSettingContainer)}
|
||||
>
|
||||
<Field className={style.fiscalYearField} label={'Fiscal year start month'}>
|
||||
<Select
|
||||
value={fiscalYearStartMonth}
|
||||
options={monthOptions}
|
||||
onChange={(value) => {
|
||||
if (onChangeFiscalYearStartMonth) {
|
||||
onChangeFiscalYearStartMonth(value.value ?? 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -93,11 +134,21 @@ const getStyle = stylesFactory((theme: GrafanaTheme2) => {
|
||||
align-items: center;
|
||||
`,
|
||||
editContainer: css`
|
||||
border-top: 1px solid ${theme.colors.border.weak};
|
||||
padding: 11px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 7px;
|
||||
`,
|
||||
spacer: css`
|
||||
margin-left: 7px;
|
||||
`,
|
||||
timeSettingContainer: css`
|
||||
padding-top: ${theme.spacing(1)};
|
||||
`,
|
||||
fiscalYearField: css`
|
||||
margin-bottom: 0px;
|
||||
`,
|
||||
timeZoneContainer: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
dateMath,
|
||||
DateTime,
|
||||
dateTimeFormat,
|
||||
dateTimeParse,
|
||||
GrafanaTheme2,
|
||||
isDateTime,
|
||||
rangeUtil,
|
||||
RawTimeRange,
|
||||
@@ -11,6 +13,8 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { Icon, Tooltip } from '../..';
|
||||
import { useStyles2 } from '../../..';
|
||||
import { Button } from '../../Button';
|
||||
import { Field } from '../../Forms/Field';
|
||||
import { Input } from '../../Input/Input';
|
||||
@@ -21,6 +25,7 @@ interface Props {
|
||||
value: TimeRange;
|
||||
onApply: (range: TimeRange) => void;
|
||||
timeZone?: TimeZone;
|
||||
fiscalYearStartMonth?: number;
|
||||
roundup?: boolean;
|
||||
isReversed?: boolean;
|
||||
}
|
||||
@@ -37,8 +42,9 @@ const ERROR_MESSAGES = {
|
||||
};
|
||||
|
||||
export const TimeRangeForm: React.FC<Props> = (props) => {
|
||||
const { value, isFullscreen = false, timeZone, onApply: onApplyFromProps, isReversed } = props;
|
||||
const { value, isFullscreen = false, timeZone, onApply: onApplyFromProps, isReversed, fiscalYearStartMonth } = props;
|
||||
const [fromValue, toValue] = valueToState(value.raw.from, value.raw.to, timeZone);
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
const [from, setFrom] = useState<InputState>(fromValue);
|
||||
const [to, setTo] = useState<InputState>(toValue);
|
||||
@@ -77,11 +83,11 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
const raw: RawTimeRange = { from: from.value, to: to.value };
|
||||
const timeRange = rangeUtil.convertRawToRange(raw, timeZone);
|
||||
const timeRange = rangeUtil.convertRawToRange(raw, timeZone, fiscalYearStartMonth);
|
||||
|
||||
onApplyFromProps(timeRange);
|
||||
},
|
||||
[from.invalid, from.value, onApplyFromProps, timeZone, to.invalid, to.value]
|
||||
[from.invalid, from.value, onApplyFromProps, timeZone, to.invalid, to.value, fiscalYearStartMonth]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
@@ -93,29 +99,47 @@ export const TimeRangeForm: React.FC<Props> = (props) => {
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
const fiscalYear = rangeUtil.convertRawToRange({ from: 'now/fy', to: 'now/fy' }, timeZone, fiscalYearStartMonth);
|
||||
|
||||
const fyTooltip = (
|
||||
<div className={style.tooltip}>
|
||||
{rangeUtil.isFiscal(value) ? (
|
||||
<Tooltip content={`Fiscal year: ${fiscalYear.from.format('MMM-DD')} - ${fiscalYear.to.format('MMM-DD')}`}>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const icon = isFullscreen ? null : <Button icon="calendar-alt" variant="secondary" onClick={onOpen} />;
|
||||
|
||||
return (
|
||||
<div aria-label="Absolute time ranges">
|
||||
<Field label="From" invalid={from.invalid} error={from.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={(event) => onChange(event.currentTarget.value, to.value)}
|
||||
addonAfter={icon}
|
||||
aria-label={selectors.components.TimePicker.fromField}
|
||||
value={from.value}
|
||||
/>
|
||||
<div className={style.fieldContainer}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={(event) => onChange(event.currentTarget.value, to.value)}
|
||||
addonAfter={icon}
|
||||
aria-label={selectors.components.TimePicker.fromField}
|
||||
value={from.value}
|
||||
/>
|
||||
{fyTooltip}
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="To" invalid={to.invalid} error={to.errorMessage}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={(event) => onChange(from.value, event.currentTarget.value)}
|
||||
addonAfter={icon}
|
||||
aria-label={selectors.components.TimePicker.toField}
|
||||
value={to.value}
|
||||
/>
|
||||
<div className={style.fieldContainer}>
|
||||
<Input
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onFocus={onFocus}
|
||||
onChange={(event) => onChange(from.value, event.currentTarget.value)}
|
||||
addonAfter={icon}
|
||||
aria-label={selectors.components.TimePicker.toField}
|
||||
value={to.value}
|
||||
/>
|
||||
{fyTooltip}
|
||||
</div>
|
||||
</Field>
|
||||
<Button data-testid={selectors.components.TimePicker.applyTimeRange} onClick={onApply}>
|
||||
Apply time range
|
||||
@@ -185,3 +209,15 @@ function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone): boolean
|
||||
const parsed = dateTimeParse(value, { roundUp, timeZone });
|
||||
return parsed.isValid();
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
fieldContainer: css`
|
||||
display: flex;
|
||||
`,
|
||||
tooltip: css`
|
||||
padding-left: ${theme.spacing(1)};
|
||||
padding-top: ${theme.spacing(0.5)};
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const getOptionsStyles = stylesFactory(() => {
|
||||
});
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
title?: string;
|
||||
options: TimeOption[];
|
||||
value?: TimeOption;
|
||||
onChange: (option: TimeOption) => void;
|
||||
@@ -69,7 +69,7 @@ const Options: React.FC<Props> = ({ options, value, onChange, title }) => {
|
||||
value={option}
|
||||
selected={isEqual(option, value)}
|
||||
onSelect={onChange}
|
||||
name={title}
|
||||
name={title ?? 'Time ranges'}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -43,7 +43,6 @@ export const TimeZonePicker: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Select
|
||||
menuShouldPortal
|
||||
value={selected}
|
||||
placeholder="Type to search (country, city, abbreviation)"
|
||||
autoFocus={autoFocus}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TimeOption } from '@grafana/data';
|
||||
import { SelectableValue, TimeOption } from '@grafana/data';
|
||||
|
||||
export const quickOptions: TimeOption[] = [
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
|
||||
@@ -17,15 +17,14 @@ export const quickOptions: TimeOption[] = [
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year' },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years' },
|
||||
];
|
||||
|
||||
export const otherOptions: TimeOption[] = [
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday' },
|
||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday' },
|
||||
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week' },
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' },
|
||||
{ from: 'now-1Q/fQ', to: 'now-1Q/fQ', display: 'Previous fiscal quarter' },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' },
|
||||
{ from: 'now-1y/fy', to: 'now-1y/fy', display: 'Previous fiscal year' },
|
||||
{ from: 'now/d', to: 'now/d', display: 'Today' },
|
||||
{ from: 'now/d', to: 'now', display: 'Today so far' },
|
||||
{ from: 'now/w', to: 'now/w', display: 'This week' },
|
||||
@@ -34,4 +33,23 @@ export const otherOptions: TimeOption[] = [
|
||||
{ from: 'now/M', to: 'now', display: 'This month so far' },
|
||||
{ from: 'now/y', to: 'now/y', display: 'This year' },
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far' },
|
||||
{ from: 'now/fQ', to: 'now', display: 'This fiscal quarter so far' },
|
||||
{ from: 'now/fQ', to: 'now/fQ', display: 'This fiscal quarter' },
|
||||
{ from: 'now/fy', to: 'now', display: 'This fiscal year so far' },
|
||||
{ from: 'now/fy', to: 'now/fy', display: 'This fiscal year' },
|
||||
];
|
||||
|
||||
export const monthOptions: Array<SelectableValue<number>> = [
|
||||
{ label: 'January', value: 0 },
|
||||
{ label: 'February', value: 1 },
|
||||
{ label: 'March', value: 2 },
|
||||
{ label: 'April', value: 3 },
|
||||
{ label: 'May', value: 4 },
|
||||
{ label: 'June', value: 5 },
|
||||
{ label: 'July', value: 6 },
|
||||
{ label: 'August', value: 7 },
|
||||
{ label: 'September', value: 8 },
|
||||
{ label: 'October', value: 9 },
|
||||
{ label: 'November', value: 10 },
|
||||
{ label: 'December', value: 11 },
|
||||
];
|
||||
@@ -62,7 +62,6 @@ const ButtonSelectComponent = <T,>(props: Props<T>) => {
|
||||
<MenuItem
|
||||
key={`${item.value}`}
|
||||
label={(item.label || item.value) as string}
|
||||
ariaLabel={(item.label || item.value) as string}
|
||||
onClick={() => onChangeInternal(item)}
|
||||
active={item.value === value?.value}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC } from 'react';
|
||||
import { escapeStringForRegex, unEscapeStringFromRegex } from '@grafana/data';
|
||||
import { Input, Icon, Button } from '@grafana/ui';
|
||||
import { Button, Icon, Input } from '..';
|
||||
import { useFocus } from '../Input/utils';
|
||||
|
||||
export interface Props {
|
||||
value: string | undefined;
|
||||
@@ -12,9 +13,19 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const FilterInput: FC<Props> = ({ value, placeholder, width, onChange, onKeyDown, autoFocus }) => {
|
||||
const [inputRef, setInputFocus] = useFocus();
|
||||
const suffix =
|
||||
value !== '' ? (
|
||||
<Button icon="times" fill="text" size="sm" onClick={() => onChange('')}>
|
||||
<Button
|
||||
icon="times"
|
||||
fill="text"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
setInputFocus();
|
||||
onChange('');
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
) : null;
|
||||
@@ -23,6 +34,7 @@ export const FilterInput: FC<Props> = ({ value, placeholder, width, onChange, on
|
||||
<Input
|
||||
autoFocus={autoFocus ?? false}
|
||||
prefix={<Icon name="search" />}
|
||||
ref={inputRef}
|
||||
suffix={suffix}
|
||||
width={width}
|
||||
type="text"
|
||||
@@ -16,6 +16,7 @@ export interface Props extends Omit<FieldProps, 'css' | 'horizontal' | 'descript
|
||||
grow?: boolean;
|
||||
/** Make field's background transparent */
|
||||
transparent?: boolean;
|
||||
htmlFor?: string;
|
||||
}
|
||||
|
||||
export const InlineField: FC<Props> = ({
|
||||
@@ -27,13 +28,14 @@ export const InlineField: FC<Props> = ({
|
||||
loading,
|
||||
disabled,
|
||||
className,
|
||||
htmlFor,
|
||||
grow,
|
||||
transparent,
|
||||
...htmlProps
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, grow);
|
||||
const inputId = getChildId(children);
|
||||
const inputId = htmlFor ?? getChildId(children);
|
||||
|
||||
const labelElement =
|
||||
typeof label === 'string' ? (
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
formattedValueToString,
|
||||
FieldConfig,
|
||||
ThresholdsMode,
|
||||
GAUGE_DEFAULT_MAXIMUM,
|
||||
GAUGE_DEFAULT_MINIMUM,
|
||||
getActiveThreshold,
|
||||
Threshold,
|
||||
getColorForTheme,
|
||||
@@ -58,15 +60,15 @@ export class Gauge extends PureComponent<Props> {
|
||||
const { field, theme, value } = this.props;
|
||||
|
||||
if (field.color?.mode !== FieldColorModeId.Thresholds) {
|
||||
return [{ value: field.min ?? 0, color: value.color ?? FALLBACK_COLOR }];
|
||||
return [{ value: field.min ?? GAUGE_DEFAULT_MINIMUM, color: value.color ?? FALLBACK_COLOR }];
|
||||
}
|
||||
|
||||
const thresholds = field.thresholds ?? Gauge.defaultProps.field?.thresholds!;
|
||||
const isPercent = thresholds.mode === ThresholdsMode.Percentage;
|
||||
const steps = thresholds.steps;
|
||||
|
||||
let min = field.min ?? 0;
|
||||
let max = field.max ?? 100;
|
||||
let min = field.min ?? GAUGE_DEFAULT_MINIMUM;
|
||||
let max = field.max ?? GAUGE_DEFAULT_MAXIMUM;
|
||||
|
||||
if (isPercent) {
|
||||
min = 0;
|
||||
@@ -116,8 +118,8 @@ export class Gauge extends PureComponent<Props> {
|
||||
const fontSize = this.props.text?.valueSize ?? calculateFontSize(text, valueWidth, dimension, 1, gaugeWidth * 1.7);
|
||||
const thresholdLabelFontSize = Math.max(fontSize / 2.5, 12);
|
||||
|
||||
let min = field.min ?? 0;
|
||||
let max = field.max ?? 100;
|
||||
let min = field.min ?? GAUGE_DEFAULT_MINIMUM;
|
||||
let max = field.max ?? GAUGE_DEFAULT_MAXIMUM;
|
||||
let numeric = value.numeric;
|
||||
|
||||
if (field.thresholds?.mode === ThresholdsMode.Percentage) {
|
||||
|
||||
@@ -79,13 +79,12 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
||||
};
|
||||
const renderMenuGroupItems = () => {
|
||||
return itemsToRender?.map((group, index) => (
|
||||
<MenuGroup key={`${group.label}${index}`} label={group.label} ariaLabel={group.label}>
|
||||
<MenuGroup key={`${group.label}${index}`} label={group.label}>
|
||||
{(group.items || []).map((item) => (
|
||||
<MenuItem
|
||||
key={`${item.label}`}
|
||||
url={item.url}
|
||||
label={item.label}
|
||||
ariaLabel={item.label}
|
||||
target={item.target}
|
||||
icon={item.icon}
|
||||
active={item.active}
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface GraphNGState {
|
||||
}
|
||||
|
||||
/**
|
||||
* "Time as X" core component, expectes ascending x
|
||||
* "Time as X" core component, expects ascending x
|
||||
*/
|
||||
export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
static contextType = PanelContextRoot;
|
||||
@@ -115,7 +115,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
state = {
|
||||
alignedFrame,
|
||||
alignedData: config!.prepData!(alignedFrame),
|
||||
alignedData: config!.prepData!([alignedFrame]) as AlignedData,
|
||||
config,
|
||||
};
|
||||
|
||||
@@ -138,7 +138,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
const u = this.plotInstance.current;
|
||||
if (u) {
|
||||
// Try finding left position on time axis
|
||||
const left = u.valToPos(evt.payload.point.time, 'time');
|
||||
const left = u.valToPos(evt.payload.point.time, 'x');
|
||||
let top;
|
||||
if (left) {
|
||||
// find midpoint between points at current idx
|
||||
@@ -195,7 +195,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
if (shouldReconfig) {
|
||||
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
|
||||
newState.alignedData = newState.config.prepData!(newState.alignedFrame);
|
||||
newState.alignedData = newState.config.prepData!([newState.alignedFrame]) as AlignedData;
|
||||
pluginLog('GraphNG', false, 'config recreated', newState.config);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user