Compare commits

...

161 Commits

Author SHA1 Message Date
Grot (@grafanabot)
8a2c78d3f8 "Release: Updated versions in package to 7.4.5" (#32089) 2021-03-18 10:23:36 +01:00
Grot (@grafanabot)
010f20c1c8 Elasticsearch: Fix query initialization logic & query transformation from Promethous/Loki (#31322) (#31441)
* Elasticsearch: Fix query initialization logic

* Only import prometheus & loki queries as log queries

(cherry picked from commit 4429f2cf58)

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
2021-02-24 11:29:10 +01:00
Grot (@grafanabot)
0587281d99 Streaming: Fixes an issue with time series panel and streaming data source when scrolling back from being out of view (#31431) (#31442)
* Streaming: Fixes an issue with time series panel and streaming data source when scrolling back from being out of view

* Slight tweak

(cherry picked from commit 59c060f1f1)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-24 11:13:40 +01:00
Grot (@grafanabot)
beff0fbda8 test: allow check for Table as well as Graph for Explore e2e flow (#31290) (#31349)
* use table for explore flow screenshot

* reverse logic

* update matchExploreTable type

(cherry picked from commit 7f8fb2b55f)

Co-authored-by: Vicky Lee <36230812+vickyyyyyyy@users.noreply.github.com>
2021-02-22 17:23:45 +00:00
Grot (@grafanabot)
d0f940c268 QueryEditor: handle query.hide changes in angular based query-editors (#31336) (#31385)
* handle query.hide changes in angular query-editors

* better comment

(cherry picked from commit 7ccba047c6)

Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>
2021-02-22 16:48:35 +01:00
Arve Knudsen
59b730c372 Chore: Remove gotest.tools dependency (#31391) (#31395)
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
(cherry picked from commit 6e549bc95d)
2021-02-22 14:20:28 +01:00
Grot (@grafanabot)
d4090a1d12 AdHocVariables: Fixes crash when values are stored as numbers (#31382) (#31386)
(cherry picked from commit 6f3088ae85)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-22 10:54:24 +01:00
Grot (@grafanabot)
e106dfdaad DatasourceSrv: Fix instance retrieval when datasource variable value set to "default" (#31347) (#31348)
* Failing tests

* Fixed

(cherry picked from commit 0d6e5298b7)

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
2021-02-19 12:59:46 +01:00
Grot (@grafanabot)
0423d54afd DashboardLinks: Fixes another issue where dashboard links cause full page reload (#31334) (#31343)
(cherry picked from commit 2b7628c69e)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-19 12:11:19 +01:00
Grot (@grafanabot)
3457c0aa76 SAML: single logout only enabled in enterprise (#31325) (#31344)
(cherry picked from commit fe74c51d68)

Co-authored-by: Leonard Gram <leo@xlson.com>
2021-02-19 10:19:22 +01:00
Grot (@grafanabot)
a41238959c LibraryPanels: Syncs panel title with name (#31311) (#31342)
(cherry picked from commit 0a4c3b8779)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-19 06:35:17 +01:00
Grot (@grafanabot)
111f5234a2 Table: Fixes issue with fixed min and auto max with bar gauge cell (#31316) (#31329)
(cherry picked from commit e9576853c1)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-18 17:26:45 +01:00
Grot (@grafanabot)
29e75ad97b Make Datetime local (No date if today) working (#31274) (#31275)
(cherry picked from commit 0253685304)

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
2021-02-17 12:39:25 +01:00
Grot (@grafanabot)
0cc1a4369a "Release: Updated versions in package to 7.4.2" (#31272) 2021-02-17 12:38:23 +01:00
Jack Westbrook
ef677f34b1 [v7.4.x] Chore: grafana-toolkit uses grafana-ui and grafana-data workspaces (#31269)
* Chore: grafana-toolkit uses grafana-ui and grafana-data workspaces (#30701)

* chore(grafana-toolkit): use workspace versions of grafana/ui and grafana/data

* chore: replace references to popperjs 1 typings with popperjs 2 typings

(cherry picked from commit 4b25310941)

* revert(grafana-toolkit): put back 7.4 version of eslint-config (2.1.0)
2021-02-17 11:05:27 +01:00
Grot (@grafanabot)
064546f382 Snapshots: Disallow anonymous user to create snapshots (#31263) (#31266)
(cherry picked from commit 8f20b13f1c)

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
2021-02-17 10:12:44 +01:00
Grot (@grafanabot)
b85da0cc0a only update usagestats every 30min (#31131) (#31262)
Signed-off-by: bergquist <carl.bergquist@gmail.com>
(cherry picked from commit b5cbbc3db1)

Co-authored-by: Carl Bergquist <carl.bergquist@gmail.com>
2021-02-17 09:33:25 +01:00
Grot (@grafanabot)
8b4c370752 Prometheus: Fix enabling of disabled queries when editing in dashboard (#31055) (#31248)
* Fix disable bug by passing hide prop

* Make more universal fix

* Revert to original fix

* Update public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
(cherry picked from commit 4f61edd28d)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-02-16 16:55:02 +01:00
Grot (@grafanabot)
44008445b8 CloudWatch: Ensure empty query row errors are not passed to the panel (#31172) (#31245)
* ensure empty errors are not passed to the panel

* make it a oneliner

(cherry picked from commit 92ae019f8e)

Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
2021-02-16 16:23:17 +01:00
Grot (@grafanabot)
65d8420007 StatPanels: Fixes to palette color scheme is not cleared when loading panel (#31126) (#31246)
(cherry picked from commit b3c32277dd)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-16 16:09:07 +01:00
Grot (@grafanabot)
0e60372c09 QueryEditors: Fixes issue that happens after moving queries then editing would update other queries (#31193) (#31244)
(cherry picked from commit 0c3c17592e)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-16 16:05:02 +01:00
Grot (@grafanabot)
dafb37e819 LibraryPanels: Disconnect before connect during dashboard save (#31235) (#31238)
* LibraryPanels: Disconnect before connect during dashboard save

* Tests: fixed test

* Chore: updates after PR comments

* Chore: changes from context.Background() to c.Context.Req.Context()

* Chore: fixes lint issue

(cherry picked from commit 06e6bcf091)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-16 13:27:19 +01:00
Grot (@grafanabot)
170b0f329c SqlDataSources: Fixes the Show Generated SQL button in query editors (#31236) (#31239)
* SqlDataSources: Fixes the Show Generated SQL button in query editors

* No need to protect against duplicate events now that the event emitter is isolated for each editor

(cherry picked from commit e4672906f0)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-16 13:12:06 +01:00
Grot (@grafanabot)
1c4712aeeb Variables: Adds back default option for data source variable (#31208) (#31232)
* Variables: Adds back default option for data source variable

* Chore: updates after PR comments

(cherry picked from commit f993f2c7cc)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-16 07:51:01 +01:00
Grot (@grafanabot)
c4774ec6ae IPv6: Support host address configured with enclosing square brackets (#31226) (#31228)
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
(cherry picked from commit d27a72f859)

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
2021-02-15 18:21:47 +01:00
Grot (@grafanabot)
dda38c55e5 Postgres: Fix timeGroup macro converts long intervals to invalid numbers when TimescaleDB is enabled (#31179) (#31224)
Fixes #27253

(cherry picked from commit d56c5285e2)

Co-authored-by: Ricky Putra <kurokochin@users.noreply.github.com>
2021-02-15 17:51:39 +01:00
Grot (@grafanabot)
d087a69e2e Remove last synchronisation field from LDAP debug view (#30984) (#31221)
* Remove last synchronisation field from LDAP debug view

* Apply review comments

(cherry picked from commit f9a293afea)

Co-authored-by: Tania B <yalyna.ts@gmail.com>
2021-02-15 17:58:18 +02:00
Torkel Ödegaard
1adce1fcfc [v7.4.x]: Sync drone config from master to stable release branch (#31213) 2021-02-15 12:26:43 +01:00
Grot (@grafanabot)
dbad6a9182 DataSourceSrv: Filter out non queryable data sources by default (#31144) (#31214)
(cherry picked from commit 50faeb3078)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-15 12:06:27 +01:00
Grot (@grafanabot)
3efcb19cb2 Alerting: Fix modal text for deleting obsolete notifier (#31171) (#31209)
(cherry picked from commit dde11215e9)

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
2021-02-15 11:01:51 +02:00
Grot (@grafanabot)
6e68a7ac59 Variables: Fixes missing empty elements from regex filters (#31156) (#31201)
* Variables: Fixes missing empty elements from regex filters

* Chore: cleanup comment

* Chore: removes unused import

(cherry picked from commit 39993a6884)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-15 06:24:57 +01:00
Grot (@grafanabot)
b0f6cf8669 DashboardLinks: Fixes links always cause full page reload (#31178) (#31181)
(cherry picked from commit 9629dded42)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-12 18:14:45 +01:00
Grot (@grafanabot)
e42d6931c4 DashboardListPanel: Fixes issue with folder picker always showing All and using old form styles (#31160) (#31162)
(cherry picked from commit a17661d198)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-12 16:36:35 +01:00
Grot (@grafanabot)
c8a7044504 Permissions: Fix team and role permissions on folders/dashboards not displayed for non Grafana Admin users (#31132) (#31176)
* Cfg: fix hidden users initialization

* add tests

* do not call isHiddenUser function for non-user permission

* do not call isHiddenUser function for non-user permission

(cherry picked from commit 7f1f559929)

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>
2021-02-12 16:32:35 +01:00
Grot (@grafanabot)
5f20a59b85 Prometheus: Multiply exemplars timestamp to follow api change (#31143) (#31170)
(cherry picked from commit 8c35ed4014)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2021-02-12 15:16:18 +01:00
Grot (@grafanabot)
07649d1313 "Release: Updated versions in package to 7.4.1" (#31128) 2021-02-11 12:08:13 +00:00
Grot (@grafanabot)
994326759e Transforms: Fixes Outer join issue with duplicate field names not getting the same unique field names as before (#31121) (#31127)
* Transformations: Fixed duplicate name issue in outer join transform

* Think this is working

* Updated tests

* Updated snapshot

* Fix broken tests (#31123)

* Fix broken tests

* Fix remaining faling tests

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
(cherry picked from commit 699724581d)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-11 11:58:55 +00:00
Grot (@grafanabot)
2c3eb7ddae MuxWriter: Handle error for already closed file (#31119) (#31120)
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
(cherry picked from commit 7394c98d38)

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
2021-02-11 11:06:32 +01:00
Grot (@grafanabot)
b9d92efdff Logging: sourcemap transform asset urls from CDN in logged stacktraces (#31115) (#31117)
(cherry picked from commit 5c9a10d423)

Co-authored-by: Domas <domasx2@gmail.com>
2021-02-11 11:20:11 +02:00
Grot (@grafanabot)
ba9ab09ad6 Exemplars: Change CTA style (#30880) (#31105)
* Exemplars: Change CTA style

* Address review feedbacks

* Fix table column aligning

* Minor alignments + uncontrolled component warning fix

(cherry picked from commit cc463f30a4)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2021-02-10 18:46:58 +01:00
Grot (@grafanabot)
14a89707e6 test: add support for timeout to be passed in for addDatasource (#30736) (#31090)
* add support for timeout to be passed in for addDatasource

* fix merge update

(cherry picked from commit 25bcbb7d8e)

Co-authored-by: Vicky Lee <36230812+vickyyyyyyy@users.noreply.github.com>
2021-02-10 15:51:21 +00:00
Andrej Ocenas
fada9fcbff Influx: Make max series limit configurable and show the limiting message if applied (#31025) (#31100)
* Add configuration in ConfigEditor and default to 1000

* Show data in explore if any even if there is an error

* Update pkg/tsdb/influxdb/flux/executor.go

* Better handling of defaults

* Add test for runQuery to show data even with error

* Update public/app/store/configureStore.ts

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>

* Update public/app/plugins/datasource/influxdb/components/ConfigEditor.tsx

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>

* Update tooltip

* Update input

* Lint fixes

* Update snapshots

* Update decorator tests

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
(cherry picked from commit e0448513eb)
2021-02-10 16:01:21 +01:00
Grot (@grafanabot)
762c694092 Elasticsearch: fix log row context erroring out (#31088) (#31094)
(cherry picked from commit c7e007d199)

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
2021-02-10 13:57:00 +00:00
Grot (@grafanabot)
02cd1faf88 test: update addDashboard flow for v7.4.0 changes (#31059) (#31084)
* update addDashboard flow for v7.4.0 changes

* remove hide flow and if around set time range

(cherry picked from commit cff6f5fec3)

Co-authored-by: Vicky Lee <36230812+vickyyyyyyy@users.noreply.github.com>
2021-02-10 12:11:07 +01:00
Grot (@grafanabot)
368bd2d7bb Usage stats: Adds source/distributor setting (#31039) (#31076)
Signed-off-by: bergquist <carl.bergquist@gmail.com>
(cherry picked from commit d1b9fddb4f)

Co-authored-by: Carl Bergquist <carl.bergquist@gmail.com>
2021-02-10 11:08:14 +01:00
Grot (@grafanabot)
97f8bc7250 DashboardLinks: Fixes crash when link has no title (#31008) (#31050)
* DashboardLinks: Fixes crash when link misses title

* Chore: updates after PR comments

(cherry picked from commit 297ff9a168)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-10 06:08:11 +01:00
Grot (@grafanabot)
73f933b691 Make value mappings correctly interpret numeric-like strings (#30893) (#30912)
* Make value mappings corectly interprete numeric-like strings

* More tests

* Update packages/grafana-data/src/utils/valueMappings.ts

* Update packages/grafana-data/src/utils/valueMappings.ts

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>

* Fix issue detected by singlestat test

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
(cherry picked from commit f47b72304c)

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
2021-02-09 18:03:44 +01:00
Grot (@grafanabot)
7924e6fec6 Elasticsearch: Fix alias field value not being shown in query editor (#30992) (#31037)
* Make imports relative

* Fix Alias field value not being shown

(cherry picked from commit b46235715d)

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
2021-02-09 17:56:04 +01:00
Grot (@grafanabot)
cefbec44fb BarGauge: Improvements to value sizing and table inner width calculations (#30990) (#31032)
* BarGauge: Increase min value width and fix height when setting manual text size

* updated snapshot

* Big improvement to bar gauge value sizing and fixing table gauge sizing

* removed unused const

* added a unit test

(cherry picked from commit 39d7ebc7d1)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-09 17:51:50 +01:00
Grot (@grafanabot)
a200db0d05 convert path to posix by default (#31045) (#31053)
(cherry picked from commit da3f963987)

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
2021-02-09 16:28:02 +01:00
Grot (@grafanabot)
989f6933cd Alerting: Fixes so notification channels are properly deleted (#31040) (#31046)
(cherry picked from commit f43d834a59)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-09 15:50:13 +01:00
Arve Knudsen
4d74e49285 Drone: Fix deployment image (#31027) (#31029)
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
(cherry picked from commit d963c6d868)
2021-02-09 14:05:10 +01:00
Grot (@grafanabot)
b98b3cf260 Graph: Fixes so graph is shown for non numeric time values (#30972) (#31014)
* Graph: Fixes so graph is shown for non numeric time values

* Tests: changes times to UTC

* Tests: forgot one value in time array

* GraphNG: make time series panel work with string time stamps (#30981)

* Make Time series panel work with data that time is represented as string

* Map to for

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
(cherry picked from commit bd28512d29)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-09 10:05:11 +01:00
Grot (@grafanabot)
9a4fe5d17f instrumentation: make the first database histogram bucket smaller (#30995) (#31001)
Signed-off-by: bergquist <carl.bergquist@gmail.com>
(cherry picked from commit 49e394e167)

Co-authored-by: Carl Bergquist <carl.bergquist@gmail.com>
2021-02-09 09:54:13 +01:00
Grot (@grafanabot)
840b55926b Build: Releases e2e and e2e-selectors too (#31006) (#31007)
(cherry picked from commit 410ab72bc4)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-09 07:53:13 +01:00
Grot (@grafanabot)
7e7f90ed6c TextPanel: Fixes so panel title is updated when variables change (#30884) (#31005)
* TextPanel: Fixes so panel title is updated when variables change

* Tests: fixes tests

* Chore: updates after PR comments

(cherry picked from commit f42bb84cbf)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-09 06:23:22 +01:00
Grot (@grafanabot)
68597e8cfb StatPanel: Fixes issue formatting date values using unit option (#30979) (#30991)
(cherry picked from commit 907d3a2aac)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-08 17:24:47 +01:00
Grot (@grafanabot)
a4f8c56a0a Units: Fixes formatting of duration units (#30982) (#30986)
(cherry picked from commit 3a6af0a639)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-08 13:23:33 +01:00
Grot (@grafanabot)
e7ea30c8fd Elasticsearch: Show Size setting for raw_data metric (#30980) (#30983)
(cherry picked from commit 0a7c6c689f)

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
2021-02-08 11:13:12 +00:00
Grot (@grafanabot)
b4181a077f Logging: sourcemap support for frontend stacktraces (#30590) (#30976)
(cherry picked from commit 21817055bd)

Co-authored-by: Domas <domasx2@gmail.com>
2021-02-08 12:09:34 +02:00
Grot (@grafanabot)
bc1f03403b e2e: extends selector factory to plugins (#30932) (#30934)
(cherry picked from commit 32dde95a7b)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-05 14:23:48 +01:00
Grot (@grafanabot)
8b79f0d7c5 Variables: Adds queryparam formatting option (#30858) (#30924)
* Variables: Adds queryparam formatting option

* Chore: fixes strict errors

* Chore: changes after PR comments

(cherry picked from commit 2a3aa95163)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-05 08:04:45 +01:00
Grot (@grafanabot)
8c52f69395 Exemplars: change api to reflect latest changes (#30910) (#30915)
(cherry picked from commit 48334ab863)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2021-02-04 20:21:26 +01:00
Grot (@grafanabot)
c2203b9859 "Release: Updated versions in package to 7.4.0" (#30898) 2021-02-04 11:11:00 +00:00
Grot (@grafanabot)
089d84cf5c DataSourceSettings: Adds info box and link to Grafana Cloud (#30891) (#30896)
* Updated

* DataSourceSettings: Adds info box and link to Grafana Cloud

* Updated wording

* Less words

* use variables

* Fixing ts issues

* fixed import

(cherry picked from commit 56ef7c4a4c)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-04 11:58:02 +01:00
Grot (@grafanabot)
0c3bee2530 GrafanaUI: Add a way to persistently close InfoBox (#30716) (#30895)
* GrafanaUI: Add a way to persistently close InfoBox

InfoBox and FeatureInfoBox can take up a lot of screen realestate. This makes it easy to let the user close the boxes.

* Migrate InfoBox story to controls

(cherry picked from commit 99acad4448)

Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
2021-02-04 11:31:31 +01:00
Peter Holmberg
403b7c6177 [7.4.x] AlertingNG: List saved Alert definitions in Alert Rule list (30890)(30603) 2021-02-04 10:34:52 +01:00
Grot (@grafanabot)
3cfa2c12db Alerting: Fixes alert panel header icon not showing (#30840) (#30885)
* Alerting: Fixes alert panel header icon not showing

* Remove as any

(cherry picked from commit 01b10ab436)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-04 09:18:19 +01:00
Leonard Gram
fdf52dd45c Plugins: Requests validator (#30445) (#30877)
* Introduce PluginRequestValidator abstraction with a NoOp implementation

* Update PluginRequestValidator abstraction to use the dsURL instead

* Inject PluginRequestValidator into the HTTPServer and validate requests going through data source proxy

* Inject PluginRequestValidator into the BackendPluginManager and validate requests going through it

* Validate requests going through QueryMetrics & QueryMetricsV2

* Validate BackendPluginManager health requests

* Fix backend plugins manager tests

* Validate requests going through alerting service

* Fix tests

* fix tests

* goimports

Co-authored-by: Leonard Gram <leo@xlson.com>
(cherry picked from commit 6415d2802e)

Co-authored-by: Joan López de la Franca Beltran <joanjan14@gmail.com>
2021-02-04 08:05:18 +01:00
Grot (@grafanabot)
87cb290cbd PanelLibrary: Adds library panel meta information to dashboard json (#30770) (#30883)
(cherry picked from commit 179f35a537)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-04 06:53:07 +01:00
Grot (@grafanabot)
39aae712cf bump grabpl version to 0.5.36 (#30874) (#30878)
* bump grabpl version to 0.5.36

* 2 missing shell script

(cherry picked from commit c24a4c1803)

Co-authored-by: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com>
2021-02-03 23:20:57 +01:00
Grot (@grafanabot)
dd957acc5b Chore: remove __debug_bin (#30725) (#30857)
(cherry picked from commit e0356f7b13)

Co-authored-by: 大可 <hnlq.sysu@gmail.com>
2021-02-03 18:49:54 +02:00
Grot (@grafanabot)
b6e65e8b3c Grafana-ui: fixes closing modals with escape key (#30745) (#30873)
* feat(grafana-ui): add an escape key listener to Modal

* Update packages/grafana-ui/src/components/Modal/Modal.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* feat(grafana-ui): add closeOnEscape prop to control Modal behaviour

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
(cherry picked from commit 3066b38c5e)

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
2021-02-03 17:23:05 +01:00
Grot (@grafanabot)
5fa729804a DashboardLinks: Support variable expression in to tooltip - Issue #30409 (#30569) (#30852)
* add compile variable in tooltip link

* test(link_srv): introduce getAnchorInfo test

* test(link_srv): introduce tests for getLinkUrl

* test(link_srv): refer to anchorInfo.url rather than hardcode expected

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
(cherry picked from commit 141202f518)

Co-authored-by: Ha. Huynh Sam <huynhha12798@gmail.com>
2021-02-03 17:22:00 +01:00
Grot (@grafanabot)
299759c1ba Add alt text to plugin logos (#30710) (#30872)
(cherry picked from commit 3390c6a852)

Co-authored-by: Besart Berisha <besart.ber@gmail.com>
2021-02-03 17:17:06 +01:00
Grot (@grafanabot)
0cf5d1c561 InfluxDB: Add http configuration when selecting InfluxDB v2 flavor (#30827) (#30870)
* Add dev env block for influx2

* Add http settings to influx config

* Update devenv/docker/blocks/influxdb2/docker-compose.yaml

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
(cherry picked from commit 7db00ed6a0)

Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
2021-02-03 17:15:54 +01:00
Grot (@grafanabot)
6a74859ca1 Prometheus: Set type of labels to string (#30831) (#30835)
(cherry picked from commit 0427df8f60)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-02-03 17:11:10 +01:00
Grot (@grafanabot)
d2dc8416f6 AlertingNG: change API permissions (#30781) (#30814)
(cherry picked from commit 5d029abc42)

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
2021-02-03 17:35:44 +02:00
Grot (@grafanabot)
bf9940b3b2 Grafana-ui: fixes no data message in Table component (#30821) (#30855)
* Wip

* fix(grafana-ui): add no data message to Table component

(cherry picked from commit 4f684cc498)

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
2021-02-03 10:29:27 +01:00
Grot (@grafanabot)
6c3abc6d01 Prometheus: Add tooltip to explain possibility to use patterns in text and title fields in annotations (#30825) (#30843)
* Add tip for using pattern

* Update public/app/plugins/datasource/prometheus/partials/annotations.editor.html

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
(cherry picked from commit 25ef563a53)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-02-03 09:33:22 +01:00
Grot (@grafanabot)
45bad20884 Chore: add more docs annotations (#30847) (#30851)
(cherry picked from commit 3a343aa547)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-02-02 22:45:53 -08:00
Grot (@grafanabot)
a612211ce3 BarChart: inside-align strokes, upgrade uPlot to 1.6.4. (#30806) (#30846)
(cherry picked from commit b5400922e2)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2021-02-02 15:09:25 -06:00
Grot (@grafanabot)
1108da9574 Transforms: allow boolean in field calculations (#30802) (#30845)
(cherry picked from commit c9c7bfbcaa)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-02-02 12:51:28 -08:00
Grot (@grafanabot)
880c78f1d7 CDN: Fixes cdn path when Grafana is under sub path (#30822) (#30823)
(cherry picked from commit 64254eaa82)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-02 14:03:17 +01:00
Grot (@grafanabot)
22171be811 bump cypress to 6.3.0 (#30644) (#30819)
* bump cypress to 6.3.0

* fix inspect-drawer spec

* fix panelEdit_base spec

* fix select-focus spec

* Apply suggestions from code review

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>

* add be.visible assertion to new-query-variable spec to avoid flakiness

* increase waits in new-query-variable spec to avoid flakiness

* increase waits in new-query-variable spec by another 500

* remove be.visible assertion added before

* Chore: trying to fix flakiness

* skip the new-variable-query e2e test

* Chore: refactor so we might avoid flakiness

* Revert "skip the new-variable-query e2e test"

This reverts commit 203c1875c2.

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
(cherry picked from commit b8dd50c297)

Co-authored-by: Vicky Lee <36230812+vickyyyyyyy@users.noreply.github.com>
2021-02-02 11:56:39 +01:00
Grot (@grafanabot)
5428200593 Expressions: Measure total transformation requests and elapsed time (#30514) (#30789)
* Measure transformation number and elapsed time

* Change histogram to summary

* Apply suggestions from code review

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
(cherry picked from commit 2f394a219b)

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
2021-02-02 12:49:00 +02:00
Grot (@grafanabot)
a39c19dfa5 Grafana-UI: Add story/docs for ErrorBoundary (#30304) (#30811)
(cherry picked from commit eb83135ba9)

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
2021-02-02 12:05:00 +02:00
Torkel Ödegaard
9dc3574cc1 [v7.4.x]: Menu: Mark menu components as internal (#30801) 2021-02-02 10:19:59 +01:00
Grot (@grafanabot)
0f6d0f40ff Graph: Fixes auto decimals issue in legend and tooltip (#30628) (#30635)
* Graph: Fixes auto decimals issue in legend and tooltip

* Updated decimals

(cherry picked from commit f109f06485)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-02 09:54:17 +01:00
Grot (@grafanabot)
e489013ae1 GraphNG: Disable Plot logging by default (#30390) (#30500)
* Disable Plot loggging by default

* Fix

(cherry picked from commit ffa68f6f91)

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
2021-02-02 09:54:03 +01:00
Grot (@grafanabot)
3969177094 Storybook: Migrate card story to use controls (#30535) (#30549)
(cherry picked from commit 9c20698dfe)

Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
2021-02-02 09:53:55 +01:00
Grot (@grafanabot)
f2d07b6b8e GraphNG: add bar alignment option (#30499) (#30790)
* GraphNG: add bar alignment option

* Fix builders

(cherry picked from commit 820866e425)

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
2021-02-02 09:53:30 +01:00
Grot (@grafanabot)
2b3f48a44c Variables: Clears drop down state when leaving dashboard (#30810) (#30812)
(cherry picked from commit 6994f19d1f)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-02 09:37:55 +01:00
Grot (@grafanabot)
8b1c6ed1b3 Add missing callback dependency (#30797) (#30809)
(cherry picked from commit 64a1003a28)

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
2021-02-02 10:05:53 +02:00
Grot (@grafanabot)
6c9e8549ca GraphNG: improve behavior when switching between solid/dash/dots (#30796) (#30799)
(cherry picked from commit fcac59107c)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-02-01 10:07:17 -08:00
Grot (@grafanabot)
4fc57f13ab Add width for Variable Editors (#30791) (#30795)
(cherry picked from commit 76f77f86c5)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-02-01 17:12:08 +01:00
Grot (@grafanabot)
d1c95055ca Panels: Fixes so panels are refreshed when scrolling past them fast (#30784) (#30792)
(cherry picked from commit 08eee87148)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-02-01 15:25:25 +01:00
Grot (@grafanabot)
264f790728 PanelEdit: Trigger refresh when changing data source (#30744) (#30767)
(cherry picked from commit a8a3e02699)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-01 12:18:17 +01:00
Grot (@grafanabot)
60e7db2346 AlertingNG: Enable UI to Save Alert Definitions (#30394) (#30548)
* transform state to what the api expects

* add expr prop to dataquery

* Add evalutate field

* add refid picker to options

* minor fix to enable save

* fix  import

* more fixes after merge

* use default datasource if not changed

* replace name with title

* Change name in ui as well

* remove not used loadDataSources function

* prettier fixes

* look up datasource

* correct datasource per query model

* revert dataquery change, use expressionid const

* fix for type

* fix faulty const

* description readonly

(cherry picked from commit 529f564bd4)

Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
2021-02-01 11:49:52 +01:00
Grot (@grafanabot)
7374a963be CDN: Fix passing correct prefix to GetContentDeliveryURL (#30777) (#30779)
(cherry picked from commit 561a0a2995)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-01 11:42:45 +01:00
Grot (@grafanabot)
64c13cd46b CDN: Adds support for serving assets over a CDN (#30691) (#30776)
* CDN: Initial poc support for serving assets over a CDN

* Minor fix

* added build path and test

* fix lint error

* Added edition to cdn path

* Move master builds to a separate path

* Added error handling for the url parsing, changed setting name, and added docs

* Updated sample.ini

* Some property renames

* updated

* Minor update to html

* index template improvements

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Added ContentDeliveryPrefix to Licence service

* updated docs

* Updated test mock

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
(cherry picked from commit c04bc805b5)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-02-01 11:00:46 +01:00
Grot (@grafanabot)
5bb9731a84 Explore: Update styling of buttons (#30493) (#30508)
* Switch deprecared toggle group for radio buttons

* Create transparent version of field, label and witch

* Replace divs wiith components

* Move styling from scss to js

* Update buttons

* Remove log generating file

* Update level button

(cherry picked from commit 56fb04e94c)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-02-01 10:56:17 +01:00
Grot (@grafanabot)
836a07a670 Loki: Append refId to logs uid (#30418) (#30537)
* Append refId to Loki uid, add tests

* Fix linting

* Fix linting

* Hopefully finally fix linting errors

(cherry picked from commit 6692e1c332)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-02-01 10:28:42 +01:00
Grot (@grafanabot)
b78166f8df skip symlinks to directories when generating plugin manifest (#30721) (#30738)
(cherry picked from commit 06061c8741)

Co-authored-by: Dan Cech <dcech@grafana.com>
2021-01-29 13:43:57 -08:00
Grot (@grafanabot)
563e98d334 Mobile: Fixes issue scrolling on mobile in chrome (#30746) (#30750)
(cherry picked from commit 5e37361182)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-01-29 13:43:11 -08:00
Grot (@grafanabot)
12a9097742 BarChart: add alpha bar chart panel (#30323) (#30754)
(cherry picked from commit 26b168f7eb)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-01-29 13:42:20 -08:00
Grot (@grafanabot)
021ef24ea9 Datasource: Use json-iterator configuration compatible with standard library (#30732) (#30739)
This will make sure that any map keys in JSON is ordered in /api/ds/query response.

(cherry picked from commit f62eb28f3e)

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
2021-01-29 16:17:03 +01:00
Grot (@grafanabot)
1e8019aff5 Variables: Fixes so text format will show All instead of custom all (#30730) (#30731)
(cherry picked from commit 8744ad361b)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-29 09:45:55 +01:00
Grot (@grafanabot)
cd4524aba0 AlertingNG: pause/unpause definitions via the API (#30627) (#30672)
* AlertingNG: pause/unpause definitions via the API

* Apply suggestions from code review

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>

* Enable pausing/unpausing multiple definitions

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
(cherry picked from commit 1c158744e8)

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
2021-01-29 10:01:03 +02:00
Grot (@grafanabot)
a2e638352b PanelLibrary: better handling of deleted panels (#30709) (#30726)
(cherry picked from commit 0b1f5c5e32)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-29 06:39:42 +01:00
Grot (@grafanabot)
91f5cd23c2 Transform: improve the "outer join" performance/behavior (#30407) (#30722)
(cherry picked from commit db9a8bf04a)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-01-28 16:34:15 -08:00
Grot (@grafanabot)
7aa169af9e DashboardPicker: switch to promise-based debounce, return dashboard UID (#30706) (#30714)
* Use uid in dashboard picker

* Set both id and uid from picker

* Use debounce-promise

* Simplify logic

* Use exact package versions

(cherry picked from commit e36b035c05)

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
2021-01-28 20:12:51 +02:00
Grot (@grafanabot)
b22ecf33f1 Use connected GraphNG in Explore (#30707) (#30708)
(cherry picked from commit 78433032ab)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-01-28 14:55:35 +01:00
Grot (@grafanabot)
31df45f316 PanelLibrary: changes casing of responses and adds meta property (#30668) (#30711)
* PanelLibrary: changes casing of responses and adds meta property

* Chore: updates comments

* Chore: updates after PR comments

* Chore: changes casing of orgId

(cherry picked from commit 0a8eae2c12)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-28 14:52:08 +01:00
Grot (@grafanabot)
5e20794cfe DeployImage: Switch base images to Debian (#30684) (#30699)
* DeployImage: Switch base images to Debian

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
(cherry picked from commit fdd6a84d82)

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
2021-01-28 13:49:32 +01:00
Grot (@grafanabot)
b768649a10 Trace: trace to logs design update (#30637) (#30702)
* Add new icon to custom icons

* Show button in span detail

(cherry picked from commit 4147c3b907)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2021-01-28 12:56:42 +01:00
Grot (@grafanabot)
007efb3691 Influx: Show all datapoints for dynamically windowed flux query (#30688) (#30703)
(cherry picked from commit 1bdd3eb3dd)

Co-authored-by: David <david.kaltschmidt@gmail.com>
2021-01-28 12:16:22 +01:00
Grot (@grafanabot)
d38179bc79 ci(npm-publish): add missing github package token to env vars (#30665) (#30673)
(cherry picked from commit 54b1ce2cdb)

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
2021-01-28 09:49:56 +01:00
Ivana Huckova
05f18add0b Loki: Improve live tailing errors and fix Explore's logs container type errors (#30517) (#30681)
* If error - catch and show, if no logs - return null

* Refactor LogsContainer to use ConnectedProps

* Fix typescript error

* Remove no logs check

* Include review feedback

* Add SplitOpen type to createSpanLink and TraceView

(cherry picked from commit 347343f583)
2021-01-28 09:07:07 +01:00
Grot (@grafanabot)
b3425159e1 Grafana-UI: Fix setting default value for MultiSelect (#30671) (#30687)
* Grafana-ui: Default value to undefned vs empty array

* Grafana-ui: Remove log

* Grafana-ui: Update tests

(cherry picked from commit aad7d495ec)

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
2021-01-28 09:39:00 +02:00
Grot (@grafanabot)
89d5428954 Explore: Fix jumpy live tailing (#30650) (#30677)
* Fix jumpy live tailing

* Fix test

(cherry picked from commit e34d9e1c32)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-01-27 16:41:54 +01:00
Grot (@grafanabot)
844c5a14e8 Docs: Refer to product docs in whats new for alerting templating feature (#30652) (#30670)
* Docs: Refer to product docs in whats new for alerting templating feature

* move link below image

* remove whitespace

(cherry picked from commit bdf00d3a59)

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
2021-01-27 15:25:58 +01:00
Grot (@grafanabot)
7d670ee7e1 Variables: Fixes display value when using capture groups in regex (#30636) (#30661)
* Variables: Fixes display value for variables with regex

* Chore: adds a test for getAllMatches

(cherry picked from commit b5d7f1e7d8)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-27 07:13:17 +01:00
Grot (@grafanabot)
b6acd8b685 Docs: Fix expressions enabled description (#30589) (#30651)
(cherry picked from commit 94a29759ab)

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
2021-01-26 10:16:44 -08:00
Grot (@grafanabot)
b4cc173235 Licensing Docs: Adding license restrictions docs (#30216) (#30648)
(cherry picked from commit 12a7b342b9)

Co-authored-by: Vardan Torosyan <vardants@gmail.com>
2021-01-26 18:22:16 +01:00
Grot (@grafanabot)
0664013253 DashboardSettings: fixes vertical scrolling (#30640) (#30643)
* fix(dashboardsettings): allow view to scroll vertically

* refactor(dashboardsettings): use theme bg colour instead of palette colour

(cherry picked from commit aaa6ebb231)

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
2021-01-26 16:01:57 +01:00
Jack Westbrook
365c008d70 chore: bump redux toolkit to 1.5.0 for immer 8.0.1 vulnerability fix (#30605) (#30631)
(cherry picked from commit 10aabe8ce3)
2021-01-26 14:08:34 +01:00
Grot (@grafanabot)
ca6a767dc0 Explore: Fix loading visualisation on the top of the new time series panel (#30553) (#30557)
* Fix loading for time series graph panel

* Fix test by adding loading prop to dummyProps

(cherry picked from commit 5d52e50f6f)

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
2021-01-26 13:48:24 +01:00
Grot (@grafanabot)
aadbb1ebf4 Footer: Fixes layout issue in footer (#30443) (#30494)
* Footer: Fixes missing icon issue causing footer layout issue

* Another fix

(cherry picked from commit 5c8d662bfc)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-01-26 13:47:45 +01:00
Grot (@grafanabot)
3875ae2319 Variables: Fixes so queries work for numbers values too (#30602) (#30624)
* Variables: Fixes so queries work for numbers values too

* Chore: refactor after PR comments

(cherry picked from commit fad81e1696)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-26 09:06:14 +01:00
Grot (@grafanabot)
5052887efb Admin: Fixes so form values are filled in from backend (#30544) (#30623)
* Admin: Fixes so form values are filled in from backend

* Chore: tidy up the imports

(cherry picked from commit a8056e2c9d)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-26 08:09:20 +01:00
Grot (@grafanabot)
3e02e2c12c Docs: Update 7.4 What's New to use more correct description of alerting notification template feature (#30502) (#30614)
* use more correct description of feature

* reference variable templating

* add word

(cherry picked from commit ad7d75c14d)

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
2021-01-25 11:41:05 -08:00
Grot (@grafanabot)
c6df872d4a NodeGraph: Add docs (#30504) (#30613)
* Add draft docs

* Update node-graph.md

* Update whats-new-in-v7-4.md

* Update images and add x-ray link

* Update docs/sources/panels/visualizations/node-graph.md

* Update docs/sources/panels/visualizations/node-graph.md

* Update node-graph.md

* Add definition of node and edge

* Update docs/sources/panels/visualizations/node-graph.md

* Update docs/sources/panels/visualizations/node-graph.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
(cherry picked from commit abf2410d16)

Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
2021-01-25 11:19:02 -08:00
Grot (@grafanabot)
e1f512181f Cloud Monitoring: Fix legend naming with display name override (#30440) (#30503)
* Cloud Monitoring: Fix legend naming with display name override

* include MQL queries

* cover all bases

* refactor

(cherry picked from commit 7562c6749d)

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
2021-01-25 20:17:16 +01:00
Grot (@grafanabot)
fcfb12d0b3 Expressions: Add option to disable feature (#30541) (#30558)
* Expressions: Add option to disable feature

* Apply suggestions from code review

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
(cherry picked from commit 9ada4b6052)

Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
2021-01-25 15:58:52 +02:00
Grot (@grafanabot)
7592031b36 OldGraph: Fix height issue in Firefox (#30565) (#30582)
(cherry picked from commit 15683319e0)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2021-01-25 08:40:28 +01:00
Grot (@grafanabot)
77e924295b XY Chart: fix editor error with empty frame (no fields) (#30573) (#30577)
(cherry picked from commit 08312897c8)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-01-24 08:59:39 -08:00
Grot (@grafanabot)
5a07677422 XY Chart: share legend config with timeseries (#30559) (#30566)
(cherry picked from commit 8c1a79f24b)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-01-22 15:20:12 -08:00
Grot (@grafanabot)
b6a9ef0919 DataFrame: cache frame/field index in field state (#30529) (#30560)
(cherry picked from commit f2327baf66)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-01-22 10:46:41 -08:00
Grot (@grafanabot)
b31aed4283 Prometheus: Fix show query instead of Value if no __name__ and metric (#30511) (#30556)
Fixes #29466

(cherry picked from commit 38c1d45035)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2021-01-22 18:31:06 +01:00
Grot (@grafanabot)
045f4f4f4a Decimals: Big Improvements to auto decimals and fixes to auto decimals bug found in 7.4-beta1 (#30519) (#30550)
* Decimals: Nukes scaledDecimals from the earth it was an abomination

* Moved move tests

* Fixed test

* Updated tests

* Updated test

(cherry picked from commit 6bdc9fac45)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-01-22 17:16:23 +01:00
Jack Westbrook
14d44553c5 chore: update packages dependent on dot-prop to fix security vulnerability (#30432) (#30487)
(cherry picked from commit e4d8cdfcdf)
2021-01-22 10:18:59 +01:00
Grot (@grafanabot)
f02dd1c6aa GraphNG: uPlot 1.6.3 (fix bands not filling below 0). close #30523. (#30527) (#30528)
(cherry picked from commit 92a0ad7273)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2021-01-21 20:40:58 -08:00
Grot (@grafanabot)
13f4755ac2 GraphNG: uPlot 1.6.2 (#30521) (#30522)
(cherry picked from commit 87ef5598e6)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2021-01-21 18:21:26 -06:00
Grot (@grafanabot)
18bcbc4903 Chore: Upgrade grabpl version (#30486) (#30513)
(cherry picked from commit bb1f84dbc7)

Co-authored-by: Tania B <yalyna.ts@gmail.com>
2021-01-21 19:28:03 +02:00
Grot (@grafanabot)
db9ec5ccae grafana/ui: Fix internal import from grafana/data (#30439) (#30507)
(cherry picked from commit c7e6d14d34)

Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
2021-01-21 17:58:43 +01:00
Grot (@grafanabot)
6b8e230f45 prevent field config from being overwritten (#30437) (#30442)
(cherry picked from commit d9d27340b5)

Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
2021-01-21 15:50:50 +01:00
Grot (@grafanabot)
5abe8852c1 Chore: upgrade NPM security vulnerabilities (#30397) (#30495)
* chore: bump serialize-javascript dependents to use 3.1.0+

* chore: manually bump is-my-json-valid to 2.20.5

* chore: resolve kind-of@6 to 6.0.3

* chore: bump webpack-dev-server to solve faye-websocket vulnerability

(cherry picked from commit 930c19eb09)

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
2021-01-21 15:29:43 +01:00
Grot (@grafanabot)
8b9cb9d034 TimeSeriesPanel: Fixed default value for gradientMode (#30484) (#30492)
(cherry picked from commit 59ef36812e)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-01-21 14:24:38 +01:00
Grot (@grafanabot)
804c3a9be0 Admin: Fixes so whole org drop down is visible when adding users to org (#30481) (#30497)
* Modal: Admin: Fixes so whole org drop down is visible

* Tests: fixes failing tests

* Chore: cleans up the return type

* Chore: changes after PR comments

* Chore: changes after PR comments

(cherry picked from commit ffd39933d4)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-21 14:19:56 +01:00
Grot (@grafanabot)
648efa9391 Chore: adds wait to e2e test (#30488) (#30490)
(cherry picked from commit de511b0f48)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-21 12:55:52 +01:00
Grot (@grafanabot)
83f0acfc03 Graph: Fixes so only users with correct permissions can add annotations (#30419) (#30466)
* Graph: Fixes so only users with edit permissions can add annotations

* Tests: corrects test message text

* Chore: changes after PR comments

(cherry picked from commit e1243e07ca)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-21 08:25:39 +01:00
Grot (@grafanabot)
88c70cd762 Alerting: Hides threshold handle for percentual thresholds (#30431) (#30467)
(cherry picked from commit 98406d6c42)

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-01-21 08:03:42 +01:00
Grot (@grafanabot)
fed65b983a Timeseries: only migrage point size when configured (#30461) (#30470)
(cherry picked from commit 2ec4784190)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2021-01-21 07:49:26 +01:00
Grot (@grafanabot)
39c5f1705e Expressions: Fix button icon (#30444) (#30450)
(cherry picked from commit b9fd4dba36)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-01-21 06:06:22 +01:00
Grot (@grafanabot)
45f71a94f8 PanelModel: Make sure the angular options are passed to react panel type changed handler (#30441) (#30451)
(cherry picked from commit 05e37e9253)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-01-21 06:05:58 +01:00
Grot (@grafanabot)
18f5bb15b7 Docs: Fix img link for alert notification template (#30436) (#30447)
* fix img link

* update image name

(cherry picked from commit 0c67ceadb8)

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
2021-01-21 05:59:10 +01:00
Grot (@grafanabot)
594733024a Chore: Upgrade build pipeline tool (#30456) (#30457)
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
(cherry picked from commit bd71eb23df)

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
2021-01-20 22:04:00 +01:00
Grot (@grafanabot)
f666d108b4 PanelOptions: Refactoring applying panel and field options out of PanelModel and add property clean up for properties not in field config registry (#30389) (#30438)
* PanelOptions: Refactoring on applying panel and field options from PanelModel

* Progress

* Filtering out props

* downgraded prettier

* Fixes

* Initial simple remember and restore for custom and overrides

* clearing custom options and overrides and restoring works

* actually use the function

* Added type for options cache

* minor fix

* Updated with new prettier

* Added old field config to panel type change handler

* Update public/app/features/dashboard/state/getPanelOptionsWithDefaults.test.ts

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
(cherry picked from commit 15033d0011)

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
2021-01-20 18:27:19 +01:00
Grot (@grafanabot)
228d804962 "Release: Updated versions in package to 7.4.0-beta.1" (#30427) 2021-01-20 11:09:55 +00:00
Giordano Ricci
e9242cf546 Chore: Update what's new URL (#30423) 2021-01-20 10:51:35 +00:00
419 changed files with 17796 additions and 6141 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -66,6 +66,9 @@ cert_key =
# Unix socket path
socket = /tmp/grafana.sock
# CDN Url
cdn_url =
#################################### Database ############################
[database]
# You can configure the database connection by specifying type, host, name, user and password
@@ -159,6 +162,9 @@ send_user_header = false
# Change this option to false to disable reporting.
reporting_enabled = true
# The name of the distributor of the Grafana instance. Ex hosted-grafana, grafana-labs
reporting_distributor = grafana-labs
# Set to false to disable all checks to https://grafana.com
# for new versions (grafana itself and plugins), check is used
# in some UI views to notify that grafana or plugin update exists
@@ -893,3 +899,7 @@ use_browser_locale = false
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
default_timezone = browser
[expressions]
# Enable or disable the expressions functionality.
enabled = true

View File

@@ -67,6 +67,9 @@
# Unix socket path
;socket =
# CDN Url
;cdn_url =
#################################### Database ####################################
[database]
# You can configure the database connection by specifying type, host, name, user and password
@@ -165,6 +168,9 @@
# Change this option to false to disable reporting.
;reporting_enabled = true
# The name of the distributor of the Grafana instance. Ex hosted-grafana, grafana-labs
;reporting_distributor = grafana-labs
# Set to false to disable all checks to https://grafana.net
# for new versions (grafana itself and plugins), check is used
# in some UI views to notify that grafana or plugin update exists
@@ -883,3 +889,7 @@
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
;default_timezone = browser
[expressions]
# Enable or disable the expressions functionality.
;enabled = true

View File

@@ -27,14 +27,14 @@
"overrides": []
},
"gridPos": {
"h": 16,
"h": 18,
"w": 24,
"x": 0,
"y": 0
},
"id": 11,
"options": {
"content": "## Global variables\n\n* `__dashboard` = `${__dashboard}`\n* `__dashboard.name` = `${__dashboard.name}`\n* `__dashboard.uid` = `${__dashboard.uid}`\n* `__org.name` = `${__org.name}`\n* `__org.id` = `${__org.id}`\n* `__user.id` = `${__user.id}`\n* `__user.login` = `${__user.login}`\n* `__user.email` = `${__user.email}`\n \n## Formats\n\n* `Server:raw` = `${Server:raw}`\n* `Server:regex` = `${Server:regex}`\n* `Server:lucene` = `${Server:lucene}`\n* `Server:glob` = `${Server:glob}`\n* `Server:pipe` = `${Server:pipe}`\n* `Server:distributed` = `${Server:distributed}`\n* `Server:csv` = `${Server:csv}`\n* `Server:html` = `${Server:html}`\n* `Server:json` = `${Server:json}`\n* `Server:percentencode` = `${Server:percentencode}`\n* `Server:singlequote` = `${Server:singlequote}`\n* `Server:doublequote` = `${Server:doublequote}`\n* `Server:sqlstring` = `${Server:sqlstring}`\n* `Server:date` = `${Server:date}`\n* `Server:text` = `${Server:text}`\n\n",
"content": "## Global variables\n\n* `__dashboard` = `${__dashboard}`\n* `__dashboard.name` = `${__dashboard.name}`\n* `__dashboard.uid` = `${__dashboard.uid}`\n* `__org.name` = `${__org.name}`\n* `__org.id` = `${__org.id}`\n* `__user.id` = `${__user.id}`\n* `__user.login` = `${__user.login}`\n* `__user.email` = `${__user.email}`\n \n## Formats\n\n* `Server:raw` = `${Server:raw}`\n* `Server:regex` = `${Server:regex}`\n* `Server:lucene` = `${Server:lucene}`\n* `Server:glob` = `${Server:glob}`\n* `Server:pipe` = `${Server:pipe}`\n* `Server:distributed` = `${Server:distributed}`\n* `Server:csv` = `${Server:csv}`\n* `Server:html` = `${Server:html}`\n* `Server:json` = `${Server:json}`\n* `Server:percentencode` = `${Server:percentencode}`\n* `Server:singlequote` = `${Server:singlequote}`\n* `Server:doublequote` = `${Server:doublequote}`\n* `Server:sqlstring` = `${Server:sqlstring}`\n* `Server:date` = `${Server:date}`\n* `Server:text` = `${Server:text}`\n* `Server:queryparam` = `${Server:queryparam}`\n\n",
"mode": "markdown"
},
"pluginVersion": "7.1.0",

View File

@@ -142,7 +142,8 @@
},
"id": 3,
"libraryPanel": {
"uid": "MAnX2ifMk"
"uid": "MAnX2ifMk",
"name": "React Table"
}
},
{
@@ -154,7 +155,8 @@
},
"id": 2,
"libraryPanel": {
"uid": "g1sNpCaMz"
"uid": "g1sNpCaMz",
"name": "React Gauge"
}
}
],

View File

@@ -0,0 +1,40 @@
influxdb:
image: quay.io/influxdb/influxdb:v2.0.3
container_name: influxdb2
ports:
- '8086:8086'
environment:
INFLUXDB_REPORTING_DISABLED: 'true'
volumes:
- ./docker/blocks/influxdb2/influxdb.conf:/etc/influxdb/influxdb.conf
# Use the influx cli to set up an influxdb instance.
influxdb_cli:
links:
- influxdb
image: quay.io/influxdb/influxdb:v2.0.3
# Use these same configurations parameters in your telegraf configuration, mytelegraf.conf.
entrypoint: influx setup --bucket mybucket -t mytoken -o myorg --username=grafana --password=grafana12345 --host=http://influxdb:8086 -f
# Wait for the influxd service in the influxdb container has fully bootstrapped before trying to setup an influxdb instance with the influxdb_cli service.
restart: on-failure:10
depends_on:
- influxdb
fake-influxdb-data:
image: grafana/fake-data-gen
links:
- influxdb
environment:
FD_DATASOURCE: influxdb
FD_PORT: 8086
telegraf:
image: telegraf
links:
- influxdb
depends_on:
- influxdb_cli
volumes:
- ./docker/blocks/influxdb2/telegraf.conf:/etc/telegraf/telegraf.conf:ro
- /var/log:/var/log
- ../data/log:/var/log/grafana

View File

@@ -0,0 +1,93 @@
reporting-disabled = false
[meta]
# Where the metadata/raft database is stored
dir = "/var/lib/influxdb/meta"
retention-autocreate = true
# If log messages are printed for the meta service
logging-enabled = true
pprof-enabled = false
# The default duration for leases.
lease-duration = "1m0s"
[data]
# Controls if this node holds time series data shards in the cluster
enabled = true
dir = "/var/lib/influxdb/data"
# These are the WAL settings for the storage engine >= 0.9.3
wal-dir = "/var/lib/influxdb/wal"
wal-logging-enabled = true
[coordinator]
write-timeout = "10s"
max-concurrent-queries = 0
query-timeout = "0"
log-queries-after = "0"
max-select-point = 0
max-select-series = 0
max-select-buckets = 0
[retention]
enabled = true
check-interval = "30m"
[shard-precreation]
enabled = true
check-interval = "10m"
advance-period = "30m"
[monitor]
store-enabled = true # Whether to record statistics internally.
store-database = "_internal" # The destination database for recorded statistics
store-interval = "10s" # The interval at which to record statistics
[admin]
enabled = true
bind-address = ":8083"
https-enabled = false
https-certificate = "/etc/ssl/influxdb.pem"
[http]
enabled = true
bind-address = ":8086"
auth-enabled = true
log-enabled = true
write-tracing = false
pprof-enabled = false
https-enabled = false
https-certificate = "/etc/ssl/influxdb.pem"
### Use a separate private key location.
# https-private-key = ""
max-row-limit = 10000
realm = "InfluxDB"
unix-socket-enabled = false # enable http service over unix domain socket
# bind-socket = "/var/run/influxdb.sock"
flux-enabled = true
[subscriber]
enabled = true
[[graphite]]
enabled = false
[[collectd]]
enabled = false
[[opentsdb]]
enabled = false
[[udp]]
enabled = false
[continuous_queries]
log-enabled = true
enabled = true
# run-interval = "1s" # interval for how often continuous queries will be checked if they need to run

File diff suppressed because it is too large Load Diff

View File

@@ -259,6 +259,15 @@ Path to the certificate key file (if `protocol` is set to `https` or `h2`).
Path where the socket should be created when `protocol=socket`. Make sure that Grafana has appropriate permissions before you change this setting.
### cdn_url
> **Note**: Available in Grafana v7.4 and later versions.
Specify a full HTTP URL address to the root of your Grafana CDN assets. Grafana will add edition and version paths.
For example, given a cdn url like `https://cdn.myserver.com` grafana will try to load a javascript file from
`http://cdn.myserver.com/grafana-oss/v7.4.0/public/build/app.<hash>.js`.
<hr />
## [database]
@@ -1505,3 +1514,8 @@ Set this to `true` to have date formats automatically derived from your browser
### default_timezone
Used as the default time zone for user preferences. Can be either `browser` for the browser local time zone or a time zone name from the IANA Time Zone database, such as `UTC` or `Europe/Amsterdam`.
## [expressions]
> **Note:** This feature is available in Grafana v7.4 and later versions.
### enabled
Set this to `false` to disable expressions and hide them in the Grafana UI. Default is `true`.

View File

@@ -2,7 +2,7 @@
title = "Activate an Enterprise license"
description = "Activate an Enterprise license"
keywords = ["grafana", "licensing", "enterprise"]
weight = 7
weight = 100
+++
# Activate an Enterprise license

View File

@@ -2,7 +2,7 @@
title = "License Expiration"
description = ""
keywords = ["grafana", "licensing"]
weight = 8
weight = 120
+++
# License expiration

View File

@@ -0,0 +1,49 @@
+++
title = "License restrictions"
description = "Grafana Enterprise license restrictions"
keywords = ["grafana", "licensing", "enterprise"]
weight = 110
+++
# License restrictions
Enterprise licenses are limited by the number of active users, a license expiration date, and the URL of the Grafana instance.
## User limits
Grafana licenses allow for a certain number of active users per instance. An active user is any user that has signed in to Grafana within the past 30 days.
In the context of licensing, each user is classified as either a viewer or an editor:
- An editor is a user who has permission to edit and save a dashboard. Examples of editors are as follows:
- Grafana server administrators.
- Users who are assigned an organizational role of Editor or Admin.
- Users that have been granted Admin or Edit permissions at the dashboard or folder level. Refer to [Dashboard and folder permissions](https://grafana.com/docs/grafana/latest/permissions/dashboard_folder_permissions/).
- A viewer is a user with the Viewer role, which does not permit the user to save a dashboard.
Restrictions are applied separately for viewers and editors.
When the number of maximum active viewers or editors is reached, Grafana displays a warning banner.
## Expiration date
The license expiration date is the date when a license is no longer active. As the license expiration date approaches, Grafana Enterprise displays a banner.
## License URL
License URL is the root URL of your Grafana instance. The license will not work on an instance of Grafana with a different root URL.
## Download a dashboard and folder permissions report
This CSV report helps to identify users, teams, and roles that have been granted Admin or Edit permissions at the dashboard or folder level.
To download the report:
1. Hover your cursor over the **Server Admin** (shield) icon in the side menu and then click **Licensing**.
2. At the bottom of the page, click **Download report**.
## Update license restrictions
To increase the number of licensed users within Grafana, extend a license, or change your licensed URL, contact [Grafana support](https://grafana.com/profile/org#support) or your Grafana Labs account team. They will update your license, which you can activate from within Grafana.
For instructions on how to activate your license after it is updated, refer to
[Activate an Enterprise license]({{< relref "./activate-license.md" >}})

View File

@@ -49,6 +49,6 @@ Value-specific variables are available under ``__value`` namespace:
When linking to another dashboard that uses template variables, select variable values for whoever clicks the link.
``var-myvar=${myvar}`` - where ``myvar`` is a name of the template variable that matches one in the current dashboard that you want to use.
``${myvar:queryparams}`` - where ``myvar`` is a name of the template variable that matches one in the current dashboard that you want to use.
If you want to add all of the current dashboard's variables to the URL, then use ``__all_variables``.

View File

@@ -0,0 +1,95 @@
+++
title = "Node graph"
keywords = ["grafana", "dashboard", "documentation", "panels", "node graph", "directed graph"]
weight = 850
+++
# Node graph panel
> **Note:** This panel is currently in beta. Expect changes in future releases.
The _Node graph_ can visualize directed graphs or networks. It uses directed force layout to effectively position the nodes so it can help with displaying complex infrastructure maps, hierarchies or execution diagrams.
![Node graph panel](/img/docs/node-graph/node-graph-7-4.png "Node graph")
## Data requirements
The Node graph panel requires specific shape of the data to be able to display it's nodes and edges. This means not every data source or query can be visualized in this panel. If you want to use this as a data source developer see the section about data API.
The Node graph visualization consists of _nodes_ and _edges_.
- A _node_ is displayed as a circle. A node might represent an application, a service, or anything else that is relevant from an application perspective.
- An _edge_ is displayed as a line that connects two nodes. The connection might be a request, an execution, or some other relationship between the two nodes.
Both nodes and edges can have associated metadata or statistics. The data source defines what information and values is shown, so different data sources can show different type of values or not show some values.
### Nodes
> **Note:** At this moment node graph can show only 1,500 nodes. If this limit is crossed a warning will be visible in upper right corner.
Usually, nodes show two statistical values inside the node and two identifiers just below the node, usually name and type. Nodes can also show another set of values as a color circle around the node, with sections of different color represents different values that should add up to 1.
For example you can have percentage of errors represented by red portion of the circle. Additional details can be displayed in a context menu when which is displayed when you click on the node. There also can be additional links in the context menu that can target either other parts of Grafana or any external link.
![Node graph navigation](/img/docs/node-graph/node-graph-navigation-7-4.gif "Node graph navigation")
### Edges
Edges can also show statistics when you hover over the edge. Similar to nodes, you can open a context menu with additional details and links by clicking on the edge.
The first data source supporting this visualization is X-Ray data source for it's Service map feature. For more information, refer to the [X-Ray plugin documentation](https://grafana.com/grafana/plugins/grafana-x-ray-datasource).
## Navigating the node graph
You can pan and zoom in or out the node graph.
### Pan
You can pan the view by clicking outside of any node or edge and dragging your mouse.
### Zoom in or out
Use the buttons on the upper left corner or use the mouse wheel, touch pad scroll, together with either Ctrl or Cmd key to zoom in or out.
## Data API
This visualization needs a specific shape of the data to be returned from the data source in order to correctly display it.
Data source needs to return two data frames, one for nodes and one for edges and you also have to set `frame.meta.preferredVisualisationType = 'nodeGraph'` on both data frames.
### Node parameters
Required fields:
| Field name | Type | Description |
|------------|---------|-------------|
| id | string | Unique identifier of the node. This ID is referenced by edge in it's source and target field. |
Optional fields:
| Field name | Type | Description |
|------------|---------|-------------|
| title | string | Name of the node visible in just under the node. |
| subTitle | string | Additional, name, type or other identifier that will be shown right under the title. |
| mainStat | string/number | First stat shown inside the node itself. Can be either string in which case the value will be shown as it is or it can be a number in which case any unit associated with that field will be also shown. |
| secondaryStat | string/number | Same as mainStat but shown right under it inside the node. |
| arc__* | number | Any field prefixed with `arc__` will be used to create the color circle around the node. All values in these fields should add up to 1. You can specify color using `config.color.fixedColor`. |
| detail__* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. |
### Edge parameters
Required fields:
| Field name | Type | Description |
|------------|---------|-------------|
| id | string | Unique identifier of the edge. |
| source | string | Id of the source node. |
| target | string | Id of the target. |
Optional fields:
| Field name | Type | Description |
|------------|---------|-------------|
| mainStat | string/number | First stat shown in the overlay when hovering over the edge. Can be either string in which case the value will be shown as it is or it can be a number in which case any unit associated with that field will be also shown |
| secondaryStat | string/number | Same as mainStat but shown right under it. |
| detail__* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. |

View File

@@ -149,3 +149,13 @@ servers = ["test1", "test2"]
String to interpolate: '${servers:text}'
Interpolation result: "test1 + test2"
```
## Query parameters
Formats single- and multi-valued variables into their query parameter representation. Example: `var-foo=value1&var-foo=value2`
```bash
servers = ["test1", "test2"]
String to interpolate: '${servers:queryparam}'
Interpolation result: "var-servers=test1&var-servers=test2"
```

View File

@@ -32,6 +32,8 @@ All the information and stats shown in the Node graph beta are driven by the dat
For more details about how to use the X-Ray service map feature, see the [X-Ray plugin documentation](https://grafana.com/grafana/plugins/grafana-x-ray-datasource).
For more information, refer to [Node graph panel]({{< relref "../panels/visualizations/node-graph.md" >}}).
### New transformations
The following transformations were added in Grafana 7.4.
@@ -77,11 +79,13 @@ The main use case is for [multi-dimensional](https://grafana.com/docs/grafana/la
> **Note:** Queries built with this feature may break with minor version upgrades until Grafana 8 is released.
### Variable support in alert notifications
### Alert notification query label interpolation
You can now provide detailed information to alert notification recipients by injecting alert query data into an alert notification. Labels that exist from the evaluation of the alert query can be used in the alert rule name and in the alert notification message fields. The alert label data is injected into the notification fields when the alert is in the alerting state. When there are multiple unique values for the same label, the values are comma-separated.
You can now provide detailed information to alert notification recipients by injecting alert label data as template variables into an alert notification. Labels that exist from the evaluation of the alert query can be used in the alert rule name and in the alert notification message fields using the `${Label}` syntax. The alert label data is automatically injected into the notification fields when the alert is in the alerting state. When there are multiple unique values for the same label, the values are comma-separated.
![Variable support in alert notifications](/img/docs/v74/alert-variable-notifications.png)
![Variable support in alert notifications](/img/docs/alerting/alert-notification-template-7-4.png)
For more information, refer to the [alert notification docs]({{< relref "../alerting/notifications.md#notification-templating" >}}).
### Content security policy support

View File

@@ -35,11 +35,12 @@ e2e.scenario({
`Server:sqlstring = 'A''A"A','BB\\\B','CCC'`,
`Server:date = null`,
`Server:text = All`,
`Server:queryparam = var-Server=All`,
];
e2e()
.get('.markdown-html li')
.should('have.length', 23)
.should('have.length', 24)
.each((element) => {
items.push(element.text());
})

View File

@@ -75,36 +75,36 @@ const expectDrawerTabsAndContent = () => {
expect(li.text()).equals('Data');
});
e2e.components.PanelInspector.Data.content().should('be.visible');
e2e.components.PanelInspector.Stats.content().should('not.be.visible');
e2e.components.PanelInspector.Json.content().should('not.be.visible');
e2e.components.PanelInspector.Query.content().should('not.be.visible');
e2e.components.PanelInspector.Stats.content().should('not.exist');
e2e.components.PanelInspector.Json.content().should('not.exist');
e2e.components.PanelInspector.Query.content().should('not.exist');
// other tabs should also be visible, click on each to see if we get any console errors
e2e.components.Tab.title('Stats').should('be.visible').click();
e2e.components.PanelInspector.Stats.content().should('be.visible');
e2e.components.PanelInspector.Data.content().should('not.be.visible');
e2e.components.PanelInspector.Json.content().should('not.be.visible');
e2e.components.PanelInspector.Query.content().should('not.be.visible');
e2e.components.PanelInspector.Data.content().should('not.exist');
e2e.components.PanelInspector.Json.content().should('not.exist');
e2e.components.PanelInspector.Query.content().should('not.exist');
e2e.components.Tab.title('JSON').should('be.visible').click();
e2e.components.PanelInspector.Json.content().should('be.visible');
e2e.components.PanelInspector.Data.content().should('not.be.visible');
e2e.components.PanelInspector.Stats.content().should('not.be.visible');
e2e.components.PanelInspector.Query.content().should('not.be.visible');
e2e.components.PanelInspector.Data.content().should('not.exist');
e2e.components.PanelInspector.Stats.content().should('not.exist');
e2e.components.PanelInspector.Query.content().should('not.exist');
e2e.components.Tab.title('Query').should('be.visible').click();
e2e.components.PanelInspector.Query.content().should('be.visible');
e2e.components.PanelInspector.Data.content().should('not.be.visible');
e2e.components.PanelInspector.Stats.content().should('not.be.visible');
e2e.components.PanelInspector.Json.content().should('not.be.visible');
e2e.components.PanelInspector.Data.content().should('not.exist');
e2e.components.PanelInspector.Stats.content().should('not.exist');
e2e.components.PanelInspector.Json.content().should('not.exist');
});
};
const expectDrawerClose = () => {
// close using close button
e2e.components.Drawer.General.close().click();
e2e.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`).should('not.be.visible');
e2e.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`).should('not.exist');
};
const expectDrawerExpandAndContract = (viewPortWidth: number) => {

View File

@@ -27,7 +27,7 @@ e2e.scenario({
});
e2e.components.QueryTab.content().should('be.visible');
e2e.components.TransformTab.content().should('not.exist');
e2e.components.AlertTab.content().should('not.be.visible');
e2e.components.AlertTab.content().should('not.exist');
// Bottom pane tabs
// Can change to Transform tab
@@ -36,8 +36,8 @@ e2e.scenario({
expect(li.text()).equals('Transform0'); // there's no transform so therefore Transform + 0
});
e2e.components.Transforms.card('Merge').scrollIntoView().should('be.visible');
e2e.components.QueryTab.content().should('not.be.visible');
e2e.components.AlertTab.content().should('not.be.visible');
e2e.components.QueryTab.content().should('not.exist');
e2e.components.AlertTab.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();
@@ -45,7 +45,7 @@ e2e.scenario({
expect(li.text()).equals('Alert0'); // there's no alert so therefore Alert + 0
});
e2e.components.AlertTab.content().should('be.visible');
e2e.components.QueryTab.content().should('not.be.visible');
e2e.components.QueryTab.content().should('not.exist');
e2e.components.TransformTab.content().should('not.exist');
e2e.components.Tab.title('Query').should('be.visible').click();
@@ -56,18 +56,18 @@ e2e.scenario({
// Can toggle on/off sidebar
e2e.components.PanelEditor.OptionsPane.close().should('be.visible');
e2e.components.PanelEditor.OptionsPane.open().should('not.be.visible');
e2e.components.PanelEditor.OptionsPane.open().should('not.exist');
// close options pane
e2e.components.PanelEditor.OptionsPane.close().click();
e2e.components.PanelEditor.OptionsPane.open().should('be.visible');
e2e.components.PanelEditor.OptionsPane.close().should('not.be.visible');
e2e.components.PanelEditor.OptionsPane.content().should('not.be.visible');
e2e.components.PanelEditor.OptionsPane.close().should('not.exist');
e2e.components.PanelEditor.OptionsPane.content().should('not.exist');
// open options pane
e2e.components.PanelEditor.OptionsPane.open().click();
e2e.components.PanelEditor.OptionsPane.close().should('be.visible');
e2e.components.PanelEditor.OptionsPane.open().should('not.be.visible');
e2e.components.PanelEditor.OptionsPane.open().should('not.exist');
e2e.components.PanelEditor.OptionsPane.content().should('be.visible');
// Can change visualisation type
@@ -86,7 +86,7 @@ e2e.scenario({
});
// Data pane should not be rendered
e2e.components.PanelEditor.DataPane.content().should('not.be.visible');
e2e.components.PanelEditor.DataPane.content().should('not.exist');
// Change to Table panel
e2e.components.PluginVisualization.item('Table').scrollIntoView().should('be.visible').click();
@@ -103,12 +103,12 @@ e2e.scenario({
e2e.components.PanelEditor.OptionsPane.tab('Field').click();
e2e.components.FieldConfigEditor.content().should('be.visible');
e2e.components.OverridesConfigEditor.content().should('not.be.visible');
e2e.components.OverridesConfigEditor.content().should('not.exist');
e2e.components.PanelEditor.OptionsPane.tab('Field').should('be.visible');
e2e.components.PanelEditor.OptionsPane.tab('Overrides').should('be.visible').click();
e2e.components.OverridesConfigEditor.content().should('be.visible');
e2e.components.FieldConfigEditor.content().should('not.be.visible');
e2e.components.FieldConfigEditor.content().should('not.exist');
},
});

View File

@@ -17,7 +17,7 @@ e2e.scenario({
e2e.components.Select.option().should('be.visible').first().click();
e2e.components.Select.input().should('be.visible').should('have.focus');
e2e.components.Select.input().should('exist').should('have.focus');
});
e2e.pages.Dashboard.Settings.General.title().click();
@@ -25,7 +25,7 @@ e2e.scenario({
e2e.components.FolderPicker.container()
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').should('not.have.focus');
e2e.components.Select.input().should('exist').should('not.have.focus');
});
},
});

View File

@@ -95,27 +95,34 @@ describe('Variables - Add variable', () => {
.type('*')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput()
.should('be.visible')
.type('/.*C.*/')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().should('be.visible').click();
e2e().wait(500);
e2e().wait(1500);
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').eq(1).should('be.visible').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
.should('be.visible')
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('have.length', 4)
.eq(3)
.within(() => {
e2e().get('.variable-option').should('have.length', 3);
});
e2e().get('.variable-link-wrapper').should('be.visible').click();
e2e().wait(500);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
.should('be.visible')
.within(() => {
e2e().get('.variable-option').should('have.length', 1);
});
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible');
});
});
it('adding a multi value query variable', () => {
@@ -142,6 +149,11 @@ describe('Variables - Add variable', () => {
.type('*')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput()
.should('be.visible')
.type('/.*C.*/')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch()
.click({ force: true })
.should('be.checked');
@@ -164,18 +176,20 @@ describe('Variables - Add variable', () => {
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').eq(1).should('be.visible').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
.should('be.visible')
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('have.length', 4)
.eq(3)
.within(() => {
e2e().get('.variable-option').should('have.length', 4);
});
e2e().get('.variable-link-wrapper').should('be.visible').click();
e2e().wait(500);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
.should('be.visible')
.within(() => {
e2e().get('.variable-option').should('have.length', 2);
});
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible');
});
});
});

1
go.mod
View File

@@ -34,6 +34,7 @@ require (
github.com/getsentry/sentry-go v0.9.0
github.com/go-macaron/binding v0.0.0-20190806013118-0b4f37bab25b
github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07
github.com/go-sourcemap/sourcemap v2.1.3+incompatible
github.com/go-sql-driver/mysql v1.5.0
github.com/go-stack/stack v1.8.0
github.com/gobwas/glob v0.2.3

2
go.sum
View File

@@ -485,6 +485,8 @@ github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7
github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
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-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=

View File

@@ -1,6 +1,8 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "7.4.0-pre.0"
"packages": [
"packages/*"
],
"version": "7.4.5"
}

View File

@@ -3,7 +3,7 @@
"license": "Apache-2.0",
"private": true,
"name": "grafana",
"version": "7.4.0-pre",
"version": "7.4.5",
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
@@ -46,7 +46,7 @@
"ci:test-frontend": "yarn run prettier:check && yarn run typecheck && yarn run lint && yarn run test:ci && yarn grafana-toolkit node-version-check && ./scripts/ci-check-strict.sh"
},
"grafana": {
"whatsNewUrl": "https://grafana.com/docs/grafana/latest/guides/whats-new-in-v7-3/",
"whatsNewUrl": "https://grafana.com/docs/grafana/latest/guides/whats-new-in-v7-4/",
"releaseNotesUrl": "https://grafana.com/docs/grafana/latest/release-notes/"
},
"husky": {
@@ -89,6 +89,7 @@
"@types/d3": "5.7.2",
"@types/d3-force": "^2.1.0",
"@types/d3-scale-chromatic": "1.3.1",
"@types/debounce-promise": "3.1.3",
"@types/enzyme": "3.10.5",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/file-saver": "2.0.1",
@@ -168,7 +169,7 @@
"monaco-editor-webpack-plugin": "1.9.0",
"mutationobserver-shim": "0.3.3",
"ngtemplate-loader": "2.0.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"optimize-css-assets-webpack-plugin": "5.0.4",
"postcss-browser-reporter": "0.6.0",
"postcss-loader": "3.0.0",
"postcss-reporter": "6.0.1",
@@ -184,7 +185,7 @@
"sass-loader": "8.0.2",
"sinon": "8.1.1",
"style-loader": "1.1.3",
"terser-webpack-plugin": "2.3.5",
"terser-webpack-plugin": "2.3.7",
"ts-jest": "26.4.4",
"ts-node": "9.0.0",
"tslib": "2.0.3",
@@ -193,14 +194,14 @@
"webpack-bundle-analyzer": "3.6.0",
"webpack-cleanup-plugin": "0.5.1",
"webpack-cli": "3.3.10",
"webpack-dev-server": "3.10.3",
"webpack-dev-server": "3.11.1",
"webpack-merge": "4.2.2",
"zone.js": "0.7.8"
},
"dependencies": {
"@grafana/slate-react": "0.22.9-grafana",
"@popperjs/core": "2.5.4",
"@reduxjs/toolkit": "1.3.4",
"@reduxjs/toolkit": "1.5.0",
"@sentry/browser": "5.25.0",
"@sentry/types": "5.24.2",
"@sentry/utils": "5.24.2",
@@ -234,6 +235,7 @@
"d3-force": "^2.1.1",
"d3-scale-chromatic": "1.5.0",
"dangerously-set-html-content": "1.0.6",
"debounce-promise": "3.1.2",
"emotion": "10.0.27",
"eventemitter3": "4.0.0",
"fast-text-encoding": "^1.0.0",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "7.4.0-pre.0",
"version": "7.4.5",
"description": "Grafana Data Library",
"keywords": [
"typescript"

View File

@@ -144,33 +144,6 @@ describe('Format value', () => {
expect(result.text).toEqual('10.0');
});
it('should set auto decimals, 1 significant', () => {
const value = 3.23;
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('3.23');
});
it('should set auto decimals, 2 significant', () => {
const value = 0.0245;
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('0.0245');
});
it('should set auto decimals correctly for value 0.333333333333', () => {
const value = 1 / 3;
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('0.333');
});
it('should use override decimals', () => {
const value = 100030303;
const instance = getDisplayProcessorFromConfig({ decimals: 2, unit: 'bytes' });
const disp = instance(value);
expect(disp.text).toEqual('95.40');
expect(disp.suffix).toEqual(' MiB');
});
it('should return mapped value if there are matching value mappings', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
@@ -211,7 +184,7 @@ describe('Format value', () => {
const value = 1000;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.0');
expect(disp.text).toEqual('1');
expect(disp.suffix).toEqual(' K');
});
@@ -219,7 +192,7 @@ describe('Format value', () => {
const value = 1200;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.2');
expect(disp.text).toEqual('1.20');
expect(disp.suffix).toEqual(' K');
});
@@ -235,7 +208,7 @@ describe('Format value', () => {
const value = 1000000;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.0');
expect(disp.text).toEqual('1');
expect(disp.suffix).toEqual(' Mil');
});
@@ -243,9 +216,17 @@ describe('Format value', () => {
const value = 1500000;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.5');
expect(disp.text).toEqual('1.50');
expect(disp.suffix).toEqual(' Mil');
});
it('with value 128000000 and unit bytes', () => {
const value = 1280000125;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'bytes' });
const disp = instance(value);
expect(disp.text).toEqual('1.19');
expect(disp.suffix).toEqual(' GiB');
});
});
describe('Date display options', () => {

View File

@@ -4,7 +4,7 @@ import _ from 'lodash';
// Types
import { Field, FieldType } from '../types/dataFrame';
import { GrafanaTheme } from '../types/theme';
import { DecimalCount, DecimalInfo, DisplayProcessor, DisplayValue } from '../types/displayValue';
import { DisplayProcessor, DisplayValue } from '../types/displayValue';
import { getValueFormat } from '../valueFormats/valueFormats';
import { getMappedValue } from '../utils/valueMappings';
import { dateTime } from '../datetime';
@@ -27,9 +27,11 @@ interface DisplayProcessorOptions {
// Reasonable units for time
const timeFormats: KeyValue<boolean> = {
dateTimeAsIso: true,
dateTimeAsIsoSmart: true,
dateTimeAsIsoNoDateIfToday: true,
dateTimeAsUS: true,
dateTimeAsUSSmart: true,
dateTimeAsUSNoDateIfToday: true,
dateTimeAsLocal: true,
dateTimeAsLocalNoDateIfToday: true,
dateTimeFromNow: true,
};
@@ -86,8 +88,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
if (!isNaN(numeric)) {
if (shouldFormat && !_.isBoolean(value)) {
const { decimals, scaledDecimals } = getDecimalsForValue(value, config.decimals);
const v = formatFunc(numeric, decimals, scaledDecimals, options.timeZone);
const v = formatFunc(numeric, config.decimals, null, options.timeZone);
text = v.text;
suffix = v.suffix;
prefix = v.prefix;
@@ -137,53 +138,6 @@ function toStringProcessor(value: any): DisplayValue {
return { text: _.toString(value), numeric: toNumber(value) };
}
function getSignificantDigitCount(n: number) {
//remove decimal and make positive
n = Math.abs(+String(n).replace('.', ''));
if (n === 0) {
return 0;
}
// kill the 0s at the end of n
while (n !== 0 && n % 10 === 0) {
n /= 10;
}
// get number of digits
return Math.floor(Math.log(n) / Math.LN10) + 1;
}
export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
if (_.isNumber(decimalOverride)) {
// It's important that scaledDecimals is null here
return { decimals: decimalOverride, scaledDecimals: null };
}
if (value === 0) {
return { decimals: 0, scaledDecimals: 0 };
}
const digits = getSignificantDigitCount(value);
const log10 = Math.floor(Math.log(Math.abs(value)) / Math.LN10);
let dec = -log10 + 1;
const magn = Math.pow(10, -dec);
const norm = value / magn; // norm is between 1.0 and 10.0
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
++dec;
}
if (value % 1 === 0) {
dec = 0;
}
const decimals = Math.max(0, dec);
const scaledDecimals = decimals - log10 + digits - 1;
return { decimals, scaledDecimals };
}
export function getRawDisplayProcessor(): DisplayProcessor {
return (value: any) => ({
text: `${value}`,

View File

@@ -38,10 +38,8 @@ export function getFieldDisplayName(field: Field, frame?: DataFrame, allFrames?:
}
const displayName = calculateFieldDisplayName(field, frame, allFrames);
field.state = {
...field.state,
displayName,
};
field.state = field.state || {};
field.state.displayName = displayName;
return displayName;
}

View File

@@ -15,4 +15,4 @@ export { sortThresholds, getActiveThreshold } from './thresholds';
export { applyFieldOverrides, validateFieldConfig, applyRawFieldOverrides } from './fieldOverrides';
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
export { getFieldDisplayName, getFrameDisplayName } from './fieldState';
export { getScaleCalculator } from './scale';
export { getScaleCalculator, getFieldConfigWithMinMax } from './scale';

View File

@@ -65,14 +65,20 @@ function getMinMaxAndDelta(field: Field): NumericRange {
};
}
/**
* @internal
*/
export function getFieldConfigWithMinMax(field: Field, local?: boolean): FieldConfig {
const { config } = field;
let { min, max } = config;
if (isNumber(min) && !isNumber(max)) {
return config; // noop
if (isNumber(min) && isNumber(max)) {
return config;
}
if (local || !field.state?.range) {
return { ...config, ...getMinMaxAndDelta(field) };
}
return { ...config, ...field.state.range };
}

View File

@@ -18,5 +18,5 @@ export {
BasicValueMatcherOptions,
RangeValueMatcherOptions,
} from './transformations/matchers/valueMatchers/types';
export { PanelPlugin, SetFieldConfigOptionsArgs } from './panel/PanelPlugin';
export { PanelPlugin, SetFieldConfigOptionsArgs, StandardOptionConfig } from './panel/PanelPlugin';
export { createFieldConfigRegistry } from './panel/registryFactories';

View File

@@ -16,7 +16,8 @@ import { deprecationWarning } from '../utils';
import { FieldConfigOptionsRegistry } from '../field';
import { createFieldConfigRegistry } from './registryFactories';
type StandardOptionConfig = {
/** @beta */
export type StandardOptionConfig = {
defaultValue?: any;
settings?: any;
};
@@ -130,6 +131,7 @@ export class PanelPlugin<
set(result, editor.id, editor.defaultValue);
}
}
return result;
}
@@ -138,6 +140,10 @@ export class PanelPlugin<
configDefaults.custom = {} as TFieldConfigOptions;
for (const option of this.fieldConfigRegistry.list()) {
if (option.defaultValue === undefined) {
continue;
}
set(configDefaults, option.id, option.defaultValue);
}

View File

@@ -11,4 +11,4 @@ export {
} from './standardTransformersRegistry';
export { RegexpOrNamesMatcherOptions, ByNamesMatcherOptions, ByNamesMatcherMode } from './matchers/nameMatcher';
export { RenameByRegexTransformerOptions } from './transformers/renameByRegex';
export { outerJoinDataFrames } from './transformers/seriesToColumns';
export { outerJoinDataFrames } from './transformers/joinDataFrames';

View File

@@ -21,6 +21,7 @@ const seriesBC = toDataFrame({
{ name: 'B', type: FieldType.number, values: [2, 200] },
{ name: 'C', type: FieldType.number, values: [3, 300] },
{ name: 'D', type: FieldType.string, values: ['first', 'second'] },
{ name: 'E', type: FieldType.boolean, values: [true, false] },
],
});
@@ -48,6 +49,7 @@ describe('calculateField transformer w/ timeseries', () => {
B: 2,
C: 3,
D: 'first',
E: true,
'The Total': 6,
TheTime: 1000,
},
@@ -56,6 +58,7 @@ describe('calculateField transformer w/ timeseries', () => {
B: 200,
C: 300,
D: 'second',
E: false,
'The Total': 600,
TheTime: 2000,
},
@@ -129,7 +132,7 @@ describe('calculateField transformer w/ timeseries', () => {
mode: CalculateFieldMode.BinaryOperation,
binary: {
left: 'B',
operation: BinaryOperationID.Add,
operator: BinaryOperationID.Add,
right: 'C',
},
replaceFields: true,
@@ -160,7 +163,7 @@ describe('calculateField transformer w/ timeseries', () => {
mode: CalculateFieldMode.BinaryOperation,
binary: {
left: 'B',
operation: BinaryOperationID.Add,
operator: BinaryOperationID.Add,
right: '2',
},
replaceFields: true,
@@ -183,4 +186,37 @@ describe('calculateField transformer w/ timeseries', () => {
]);
});
});
it('boolean field', async () => {
const cfg = {
id: DataTransformerID.calculateField,
options: {
mode: CalculateFieldMode.BinaryOperation,
binary: {
left: 'E',
operator: BinaryOperationID.Multiply,
right: '1',
},
replaceFields: true,
},
};
await expect(transformDataFrame([cfg], [seriesBC])).toEmitValuesWith((received) => {
const data = received[0];
const filtered = data[0];
const rows = new DataFrameView(filtered).toArray();
expect(rows).toMatchInlineSnapshot(`
Array [
Object {
"E * 1": 1,
"TheTime": 1000,
},
Object {
"E * 1": 0,
"TheTime": 2000,
},
]
`);
});
});
});

View File

@@ -7,6 +7,7 @@ import { getFieldMatcher } from '../matchers';
import { FieldMatcherID } from '../matchers/ids';
import { RowVector } from '../../vector/RowVector';
import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector';
import { AsNumberVector } from '../../vector/AsNumberVector';
import { getTimeField } from '../../dataframe/processDataFrame';
import defaults from 'lodash/defaults';
import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators';
@@ -187,6 +188,9 @@ function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string, allFr
for (const f of frame.fields) {
if (name === getFieldDisplayName(f, frame, allFrames)) {
if (f.type === FieldType.boolean) {
return new AsNumberVector(f.values);
}
return f.values;
}
}

View File

@@ -49,52 +49,74 @@ describe('ensureColumns transformer', () => {
const frame = filtered[0];
expect(frame.fields.length).toEqual(5);
expect(filtered[0]).toEqual(
toDataFrame({
fields: [
{
name: 'TheTime',
type: 'time',
config: {},
values: [1000, 2000],
labels: undefined,
expect(filtered[0]).toMatchInlineSnapshot(`
Object {
"fields": Array [
Object {
"config": Object {},
"name": "TheTime",
"state": Object {
"displayName": "TheTime",
},
"type": "time",
"values": Array [
1000,
2000,
],
},
{
name: 'A',
type: 'number',
config: {},
values: [1, 100],
labels: {},
Object {
"config": Object {},
"labels": Object {},
"name": "A",
"state": Object {},
"type": "number",
"values": Array [
1,
100,
],
},
{
name: 'B',
type: 'number',
config: {},
values: [2, 200],
labels: {},
Object {
"config": Object {},
"labels": Object {},
"name": "B",
"state": Object {},
"type": "number",
"values": Array [
2,
200,
],
},
{
name: 'C',
type: 'number',
config: {},
values: [3, 300],
labels: {},
Object {
"config": Object {},
"labels": Object {},
"name": "C",
"state": Object {},
"type": "number",
"values": Array [
3,
300,
],
},
{
name: 'D',
type: 'string',
config: {},
values: ['first', 'second'],
labels: {},
Object {
"config": Object {},
"labels": Object {},
"name": "D",
"state": Object {},
"type": "string",
"values": Array [
"first",
"second",
],
},
],
meta: {
transformations: ['ensureColumns'],
"length": 2,
"meta": Object {
"transformations": Array [
"ensureColumns",
],
},
name: undefined,
refId: undefined,
})
);
}
`);
});
});

View File

@@ -0,0 +1,297 @@
import { toDataFrame } from '../../dataframe/processDataFrame';
import { FieldType } from '../../types/dataFrame';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { ArrayVector } from '../../vector';
import { calculateFieldTransformer } from './calculateField';
import { isLikelyAscendingVector, outerJoinDataFrames } from './joinDataFrames';
describe('align frames', () => {
beforeAll(() => {
mockTransformationsRegistry([calculateFieldTransformer]);
});
it('by first time field', () => {
const series1 = toDataFrame({
fields: [
{ name: 'TheTime', type: FieldType.time, values: [1000, 2000] },
{ name: 'A', type: FieldType.number, values: [1, 100] },
],
});
const series2 = toDataFrame({
fields: [
{ name: '_time', type: FieldType.time, values: [1000, 1500, 2000] },
{ name: 'A', type: FieldType.number, values: [2, 20, 200] },
{ name: 'B', type: FieldType.number, values: [3, 30, 300] },
{ name: 'C', type: FieldType.string, values: ['first', 'second', 'third'] },
],
});
const out = outerJoinDataFrames({ frames: [series1, series2] })!;
expect(
out.fields.map((f) => ({
name: f.name,
values: f.values.toArray(),
}))
).toMatchInlineSnapshot(`
Array [
Object {
"name": "TheTime",
"values": Array [
1000,
1500,
2000,
],
},
Object {
"name": "A",
"values": Array [
1,
undefined,
100,
],
},
Object {
"name": "A",
"values": Array [
2,
20,
200,
],
},
Object {
"name": "B",
"values": Array [
3,
30,
300,
],
},
Object {
"name": "C",
"values": Array [
"first",
"second",
"third",
],
},
]
`);
});
it('unsorted input keep indexes', () => {
//----------
const series1 = toDataFrame({
fields: [
{ name: 'TheTime', type: FieldType.time, values: [1000, 2000, 1500] },
{ name: 'A1', type: FieldType.number, values: [1, 2, 15] },
],
});
const series3 = toDataFrame({
fields: [
{ name: 'Time', type: FieldType.time, values: [2000, 1000] },
{ name: 'A2', type: FieldType.number, values: [2, 1] },
],
});
let out = outerJoinDataFrames({ frames: [series1, series3], keepOriginIndices: true })!;
expect(
out.fields.map((f) => ({
name: f.name,
values: f.values.toArray(),
state: f.state,
}))
).toMatchInlineSnapshot(`
Array [
Object {
"name": "TheTime",
"state": Object {
"origin": Object {
"fieldIndex": 0,
"frameIndex": 0,
},
},
"values": Array [
1000,
1500,
2000,
],
},
Object {
"name": "A1",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 0,
},
},
"values": Array [
1,
15,
2,
],
},
Object {
"name": "A2",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 1,
},
},
"values": Array [
1,
undefined,
2,
],
},
]
`);
// Fast path still adds origin indecies
out = outerJoinDataFrames({ frames: [series1], keepOriginIndices: true })!;
expect(
out.fields.map((f) => ({
name: f.name,
state: f.state,
}))
).toMatchInlineSnapshot(`
Array [
Object {
"name": "TheTime",
"state": Object {
"origin": Object {
"fieldIndex": 0,
"frameIndex": 0,
},
},
},
Object {
"name": "A1",
"state": Object {
"origin": Object {
"fieldIndex": 1,
"frameIndex": 0,
},
},
},
]
`);
});
it('sort single frame', () => {
const series1 = toDataFrame({
fields: [
{ name: 'TheTime', type: FieldType.time, values: [6000, 2000, 1500] },
{ name: 'A1', type: FieldType.number, values: [1, 22, 15] },
],
});
const out = outerJoinDataFrames({ frames: [series1], enforceSort: true, keepOriginIndices: true })!;
expect(
out.fields.map((f) => ({
name: f.name,
values: f.values.toArray(),
}))
).toMatchInlineSnapshot(`
Array [
Object {
"name": "TheTime",
"values": Array [
1500,
2000,
6000,
],
},
Object {
"name": "A1",
"values": Array [
15,
22,
1,
],
},
]
`);
});
it('supports duplicate times', () => {
//----------
// NOTE!!!
// * ideally we would *keep* dupicate fields
//----------
const series1 = toDataFrame({
fields: [
{ name: 'TheTime', type: FieldType.time, values: [1000, 2000] },
{ name: 'A', type: FieldType.number, values: [1, 100] },
],
});
const series3 = toDataFrame({
fields: [
{ name: 'Time', type: FieldType.time, values: [1000, 1000, 1000] },
{ name: 'A', type: FieldType.number, values: [2, 20, 200] },
],
});
const out = outerJoinDataFrames({ frames: [series1, series3] })!;
expect(
out.fields.map((f) => ({
name: f.name,
values: f.values.toArray(),
}))
).toMatchInlineSnapshot(`
Array [
Object {
"name": "TheTime",
"values": Array [
1000,
2000,
],
},
Object {
"name": "A",
"values": Array [
1,
100,
],
},
Object {
"name": "A",
"values": Array [
200,
undefined,
],
},
]
`);
});
describe('check ascending data', () => {
it('simple ascending', () => {
const v = new ArrayVector([1, 2, 3, 4, 5]);
expect(isLikelyAscendingVector(v)).toBeTruthy();
});
it('simple ascending with null', () => {
const v = new ArrayVector([null, 2, 3, 4, null]);
expect(isLikelyAscendingVector(v)).toBeTruthy();
});
it('single value', () => {
const v = new ArrayVector([null, null, null, 4, null]);
expect(isLikelyAscendingVector(v)).toBeTruthy();
expect(isLikelyAscendingVector(new ArrayVector([4]))).toBeTruthy();
expect(isLikelyAscendingVector(new ArrayVector([]))).toBeTruthy();
});
it('middle values', () => {
const v = new ArrayVector([null, null, 5, 4, null]);
expect(isLikelyAscendingVector(v)).toBeFalsy();
});
it('decending', () => {
expect(isLikelyAscendingVector(new ArrayVector([7, 6, null]))).toBeFalsy();
expect(isLikelyAscendingVector(new ArrayVector([7, 8, 6]))).toBeFalsy();
});
});
});

View File

@@ -0,0 +1,318 @@
import { DataFrame, Field, FieldMatcher, FieldType, Vector } from '../../types';
import { ArrayVector } from '../../vector';
import { fieldMatchers } from '../matchers';
import { FieldMatcherID } from '../matchers/ids';
import { getTimeField, sortDataFrame } from '../../dataframe';
export function pickBestJoinField(data: DataFrame[]): FieldMatcher {
const { timeField } = getTimeField(data[0]);
if (timeField) {
return fieldMatchers.get(FieldMatcherID.firstTimeField).get({});
}
let common: string[] = [];
for (const f of data[0].fields) {
if (f.type === FieldType.number) {
common.push(f.name);
}
}
for (let i = 1; i < data.length; i++) {
const names: string[] = [];
for (const f of data[0].fields) {
if (f.type === FieldType.number) {
names.push(f.name);
}
}
common = common.filter((v) => !names.includes(v));
}
return fieldMatchers.get(FieldMatcherID.byName).get(common[0]);
}
/**
* @alpha
*/
export interface JoinOptions {
/**
* The input fields
*/
frames: DataFrame[];
/**
* The field to join -- frames that do not have this field will be droppped
*/
joinBy?: FieldMatcher;
/**
* Optionally filter the non-join fields
*/
keep?: FieldMatcher;
/**
* When the result is a single frame, this will to a quick check to see if the values are sorted,
* and sort if necessary. If the first/last values are in order the whole vector is assumed to be
* sorted
*/
enforceSort?: boolean;
/**
* @internal -- used when we need to keep a reference to the original frame/field index
*/
keepOriginIndices?: boolean;
}
function getJoinMatcher(options: JoinOptions): FieldMatcher {
return options.joinBy ?? pickBestJoinField(options.frames);
}
/**
* This will return a single frame joined by the first matching field. When a join field is not specified,
* the default will use the first time field
*/
export function outerJoinDataFrames(options: JoinOptions): DataFrame | undefined {
if (!options.frames?.length) {
return undefined;
}
if (options.frames.length === 1) {
let frame = options.frames[0];
if (options.keepOriginIndices) {
frame = {
...frame,
fields: frame.fields.map((f, fieldIndex) => {
const copy = { ...f };
const origin = {
frameIndex: 0,
fieldIndex,
};
if (copy.state) {
copy.state.origin = origin;
} else {
copy.state = { origin };
}
return copy;
}),
};
}
if (options.enforceSort) {
const joinFieldMatcher = getJoinMatcher(options);
const joinIndex = frame.fields.findIndex((f) => joinFieldMatcher(f, frame, options.frames));
if (joinIndex >= 0) {
if (!isLikelyAscendingVector(frame.fields[joinIndex].values)) {
return sortDataFrame(frame, joinIndex);
}
}
}
return frame;
}
const nullModes: JoinNullMode[][] = [];
const allData: AlignedData[] = [];
const originalFields: Field[] = [];
const joinFieldMatcher = getJoinMatcher(options);
for (let frameIndex = 0; frameIndex < options.frames.length; frameIndex++) {
const frame = options.frames[frameIndex];
if (!frame || !frame.fields?.length) {
continue; // skip the frame
}
const nullModesFrame: JoinNullMode[] = [NULL_REMOVE];
let join: Field | undefined = undefined;
let fields: Field[] = [];
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
const field = frame.fields[fieldIndex];
field.state = field.state || {};
if (!join && joinFieldMatcher(field, frame, options.frames)) {
join = field;
} else {
if (options.keep && !options.keep(field, frame, options.frames)) {
continue; // skip field
}
// Support the standard graph span nulls field config
nullModesFrame.push(field.config.custom?.spanNulls ? NULL_REMOVE : NULL_EXPAND);
let labels = field.labels ?? {};
if (frame.name) {
labels = { ...labels, name: frame.name };
}
fields.push({
...field,
labels, // add the name label from frame
});
}
if (options.keepOriginIndices) {
field.state.origin = {
frameIndex,
fieldIndex,
};
}
}
if (!join) {
continue; // skip the frame
}
if (originalFields.length === 0) {
originalFields.push(join); // first join field
}
nullModes.push(nullModesFrame);
const a: AlignedData = [join.values.toArray()]; //
for (const field of fields) {
a.push(field.values.toArray());
originalFields.push(field);
// clear field displayName state
delete field.state?.displayName;
}
allData.push(a);
}
const joined = join(allData, nullModes);
return {
// ...options.data[0], // keep name, meta?
length: joined[0].length,
fields: originalFields.map((f, index) => ({
...f,
values: new ArrayVector(joined[index]),
})),
};
}
//--------------------------------------------------------------------------------
// Below here is copied from uplot (MIT License)
// https://github.com/leeoniya/uPlot/blob/master/src/utils.js#L325
// This avoids needing to import uplot into the data package
//--------------------------------------------------------------------------------
// Copied from uplot
type AlignedData = [number[], ...Array<Array<number | null>>];
// nullModes
const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true)
const NULL_RETAIN = 1; // nulls are retained, with alignment artifacts set to undefined (default)
const NULL_EXPAND = 2; // nulls are expanded to include any adjacent alignment artifacts
type JoinNullMode = number; // NULL_IGNORE | NULL_RETAIN | NULL_EXPAND;
// sets undefined values to nulls when adjacent to existing nulls (minesweeper)
function nullExpand(yVals: Array<number | null>, nullIdxs: number[], alignedLen: number) {
for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) {
let nullIdx = nullIdxs[i];
if (nullIdx > lastNullIdx) {
xi = nullIdx - 1;
while (xi >= 0 && yVals[xi] == null) {
yVals[xi--] = null;
}
xi = nullIdx + 1;
while (xi < alignedLen && yVals[xi] == null) {
yVals[(lastNullIdx = xi++)] = null;
}
}
}
}
// nullModes is a tables-matched array indicating how to treat nulls in each series
function join(tables: AlignedData[], nullModes: number[][]) {
const xVals = new Set<number>();
for (let ti = 0; ti < tables.length; ti++) {
let t = tables[ti];
let xs = t[0];
let len = xs.length;
for (let i = 0; i < len; i++) {
xVals.add(xs[i]);
}
}
let data = [Array.from(xVals).sort((a, b) => a - b)];
let alignedLen = data[0].length;
let xIdxs = new Map();
for (let i = 0; i < alignedLen; i++) {
xIdxs.set(data[0][i], i);
}
for (let ti = 0; ti < tables.length; ti++) {
let t = tables[ti];
let xs = t[0];
for (let si = 1; si < t.length; si++) {
let ys = t[si];
let yVals = Array(alignedLen).fill(undefined);
let nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;
let nullIdxs = [];
for (let i = 0; i < ys.length; i++) {
let yVal = ys[i];
let alignedIdx = xIdxs.get(xs[i]);
if (yVal == null) {
if (nullMode !== NULL_REMOVE) {
yVals[alignedIdx] = yVal;
if (nullMode === NULL_EXPAND) {
nullIdxs.push(alignedIdx);
}
}
} else {
yVals[alignedIdx] = yVal;
}
}
nullExpand(yVals, nullIdxs, alignedLen);
data.push(yVals);
}
}
return data;
}
// Quick test if the first and last points look to be ascending
// Only exported for tests
export function isLikelyAscendingVector(data: Vector): boolean {
let first: any = undefined;
for (let idx = 0; idx < data.length; idx++) {
const v = data.get(idx);
if (v != null) {
if (first != null) {
if (first > v) {
return false; // descending
}
break;
}
first = v;
}
}
let idx = data.length - 1;
while (idx >= 0) {
const v = data.get(idx--);
if (v != null) {
if (first > v) {
return false;
}
return true;
}
}
return true; // only one non-null point
}

View File

@@ -2,7 +2,6 @@ import {
ArrayVector,
DataTransformerConfig,
DataTransformerID,
Field,
FieldType,
toDataFrame,
transformDataFrame,
@@ -45,58 +44,94 @@ describe('SeriesToColumns Transformer', () => {
(received) => {
const data = received[0];
const filtered = data[0];
expect(filtered.fields).toEqual([
{
name: 'time',
state: {
displayName: 'time',
expect(filtered.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {},
"name": "time",
"state": Object {
"displayName": "time",
},
"type": "time",
"values": Array [
1000,
3000,
4000,
5000,
6000,
7000,
],
},
type: FieldType.time,
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
config: {},
labels: undefined,
},
{
name: 'temperature',
state: {
displayName: 'temperature even',
Object {
"config": Object {},
"labels": Object {
"name": "even",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
undefined,
10.3,
10.4,
10.5,
10.6,
undefined,
],
},
type: FieldType.number,
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
config: {},
labels: { name: 'even' },
},
{
name: 'humidity',
state: {
displayName: 'humidity even',
Object {
"config": Object {},
"labels": Object {
"name": "even",
},
"name": "humidity",
"state": Object {},
"type": "number",
"values": Array [
undefined,
10000.3,
10000.4,
10000.5,
10000.6,
undefined,
],
},
type: FieldType.number,
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
config: {},
labels: { name: 'even' },
},
{
name: 'temperature',
state: {
displayName: 'temperature odd',
Object {
"config": Object {},
"labels": Object {
"name": "odd",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
11.1,
11.3,
undefined,
11.5,
undefined,
11.7,
],
},
type: FieldType.number,
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
config: {},
labels: { name: 'odd' },
},
{
name: 'humidity',
state: {
displayName: 'humidity odd',
Object {
"config": Object {},
"labels": Object {
"name": "odd",
},
"name": "humidity",
"state": Object {},
"type": "number",
"values": Array [
11000.1,
11000.3,
undefined,
11000.5,
undefined,
11000.7,
],
},
type: FieldType.number,
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
config: {},
labels: { name: 'odd' },
},
]);
]
`);
}
);
});
@@ -113,58 +148,7 @@ describe('SeriesToColumns Transformer', () => {
(received) => {
const data = received[0];
const filtered = data[0];
expect(filtered.fields).toEqual([
{
name: 'temperature',
state: {
displayName: 'temperature',
},
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6, 11.1, 11.3, 11.5, 11.7]),
config: {},
labels: undefined,
},
{
name: 'time',
state: {
displayName: 'time even',
},
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000, null, null, null, null]),
config: {},
labels: { name: 'even' },
},
{
name: 'humidity',
state: {
displayName: 'humidity even',
},
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6, null, null, null, null]),
config: {},
labels: { name: 'even' },
},
{
name: 'time',
state: {
displayName: 'time odd',
},
type: FieldType.time,
values: new ArrayVector([null, null, null, null, 1000, 3000, 5000, 7000]),
config: {},
labels: { name: 'odd' },
},
{
name: 'humidity',
state: {
displayName: 'humidity odd',
},
type: FieldType.number,
values: new ArrayVector([null, null, null, null, 11000.1, 11000.3, 11000.5, 11000.7]),
config: {},
labels: { name: 'odd' },
},
]);
expect(filtered.fields).toMatchInlineSnapshot(`Array []`);
}
);
});
@@ -185,58 +169,94 @@ describe('SeriesToColumns Transformer', () => {
(received) => {
const data = received[0];
const filtered = data[0];
expect(filtered.fields).toEqual([
{
name: 'time',
state: {
displayName: 'time',
expect(filtered.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {},
"name": "time",
"state": Object {
"displayName": "time",
},
"type": "time",
"values": Array [
1000,
3000,
4000,
5000,
6000,
7000,
],
},
type: FieldType.time,
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
config: {},
labels: undefined,
},
{
name: 'temperature',
state: {
displayName: 'temperature even',
Object {
"config": Object {},
"labels": Object {
"name": "even",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
undefined,
10.3,
10.4,
10.5,
10.6,
undefined,
],
},
type: FieldType.number,
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
config: {},
labels: { name: 'even' },
},
{
name: 'humidity',
state: {
displayName: 'humidity even',
Object {
"config": Object {},
"labels": Object {
"name": "even",
},
"name": "humidity",
"state": Object {},
"type": "number",
"values": Array [
undefined,
10000.3,
10000.4,
10000.5,
10000.6,
undefined,
],
},
type: FieldType.number,
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
config: {},
labels: { name: 'even' },
},
{
name: 'temperature',
state: {
displayName: 'temperature odd',
Object {
"config": Object {},
"labels": Object {
"name": "odd",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
11.1,
11.3,
undefined,
11.5,
undefined,
11.7,
],
},
type: FieldType.number,
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
config: {},
labels: { name: 'odd' },
},
{
name: 'humidity',
state: {
displayName: 'humidity odd',
Object {
"config": Object {},
"labels": Object {
"name": "odd",
},
"name": "humidity",
"state": Object {},
"type": "number",
"values": Array [
11000.1,
11000.3,
undefined,
11000.5,
undefined,
11000.7,
],
},
type: FieldType.number,
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
config: {},
labels: { name: 'odd' },
},
]);
]
`);
}
);
});
@@ -270,40 +290,54 @@ describe('SeriesToColumns Transformer', () => {
(received) => {
const data = received[0];
const filtered = data[0];
const expected: Field[] = [
{
name: 'time',
state: {
displayName: 'time',
expect(filtered.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {},
"name": "time",
"state": Object {
"displayName": "time",
},
"type": "time",
"values": Array [
1000,
2000,
3000,
4000,
],
},
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000]),
config: {},
labels: undefined,
},
{
name: 'temperature',
type: FieldType.number,
values: new ArrayVector([1, 3, 5, 7]),
config: {},
state: {
displayName: 'temperature temperature',
Object {
"config": Object {},
"labels": Object {
"name": "temperature",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
1,
3,
5,
7,
],
},
labels: { name: 'temperature' },
},
{
name: 'temperature',
state: {
displayName: 'temperature B',
Object {
"config": Object {},
"labels": Object {
"name": "B",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
2,
4,
6,
8,
],
},
type: FieldType.number,
values: new ArrayVector([2, 4, 6, 8]),
config: {},
labels: { name: 'B' },
},
];
expect(filtered.fields).toEqual(expected);
]
`);
}
);
});
@@ -341,31 +375,51 @@ describe('SeriesToColumns Transformer', () => {
await expect(transformDataFrame([cfg], [frame1, frame2, frame3])).toEmitValuesWith((received) => {
const data = received[0];
const filtered = data[0];
expect(filtered.fields).toEqual([
{
name: 'time',
state: { displayName: 'time' },
type: FieldType.time,
values: new ArrayVector([1, 2, 3]),
config: {},
},
{
name: 'temperature',
state: { displayName: 'temperature A' },
type: FieldType.number,
values: new ArrayVector([10, 11, 12]),
config: {},
labels: { name: 'A' },
},
{
name: 'temperature',
state: { displayName: 'temperature C' },
type: FieldType.number,
values: new ArrayVector([20, 22, 24]),
config: {},
labels: { name: 'C' },
},
]);
expect(filtered.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {},
"name": "time",
"state": Object {
"displayName": "time",
},
"type": "time",
"values": Array [
1,
2,
3,
],
},
Object {
"config": Object {},
"labels": Object {
"name": "A",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
10,
11,
12,
],
},
Object {
"config": Object {},
"labels": Object {
"name": "C",
},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
20,
22,
24,
],
},
]
`);
});
});
@@ -394,31 +448,41 @@ describe('SeriesToColumns Transformer', () => {
await expect(transformDataFrame([cfg], [frame1, frame2])).toEmitValuesWith((received) => {
const data = received[0];
const filtered = data[0];
expect(filtered.fields).toEqual([
{
name: 'time',
state: { displayName: 'time' },
type: FieldType.time,
values: new ArrayVector([1]),
config: {},
},
{
name: 'temperature',
state: { displayName: 'temperature 1' },
type: FieldType.number,
values: new ArrayVector([10]),
config: {},
labels: {},
},
{
name: 'temperature',
state: { displayName: 'temperature 2' },
type: FieldType.number,
values: new ArrayVector([20]),
config: {},
labels: {},
},
]);
expect(filtered.fields).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {},
"name": "time",
"state": Object {
"displayName": "time",
},
"type": "time",
"values": Array [
1,
],
},
Object {
"config": Object {},
"labels": Object {},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
10,
],
},
Object {
"config": Object {},
"labels": Object {},
"name": "temperature",
"state": Object {},
"type": "number",
"values": Array [
20,
],
},
]
`);
});
});
});

View File

@@ -1,153 +1,36 @@
import { map } from 'rxjs/operators';
import { DataFrame, DataTransformerInfo, Field } from '../../types';
import { DataTransformerInfo, FieldMatcher } from '../../types';
import { DataTransformerID } from './ids';
import { MutableDataFrame } from '../../dataframe';
import { ArrayVector } from '../../vector';
import { getFieldDisplayName } from '../../field/fieldState';
import { outerJoinDataFrames } from './joinDataFrames';
import { fieldMatchers } from '../matchers';
import { FieldMatcherID } from '../matchers/ids';
export interface SeriesToColumnsOptions {
byField?: string;
byField?: string; // empty will pick the field automatically
}
const DEFAULT_KEY_FIELD = 'Time';
export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOptions> = {
id: DataTransformerID.seriesToColumns,
name: 'Series as columns',
name: 'Series as columns', // Called 'Outer join' in the UI!
description: 'Groups series by field and returns values as columns',
defaultOptions: {
byField: DEFAULT_KEY_FIELD,
byField: undefined, // DEFAULT_KEY_FIELD,
},
operator: (options) => (source) =>
source.pipe(
map((data) => {
return outerJoinDataFrames(data, options);
if (data.length > 1) {
let joinBy: FieldMatcher | undefined = undefined;
if (options.byField) {
joinBy = fieldMatchers.get(FieldMatcherID.byName).get(options.byField);
}
const joined = outerJoinDataFrames({ frames: data, joinBy });
if (joined) {
return [joined];
}
}
return data;
})
),
};
/**
* @internal
*/
export function outerJoinDataFrames(data: DataFrame[], options: SeriesToColumnsOptions) {
const keyFieldMatch = options.byField || DEFAULT_KEY_FIELD;
const allFields: FieldsToProcess[] = [];
for (let frameIndex = 0; frameIndex < data.length; frameIndex++) {
const frame = data[frameIndex];
const keyField = findKeyField(frame, keyFieldMatch);
if (!keyField) {
continue;
}
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
const sourceField = frame.fields[fieldIndex];
if (sourceField === keyField) {
continue;
}
let labels = sourceField.labels ?? {};
if (frame.name) {
labels = { ...labels, name: frame.name };
}
allFields.push({
keyField,
sourceField,
newField: {
...sourceField,
state: null,
values: new ArrayVector([]),
labels,
},
});
}
}
// if no key fields or more than one value field
if (allFields.length <= 1) {
return data;
}
const resultFrame = new MutableDataFrame();
resultFrame.addField({
...allFields[0].keyField,
values: new ArrayVector([]),
});
for (const item of allFields) {
item.newField = resultFrame.addField(item.newField);
}
const keyFieldTitle = getFieldDisplayName(resultFrame.fields[0], resultFrame);
const byKeyField: { [key: string]: { [key: string]: any } } = {};
/*
this loop creates a dictionary object that groups the key fields values
{
"key field first value as string" : {
"key field name": key field first value,
"other series name": other series value
"other series n name": other series n value
},
"key field n value as string" : {
"key field name": key field n value,
"other series name": other series value
"other series n name": other series n value
}
}
*/
for (let fieldIndex = 0; fieldIndex < allFields.length; fieldIndex++) {
const { sourceField, keyField, newField } = allFields[fieldIndex];
const newFieldTitle = getFieldDisplayName(newField, resultFrame);
for (let valueIndex = 0; valueIndex < sourceField.values.length; valueIndex++) {
const value = sourceField.values.get(valueIndex);
const keyValue = keyField.values.get(valueIndex);
if (!byKeyField[keyValue]) {
byKeyField[keyValue] = { [newFieldTitle]: value, [keyFieldTitle]: keyValue };
} else {
byKeyField[keyValue][newFieldTitle] = value;
}
}
}
const keyValueStrings = Object.keys(byKeyField);
for (let rowIndex = 0; rowIndex < keyValueStrings.length; rowIndex++) {
const keyValueAsString = keyValueStrings[rowIndex];
for (let fieldIndex = 0; fieldIndex < resultFrame.fields.length; fieldIndex++) {
const field = resultFrame.fields[fieldIndex];
const otherColumnName = getFieldDisplayName(field, resultFrame);
const value = byKeyField[keyValueAsString][otherColumnName] ?? null;
field.values.add(value);
}
}
return [resultFrame];
}
function findKeyField(frame: DataFrame, matchTitle: string): Field | null {
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
const field = frame.fields[fieldIndex];
if (matchTitle === getFieldDisplayName(field)) {
return field;
}
}
return null;
}
interface FieldsToProcess {
newField: Field;
sourceField: Field;
keyField: Field;
}

View File

@@ -18,12 +18,21 @@ export interface BuildInfo {
*/
isEnterprise: boolean;
env: string;
edition: string;
edition: GrafanaEdition;
latestVersion: string;
hasUpdate: boolean;
hideVersion: boolean;
}
/**
* @internal
*/
export enum GrafanaEdition {
OpenSource = 'Open Source',
Pro = 'Pro',
Enterprise = 'Enterprise',
}
/**
* Describes available feature toggles in Grafana. These can be configured via the
* `conf/custom.ini` to enable features under development or not yet available in
@@ -54,7 +63,7 @@ export interface LicenseInfo {
licenseUrl: string;
stateInfo: string;
hasValidLicense: boolean;
edition: string;
edition: GrafanaEdition;
}
/**

View File

@@ -162,6 +162,13 @@ export interface FieldState {
* Useful for assigning color to series by looking up a color in a palette using this index
*/
seriesIndex?: number;
/**
* Location of this field within the context frames results
*
* @internal -- we will try to make this unnecessary
*/
origin?: DataFrameFieldIndex;
}
export interface NumericRange {
@@ -206,7 +213,8 @@ export const TIME_SERIES_METRIC_FIELD_NAME = 'Metric';
/**
* Describes where a specific data frame field is located within a
* dataset of type DataFrame[]
* @public
*
* @internal -- we will try to make this unnecessary
*/
export interface DataFrameFieldIndex {
frameIndex: number;

View File

@@ -33,7 +33,7 @@ export interface DataLink<T extends DataQuery = any> {
onClick?: (event: DataLinkClickEvent) => void;
// If dataLink represents internal link this has to be filled. Internal link is defined as a query in a particular
// datas ource that we want to show to the user. Usually this results in a link to explore but can also lead to
// data source that we want to show to the user. Usually this results in a link to explore but can also lead to
// more custom onClick behaviour if needed.
// @internal and subject to change in future releases
internal?: InternalDataLink<T>;

View File

@@ -186,6 +186,11 @@ export abstract class DataSourceApi<
*/
readonly type: string;
/**
* Set in constructor
*/
readonly uid: string;
/**
* min interval range
*/
@@ -196,6 +201,7 @@ export abstract class DataSourceApi<
this.id = instanceSettings.id;
this.type = instanceSettings.type;
this.meta = {} as DataSourcePluginMeta;
this.uid = instanceSettings.uid;
}
/**
@@ -588,6 +594,9 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS
withCredentials?: boolean;
}
/**
* @deprecated -- use {@link DataSourceInstanceSettings} instead
*/
export interface DataSourceSelectItem {
name: string;
value: string | null;

View File

@@ -0,0 +1,7 @@
/**
* A coordinate on a two dimensional plane.
*/
export interface CartesianCoords2D {
x: number;
y: number;
}

View File

@@ -29,5 +29,6 @@ export * from './explore';
export * from './legacyEvents';
export * from './live';
export * from './variables';
export * from './geometry';
export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config';

View File

@@ -1,3 +1,5 @@
import { DataFrame } from './dataFrame';
/**
* Base class for editor builders
*
@@ -49,5 +51,5 @@ export interface OptionEditorConfig<TOptions, TSettings = any, TValue = any> {
/**
* Function that enables configuration of when option editor should be shown based on current panel option properties.
*/
showIf?: (currentOptions: TOptions) => boolean | undefined;
showIf?: (currentOptions: TOptions, data?: DataFrame[]) => boolean | undefined;
}

View File

@@ -131,7 +131,8 @@ export type PanelMigrationHandler<TOptions = any> = (panel: PanelModel<TOptions>
export type PanelTypeChangedHandler<TOptions = any> = (
panel: PanelModel<TOptions>,
prevPluginId: string,
prevOptions: any
prevOptions: Record<string, any>,
prevFieldConfig: FieldConfigSource
) => Partial<TOptions>;
export type PanelOptionEditorsRegistry = Registry<PanelOptionsEditorItem>;

View File

@@ -1,4 +1,4 @@
import { getMappedValue } from './valueMappings';
import { getMappedValue, isNumeric } from './valueMappings';
import { ValueMapping, MappingType } from '../types';
describe('Format value with value mappings', () => {
@@ -79,14 +79,75 @@ describe('Format value with value mappings', () => {
expect(getMappedValue(valueMappings, value).text).toEqual('1-20');
});
it('should map value text to mapping', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, text: 'ELVA', type: MappingType.ValueToText, value: 'elva' },
];
describe('text mapping', () => {
it('should map value text to mapping', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, text: 'ELVA', type: MappingType.ValueToText, value: 'elva' },
];
const value = 'elva';
const value = 'elva';
expect(getMappedValue(valueMappings, value).text).toEqual('ELVA');
expect(getMappedValue(valueMappings, value).text).toEqual('ELVA');
});
it.each`
value | expected
${'2/0/12'} | ${{ id: 1, text: 'mapped value 1', type: MappingType.ValueToText, value: '2/0/12' }}
${'2/1/12'} | ${undefined}
${'2:0'} | ${{ id: 3, text: 'mapped value 3', type: MappingType.ValueToText, value: '2:0' }}
${'2:1'} | ${undefined}
${'20whatever'} | ${{ id: 2, text: 'mapped value 2', type: MappingType.ValueToText, value: '20whatever' }}
${'20whateve'} | ${undefined}
${'20'} | ${undefined}
${'00020.4'} | ${undefined}
${'192.168.1.1'} | ${{ id: 4, text: 'mapped value ip', type: MappingType.ValueToText, value: '192.168.1.1' }}
${'192'} | ${undefined}
${'192.168'} | ${undefined}
${'192.168.1'} | ${undefined}
${'9.90'} | ${{ id: 5, text: 'OK', type: MappingType.ValueToText, value: '9.9' }}
`('numeric-like text mapping, value:${value', ({ value, expected }) => {
const valueMappings: ValueMapping[] = [
{ id: 1, text: 'mapped value 1', type: MappingType.ValueToText, value: '2/0/12' },
{ id: 2, text: 'mapped value 2', type: MappingType.ValueToText, value: '20whatever' },
{ id: 3, text: 'mapped value 3', type: MappingType.ValueToText, value: '2:0' },
{ id: 4, text: 'mapped value ip', type: MappingType.ValueToText, value: '192.168.1.1' },
{ id: 5, text: 'OK', type: MappingType.ValueToText, value: '9.9' },
];
expect(getMappedValue(valueMappings, value)).toEqual(expected);
});
});
});
describe('isNumeric', () => {
it.each`
value | expected
${123} | ${true}
${'123'} | ${true}
${' 123'} | ${true}
${' 123 '} | ${true}
${-123.4} | ${true}
${'-123.4'} | ${true}
${0.41} | ${true}
${'.41'} | ${true}
${0x12} | ${true}
${'0x12'} | ${true}
${'000123.4'} | ${true}
${2e64} | ${true}
${'2e64'} | ${true}
${1e10000} | ${true}
${'1e10000'} | ${true}
${Infinity} | ${true}
${'abc'} | ${false}
${' '} | ${false}
${null} | ${false}
${undefined} | ${false}
${NaN} | ${false}
${''} | ${false}
${{}} | ${false}
${true} | ${false}
${[]} | ${false}
`('detects numeric values', ({ value, expected }) => {
expect(isNumeric(value)).toEqual(expected);
});
});

View File

@@ -15,20 +15,23 @@ const addValueToTextMappingText = (
return allValueMappings.concat(valueToTextMapping);
}
const valueAsNumber = parseFloat(value as string);
const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
let valueAsNumber, valueToTextMappingAsNumber;
if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
if (value === valueToTextMapping.value) {
if (isNumeric(value as string) && isNumeric(valueToTextMapping.value)) {
valueAsNumber = parseFloat(value as string);
valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
if (valueAsNumber === valueToTextMappingAsNumber) {
return allValueMappings.concat(valueToTextMapping);
}
}
if (valueAsNumber !== valueToTextMappingAsNumber) {
return allValueMappings;
}
return allValueMappings.concat(valueToTextMapping);
if (value === valueToTextMapping.value) {
return allValueMappings.concat(valueToTextMapping);
}
return allValueMappings;
};
const addRangeToTextMappingText = (
@@ -93,3 +96,12 @@ const isNullValueMap = (mapping: ValueMap): boolean => {
}
return mapping.value.toLowerCase() === 'null';
};
// Ref https://stackoverflow.com/a/42356340
export function isNumeric(num: any) {
if (num === true) {
return false;
}
return Boolean(Number(num));
}

View File

@@ -31,7 +31,7 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
[Interval.Millisecond]: 0.001,
};
export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toNanoSeconds(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
@@ -39,21 +39,21 @@ export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecim
if (Math.abs(size) < 1000) {
return { text: toFixed(size, decimals), suffix: ' ns' };
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs');
return toFixedScaled(size / 1000, decimals, ' µs');
} else if (Math.abs(size) < 1000000000) {
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms');
return toFixedScaled(size / 1000000, decimals, ' ms');
} else if (Math.abs(size) < 60000000000) {
return toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s');
return toFixedScaled(size / 1000000000, decimals, ' s');
} else if (Math.abs(size) < 3600000000000) {
return toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min');
return toFixedScaled(size / 60000000000, decimals, ' min');
} else if (Math.abs(size) < 86400000000000) {
return toFixedScaled(size / 3600000000000, decimals, scaledDecimals, 13, ' hour');
return toFixedScaled(size / 3600000000000, decimals, ' hour');
} else {
return toFixedScaled(size / 86400000000000, decimals, scaledDecimals, 14, ' day');
return toFixedScaled(size / 86400000000000, decimals, ' day');
}
}
export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toMicroSeconds(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
@@ -61,9 +61,9 @@ export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDeci
if (Math.abs(size) < 1000) {
return { text: toFixed(size, decimals), suffix: ' µs' };
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms');
return toFixedScaled(size / 1000, decimals, ' ms');
} else {
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s');
return toFixedScaled(size / 1000000, decimals, ' s');
}
}
@@ -76,19 +76,19 @@ export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDeci
return { text: toFixed(size, decimals), suffix: ' ms' };
} else if (Math.abs(size) < 60000) {
// Less than 1 min
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s');
return toFixedScaled(size / 1000, decimals, ' s');
} else if (Math.abs(size) < 3600000) {
// Less than 1 hour, divide in minutes
return toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min');
return toFixedScaled(size / 60000, decimals, ' min');
} else if (Math.abs(size) < 86400000) {
// Less than one day, divide in hours
return toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour');
return toFixedScaled(size / 3600000, decimals, ' hour');
} else if (Math.abs(size) < 31536000000) {
// Less than one year, divide in days
return toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day');
return toFixedScaled(size / 86400000, decimals, ' day');
}
return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
return toFixedScaled(size / 31536000000, decimals, ' year');
}
export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount {
@@ -98,7 +98,7 @@ export function trySubstract(value1: DecimalCount, value2: DecimalCount): Decima
return undefined;
}
export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toSeconds(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
@@ -110,37 +110,37 @@ export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?
// Less than 1 µs, divide in ns
if (Math.abs(size) < 0.000001) {
return toFixedScaled(size * 1e9, decimals, trySubstract(scaledDecimals, decimals), -9, ' ns');
return toFixedScaled(size * 1e9, decimals, ' ns');
}
// Less than 1 ms, divide in µs
if (Math.abs(size) < 0.001) {
return toFixedScaled(size * 1e6, decimals, trySubstract(scaledDecimals, decimals), -6, ' µs');
return toFixedScaled(size * 1e6, decimals, ' µs');
}
// Less than 1 second, divide in ms
if (Math.abs(size) < 1) {
return toFixedScaled(size * 1e3, decimals, trySubstract(scaledDecimals, decimals), -3, ' ms');
return toFixedScaled(size * 1e3, decimals, ' ms');
}
if (Math.abs(size) < 60) {
return { text: toFixed(size, decimals), suffix: ' s' };
} else if (Math.abs(size) < 3600) {
// Less than 1 hour, divide in minutes
return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min');
return toFixedScaled(size / 60, decimals, ' min');
} else if (Math.abs(size) < 86400) {
// Less than one day, divide in hours
return toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour');
return toFixedScaled(size / 3600, decimals, ' hour');
} else if (Math.abs(size) < 604800) {
// Less than one week, divide in days
return toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day');
return toFixedScaled(size / 86400, decimals, ' day');
} else if (Math.abs(size) < 31536000) {
// Less than one year, divide in week
return toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week');
return toFixedScaled(size / 604800, decimals, ' week');
}
return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
return toFixedScaled(size / 3.15569e7, decimals, ' year');
}
export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toMinutes(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
@@ -148,17 +148,17 @@ export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?
if (Math.abs(size) < 60) {
return { text: toFixed(size, decimals), suffix: ' min' };
} else if (Math.abs(size) < 1440) {
return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour');
return toFixedScaled(size / 60, decimals, ' hour');
} else if (Math.abs(size) < 10080) {
return toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day');
return toFixedScaled(size / 1440, decimals, ' day');
} else if (Math.abs(size) < 604800) {
return toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week');
return toFixedScaled(size / 10080, decimals, ' week');
} else {
return toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year');
return toFixedScaled(size / 5.25948e5, decimals, ' year');
}
}
export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toHours(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
@@ -166,15 +166,15 @@ export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?:
if (Math.abs(size) < 24) {
return { text: toFixed(size, decimals), suffix: ' hour' };
} else if (Math.abs(size) < 168) {
return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day');
return toFixedScaled(size / 24, decimals, ' day');
} else if (Math.abs(size) < 8760) {
return toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week');
return toFixedScaled(size / 168, decimals, ' week');
} else {
return toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year');
return toFixedScaled(size / 8760, decimals, ' year');
}
}
export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toDays(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
@@ -182,9 +182,9 @@ export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: D
if (Math.abs(size) < 7) {
return { text: toFixed(size, decimals), suffix: ' day' };
} else if (Math.abs(size) < 365) {
return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week');
return toFixedScaled(size / 7, decimals, ' week');
} else {
return toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year');
return toFixedScaled(size / 365, decimals, ' year');
}
}
@@ -227,7 +227,7 @@ export function toDuration(size: number, decimals: DecimalCount, timeScale: Inte
let decrementDecimals = false;
let decimalsCount = 0;
if (decimals !== null || decimals !== undefined) {
if (decimals !== null && decimals !== undefined) {
decimalsCount = decimals as number;
}
@@ -340,8 +340,8 @@ export function toDurationInDaysHoursMinutesSeconds(size: number): FormattedValu
return { text: dayString + hmsString.text };
}
export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount): FormattedValue {
return toSeconds(size / 100, decimals, scaledDecimals);
export function toTimeTicks(size: number, decimals: DecimalCount): FormattedValue {
return toSeconds(size / 100, decimals);
}
export function toClockMilliseconds(size: number, decimals: DecimalCount): FormattedValue {

View File

@@ -12,77 +12,69 @@ interface ValueFormatTest {
result: string;
}
const formatTests: ValueFormatTest[] = [
// Currency
{ id: 'currencyUSD', decimals: 2, value: 1532.82, result: '$1.53K' },
{ id: 'currencyKRW', decimals: 2, value: 1532.82, result: '₩1.53K' },
{ id: 'currencyIDR', decimals: 2, value: 1532.82, result: 'Rp1.53K' },
// Standard
{ id: 'ms', decimals: 4, value: 0.0024, result: '0.0024 ms' },
{ id: 'ms', decimals: 0, value: 100, result: '100 ms' },
{ id: 'ms', decimals: 2, value: 1250, result: '1.25 s' },
{ id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hour' },
{ id: 'ms', decimals: 0, value: 1200, result: '1 s' },
{ id: 'short', decimals: 0, scaledDecimals: -1, value: 98765, result: '98.77 K' },
{ id: 'short', decimals: 0, scaledDecimals: 0, value: 9876543, result: '9.876543 Mil' },
{ id: 'short', decimals: 2, scaledDecimals: null, value: 9876543, result: '9.88 Mil' },
{ id: 'kbytes', decimals: 3, value: 10000000, result: '9.537 GiB' },
{ id: 'deckbytes', decimals: 3, value: 10000000, result: '10.000 GB' },
{ id: 'megwatt', decimals: 3, value: 1000, result: '1.000 GW' },
{ id: 'kohm', decimals: 3, value: 1000, result: '1.000 MΩ' },
{ id: 'Mohm', decimals: 3, value: 1000, result: '1.000 GΩ' },
{ id: 'farad', decimals: 3, value: 1000, result: '1.000 kF' },
{ id: 'µfarad', decimals: 3, value: 1000, result: '1.000 mF' },
{ id: 'nfarad', decimals: 3, value: 1000, result: '1.000 µF' },
{ id: 'pfarad', decimals: 3, value: 1000, result: '1.000 nF' },
{ id: 'ffarad', decimals: 3, value: 1000, result: '1.000 pF' },
{ id: 'henry', decimals: 3, value: 1000, result: '1.000 kH' },
{ id: 'mhenry', decimals: 3, value: 1000, result: '1.000 H' },
{ id: 'µhenry', decimals: 3, value: 1000, result: '1.000 mH' },
// Suffix (unknown units append to the end)
{ id: 'a', decimals: 0, value: 1532.82, result: '1533 a' },
{ id: 'b', decimals: 0, value: 1532.82, result: '1533 b' },
// Prefix (unknown units append to the end)
{ id: 'prefix:b', value: 1532.82, result: 'b1533' },
{ id: 'suffix:d', value: 1532.82, result: '1533 d' },
// SI Units
{ id: 'si:µF', value: 1234, decimals: 2, result: '1.23 mF' },
{ id: 'si:µF', value: 1234000000, decimals: 2, result: '1.23 kF' },
{ id: 'si:µF', value: 1234000000000000, decimals: 2, result: '1.23 GF' },
// Counts (suffix)
{ id: 'count:xpm', value: 1234567, decimals: 2, result: '1.23M xpm' },
{ id: 'count:x/min', value: 1234, decimals: 2, result: '1.23K x/min' },
// Currency (prefix)
{ id: 'currency:@', value: 1234567, decimals: 2, result: '@1.23M' },
{ id: 'currency:@', value: 1234, decimals: 2, result: '@1.23K' },
// Time format
{ id: 'time:YYYY', decimals: 0, value: dateTime(new Date(1999, 6, 2)).valueOf(), result: '1999' },
{ id: 'time:YYYY.MM', decimals: 0, value: dateTime(new Date(2010, 6, 2)).valueOf(), result: '2010.07' },
{ id: 'dateTimeAsIso', decimals: 0, value: dateTime(new Date(2010, 6, 2)).valueOf(), result: '2010-07-02 00:00:00' },
{
id: 'dateTimeAsUS',
decimals: 0,
value: dateTime(new Date(2010, 6, 2)).valueOf(),
result: '07/02/2010 12:00:00 am',
},
{
id: 'dateTimeAsSystem',
decimals: 0,
value: dateTime(new Date(2010, 6, 2)).valueOf(),
result: '2010-07-02 00:00:00',
},
];
describe('valueFormats', () => {
it.each`
format | decimals | value | expected
${'currencyUSD'} | ${2} | ${1532.82} | ${'$1.53K'}
${'currencyKRW'} | ${2} | ${1532.82} | ${'₩1.53K'}
${'currencyIDR'} | ${2} | ${1532.82} | ${'Rp1.53K'}
${'none'} | ${undefined} | ${3.23} | ${'3.23'}
${'none'} | ${undefined} | ${0.0245} | ${'0.0245'}
${'none'} | ${undefined} | ${1 / 3} | ${'0.333'}
${'ms'} | ${4} | ${0.0024} | ${'0.0024 ms'}
${'ms'} | ${0} | ${100} | ${'100 ms'}
${'ms'} | ${2} | ${1250} | ${'1.25 s'}
${'ms'} | ${1} | ${10000086.123} | ${'2.8 hour'}
${'ms'} | ${undefined} | ${1000} | ${'1 s'}
${'ms'} | ${0} | ${1200} | ${'1 s'}
${'short'} | ${undefined} | ${1000} | ${'1 K'}
${'short'} | ${undefined} | ${1200} | ${'1.20 K'}
${'short'} | ${undefined} | ${1250} | ${'1.25 K'}
${'short'} | ${undefined} | ${1000000} | ${'1 Mil'}
${'short'} | ${undefined} | ${1500000} | ${'1.50 Mil'}
${'short'} | ${undefined} | ${1000120} | ${'1.00 Mil'}
${'short'} | ${undefined} | ${98765} | ${'98.8 K'}
${'short'} | ${undefined} | ${9876543} | ${'9.88 Mil'}
${'short'} | ${undefined} | ${9876543} | ${'9.88 Mil'}
${'kbytes'} | ${undefined} | ${10000000} | ${'9.54 GiB'}
${'deckbytes'} | ${undefined} | ${10000000} | ${'10 GB'}
${'megwatt'} | ${3} | ${1000} | ${'1.000 GW'}
${'kohm'} | ${3} | ${1000} | ${'1.000 MΩ'}
${'Mohm'} | ${3} | ${1000} | ${'1.000 GΩ'}
${'farad'} | ${3} | ${1000} | ${'1.000 kF'}
${'µfarad'} | ${3} | ${1000} | ${'1.000 mF'}
${'nfarad'} | ${3} | ${1000} | ${'1.000 µF'}
${'pfarad'} | ${3} | ${1000} | ${'1.000 nF'}
${'ffarad'} | ${3} | ${1000} | ${'1.000 pF'}
${'henry'} | ${3} | ${1000} | ${'1.000 kH'}
${'mhenry'} | ${3} | ${1000} | ${'1.000 H'}
${'µhenry'} | ${3} | ${1000} | ${'1.000 mH'}
${'a'} | ${0} | ${1532.82} | ${'1533 a'}
${'b'} | ${0} | ${1532.82} | ${'1533 b'}
${'prefix:b'} | ${undefined} | ${1532.82} | ${'b1533'}
${'suffix:d'} | ${undefined} | ${1532.82} | ${'1533 d'}
${'si:µF'} | ${2} | ${1234} | ${'1.23 mF'}
${'si:µF'} | ${2} | ${1234000000} | ${'1.23 kF'}
${'si:µF'} | ${2} | ${1234000000000000} | ${'1.23 GF'}
${'count:xpm'} | ${2} | ${1234567} | ${'1.23M xpm'}
${'count:x/min'} | ${2} | ${1234} | ${'1.23K x/min'}
${'currency:@'} | ${2} | ${1234567} | ${'@1.23M'}
${'currency:@'} | ${2} | ${1234} | ${'@1.23K'}
${'time:YYYY'} | ${0} | ${dateTime(new Date(1999, 6, 2)).valueOf()} | ${'1999'}
${'time:YYYY.MM'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010.07'}
${'dateTimeAsIso'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'}
${'dateTimeAsUS'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'07/02/2010 12:00:00 am'}
${'dateTimeAsSystem'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'}
${'dtdurationms'} | ${undefined} | ${100000} | ${'1 minute'}
`(
'With format=$format decimals=$decimals and value=$value then result shoudl be = $expected',
async ({ format, value, decimals, expected }) => {
const result = getValueFormat(format)(value, decimals, undefined, undefined);
const full = formattedValueToString(result);
expect(full).toBe(expected);
}
);
it('Manually check a format', () => {
// helpful for adding tests one at a time with the debugger
const tests: ValueFormatTest[] = [
@@ -94,16 +86,6 @@ describe('valueFormats', () => {
expect(full).toBe(test.result);
});
for (const test of formatTests) {
describe(`value format: ${test.id}`, () => {
it(`should translate ${test.value} as ${test.result}`, () => {
const result = getValueFormat(test.id)(test.value, test.decimals, test.scaledDecimals);
const full = formattedValueToString(result);
expect(full).toBe(test.result);
});
});
}
describe('normal cases', () => {
it('toFixed should handle number correctly if decimal is null', () => {
expect(toFixed(100)).toBe('100');

View File

@@ -45,10 +45,15 @@ export function toFixed(value: number, decimals?: DecimalCount): string {
if (value === null) {
return '';
}
if (value === Number.NEGATIVE_INFINITY || value === Number.POSITIVE_INFINITY) {
return value.toLocaleString();
}
if (decimals === null || decimals === undefined) {
decimals = getDecimalsForValue(value);
}
const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
const formatted = String(Math.round(value * factor) / factor);
@@ -57,31 +62,37 @@ export function toFixed(value: number, decimals?: DecimalCount): string {
return formatted;
}
// If tickDecimals was specified, ensure that we have exactly that
// much precision; otherwise default to the value's own precision.
if (decimals != null) {
const decimalPos = formatted.indexOf('.');
const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
if (precision < decimals) {
return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision);
}
const decimalPos = formatted.indexOf('.');
const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
if (precision < decimals) {
return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision);
}
return formatted;
}
export function toFixedScaled(
value: number,
decimals: DecimalCount,
scaledDecimals: DecimalCount,
additionalDecimals: number,
ext?: string
): FormattedValue {
if (scaledDecimals === null || scaledDecimals === undefined) {
return { text: toFixed(value, decimals), suffix: ext };
function getDecimalsForValue(value: number): number {
const log10 = Math.floor(Math.log(Math.abs(value)) / Math.LN10);
let dec = -log10 + 1;
const magn = Math.pow(10, -dec);
const norm = value / magn; // norm is between 1.0 and 10.0
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
++dec;
}
if (value % 1 === 0) {
dec = 0;
}
const decimals = Math.max(0, dec);
return decimals;
}
export function toFixedScaled(value: number, decimals: DecimalCount, ext?: string): FormattedValue {
return {
text: toFixed(value, scaledDecimals + additionalDecimals),
text: toFixed(value, decimals),
suffix: ext,
};
}
@@ -126,10 +137,6 @@ export function scaledUnits(factor: number, extArray: string[]): ValueFormatter
}
}
if (steps > 0 && scaledDecimals !== null && scaledDecimals !== undefined) {
decimals = scaledDecimals + 3 * steps;
}
return { text: toFixed(size, decimals), suffix: extArray[steps] };
};
}

View File

@@ -1,6 +1,9 @@
import { MutableVector } from '../types/vector';
import { FunctionalVector } from './FunctionalVector';
/**
* @public
*/
export class ArrayVector<T = any> extends FunctionalVector<T> implements MutableVector<T> {
buffer: T[];

View File

@@ -0,0 +1,21 @@
import { Vector } from '../types';
import { FunctionalVector } from './FunctionalVector';
/**
* This will force all values to be numbers
*
* @public
*/
export class AsNumberVector extends FunctionalVector<number> {
constructor(private field: Vector) {
super();
}
get length() {
return this.field.length;
}
get(index: number) {
return +this.field.get(index);
}
}

View File

@@ -2,6 +2,9 @@ import { Vector } from '../types/vector';
import { vectorToArray } from './vectorToArray';
import { BinaryOperation } from '../utils/binaryOperators';
/**
* @public
*/
export class BinaryOperationVector implements Vector<number> {
constructor(private left: Vector<number>, private right: Vector<number>, private operation: BinaryOperation) {}

View File

@@ -14,6 +14,8 @@ interface CircularOptions<T> {
*
* This supports adding to the 'head' or 'tail' and will grow the buffer
* to match a configured capacity.
*
* @public
*/
export class CircularVector<T = any> extends FunctionalVector<T> implements MutableVector<T> {
private buffer: T[];

View File

@@ -1,5 +1,8 @@
import { Vector } from '../types/vector';
/**
* @public
*/
export class ConstantVector<T = any> implements Vector<T> {
constructor(private value: T, private len: number) {}

View File

@@ -1,10 +1,15 @@
import { Vector } from '../types/vector';
import { DisplayProcessor } from '../types';
import { formattedValueToString } from '../valueFormats';
import { vectorToArray } from './vectorToArray';
import { FunctionalVector } from './FunctionalVector';
export class FormattedVector<T = any> implements Vector<string> {
constructor(private source: Vector<T>, private formatter: DisplayProcessor) {}
/**
* @public
*/
export class FormattedVector<T = any> extends FunctionalVector<string> {
constructor(private source: Vector<T>, private formatter: DisplayProcessor) {
super();
}
get length() {
return this.source.length;
@@ -14,12 +19,4 @@ export class FormattedVector<T = any> implements Vector<string> {
const v = this.source.get(index);
return formattedValueToString(this.formatter(v));
}
toArray(): string[] {
return vectorToArray(this);
}
toJSON(): string[] {
return this.toArray();
}
}

View File

@@ -6,5 +6,6 @@ export * from './BinaryOperationVector';
export * from './SortedVector';
export * from './FormattedVector';
export * from './IndexVector';
export * from './AsNumberVector';
export { vectorator } from './FunctionalVector';

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "7.4.0-pre.0",
"version": "7.4.5",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "7.4.0-pre.0",
"version": "7.4.5",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -44,12 +44,12 @@
"types": "src/index.ts",
"dependencies": {
"@cypress/webpack-preprocessor": "4.1.3",
"@grafana/e2e-selectors": "7.4.0-pre.0",
"@grafana/e2e-selectors": "7.4.5",
"@grafana/tsconfig": "^1.0.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"blink-diff": "1.0.13",
"commander": "5.0.0",
"cypress": "^4.12.1",
"cypress": "^6.3.0",
"cypress-file-upload": "^4.0.7",
"execa": "4.0.0",
"resolve-as-bin": "2.1.0",

View File

@@ -37,7 +37,6 @@ interface AddVariableRequired {
export type PartialAddVariableConfig = Partial<AddVariableDefault> & AddVariableOptional & AddVariableRequired;
export type AddVariableConfig = AddVariableDefault & AddVariableOptional & AddVariableRequired;
// @todo this actually returns type `Cypress.Chainable<AddDashboardConfig>`
export const addDashboard = (config?: Partial<AddDashboardConfig>) => {
const fullConfig: AddDashboardConfig = {
annotations: [],
@@ -64,7 +63,7 @@ export const addDashboard = (config?: Partial<AddDashboardConfig>) => {
fullConfig.variables = addVariables(variables);
e2e.components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
}
setDashboardTimeRange(timeRange);
@@ -152,29 +151,32 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar
e2e.pages.Dashboard.Settings.Variables.List.newButton().click();
}
const { constantValue, dataSource, hide, label, name, query, regex, type } = fullConfig;
const { constantValue, dataSource, label, name, query, regex, type } = fullConfig;
// This field is key to many reactive changes
if (type !== VARIABLE_TYPE_QUERY) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().select(type);
}
// Avoid '', which is an accepted value
if (hide !== undefined) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().select(hide);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect()
.should('be.visible')
.within(() => {
e2e.components.Select.singleValue().should('have.text', 'Query').click().type(`${type}{enter}`);
});
}
if (label) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label);
}
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().type(name);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type(name);
if (
dataSource &&
(type === VARIABLE_TYPE_AD_HOC_FILTERS || type === VARIABLE_TYPE_DATASOURCE || type === VARIABLE_TYPE_QUERY)
) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().select(dataSource);
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').type(`${dataSource}{enter}`);
});
}
if (constantValue && type === VARIABLE_TYPE_CONSTANT) {
@@ -193,13 +195,12 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar
// Avoid flakiness
e2e().focused().blur();
e2e()
.contains('.gf-form-group', 'Preview of values')
.within(() => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
.should('exist')
.within((previewOfValues) => {
if (type === VARIABLE_TYPE_CONSTANT) {
e2e()
.root()
.contains(constantValue as string);
expect(previewOfValues.text()).equals(constantValue);
}
});

View File

@@ -13,6 +13,7 @@ export interface AddDataSourceConfig {
name: string;
skipTlsVerify: boolean;
type: string;
timeout?: number;
}
// @todo this actually returns type `Cypress.Chainable<AddDaaSourceConfig>`
@@ -40,6 +41,7 @@ export const addDataSource = (config?: Partial<AddDataSourceConfig>) => {
name,
skipTlsVerify,
type,
timeout,
} = fullConfig;
e2e().logToConsole('Adding data source with name:', name);
@@ -75,8 +77,13 @@ export const addDataSource = (config?: Partial<AddDataSourceConfig>) => {
form();
e2e.pages.DataSource.saveAndTest().click();
e2e.pages.DataSource.alert().should('exist').contains(expectedAlertMessage); // assertion
// use the timeout passed in if it exists, otherwise, continue to use the default
e2e.pages.DataSource.alert()
.should('exist')
.contains(expectedAlertMessage, {
timeout: timeout ?? e2e.config().defaultCommandTimeout,
});
e2e().logToConsole('Added data source with name:', name);
return e2e()

View File

@@ -34,6 +34,7 @@ interface ConfigurePanelOptional {
panelTitle?: string;
timeRange?: TimeRangeConfig;
visualizationName?: string;
matchExploreTable?: boolean;
}
interface ConfigurePanelRequired {
@@ -75,6 +76,7 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
dataSourceName,
isEdit,
isExplore,
matchExploreTable,
matchScreenshot,
panelTitle,
queriesForm,
@@ -200,7 +202,7 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
let visualization;
if (isExplore) {
visualization = e2e.pages.Explore.General.graph();
visualization = matchExploreTable ? e2e.pages.Explore.General.table() : e2e.pages.Explore.General.graph();
} else {
visualization = e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content');
}

View File

@@ -6,7 +6,7 @@
import { e2eScenario, ScenarioArguments } from './support/scenario';
import { getScenarioContext, setScenarioContext } from './support/scenarioContext';
import { e2eFactory } from './support';
import { selectors } from '@grafana/e2e-selectors';
import { E2ESelectors, Selectors, selectors } from '@grafana/e2e-selectors';
import * as flows from './flows';
import * as typings from './typings';
@@ -22,6 +22,7 @@ const e2eObject = {
flows,
getScenarioContext,
setScenarioContext,
getSelectors: <T extends Selectors>(selectors: E2ESelectors<T>) => e2eFactory({ selectors }),
};
export const e2e: (() => Cypress.cy) & typeof e2eObject = Object.assign(() => cy, e2eObject);

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "7.4.0-pre.0",
"version": "7.4.5",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -22,8 +22,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "7.4.0-pre.0",
"@grafana/ui": "7.4.0-pre.0",
"@grafana/data": "7.4.5",
"@grafana/ui": "7.4.5",
"systemjs": "0.20.19",
"systemjs-plugin-css": "0.1.37"
},

View File

@@ -68,6 +68,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
sampleRate: 1,
};
marketplaceUrl?: string;
expressionsEnabled = false;
constructor(options: GrafanaBootConfig) {
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);

View File

@@ -28,12 +28,31 @@ export interface DataSourceSrv {
/** @public */
export interface GetDataSourceListFilters {
/** Include mixed deta source by setting this to true */
mixed?: boolean;
/** Only return data sources that support metrics response */
metrics?: boolean;
/** Only return data sources that support tracing response */
tracing?: boolean;
/** Only return data sources that support annotations */
annotations?: boolean;
/**
* By default only data sources that can be queried will be returned. Meaning they have tracing,
* metrics, logs or annotations flag set in plugin.json file
* */
all?: boolean;
/** Set to true to return dashboard data source */
dashboard?: boolean;
/** Set to true to return data source variables */
variables?: boolean;
/** filter list by plugin */
pluginId?: string;
}

View File

@@ -43,7 +43,7 @@ get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-l
"b4138199aa755ebfe171b57cc46910b13258ace5fbc4eaa099c42607cd0bff32"
chmod +x /usr/local/bin/cc-test-reporter
curl -fL -o /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.33/grabpl"
curl -fL -o /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.36/grabpl"
apk add --no-cache git
# Install Mage

View File

@@ -44,7 +44,7 @@ get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-l
"b4138199aa755ebfe171b57cc46910b13258ace5fbc4eaa099c42607cd0bff32"
chmod 755 /usr/local/bin/cc-test-reporter
wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.33/grabpl"
wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.36/grabpl"
chmod +x /usr/local/bin/grabpl
# Install Mage

View File

@@ -27,7 +27,7 @@ get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-l
"b4138199aa755ebfe171b57cc46910b13258ace5fbc4eaa099c42607cd0bff32"
chmod +x /usr/local/bin/cc-test-reporter
wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.33/grabpl"
wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.36/grabpl"
chmod +x /usr/local/bin/grabpl
# Install Mage

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "7.4.0-pre.0",
"version": "7.4.5",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -28,10 +28,10 @@
"dependencies": {
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.0",
"@grafana/data": "next",
"@grafana/data": "7.4.5",
"@grafana/eslint-config": "2.1.0",
"@grafana/tsconfig": "^1.0.0-rc1",
"@grafana/ui": "next",
"@grafana/ui": "7.4.5",
"@types/command-exists": "^1.2.0",
"@types/execa": "^0.9.0",
"@types/expect-puppeteer": "3.3.1",
@@ -56,7 +56,7 @@
"command-exists": "^1.2.8",
"commander": "^5.0.0",
"concurrently": "4.1.0",
"copy-webpack-plugin": "5.1.1",
"copy-webpack-plugin": "5.1.2",
"css-loader": "3.4.2",
"eslint": "7.4.0",
"eslint-config-prettier": "7.2.0",
@@ -100,7 +100,7 @@
"semver": "^7.1.3",
"simple-git": "^1.132.0",
"style-loader": "1.1.3",
"terser-webpack-plugin": "2.3.5",
"terser-webpack-plugin": "2.3.7",
"ts-jest": "26.4.4",
"ts-loader": "6.2.1",
"ts-node": "9.0.0",

View File

@@ -13,7 +13,7 @@ async function* walk(dir: string, baseDir: string): AsyncGenerator<string, any,
} else if (d.isFile()) {
yield path.posix.relative(baseDir, entry);
} else if (d.isSymbolicLink()) {
const realPath = fs.realpathSync(entry);
const realPath = await (fs.promises as any).realpath(entry);
if (!realPath.startsWith(baseDir)) {
throw new Error(
`symbolic link ${path.posix.relative(
@@ -22,7 +22,11 @@ async function* walk(dir: string, baseDir: string): AsyncGenerator<string, any,
)} targets a file outside of the base directory: ${baseDir}`
);
}
yield path.posix.relative(baseDir, entry);
// if resolved symlink target is a file include it in the manifest
const stats = await (fs.promises as any).stat(realPath);
if (stats.isFile()) {
yield path.posix.relative(baseDir, entry);
}
}
}
}

View File

@@ -15,7 +15,6 @@ import lightTheme from '../../../public/sass/grafana.light.scss';
// @ts-ignore
import darkTheme from '../../../public/sass/grafana.dark.scss';
import { GrafanaLight, GrafanaDark } from './storybookTheme';
import { configure } from '@storybook/react';
import addons from '@storybook/addons';
const handleThemeChange = (theme: any) => {

View File

@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "7.4.0-pre.0",
"version": "7.4.5",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -27,20 +27,20 @@
},
"dependencies": {
"@emotion/core": "^10.0.27",
"@grafana/data": "7.4.0-pre.0",
"@grafana/e2e-selectors": "7.4.0-pre.0",
"@grafana/data": "7.4.5",
"@grafana/e2e-selectors": "7.4.5",
"@grafana/slate-react": "0.22.9-grafana",
"@grafana/tsconfig": "^1.0.0-rc1",
"@iconscout/react-unicons": "1.1.4",
"@popperjs/core": "2.5.4",
"@sentry/browser": "5.25.0",
"@testing-library/jest-dom": "5.11.9",
"@torkelo/react-select": "3.0.8",
"@types/hoist-non-react-statics": "3.3.1",
"@types/react-beautiful-dnd": "12.1.2",
"@types/react-color": "3.0.1",
"@types/react-select": "3.0.8",
"@types/react-table": "7.0.12",
"@testing-library/jest-dom": "5.11.9",
"@sentry/browser": "5.25.0",
"@types/slate": "0.47.1",
"@types/slate-react": "0.22.5",
"classnames": "2.2.6",
@@ -72,14 +72,14 @@
"react-transition-group": "4.4.1",
"slate": "0.47.8",
"tinycolor2": "1.4.1",
"uplot": "1.6.1"
"uplot": "1.6.4"
},
"devDependencies": {
"@rollup/plugin-commonjs": "16.0.0",
"@rollup/plugin-image": "2.0.5",
"@rollup/plugin-node-resolve": "10.0.0",
"@storybook/addon-essentials": "6.1.9",
"@storybook/addon-controls": "6.1.9",
"@storybook/addon-essentials": "6.1.9",
"@storybook/addon-knobs": "6.1.9",
"@storybook/addon-storysource": "6.1.9",
"@storybook/react": "6.1.9",
@@ -103,8 +103,8 @@
"mock-raf": "1.0.1",
"pretty-format": "25.1.0",
"react-docgen-typescript-loader": "3.7.2",
"react-test-renderer": "16.13.1",
"react-is": "16.8.0",
"react-test-renderer": "16.13.1",
"rollup": "2.33.3",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-terser": "7.0.2",

View File

@@ -0,0 +1,65 @@
import { toDataFrame, FieldType, VizOrientation } from '@grafana/data';
import React from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { BarChart } from './BarChart';
import { LegendDisplayMode } from '../VizLegend/types';
import { prepDataForStorybook } from '../../utils/storybook/data';
import { useTheme } from '../../themes';
import { select } from '@storybook/addon-knobs';
import { BarChartOptions, BarStackingMode, BarValueVisibility } from './types';
export default {
title: 'Visualizations/BarChart',
component: BarChart,
decorators: [withCenteredStory],
parameters: {
docs: {},
},
};
const getKnobs = () => {
return {
legendPlacement: select(
'Legend placement',
{
bottom: 'bottom',
right: 'right',
},
'bottom'
),
orientation: select(
'Bar orientation',
{
vertical: VizOrientation.Vertical,
horizontal: VizOrientation.Horizontal,
},
VizOrientation.Vertical
),
};
};
export const Basic: React.FC = () => {
const { legendPlacement, orientation } = getKnobs();
const theme = useTheme();
const frame = toDataFrame({
fields: [
{ name: 'x', type: FieldType.string, values: ['group 1', 'group 2'] },
{ name: 'a', type: FieldType.number, values: [10, 20] },
{ name: 'b', type: FieldType.number, values: [30, 10] },
],
});
const data = prepDataForStorybook([frame], theme);
const options: BarChartOptions = {
orientation: orientation,
legend: { displayMode: LegendDisplayMode.List, placement: legendPlacement, calcs: [] },
stacking: BarStackingMode.None,
showValue: BarValueVisibility.Always,
barWidth: 0.97,
groupWidth: 0.7,
};
return <BarChart data={data[0]} width={600} height={400} theme={theme} {...options} />;
};

View File

@@ -0,0 +1,318 @@
import React, { useCallback, useMemo, useRef } from 'react';
import {
compareDataFrameStructures,
DataFrame,
DefaultTimeZone,
formattedValueToString,
getFieldDisplayName,
getFieldSeriesColor,
getFieldColorModeForField,
TimeRange,
VizOrientation,
fieldReducers,
reduceField,
DisplayValue,
} from '@grafana/data';
import { VizLayout } from '../VizLayout/VizLayout';
import { Themeable } from '../../types';
import { useRevision } from '../uPlot/hooks';
import { UPlotChart } from '../uPlot/Plot';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '../uPlot/config';
import { useTheme } from '../../themes';
import { GraphNGLegendEvent, GraphNGLegendEventMode } from '../GraphNG/types';
import { FIXED_UNIT } from '../GraphNG/GraphNG';
import { LegendDisplayMode, VizLegendItem } from '../VizLegend/types';
import { VizLegend } from '../VizLegend/VizLegend';
import { BarChartFieldConfig, BarChartOptions, BarValueVisibility, defaultBarChartFieldConfig } from './types';
import { BarsOptions, getConfig } from './bars';
/**
* @alpha
*/
export interface Props extends Themeable, BarChartOptions {
height: number;
width: number;
data: DataFrame;
onLegendClick?: (event: GraphNGLegendEvent) => void;
onSeriesColorChange?: (label: string, color: string) => void;
}
/**
* @alpha
*/
export const BarChart: React.FunctionComponent<Props> = ({
width,
height,
data,
orientation,
groupWidth,
barWidth,
showValue,
legend,
onLegendClick,
onSeriesColorChange,
...plotProps
}) => {
if (!data || data.fields.length < 2) {
return <div>Missing data</div>;
}
// dominik? TODO? can this all be moved into `useRevision`
const compareFrames = useCallback((a?: DataFrame | null, b?: DataFrame | null) => {
if (a && b) {
return compareDataFrameStructures(a, b);
}
return false;
}, []);
const configRev = useRevision(data, compareFrames);
const theme = useTheme();
// Updates only when the structure changes
const configBuilder = useMemo(() => {
if (!orientation || orientation === VizOrientation.Auto) {
orientation = width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
}
// bar orientation -> x scale orientation & direction
let xOri: ScaleOrientation, xDir: ScaleDirection, yOri: ScaleOrientation, yDir: ScaleDirection;
if (orientation === VizOrientation.Vertical) {
xOri = ScaleOrientation.Horizontal;
xDir = ScaleDirection.Right;
yOri = ScaleOrientation.Vertical;
yDir = ScaleDirection.Up;
} else {
xOri = ScaleOrientation.Vertical;
xDir = ScaleDirection.Down;
yOri = ScaleOrientation.Horizontal;
yDir = ScaleDirection.Right;
}
const formatValue =
showValue !== BarValueVisibility.Never
? (seriesIdx: number, value: any) => formattedValueToString(data.fields[seriesIdx].display!(value))
: undefined;
// Use bar width when only one field
if (data.fields.length === 2) {
groupWidth = barWidth;
barWidth = 1;
}
const opts: BarsOptions = {
xOri,
xDir,
groupWidth,
barWidth,
formatValue,
onHover: (seriesIdx: number, valueIdx: number) => {
console.log('hover', { seriesIdx, valueIdx });
},
onLeave: (seriesIdx: number, valueIdx: number) => {
console.log('leave', { seriesIdx, valueIdx });
},
};
const config = getConfig(opts);
const builder = new UPlotConfigBuilder();
builder.addHook('init', config.init);
builder.addHook('drawClear', config.drawClear);
builder.addHook('setCursor', config.setCursor);
builder.setCursor(config.cursor);
builder.setSelect(config.select);
builder.addScale({
scaleKey: 'x',
isTime: false,
distribution: ScaleDistribution.Ordinal,
orientation: xOri,
direction: xDir,
});
builder.addAxis({
scaleKey: 'x',
isTime: false,
placement: xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left,
splits: config.xSplits,
values: config.xValues,
grid: false,
ticks: false,
gap: 15,
theme,
});
let seriesIndex = 0;
// iterate the y values
for (let i = 1; i < data.fields.length; i++) {
const field = data.fields[i];
field.state!.seriesIndex = seriesIndex++;
const customConfig: BarChartFieldConfig = { ...defaultBarChartFieldConfig, ...field.config.custom };
const scaleKey = field.config.unit || FIXED_UNIT;
const colorMode = getFieldColorModeForField(field);
const scaleColor = getFieldSeriesColor(field, theme);
const seriesColor = scaleColor.color;
builder.addSeries({
scaleKey,
pxAlign: false,
lineWidth: customConfig.lineWidth,
lineColor: seriesColor,
//lineStyle: customConfig.lineStyle,
fillOpacity: customConfig.fillOpacity,
theme,
colorMode,
pathBuilder: config.drawBars,
pointsBuilder: config.drawPoints,
show: !customConfig.hideFrom?.graph,
gradientMode: customConfig.gradientMode,
thresholds: field.config.thresholds,
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
dataFrameFieldIndex: {
fieldIndex: i,
frameIndex: 0,
},
fieldName: getFieldDisplayName(field, data),
hideInLegend: customConfig.hideFrom?.legend,
});
// The builder will manage unique scaleKeys and combine where appropriate
builder.addScale({
scaleKey,
min: field.config.min,
max: field.config.max,
softMin: customConfig.axisSoftMin,
softMax: customConfig.axisSoftMax,
orientation: yOri,
direction: yDir,
});
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
let placement = customConfig.axisPlacement;
if (!placement || placement === AxisPlacement.Auto) {
placement = AxisPlacement.Left;
}
if (xOri === 1) {
if (placement === AxisPlacement.Left) {
placement = AxisPlacement.Bottom;
}
if (placement === AxisPlacement.Right) {
placement = AxisPlacement.Top;
}
}
builder.addAxis({
scaleKey,
label: customConfig.axisLabel,
size: customConfig.axisWidth,
placement,
formatValue: (v) => formattedValueToString(field.display!(v)),
theme,
});
}
}
return builder;
}, [data, configRev, orientation, width, height]);
const onLabelClick = useCallback(
(legend: VizLegendItem, event: React.MouseEvent) => {
const { fieldIndex } = legend;
if (!onLegendClick || !fieldIndex) {
return;
}
onLegendClick({
fieldIndex,
mode: GraphNGLegendEventMode.AppendToSelection,
});
},
[onLegendClick, data]
);
const hasLegend = useRef(legend && legend.displayMode !== LegendDisplayMode.Hidden);
const legendItems = configBuilder
.getSeries()
.map<VizLegendItem | undefined>((s) => {
const seriesConfig = s.props;
const fieldIndex = seriesConfig.dataFrameFieldIndex;
if (seriesConfig.hideInLegend || !fieldIndex) {
return undefined;
}
const field = data.fields[fieldIndex.fieldIndex];
if (!field) {
return undefined;
}
return {
disabled: !seriesConfig.show ?? false,
fieldIndex,
color: seriesConfig.lineColor!,
label: seriesConfig.fieldName,
yAxis: 1,
getDisplayValues: () => {
if (!legend.calcs?.length) {
return [];
}
const fieldCalcs = reduceField({
field,
reducers: legend.calcs,
});
return legend.calcs.map<DisplayValue>((reducer) => {
return {
...field.display!(fieldCalcs[reducer]),
title: fieldReducers.get(reducer).name,
};
});
},
};
})
.filter((i) => i !== undefined) as VizLegendItem[];
let legendElement: React.ReactElement | undefined;
if (hasLegend && legendItems.length > 0) {
legendElement = (
<VizLayout.Legend position={legend.placement} maxHeight="35%" maxWidth="60%">
<VizLegend
onLabelClick={onLabelClick}
placement={legend.placement}
items={legendItems}
displayMode={legend.displayMode}
onSeriesColorChange={onSeriesColorChange}
/>
</VizLayout.Legend>
);
}
return (
<VizLayout width={width} height={height} legend={legendElement}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart
data={data}
config={configBuilder}
width={vizWidth}
height={vizHeight}
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
timeZone={DefaultTimeZone}
/>
)}
</VizLayout>
);
};

View File

@@ -0,0 +1,294 @@
import uPlot, { Axis, Series, Cursor, BBox } from 'uplot';
import { Quadtree, Rect, pointWithin } from './quadtree';
import { distribute, SPACE_BETWEEN } from './distribute';
const pxRatio = devicePixelRatio;
const groupDistr = SPACE_BETWEEN;
const barDistr = SPACE_BETWEEN;
const font = Math.round(10 * pxRatio) + 'px Arial';
type WalkTwoCb = null | ((idx: number, offPx: number, dimPx: number) => void);
function walkTwo(
groupWidth: number,
barWidth: number,
yIdx: number,
xCount: number,
yCount: number,
xDim: number,
xDraw?: WalkTwoCb,
yDraw?: WalkTwoCb
) {
distribute(xCount, groupWidth, groupDistr, null, (ix, offPct, dimPct) => {
let groupOffPx = xDim * offPct;
let groupWidPx = xDim * dimPct;
xDraw && xDraw(ix, groupOffPx, groupWidPx);
yDraw &&
distribute(yCount, barWidth, barDistr, yIdx, (iy, offPct, dimPct) => {
let barOffPx = groupWidPx * offPct;
let barWidPx = groupWidPx * dimPct;
yDraw(ix, groupOffPx + barOffPx, barWidPx);
});
});
}
/**
* @internal
*/
export interface BarsOptions {
xOri: 1 | 0;
xDir: 1 | -1;
groupWidth: number;
barWidth: number;
formatValue?: (seriesIdx: number, value: any) => string;
onHover?: (seriesIdx: number, valueIdx: any) => void;
onLeave?: (seriesIdx: number, valueIdx: any) => void;
}
/**
* @internal
*/
export function getConfig(opts: BarsOptions) {
const { xOri: ori, xDir: dir, groupWidth, barWidth, formatValue, onHover, onLeave } = opts;
let qt: Quadtree;
const drawBars: Series.PathBuilder = (u, sidx, i0, i1) => {
return uPlot.orient(
u,
sidx,
(series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect) => {
const fill = new Path2D();
const stroke = new Path2D();
let numGroups = dataX.length;
let barsPerGroup = u.series.length - 1;
let y0Pos = valToPosY(0, scaleY, yDim, yOff);
const _dir = dir * (ori === 0 ? 1 : -1);
walkTwo(groupWidth, barWidth, sidx - 1, numGroups, barsPerGroup, xDim, null, (ix, x0, wid) => {
let lft = Math.round(xOff + (_dir === 1 ? x0 : xDim - x0 - wid));
let barWid = Math.round(wid);
if (dataY[ix] != null) {
let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff);
let btm = Math.round(Math.max(yPos, y0Pos));
let top = Math.round(Math.min(yPos, y0Pos));
let barHgt = btm - top;
let strokeWidth = series.width || 0;
if (strokeWidth) {
rect(stroke, lft + strokeWidth / 2, top + strokeWidth / 2, barWid - strokeWidth, barHgt - strokeWidth);
}
rect(fill, lft, top, barWid, barHgt);
let x = ori === 0 ? Math.round(lft - xOff) : Math.round(top - yOff);
let y = ori === 0 ? Math.round(top - yOff) : Math.round(lft - xOff);
let w = ori === 0 ? barWid : barHgt;
let h = ori === 0 ? barHgt : barWid;
qt.add({ x, y, w, h, sidx: sidx, didx: ix });
}
});
return {
stroke,
fill,
};
}
);
};
const drawPoints: Series.Points.Show =
formatValue == null
? false
: (u, sidx, i0, i1) => {
u.ctx.font = font;
u.ctx.fillStyle = 'white';
uPlot.orient(
u,
sidx,
(
series,
dataX,
dataY,
scaleX,
scaleY,
valToPosX,
valToPosY,
xOff,
yOff,
xDim,
yDim,
moveTo,
lineTo,
rect
) => {
let numGroups = dataX.length;
let barsPerGroup = u.series.length - 1;
const _dir = dir * (ori === 0 ? 1 : -1);
walkTwo(groupWidth, barWidth, sidx - 1, numGroups, barsPerGroup, xDim, null, (ix, x0, wid) => {
let lft = Math.round(xOff + (_dir === 1 ? x0 : xDim - x0 - wid));
let barWid = Math.round(wid);
// prettier-ignore
if (dataY[ix] != null) {
let yPos = valToPosY(dataY[ix]!, scaleY, yDim, yOff);
/* eslint-disable no-multi-spaces */
let x = ori === 0 ? Math.round(lft + barWid / 2) : Math.round(yPos);
let y = ori === 0 ? Math.round(yPos) : Math.round(lft + barWid / 2);
u.ctx.textAlign = ori === 0 ? 'center' : dataY[ix]! >= 0 ? 'left' : 'right';
u.ctx.textBaseline = ori === 1 ? 'middle' : dataY[ix]! >= 0 ? 'bottom' : 'top';
/* eslint-enable */
u.ctx.fillText(formatValue(sidx, dataY[ix]), x, y);
}
});
}
);
return false;
};
/*
const yRange: Scale.Range = (u, dataMin, dataMax) => {
// @ts-ignore
let [min, max] = uPlot.rangeNum(0, dataMax, 0.05, true);
return [0, max];
};
*/
const xSplits: Axis.Splits = (u: uPlot, axisIdx: number) => {
const dim = ori === 0 ? u.bbox.width : u.bbox.height;
const _dir = dir * (ori === 0 ? 1 : -1);
let splits: number[] = [];
distribute(u.data[0].length, groupWidth, groupDistr, null, (di, lftPct, widPct) => {
let groupLftPx = (dim * lftPct) / pxRatio;
let groupWidPx = (dim * widPct) / pxRatio;
let groupCenterPx = groupLftPx + groupWidPx / 2;
splits.push(u.posToVal(groupCenterPx, 'x'));
});
return _dir === 1 ? splits : splits.reverse();
};
// @ts-ignore
const xValues: Axis.Values = (u) => u.data[0];
let hovered: Rect | null = null;
let barMark = document.createElement('div');
barMark.classList.add('bar-mark');
barMark.style.position = 'absolute';
barMark.style.background = 'rgba(255,255,255,0.4)';
// hide crosshair cursor & hover points
const cursor: Cursor = {
x: false,
y: false,
points: {
show: false,
},
};
// disable selection
// uPlot types do not export the Select interface prior to 1.6.4
const select: Partial<BBox> = {
show: false,
};
const init = (u: uPlot) => {
let over = u.root.querySelector('.u-over')! as HTMLElement;
over.style.overflow = 'hidden';
over.appendChild(barMark);
};
const drawClear = (u: uPlot) => {
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
qt.clear();
// clear the path cache to force drawBars() to rebuild new quadtree
u.series.forEach((s) => {
// @ts-ignore
s._paths = null;
});
};
// handle hover interaction with quadtree probing
const setCursor = (u: uPlot) => {
let found: Rect | null = null;
let cx = u.cursor.left! * pxRatio;
let cy = u.cursor.top! * pxRatio;
qt.get(cx, cy, 1, 1, (o) => {
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
found = o;
}
});
if (found) {
// prettier-ignore
if (found !== hovered) {
/* eslint-disable no-multi-spaces */
barMark.style.display = '';
barMark.style.left = found!.x / pxRatio + 'px';
barMark.style.top = found!.y / pxRatio + 'px';
barMark.style.width = found!.w / pxRatio + 'px';
barMark.style.height = found!.h / pxRatio + 'px';
hovered = found;
/* eslint-enable */
if (onHover != null) {
onHover(hovered!.sidx, hovered!.didx);
}
}
} else if (hovered != null) {
if (onLeave != null) {
onLeave(hovered!.sidx, hovered!.didx);
}
hovered = null;
barMark.style.display = 'none';
}
};
return {
// cursor & select opts
cursor,
select,
// scale & axis opts
// yRange,
xValues,
xSplits,
// pathbuilders
drawBars,
drawPoints,
// hooks
init,
drawClear,
setCursor,
};
}

View File

@@ -0,0 +1,49 @@
function roundDec(val: number, dec: number) {
return Math.round(val * (dec = 10 ** dec)) / dec;
}
export const SPACE_BETWEEN = 1;
export const SPACE_AROUND = 2;
export const SPACE_EVENLY = 3;
const coord = (i: number, offs: number, iwid: number, gap: number) => roundDec(offs + i * (iwid + gap), 6);
export type Each = (idx: number, offPct: number, dimPct: number) => void;
/**
* @internal
*/
export function distribute(numItems: number, sizeFactor: number, justify: number, onlyIdx: number | null, each: Each) {
let space = 1 - sizeFactor;
/* eslint-disable no-multi-spaces */
// prettier-ignore
let gap = (
justify === SPACE_BETWEEN ? space / (numItems - 1) :
justify === SPACE_AROUND ? space / (numItems ) :
justify === SPACE_EVENLY ? space / (numItems + 1) : 0
);
if (isNaN(gap) || gap === Infinity) {
gap = 0;
}
// prettier-ignore
let offs = (
justify === SPACE_BETWEEN ? 0 :
justify === SPACE_AROUND ? gap / 2 :
justify === SPACE_EVENLY ? gap : 0
);
/* eslint-enable */
let iwid = sizeFactor / numItems;
let _iwid = roundDec(iwid, 6);
if (onlyIdx == null) {
for (let i = 0; i < numItems; i++) {
each(i, coord(i, offs, iwid, gap), _iwid);
}
} else {
each(onlyIdx, coord(onlyIdx, offs, iwid, gap), _iwid);
}
}

View File

@@ -0,0 +1,113 @@
const MAX_OBJECTS = 10;
const MAX_LEVELS = 4;
export type Quads = [Quadtree, Quadtree, Quadtree, Quadtree];
export type Rect = { x: number; y: number; w: number; h: number; [_: string]: any };
/**
* @internal
*/
export function pointWithin(px: number, py: number, rlft: number, rtop: number, rrgt: number, rbtm: number) {
return px >= rlft && px <= rrgt && py >= rtop && py <= rbtm;
}
/**
* @internal
*/
export class Quadtree {
o: Rect[];
q: Quads | null;
constructor(public x: number, public y: number, public w: number, public h: number, public l: number = 0) {
this.o = [];
this.q = null;
}
split() {
let t = this,
x = t.x,
y = t.y,
w = t.w / 2,
h = t.h / 2,
l = t.l + 1;
t.q = [
// top right
new Quadtree(x + w, y, w, h, l),
// top left
new Quadtree(x, y, w, h, l),
// bottom left
new Quadtree(x, y + h, w, h, l),
// bottom right
new Quadtree(x + w, y + h, w, h, l),
];
}
// invokes callback with index of each overlapping quad
quads(x: number, y: number, w: number, h: number, cb: (q: Quadtree) => void) {
let t = this,
q = t.q!,
hzMid = t.x + t.w / 2,
vtMid = t.y + t.h / 2,
startIsNorth = y < vtMid,
startIsWest = x < hzMid,
endIsEast = x + w > hzMid,
endIsSouth = y + h > vtMid;
// top-right quad
startIsNorth && endIsEast && cb(q[0]);
// top-left quad
startIsWest && startIsNorth && cb(q[1]);
// bottom-left quad
startIsWest && endIsSouth && cb(q[2]);
// bottom-right quad
endIsEast && endIsSouth && cb(q[3]);
}
add(o: Rect) {
let t = this;
if (t.q != null) {
t.quads(o.x, o.y, o.w, o.h, (q) => {
q.add(o);
});
} else {
let os = t.o;
os.push(o);
if (os.length > MAX_OBJECTS && t.l < MAX_LEVELS) {
t.split();
for (let i = 0; i < os.length; i++) {
let oi = os[i];
t.quads(oi.x, oi.y, oi.w, oi.h, (q) => {
q.add(oi);
});
}
t.o.length = 0;
}
}
}
get(x: number, y: number, w: number, h: number, cb: (o: Rect) => void) {
let t = this;
let os = t.o;
for (let i = 0; i < os.length; i++) {
cb(os[i]);
}
if (t.q != null) {
t.quads(x, y, w, h, (q) => {
q.get(x, y, w, h, cb);
});
}
}
clear() {
this.o.length = 0;
this.q = null;
}
}

View File

@@ -0,0 +1,52 @@
import { VizOrientation } from '@grafana/data';
import { AxisConfig, GraphGradientMode, HideableFieldConfig } from '../uPlot/config';
import { VizLegendOptions } from '../VizLegend/types';
/**
* @alpha
*/
export enum BarStackingMode {
None = 'none',
Standard = 'standard',
Percent = 'percent',
}
/**
* @alpha
*/
export enum BarValueVisibility {
Auto = 'auto',
Never = 'never',
Always = 'always',
}
/**
* @alpha
*/
export interface BarChartOptions {
orientation: VizOrientation;
legend: VizLegendOptions;
stacking: BarStackingMode;
showValue: BarValueVisibility;
barWidth: number;
groupWidth: number;
}
/**
* @alpha
*/
export interface BarChartFieldConfig extends AxisConfig, HideableFieldConfig {
lineWidth?: number; // 0
fillOpacity?: number; // 100
gradientMode?: GraphGradientMode;
}
/**
* @alpha
*/
export const defaultBarChartFieldConfig: BarChartFieldConfig = {
lineWidth: 1,
fillOpacity: 80,
gradientMode: GraphGradientMode.None,
axisSoftMin: 0,
};

View File

@@ -10,6 +10,7 @@ import {
getTitleStyles,
getValuePercent,
BarGaugeDisplayMode,
calculateBarAndValueDimensions,
} from './BarGauge';
import { getTheme } from '../../themes';
@@ -211,4 +212,18 @@ describe('BarGauge', () => {
expect(wrapper).toMatchSnapshot();
});
});
describe('calculateBarAndValueDimensions', () => {
it('valueWidth should including paddings in valueWidth', () => {
const result = calculateBarAndValueDimensions(
getProps({
height: 30,
width: 100,
value: getValue(1, 'AA'),
orientation: VizOrientation.Horizontal,
})
);
expect(result.valueWidth).toBe(21);
});
});
});

View File

@@ -30,7 +30,6 @@ import { Themeable } from '../../types';
const MIN_VALUE_HEIGHT = 18;
const MAX_VALUE_HEIGHT = 50;
const MIN_VALUE_WIDTH = 50;
const MAX_VALUE_WIDTH = 150;
const TITLE_LINE_HEIGHT = 1.5;
const VALUE_LINE_HEIGHT = 1;
@@ -373,9 +372,15 @@ interface BarAndValueDimensions {
wrapperWidth: number;
}
function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
const { height, width, orientation, text } = props;
/**
* @internal
* Only exported for unit tests
**/
export function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
const { height, width, orientation, text, alignmentFactors } = props;
const titleDim = calculateTitleDimensions(props);
const value = alignmentFactors ?? props.value;
const valueString = formattedValueToString(value);
let maxBarHeight = 0;
let maxBarWidth = 0;
@@ -384,25 +389,27 @@ function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
let wrapperWidth = 0;
let wrapperHeight = 0;
// measure text with title font size or min 14px
const fontSizeToMeasureWith = text?.valueSize ?? Math.max(titleDim.fontSize, 12);
const realTextSize = measureText(valueString, fontSizeToMeasureWith);
const realValueWidth = realTextSize.width + VALUE_LEFT_PADDING * 2;
if (isVertical(orientation)) {
if (text?.valueSize) {
valueHeight = text.valueSize * VALUE_LINE_HEIGHT;
} else {
valueHeight = Math.min(Math.max(height * 0.1, MIN_VALUE_HEIGHT), MAX_VALUE_HEIGHT);
}
valueWidth = width;
maxBarHeight = height - (titleDim.height + valueHeight);
maxBarWidth = width;
wrapperWidth = width;
wrapperHeight = height - titleDim.height;
} else {
if (text?.valueSize) {
valueHeight = text.valueSize * VALUE_LINE_HEIGHT;
} else {
valueHeight = height - titleDim.height;
}
valueHeight = height - titleDim.height;
valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), realValueWidth);
valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), MIN_VALUE_WIDTH);
maxBarHeight = height - titleDim.height;
maxBarWidth = width - valueWidth - titleDim.width;
@@ -479,7 +486,6 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
if (isBasic) {
// Basic styles
barStyles.background = `${tinycolor(valueColor).setAlpha(0.35).toRgbString()}`;
barStyles.borderTop = `2px solid ${valueColor}`;
} else {
// Gradient styles
@@ -499,6 +505,7 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
// shift empty region back to fill gaps due to border radius
emptyBar.left = '-3px';
emptyBar.width = `${maxBarWidth - barWidth}px`;
if (isBasic) {
// Basic styles

View File

@@ -62,6 +62,7 @@ exports[`BarGauge Render with basic options should render 1`] = `
"flexGrow": 1,
"left": "-3px",
"position": "relative",
"width": "180px",
}
}
/>

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { Story } from '@storybook/react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { Card } from './Card';
import { Card, Props } from './Card';
import mdx from './Card.mdx';
import { Button } from '../Button';
import { IconButton } from '../IconButton/IconButton';
@@ -18,17 +18,20 @@ export default {
docs: {
page: mdx,
},
knobs: {
disable: true,
},
},
argTypes: {
heading: { control: { disable: true } },
description: { control: { disable: true } },
href: { control: { disable: true } },
tooltip: { control: { disable: true } },
onClick: { control: { disable: true } },
},
};
const getKnobs = () => {
const disabled = boolean('Disabled', false, 'Style props');
return { disabled };
};
export const Basic = () => {
const { disabled } = getKnobs();
export const Basic: Story<Props> = ({ disabled }) => {
return (
<Card
heading="Filter by name"
@@ -38,8 +41,7 @@ export const Basic = () => {
);
};
export const AsLink = () => {
const { disabled } = getKnobs();
export const AsLink: Story<Props> = ({ disabled }) => {
return (
<Card
href="https://grafana.com"
@@ -50,8 +52,7 @@ export const AsLink = () => {
);
};
export const WithTooltip = () => {
const { disabled } = getKnobs();
export const WithTooltip: Story<Props> = ({ disabled }) => {
return (
<Card
heading="Reduce"
@@ -62,8 +63,7 @@ export const WithTooltip = () => {
);
};
export const WithTags = () => {
const { disabled } = getKnobs();
export const WithTags: Story<Props> = ({ disabled }) => {
return (
<Card heading="Elasticsearch Custom Templated Query" disabled={disabled}>
<Card.Meta>Elastic Search</Card.Meta>
@@ -74,8 +74,7 @@ export const WithTags = () => {
);
};
export const WithMedia = () => {
const { disabled } = getKnobs();
export const WithMedia: Story<Props> = ({ disabled }) => {
return (
<Card href="https://ops-us-east4.grafana.net/api/prom" heading="1-ops-tools1-fallback" disabled={disabled}>
<Card.Meta>
@@ -90,8 +89,7 @@ export const WithMedia = () => {
</Card>
);
};
export const WithActions = () => {
const { disabled } = getKnobs();
export const WithActions: Story<Props> = ({ disabled }) => {
return (
<Card heading="1-ops-tools1-fallback" disabled={disabled}>
<Card.Meta>
@@ -119,9 +117,7 @@ export const WithActions = () => {
);
};
export const Full = () => {
const { disabled } = getKnobs();
export const Full: Story<Props> = ({ disabled }) => {
return (
<Card
heading="Card title"

View File

@@ -1,16 +1,20 @@
import { Field, LinkModel } from '@grafana/data';
import React from 'react';
import { Button } from '..';
import { ButtonProps, Button } from '../Button';
type FieldLinkProps = {
type DataLinkButtonProps = {
link: LinkModel<Field>;
buttonProps?: ButtonProps;
};
export function FieldLink({ link }: FieldLinkProps) {
/**
* @internal
*/
export function DataLinkButton({ link, buttonProps }: DataLinkButtonProps) {
return (
<a
href={link.href}
target="_blank"
target={link.target}
rel="noreferrer"
onClick={
link.onClick
@@ -23,7 +27,9 @@ export function FieldLink({ link }: FieldLinkProps) {
: undefined
}
>
<Button icon="external-link-alt">{link.title}</Button>
<Button icon="external-link-alt" variant="primary" size="sm" {...buttonProps}>
{link.title}
</Button>
</a>
);
}

View File

@@ -1,9 +1,9 @@
import React, { useState, useMemo, useContext, useRef, RefObject, memo, useEffect } from 'react';
import React, { memo, RefObject, useContext, useEffect, useMemo, useRef, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import { DataLinkSuggestions } from './DataLinkSuggestions';
import { ThemeContext, makeValue } from '../../index';
import { makeValue, ThemeContext } from '../../index';
import { SelectionReference } from './SelectionReference';
import { Portal, getFormStyles } from '../index';
import { getFormStyles, Portal } from '../index';
// @ts-ignore
import Prism, { Grammar, LanguageMap } from 'prismjs';
@@ -16,7 +16,7 @@ import { css, cx } from 'emotion';
import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate';
import { stylesFactory } from '../../themes';
import { GrafanaTheme, VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data';
import { DataLinkBuiltInVars, GrafanaTheme, VariableOrigin, VariableSuggestion } from '@grafana/data';
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
@@ -130,7 +130,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = memo(
if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
editor.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
} else {
editor.insertText(`var-${item.value}=$\{${item.value}}`);
editor.insertText(`\${${item.value}:queryparam}`);
}
setLinkUrl(editor.value);

View File

@@ -0,0 +1,104 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { DataLinksListItem, DataLinksListItemProps } from './DataLinksListItem';
const baseLink = {
url: '',
title: '',
onBuildUrl: jest.fn(),
onClick: jest.fn(),
};
function setupTestContext(options: Partial<DataLinksListItemProps>) {
const defaults: DataLinksListItemProps = {
index: 0,
link: baseLink,
data: [],
onChange: jest.fn(),
onEdit: jest.fn(),
onRemove: jest.fn(),
suggestions: [],
};
const props = { ...defaults, ...options };
const { rerender } = render(<DataLinksListItem {...props} />);
return { rerender, props };
}
describe('DataLinksListItem', () => {
describe('when link has title', () => {
it('then the link title should be visible', () => {
const link = {
...baseLink,
title: 'Some Data Link Title',
};
setupTestContext({ link });
expect(screen.getByText(/some data link title/i)).toBeInTheDocument();
});
});
describe('when link has url', () => {
it('then the link url should be visible', () => {
const link = {
...baseLink,
url: 'http://localhost:3000',
};
setupTestContext({ link });
expect(screen.getByText(/http:\/\/localhost\:3000/i)).toBeInTheDocument();
expect(screen.getByTitle(/http:\/\/localhost\:3000/i)).toBeInTheDocument();
});
});
describe('when link is missing title', () => {
it('then the link title should be replaced by [Data link title not provided]', () => {
const link = {
...baseLink,
title: (undefined as unknown) as string,
};
setupTestContext({ link });
expect(screen.getByText(/data link title not provided/i)).toBeInTheDocument();
});
});
describe('when link is missing url', () => {
it('then the link url should be replaced by [Data link url not provided]', () => {
const link = {
...baseLink,
url: (undefined as unknown) as string,
};
setupTestContext({ link });
expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument();
expect(screen.getByTitle('')).toBeInTheDocument();
});
});
describe('when link title is empty', () => {
it('then the link title should be replaced by [Data link title not provided]', () => {
const link = {
...baseLink,
title: ' ',
};
setupTestContext({ link });
expect(screen.getByText(/data link title not provided/i)).toBeInTheDocument();
});
});
describe('when link url is empty', () => {
it('then the link url should be replaced by [Data link url not provided]', () => {
const link = {
...baseLink,
url: ' ',
};
setupTestContext({ link });
expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument();
expect(screen.getByTitle('')).toBeInTheDocument();
});
});
});

View File

@@ -5,7 +5,7 @@ import { stylesFactory, useTheme } from '../../../themes';
import { HorizontalGroup, VerticalGroup } from '../../Layout/Layout';
import { IconButton } from '../../IconButton/IconButton';
interface DataLinksListItemProps {
export interface DataLinksListItemProps {
index: number;
link: DataLink;
data: DataFrame[];
@@ -19,24 +19,25 @@ interface DataLinksListItemProps {
export const DataLinksListItem: FC<DataLinksListItemProps> = ({ link, onEdit, onRemove }) => {
const theme = useTheme();
const styles = getDataLinkListItemStyles(theme);
const { title = '', url = '' } = link;
const hasTitle = link.title.trim() !== '';
const hasUrl = link.url.trim() !== '';
const hasTitle = title.trim() !== '';
const hasUrl = url.trim() !== '';
return (
<div className={styles.wrapper}>
<VerticalGroup spacing="xs">
<HorizontalGroup justify="space-between" align="flex-start" width="100%">
<div className={cx(styles.title, !hasTitle && styles.notConfigured)}>
{hasTitle ? link.title : 'Data link title not provided'}
{hasTitle ? title : 'Data link title not provided'}
</div>
<HorizontalGroup>
<IconButton name="pen" onClick={onEdit} />
<IconButton name="times" onClick={onRemove} />
</HorizontalGroup>
</HorizontalGroup>
<div className={cx(styles.url, !hasUrl && styles.notConfigured)} title={link.url}>
{hasUrl ? link.url : 'Data link url not provided'}
<div className={cx(styles.url, !hasUrl && styles.notConfigured)} title={url}>
{hasUrl ? url : 'Data link url not provided'}
</div>
</VerticalGroup>
</div>

View File

@@ -0,0 +1,71 @@
import { Field, GrafanaTheme, LinkModel } from '@grafana/data';
import { css } from 'emotion';
import React from 'react';
import { useStyles } from '../../themes';
import { Icon } from '../Icon/Icon';
import { DataLinkButton } from './DataLinkButton';
type Props = {
links: Array<LinkModel<Field>>;
};
/**
* @internal
*/
export function FieldLinkList({ links }: Props) {
const styles = useStyles(getStyles);
if (links.length === 1) {
return <DataLinkButton link={links[0]} />;
}
const externalLinks = links.filter((link) => link.target === '_blank');
const internalLinks = links.filter((link) => link.target === '_self');
return (
<>
{internalLinks.map((link, i) => {
return <DataLinkButton key={i} link={link} />;
})}
<div className={styles.wrapper}>
<p className={styles.externalLinksHeading}>External links</p>
{externalLinks.map((link, i) => (
<a key={i} href={link.href} target={link.target} className={styles.externalLink}>
<Icon name="external-link-alt" />
{link.title}
</a>
))}
</div>
</>
);
}
const getStyles = (theme: GrafanaTheme) => ({
wrapper: css`
flex-basis: 150px;
width: 100px;
margin-top: ${theme.spacing.sm};
`,
externalLinksHeading: css`
color: ${theme.colors.textWeak};
font-weight: ${theme.typography.weight.regular};
font-size: ${theme.typography.size.sm};
margin: 0;
`,
externalLink: css`
color: ${theme.colors.linkExternal};
font-weight: ${theme.typography.weight.regular};
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
text-decoration: underline;
}
div {
margin-right: ${theme.spacing.sm};
}
`,
});

View File

@@ -0,0 +1,56 @@
import { ArgsTable } from "@storybook/addon-docs/blocks";
import { ErrorBoundary, ErrorBoundaryAlert } from "./ErrorBoundary";
import { ErrorWithStack } from "./ErrorWithStack";
# ErrorBoundary
A React component that catches errors in child components. Useful for logging or displaying a fallback UI in case of errors. More information about error boundaries is available at [React documentation website](https://reactjs.org/docs/error-boundaries.html).
### Usage
```jsx
import { ErrorBoundary, Alert } from '@grafana/ui';
<ErrorBoundary>
{({ error }) => {
if (error) {
return <Alert title={error.message} />;
}
return <Component />;
}}
</ErrorBoundary>
```
# ErrorBoundaryAlert
An error boundary component with built-in alert to display in case of error.
### Usage
```jsx
import { ErrorBoundaryAlert } from '@grafana/ui';
<ErrorBoundaryAlert>
<Component />
</ErrorBoundaryAlert>
```
### Props
<ArgsTable of={ErrorBoundaryAlert}/>
# ErrorWithStack
A component that displays error together with its stack trace.
### Usage
```jsx
import { ErrorWithStack } from '@grafana/ui';
<ErrorWithStack error={new Error('Test error')} title={'Unexpected error'} errorInfo={null} />
```
### Props
<ArgsTable of={ErrorWithStack}/>

View File

@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary';
import { withCenteredStory } from '@grafana/ui/src/utils/storybook/withCenteredStory';
import mdx from './ErrorBoundary.mdx';
import { Button } from '../Button';
import { ErrorWithStack } from './ErrorWithStack';
import { Alert } from '../Alert/Alert';
export default {
title: 'General/ErrorBoundary',
component: ErrorBoundary,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const BuggyComponent = () => {
const [count, setCount] = useState(0);
if (count > 2) {
throw new Error('Crashed');
}
return (
<div>
<p>Increase the count to 3 to trigger error</p>
<Button onClick={() => setCount(count + 1)}>{count.toString()}</Button>
</div>
);
};
export const Basic = () => {
return (
<ErrorBoundary>
{({ error }) => {
if (error) {
return <Alert title={error.message} />;
}
return <BuggyComponent />;
}}
</ErrorBoundary>
);
};
export const WithStack = () => {
return <ErrorWithStack error={new Error('Test error')} title={'Unexpected error'} errorInfo={null} />;
};
export const BoundaryAlert = () => {
return (
<ErrorBoundaryAlert>
<BuggyComponent />
</ErrorBoundaryAlert>
);
};

View File

@@ -13,6 +13,8 @@ export interface Props extends Omit<FieldProps, 'css' | 'horizontal' | 'descript
labelWidth?: number | 'auto';
/** Make the field's child to fill the width of the row. Equivalent to setting `flex-grow:1` on the field */
grow?: boolean;
/** Make field's background transparent */
transparent?: boolean;
}
export const InlineField: FC<Props> = ({
@@ -25,6 +27,7 @@ export const InlineField: FC<Props> = ({
disabled,
className,
grow,
transparent,
...htmlProps
}) => {
const theme = useTheme();
@@ -37,7 +40,7 @@ export const InlineField: FC<Props> = ({
}
const labelElement =
typeof label === 'string' ? (
<InlineLabel width={labelWidth} tooltip={tooltip} htmlFor={inputId}>
<InlineLabel width={labelWidth} tooltip={tooltip} htmlFor={inputId} transparent={transparent}>
{label}
</InlineLabel>
) : (

View File

@@ -12,6 +12,8 @@ export interface Props extends Omit<LabelProps, 'css' | 'description' | 'categor
tooltip?: PopoverContent;
/** Custom width for the label */
width?: number | 'auto';
/** Make labels's background transparent */
transparent?: boolean;
/** @deprecated */
/** This prop is deprecated and is not used anymore */
isFocused?: boolean;
@@ -28,12 +30,12 @@ export const InlineLabel: FunctionComponent<Props> = ({
className,
tooltip,
width,
transparent,
as: Component = 'label',
...rest
}) => {
const theme = useTheme();
const styles = getInlineLabelStyles(theme, width);
const styles = getInlineLabelStyles(theme, transparent, width);
return (
<Component className={cx(styles.label, className)} {...rest}>
{children}
@@ -46,7 +48,7 @@ export const InlineLabel: FunctionComponent<Props> = ({
);
};
export const getInlineLabelStyles = (theme: GrafanaTheme, width?: number | 'auto') => {
export const getInlineLabelStyles = (theme: GrafanaTheme, transparent = false, width?: number | 'auto') => {
return {
label: css`
display: flex;
@@ -56,7 +58,7 @@ export const getInlineLabelStyles = (theme: GrafanaTheme, width?: number | 'auto
padding: 0 ${theme.spacing.sm};
font-weight: ${theme.typography.weight.semibold};
font-size: ${theme.typography.size.sm};
background-color: ${theme.colors.bg2};
background-color: ${transparent ? 'transparent' : theme.colors.bg2};
height: ${theme.height.md}px;
line-height: ${theme.height.md}px;
margin-right: ${theme.spacing.xs};

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import uniqueId from 'lodash/uniqueId';
import { Placement } from '@popperjs/core';
import { Tooltip } from '../../../Tooltip/Tooltip';
import * as PopperJS from 'popper.js';
import { Icon } from '../../..';
export interface Props {
@@ -11,7 +11,7 @@ export interface Props {
labelClass?: string;
switchClass?: string;
tooltip?: string;
tooltipPlacement?: PopperJS.Placement;
tooltipPlacement?: Placement;
transparent?: boolean;
onChange: (event: React.SyntheticEvent<HTMLInputElement>) => void;
}

View File

@@ -26,7 +26,6 @@ const getGraphWithLegendStyles = stylesFactory(({ placement }: GraphWithLegendPr
wrapper: css`
display: flex;
flex-direction: ${placement === 'bottom' ? 'column' : 'row'};
height: 100%;
`,
graphContainer: css`
min-height: 65%;

View File

@@ -5,18 +5,27 @@ import {
DisplayValue,
FieldConfig,
FieldMatcher,
FieldMatcherID,
fieldMatchers,
fieldReducers,
FieldType,
formattedValueToString,
getFieldDisplayName,
outerJoinDataFrames,
reduceField,
TimeRange,
TimeZone,
} from '@grafana/data';
import { alignDataFrames } from './utils';
import { useTheme } from '../../themes';
import { UPlotChart } from '../uPlot/Plot';
import { PlotProps } from '../uPlot/types';
import { AxisPlacement, DrawStyle, GraphFieldConfig, PointVisibility } from '../uPlot/config';
import {
AxisPlacement,
DrawStyle,
GraphFieldConfig,
PointVisibility,
ScaleDirection,
ScaleOrientation,
} from '../uPlot/config';
import { VizLayout } from '../VizLayout/VizLayout';
import { LegendDisplayMode, VizLegendItem, VizLegendOptions } from '../VizLegend/types';
import { VizLegend } from '../VizLegend/VizLegend';
@@ -32,12 +41,18 @@ export interface XYFieldMatchers {
x: FieldMatcher; // first match
y: FieldMatcher;
}
export interface GraphNGProps extends Omit<PlotProps, 'data' | 'config'> {
export interface GraphNGProps {
width: number;
height: number;
data: DataFrame[];
timeRange: TimeRange;
legend: VizLegendOptions;
timeZone: TimeZone;
fields?: XYFieldMatchers; // default will assume timeseries data
onLegendClick?: (event: GraphNGLegendEvent) => void;
onSeriesColorChange?: (label: string, color: string) => void;
children?: React.ReactNode;
}
const defaultConfig: GraphFieldConfig = {
@@ -64,9 +79,16 @@ export const GraphNG: React.FC<GraphNGProps> = ({
const theme = useTheme();
const hasLegend = useRef(legend && legend.displayMode !== LegendDisplayMode.Hidden);
const alignedFrameWithGapTest = useMemo(() => alignDataFrames(data, fields), [data, fields]);
const alignedFrame = alignedFrameWithGapTest?.frame;
const getDataFrameFieldIndex = alignedFrameWithGapTest?.getDataFrameFieldIndex;
const frame = useMemo(() => {
// Default to timeseries config
if (!fields) {
fields = {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
};
}
return outerJoinDataFrames({ frames: data, joinBy: fields.x, keep: fields.y, keepOriginIndices: true });
}, [data, fields]);
const compareFrames = useCallback((a?: DataFrame | null, b?: DataFrame | null) => {
if (a && b) {
@@ -98,21 +120,24 @@ export const GraphNG: React.FC<GraphNGProps> = ({
currentTimeRange.current = timeRange;
}, [timeRange]);
const configRev = useRevision(alignedFrame, compareFrames);
const configRev = useRevision(frame, compareFrames);
const configBuilder = useMemo(() => {
const builder = new UPlotConfigBuilder();
if (!alignedFrame) {
if (!frame) {
return builder;
}
// X is the first field in the aligned frame
const xField = alignedFrame.fields[0];
const xField = frame.fields[0];
let seriesIndex = 0;
if (xField.type === FieldType.time) {
builder.addScale({
scaleKey: 'x',
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
isTime: true,
range: () => {
const r = currentTimeRange.current!;
@@ -131,6 +156,8 @@ export const GraphNG: React.FC<GraphNGProps> = ({
// Not time!
builder.addScale({
scaleKey: 'x',
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
});
builder.addAxis({
@@ -141,8 +168,8 @@ export const GraphNG: React.FC<GraphNGProps> = ({
}
let indexByName: Map<string, number> | undefined = undefined;
for (let i = 0; i < alignedFrame.fields.length; i++) {
const field = alignedFrame.fields[i];
for (let i = 0; i < frame.fields.length; i++) {
const field = frame.fields[i];
const config = field.config as FieldConfig<GraphFieldConfig>;
const customConfig: GraphFieldConfig = {
...defaultConfig,
@@ -152,6 +179,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
if (field === xField || field.type !== FieldType.number) {
continue;
}
field.state!.seriesIndex = seriesIndex++;
const fmt = field.display ?? defaultFormatter;
const scaleKey = config.unit || FIXED_UNIT;
@@ -159,18 +187,20 @@ export const GraphNG: React.FC<GraphNGProps> = ({
const scaleColor = getFieldSeriesColor(field, theme);
const seriesColor = scaleColor.color;
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
// The builder will manage unique scaleKeys and combine where appropriate
builder.addScale({
scaleKey,
distribution: customConfig.scaleDistribution?.type,
log: customConfig.scaleDistribution?.log,
min: field.config.min,
max: field.config.max,
softMin: customConfig.axisSoftMin,
softMax: customConfig.axisSoftMax,
});
// The builder will manage unique scaleKeys and combine where appropriate
builder.addScale({
scaleKey,
orientation: ScaleOrientation.Vertical,
direction: ScaleDirection.Up,
distribution: customConfig.scaleDistribution?.type,
log: customConfig.scaleDistribution?.log,
min: field.config.min,
max: field.config.max,
softMin: customConfig.axisSoftMin,
softMax: customConfig.axisSoftMax,
});
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
builder.addAxis({
scaleKey,
label: customConfig.axisLabel,
@@ -182,14 +212,13 @@ export const GraphNG: React.FC<GraphNGProps> = ({
}
const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
const dataFrameFieldIndex = getDataFrameFieldIndex ? getDataFrameFieldIndex(i) : undefined;
let { fillOpacity } = customConfig;
if (customConfig.fillBelowTo) {
if (!indexByName) {
indexByName = getNamesToFieldIndex(alignedFrame);
indexByName = getNamesToFieldIndex(frame);
}
const t = indexByName.get(getFieldDisplayName(field, alignedFrame));
const t = indexByName.get(getFieldDisplayName(field, frame));
const b = indexByName.get(customConfig.fillBelowTo);
if (isNumber(b) && isNumber(t)) {
builder.addBand({
@@ -213,6 +242,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation,
lineStyle: customConfig.lineStyle,
barAlignment: customConfig.barAlignment,
pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor,
spanNulls: customConfig.spanNulls || false,
@@ -221,15 +251,15 @@ export const GraphNG: React.FC<GraphNGProps> = ({
thresholds: config.thresholds,
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
dataFrameFieldIndex,
fieldName: getFieldDisplayName(field, alignedFrame),
dataFrameFieldIndex: field.state?.origin,
fieldName: getFieldDisplayName(field, frame),
hideInLegend: customConfig.hideFrom?.legend,
});
}
return builder;
}, [configRev, timeZone]);
if (alignedFrameWithGapTest == null) {
if (!frame) {
return (
<div className="panel-empty">
<p>No data found in response</p>
@@ -262,6 +292,10 @@ export const GraphNG: React.FC<GraphNGProps> = ({
label: seriesConfig.fieldName,
yAxis: axisPlacement === AxisPlacement.Left ? 1 : 2,
getDisplayValues: () => {
if (!legend.calcs?.length) {
return [];
}
const fmt = field.display ?? defaultFormatter;
const fieldCalcs = reduceField({
field,
@@ -299,7 +333,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
<VizLayout width={width} height={height} legend={legendElement}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart
data={alignedFrameWithGapTest}
data={frame}
config={configBuilder}
width={vizWidth}
height={vizHeight}

View File

@@ -1,245 +0,0 @@
import { ArrayVector, DataFrame, FieldType, toDataFrame } from '@grafana/data';
import { AlignedFrameWithGapTest } from '../uPlot/types';
import { alignDataFrames, isLikelyAscendingVector } from './utils';
describe('alignDataFrames', () => {
describe('aligned frame', () => {
it('should align multiple data frames into one data frame', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature A', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature B', type: FieldType.number, values: [0, 2, 6, 7] },
],
}),
];
const aligned = alignDataFrames(data);
expect(aligned?.frame.fields).toEqual([
{
config: {},
state: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000]),
},
{
config: {},
state: {
displayName: 'temperature A',
seriesIndex: 0,
},
name: 'temperature A',
type: FieldType.number,
values: new ArrayVector([1, 3, 5, 7]),
},
{
config: {},
state: {
displayName: 'temperature B',
seriesIndex: 1,
},
name: 'temperature B',
type: FieldType.number,
values: new ArrayVector([0, 2, 6, 7]),
},
]);
});
it('should align multiple data frames into one data frame but only keep first time field', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time2', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature B', type: FieldType.number, values: [0, 2, 6, 7] },
],
}),
];
const aligned = alignDataFrames(data);
expect(aligned?.frame.fields).toEqual([
{
config: {},
state: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000]),
},
{
config: {},
state: {
displayName: 'temperature',
seriesIndex: 0,
},
name: 'temperature',
type: FieldType.number,
values: new ArrayVector([1, 3, 5, 7]),
},
{
config: {},
state: {
displayName: 'temperature B',
seriesIndex: 1,
},
name: 'temperature B',
type: FieldType.number,
values: new ArrayVector([0, 2, 6, 7]),
},
]);
});
it('should align multiple data frames into one data frame and skip non-numeric fields', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
{ name: 'state', type: FieldType.string, values: ['on', 'off', 'off', 'on'] },
],
}),
];
const aligned = alignDataFrames(data);
expect(aligned?.frame.fields).toEqual([
{
config: {},
state: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000]),
},
{
config: {},
state: {
displayName: 'temperature',
seriesIndex: 0,
},
name: 'temperature',
type: FieldType.number,
values: new ArrayVector([1, 3, 5, 7]),
},
]);
});
it('should align multiple data frames into one data frame and skip non-numeric fields', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
{ name: 'state', type: FieldType.string, values: ['on', 'off', 'off', 'on'] },
],
}),
];
const aligned = alignDataFrames(data);
expect(aligned?.frame.fields).toEqual([
{
config: {},
state: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000]),
},
{
config: {},
state: {
displayName: 'temperature',
seriesIndex: 0,
},
name: 'temperature',
type: FieldType.number,
values: new ArrayVector([1, 3, 5, 7]),
},
]);
});
});
describe('getDataFrameFieldIndex', () => {
let aligned: AlignedFrameWithGapTest | null;
beforeAll(() => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature A', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature B', type: FieldType.number, values: [0, 2, 6, 7] },
{ name: 'humidity', type: FieldType.number, values: [0, 2, 6, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature C', type: FieldType.number, values: [0, 2, 6, 7] },
],
}),
];
aligned = alignDataFrames(data);
});
it.each`
yDim | index
${1} | ${[0, 1]}
${2} | ${[1, 1]}
${3} | ${[1, 2]}
${4} | ${[2, 1]}
`('should return correct index for yDim', ({ yDim, index }) => {
const [frameIndex, fieldIndex] = index;
expect(aligned?.getDataFrameFieldIndex(yDim)).toEqual({
frameIndex,
fieldIndex,
});
});
});
describe('check ascending data', () => {
it('simple ascending', () => {
const v = new ArrayVector([1, 2, 3, 4, 5]);
expect(isLikelyAscendingVector(v)).toBeTruthy();
});
it('simple ascending with null', () => {
const v = new ArrayVector([null, 2, 3, 4, null]);
expect(isLikelyAscendingVector(v)).toBeTruthy();
});
it('single value', () => {
const v = new ArrayVector([null, null, null, 4, null]);
expect(isLikelyAscendingVector(v)).toBeTruthy();
expect(isLikelyAscendingVector(new ArrayVector([4]))).toBeTruthy();
expect(isLikelyAscendingVector(new ArrayVector([]))).toBeTruthy();
});
it('middle values', () => {
const v = new ArrayVector([null, null, 5, 4, null]);
expect(isLikelyAscendingVector(v)).toBeFalsy();
});
it('decending', () => {
expect(isLikelyAscendingVector(new ArrayVector([7, 6, null]))).toBeFalsy();
expect(isLikelyAscendingVector(new ArrayVector([7, 8, 6]))).toBeFalsy();
});
});
});

Some files were not shown because too many files have changed in this diff Show More