7.0.0 cherry-picks (#24726)

* TablePanel: Fix JSON tooltip positioning (#24420)

* Fix block element and overflow

* Width not needed

* Add to styles file

(cherry picked from commit f714357fbf)

* Minor UI touches (#24371)

* Justify to alignment in stat

* No stats label in inspect

* Use sentence case in panel names

* typos

* typo

* Typo

* Query -> query options

* prettier

* Transformer -> transform data

* Do not show deprecated plugins in the viz picker

* Update viz picker layout

(cherry picked from commit 63d25944c4)

* Reporting: tzdata is needed for reporting (#24422)

(cherry picked from commit de8c1b9ad7)

* Storybook: Position panel to right (#24429)

(cherry picked from commit 8257633b1d)

* Chore: move dataFramesReceived event to @grafana/data (#24443)

(cherry picked from commit 7e6ac12b9a)

* Explore: Revert QueryRows refactor (#24444)

While query fields should not rely on getting unmounted when the data source changes (and instead react to that change in e.g. componentDidUpdate()), query fields other than PromQueryField still rely on this.

(cherry picked from commit 221042c293)

* Logs: Clicking "Load more" from context overlay doesn't expand log row (#24299)

Closes #24184

(cherry picked from commit 35c097e475)

* Datasource/CloudWatch: Add data links to CloudWatch logs for deep linking to AWS (#24334)

* Datasource/CloudWatch: Fix encoding of CloudWatch Logs deep link URL

* Adds data links to cloudwatch logs responses for deep linking to aws console

* Implements PR feedback

(cherry picked from commit a655aa1ca8)

* OldTable: Make old table options align & look better (#24485)

(cherry picked from commit e333d61b97)

* Buttons: Use text color (#24439)

(cherry picked from commit c671fdbbf8)

* StatPanels: Hide calc option if all values is selected (#24416)

(cherry picked from commit 352f22042c)

* grafana/toolkit: Support paging in cherrypick task (#24402)

* Support paging in cherrypick task

* Update packages/grafana-toolkit/src/cli/tasks/cherrypick.ts

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update packages/grafana-toolkit/src/cli/tasks/cherrypick.ts

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Do not cherry pick closed PRs

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
(cherry picked from commit c5fc18ac02)

* Reduce Transform: sort order is preserved as entered by user (#24494)

(cherry picked from commit e341d4b26f)

* TimeZone: added e2e tests for the custom time zone support. (#24442)

(cherry picked from commit a521a39ebf)

* CloudWatch/Logs: Add error message when log groups are not selected (#24361)

* Add error message

* Fix empty check

(cherry picked from commit d385045d16)

* CloudWatch/Log: Fix field autocomplete suggestions inside function (#24406)

* Fix autocomplete inside function

* Remove debug log

* Add comment and return type

(cherry picked from commit b16202acc2)

* Editor: No default suggestion selected (#24479)

* QueryField: No default suggestion selected

It's been a long-standing issue that careless typing lead to unwanted
tab completion insertions. With this change the completion item list no
longer selects the first item by default. The user has to actively click
ArrowDown to select the first one.

* Added type export

* Remove width limit of typeahead list

(cherry picked from commit 34f61934a1)

* TablePanel: Fixed persisting column resize time series fields (#24505)

(cherry picked from commit f13a15ae48)

* PluginsList: Remove unused panel options (#24501)

* Remove unused panel options

* satisfy ts

(cherry picked from commit 2e69b86990)

* CloudWatch/Logs: Language provider refactor and test (#24425)

(cherry picked from commit 876011d931)

* Datasource/CloudWatch: Usability improvements (#24447)

* Datasource/CloudWatch: Improve handling of long-running queries

* Datasource/CloudWatch: Make order of dataframe fields consistent

(cherry picked from commit c26374b0b2)

* CloudWatch/Logs: Fix suggestions of fields after comma (#24520)

(cherry picked from commit 2d29997bda)

* GraphLegend: Only display scrollbar if necessary (#24527)

(cherry picked from commit cb74bc6828)

* CloudWatch/Logs: Fix fields not being refetched when log group changed (#24529)

(cherry picked from commit 55533d12fd)

* Datasource/CloudWatch: Switch to metrics mode, logs API, when choosing stats query from cheatsheet (#24398)

* Datasource/CloudWatch: Switch to metrics mode, logs API, when choosing stats query from cheatsheet

(cherry picked from commit ae7f0aeb7a)

* azuremonitor: Deep linking from Log Analytic queries to the Azure Portal (#24417)

* azuremonitor: add gzipped and base64 encoded query to metadata

for Azure Log Analytic query responses

* azure monitor: add fields to metadata for log analytics

* azuremonitor: correction to text in query editor

* azuremonitor: adds subscription id to result metadata

* azuremonitor: build deep link url for Log Analytics

Most of the information needed for building the url
comes from the backend. The workspace friendly name
and the resource group that the workspace belongs
to are fetched in a separate API call. This call is
cached otherwise there would be a workspaces call
per query on the dashboard.

* docs: azure log analytics deep linking

* Apply suggestions from code review

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* docs: fixing review comments for azure monitor

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

* Datasource/CloudWatch: Results of CloudWatch Logs stats queries are now grouped (#24396)

* Datasource/CloudWatch: Results of CloudWatch Logs stats queries are now grouped

(cherry picked from commit db91961405)

* PanelEditor: Fixed issue changing a panel from transparent back to normal  (#24483)

* PanelModel: Fixed issues with persisting some changes

* Fixed other issues

(cherry picked from commit 5caf7f59e6)

* LabelsToFields: avoid MutableDataFrame (#24493)

(cherry picked from commit c8d3d15292)

* Prometheus: Show results of instant queries only in table (#24508)

* Show results of instant queries only in table, remove them from graph

* Update table model

(cherry picked from commit 143a26769b)

* PanelEditor: Fix so defaults is selected when datasource can't be found. (#24526)

(cherry picked from commit cb4266bae0)

* Graphite: makes query annotations work again (#24556)

(cherry picked from commit 7992f8bfbc)

* azuremonitor: limit macro regex to known macros (#24528)

(cherry picked from commit 64046e9a27)

* TimePicker: fixed so you can enter a relative range in the time picker without being converted to absolute range (#24534)

* fixed so you can enter a relative date in the time range form.

* did some small adjustments to make sure proper value is stored.

(cherry picked from commit 28f54bc2f7)

* Admin Settings: makes long settings line break (#24559)

(cherry picked from commit 74ddfe07a2)

* Update light variable theme for dropdown (#24566)

(cherry picked from commit 67dab0b767)

* UI spelling: use US form of visualization (#24558)

(cherry picked from commit bffa0fa4f6)

* e2e: adds verify release (#24568)

(cherry picked from commit 7d800e7318)

* Search: Save folder expanded state (#24496)

* Search: Save folder expanded state

* Search: Remember expanded state on search close

(cherry picked from commit 0396b220a3)

* Data sources: Don't fail if URL doesn't specify protocol (#24497)

(cherry picked from commit 164242f5fd)

* Security: Tag value sanitization fix in OpenTSDB data source (#24539)

Adds HTML sanitization to the tag value of the OpenTSDB datasource.

Fixes #24537

(cherry picked from commit 125ba95686)

* Field: UI & Code consistency Title -> Display name  (#24507)

* Field: Change getFieldTitle to getFieldDisplayNamne and change the NAME of the title field config from Title to Display name

* Review feedback

* fixed unit tests

* Rename fieldConfig.title to displayName

* Fixed tests

* Added migration

* Renamed getFrameDisplayTitle to getFrameDisplayName

(cherry picked from commit 8de10a8b9f)

* Do not set insertText if the same as label (#24581)

(cherry picked from commit 20f0ee2f22)

* Plugins: Fix manifest verification (#24573)

(cherry picked from commit 892f9f789c)

* Search: Safari UI fixes (#24567)

* Search: Add height: 100% to page containers

* Search: Reset TagOption display to block

* Search: Add title to result list

* Search: Use flex-basis vs height

* Search: Remove IconButton

* Search: Do not shrink select options

* Search: Fix hasEditPermissionInFolders

* Search: Update tests

* Search: Fix prop

* Remove select wrapper class

* Update snapshot

(cherry picked from commit 15ac76a47b)

* OrgUsersTable: Fix width when there are long cells in table (#24533)

* Set width for cells and text overflow

* Add title and update snapshot

(cherry picked from commit 0c1f1dd8fd)

* Loki: Allow multiple derived fields with the same name (#24437)

(cherry picked from commit a50cb6aa1f)

* Datasource/CloudWatch: Fixes various autocomplete issues (#24583)

(cherry picked from commit c1919944ea)

* CircleCI: Update grabpl to 0.4.5 in order to include manifest with GEL (#24577)

* CircleCI: Update grabpl to 0.4.5
* GEL: Upgrade version

(cherry picked from commit 824eb94efe)

* CloudWatch/Logs: Make stats hint show consistently (#24392)

(cherry picked from commit 5feef22034)

* Table: fixes Cannot read property subRows of null (#24578)

(cherry picked from commit 2d19c046f2)

* TimePicker: some fixes and polish after testing feedback (#24585)

* make sure we render the delaynow value from dashboard to the time picker settings.

* Added the time zone in the tooltip.

(cherry picked from commit cab066f8ce)

* Onboarding: New getting started panel (#23826)

* image and card component

* change height of getting started panel

* progress

* setup basic step

* advanced steps

* step forward and backward

* do checks

* fix button size

* minor styling on butttons

* add correct links

* save tutorial click in localstorage

* types and gradients

* fix gradients

* use spacing variable

* lots of responsiveness

* add links to help

* Getting started work

* redo according to split panel design

* minor touch ups

* new background images

* split up docs card to different hrefs

* welcome bar touch ups

* hide icon on small screens

* transparent false on welcome banner

* fix urls

* source tag in welcome urls

* move images to panel dir, removed unused images

* Nicer loading message

* make the cards look nicer on wide screens

* append utm tag on render instead

* replace width with margin

* new background image for light

* remove target on a element

* removing buttonselect, add tag to href

* more polishing

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
(cherry picked from commit 36fa54a288)

* Chore: Fix strict null errors on getting started (#24605)

* fix strict nulls

* fix typings

(cherry picked from commit 1848900070)

* CircleCI: Upgrade grabpl in order to fix package publishing (#24609)

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

* Fix Safari Icon alignment (#24610)

(cherry picked from commit 04c75265b5)

* Transformations: Make transform dropdowns not cropped (#24615)

(cherry picked from commit 9e24c0944f)

* Inspect: Allow showing data without transformations and field config is applied (#24314)

* Inspect: Should not subscribe to transformed data

* PQR- allow controll whether or not field overrides and transformations should be applied

* UI for inspector data options

* fix

* Null check fix

* Update public/app/features/dashboard/components/Inspector/InspectDataTab.tsx

* Update public/app/features/dashboard/components/Inspector/InspectDataTab.tsx

* Apply transformations by default

* Update panel inspect docs

* Fix apply overrides

* Apply time formatting in panel inspect

* fix ts

* Post review update

* Update docs/sources/panels/inspect-panel.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* lazy numbering

* fix ts

* Renames

* Renames 2

* Layout update

* Run shared request without field config

* Minor details

* fix ts

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
(cherry picked from commit f23ecc40b4)

* Transformers: fix binary operation (#24604)

* check the display name

* remove unchanged line

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

* SharedPreferences: fixes so UI Theme can be set back to Default (#24628)

(cherry picked from commit cd9cbe5e16)

* Datasource/CloudWatch: Prevents hidden dataframe fields from displaying in tables (#24580)

* Datasource/CloudWatch: Prevents hidden dataframe fields from displaying in tables

(cherry picked from commit 277aee8642)

* Datasource/CloudWatch: Correctly interpolate variables in CloudWatch Logs queries (#24619)

Closes #24603

(cherry picked from commit bfd337dd00)

* Explore: Fix rendering of react query editors (#24593)

* Fix rendering of react query editors

* Refactor solution for improved readability

* Update

* Add test coverage

* Refactor

(cherry picked from commit 285ea7595d)

* CloudWatch: Don't assume that errors are awserr.Error instances (#24641)

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

* Zipkin: Fix error when span contains remoteEndpoint (#24524)

(cherry picked from commit 0f964c0214)

* Templating: fixes so Textbox variables get value from url (#24623)

(cherry picked from commit 622246d56d)

* Templating: fixes filtering options with more than 1000 entries (#24614)

* Templating: fixes filtering options with more then 1000 entries

* Chore: reduces strict null errors by 2

(cherry picked from commit 083a2ce220)

* PanelEdit: Make sure side pane is displayed with tabs by default (#24636)

* Make sure side pane is displayed with tabs by default

* Update e2e tests for

(cherry picked from commit f769ed035d)

* @grafana/ui: Update RadioButtonGroup documentation (#23143)

* Add "When to use" info to RadioButtonGroup.mdx

* Clarify more clearly when to use 'Select' instead

* Update RadioButtonGroup.mdx

Add line explaining why we want select for more than four options instead of a radio button group

* Update RadioButtonGroup.mdx

remove part about when elements are triggering changes.

Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>
(cherry picked from commit f1386c7892)

* Search: Fix select item pressing enter 404 (#24634)

* Remove appSubUrl for navigation

* stripBaseFromUrl

* Remove unused imports

(cherry picked from commit 37a2ac20c3)

* @grafana/ui: Buttons documentation (#23144)

* Add info: when to use which and how to mix buttons

* Update description for Button.mdx

- add note about scenario without primary action
- add note about secondary button being the default

* Refine docs info for secondary and destructive

Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>
(cherry picked from commit 54a90166a5)

* Datasource Config: adds back correct styles for autofill fields (#24671)

(cherry picked from commit 9481b7bcf2)

* Changelog and Readme: Update packages to beta and add Select breaking change (#24670)

(cherry picked from commit 85ae144a87)

* Orgs: Add future deprecation notice (#24502)

* Orgs: Fist draft of orgs deprecation infobox

* adds warning about potentially deprecating orgs (#24512)

* Updates

* Fixed ts issue

Co-authored-by: Carl Bergquist <carl@grafana.com>
(cherry picked from commit 2f22781cd8)

* Provisioning: Use proxy as default access mode in provisioning (#24669)

fixes #24591
fixes #19501

(cherry picked from commit 7610d5717a)

* Fallback to process.cwd() when PWD is not present (#24677)

Fixes #24582 - Error running yarn dev command

(cherry picked from commit 660f2b4e99)

* SaveDashboard: Allow editing provisioned dashboard JSON and add confirmation when JSON is copied to dashboard (#24680)

(cherry picked from commit cfac591234)

* Units: adds scale symbol for currencies with suffixed symbol (#24678)

(cherry picked from commit 51509bb2ff)

* CloudWatch/Logs: Fix panic on multiple aggregations queries (#24683)

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

* Docs: Updated instructions on screens for Dashboard Import (#24584)

* change text and image links

* Review feedback

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* uppercase JSON

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

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
(cherry picked from commit abfa1b5d74)

* CloudWatch: Unify look of query mode select between dashboard and explore (#24648)

(cherry picked from commit e754bcdcf9)

* Datasources: fixes async mount errors (#24579)

* Chore: updates Elasticsearch DataLinks test async wrapper mount

* Chore: updates Loki DervidedFields test async wrapper mount

* Chore: fixes typecheck issues

* Chore: small strict-null fix

(cherry picked from commit e11504dcd2)

* Editor: New line on Enter, run query on Shift+Enter (#24654)

* Editor: New line on Enter, run query on Shift+Enter

- default Enter behavior on query editor fields should be a new line
- special behavior should require a special key: running a query is now
done on Shift-Enter
- Plugins order had to be changed because when typeahead is shown, Enter
is accepting the suggestion

* Run with ctrl-enter, hint in query placeholder

* Fix Kusto field behavior for Enter

* Fix Kusto field behavior for default suggestion

(cherry picked from commit 01bbcf4eea)

* Add deprecation note about the unofficial first version of backend plugins (#24675)

(cherry picked from commit 0c8ee5b2c9)

* CloudWatch/Logs: Fix autocomplete after by keyword (#24644)

(cherry picked from commit 2b9cc8ba30)

* TablePanel: Do not include angular options in options when switching from angular panel (#24684)

(cherry picked from commit c59f5436c0)

* Datasources: fix pre styles (#24687)

(cherry picked from commit 7fbed90b89)

* @grafana/ui: Checkbox docs (#23141)

* Add "When to use" info to Checkbox.mdx

Added info about how checkboxes generally work and when to use another component instead.

* Replaced words with proper component names

* Update Checkbox.mdx

Elaborate on explanations. Add examples for how to use checkboxes the user-friendliest way.

* Apply suggestions from code review

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

Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>
Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
(cherry picked from commit 405145fdd3)

* Logs: Fix total bytes process calculation (#24691)

- log stats for Loki are per query
- this change tracks the query stats by refId, preventing the summing of
    the same stats across multiple series of the same response.

(cherry picked from commit 25e1238022)

* Strict null fixes: updates return type of getMessageFromError, fixes QueryOperationAction props (#24690)

* Chore: updates getMessageFromError return type

* Chore: updates the position of incorrectly placed bang operator

* Chore: updates typing on dropdown typeahead directive

* Chore: updates QueryOperationAction props to require title

* Revert "Chore: updates QueryOperationAction props to require title"

(cherry picked from commit ee12f5e742)

* Docs: Panel inspector release highlights (#24686)

* Add Panel inspector highlight

* Update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

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

* Explore: fixes loading more logs in logs context view (#24135)

(cherry picked from commit d3a8f6d026)

* Datasource/CloudWatch: Handle invalidation of log groups when switching datasources (#24703)

(cherry picked from commit 1b29d3460e)

* PanelInspect: Make header names reflect the field name in the exported CSV file (#24624)

* fix header names and filename

* use panel title as filename

* add frame argument

* escaping double quotes

* wrapping header name in  quotes

* Fix replace

* Add test for csv double quotes escaping

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

* CloudWatch/Logs: Fix query error when results are sparse (#24702)

(cherry picked from commit bc8c05137b)

* Datasource/CloudWatch: Allows a user to search for log groups that aren't there initially (#24695)

Closes #24554

(cherry picked from commit 0e8638ec92)

* Transform: fixes so we match the field based on the proper name. (#24659)

* fixes so we match the transformer based on name properly.

* changed the signature on the FieldMatcher.

* introduced a names option so you can filter in name specificly.

* changed so the matcher UI uses the new options format.

* moved the exported functions together.

* changing editors a bit.

* made the filter by name work with both regex and name filtering.

* fixed failing tests and make sure we always parse regex the same way.

* removed unused code.

* simplified to make the existing field overrides still working.

* fixed issue reported by hugo.

* added tests for the name matcher.

* added tests for filter by name.

* added more tests.

(cherry picked from commit 96f26cbd5b)

* Fix: changing so we are using date/time values in UTC during test. (#24707)

(cherry picked from commit 98d5633601)

* Docs: generated a 7.0 version of the packages reference docs. (#24708)

* fixes so we match the transformer based on name properly.

* changed the signature on the FieldMatcher.

* introduced a names option so you can filter in name specificly.

* changed so the matcher UI uses the new options format.

* moved the exported functions together.

* changing editors a bit.

* made the filter by name work with both regex and name filtering.

* fixed failing tests and make sure we always parse regex the same way.

* removed unused code.

* simplified to make the existing field overrides still working.

* fixed issue reported by hugo.

* added tests for the name matcher.

* added tests for filter by name.

* added more tests.

* generated new version of the packages docs.

* fixed spelling error.

* regenerated the docs.

(cherry picked from commit 6a0abf895e)

* Query: do not require datasource name and id (#24718)

(cherry picked from commit b73d237bf8)

* Metrictank: Fixed updated schema and dedupe strategy (#24685)

* Metrictank: Fixed updated schema and dedupe strategy

* revert schema change

* fix

(cherry picked from commit 7fcf2615e1)

* Release 7.0.0

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* CircleCI: Upgrade build pipeline tool

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Leonard Gram <leo@xlson.com>
Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
Co-authored-by: Andreas Opferkuch <andreas.opferkuch@gmail.com>
Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
Co-authored-by: David <david.kaltschmidt@gmail.com>
Co-authored-by: Daniel Lee <dan.limerick@gmail.com>
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
Co-authored-by: Rotem Reiss <reiss.r@gmail.com>
Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>
Co-authored-by: Jess <53754871+jessover9000@users.noreply.github.com>
Co-authored-by: Carl Bergquist <carl@grafana.com>
Co-authored-by: Lukas Siatka <lukasz.siatka@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
Arve Knudsen
2020-05-15 14:00:13 +02:00
committed by GitHub
co-authored by Tobias Skarhed Dominik Prokop Leonard Gram Alexander Zobnin Andreas Opferkuch kay delaney Torkel Ödegaard Peter Holmberg Hugo Häggmark Marcus Andersson Andrej Ocenas David Daniel Lee Ryan McKinley Ivana Huckova Zoltán Bedi Alex Khomenko Rotem Reiss Agnès Toulet Jess Carl Bergquist Lukas Siatka Marcus Efraimsson
parent 2169fc1a3f
commit 0167f43d74
402 changed files with 7752 additions and 2674 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Grafana Data Library
> **@grafana/data is currently in ALPHA**. Core API is unstable and can be a subject of breaking changes!
> **@grafana/data is currently in BETA**.
This package holds the root data types and functions used within Grafana.
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "7.0.0-beta.3",
"version": "7.0.0",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -57,9 +57,9 @@ export class FieldCache {
return types && types.length > 0;
}
getFirstFieldOfType(type: FieldType): FieldWithIndex | undefined {
getFirstFieldOfType(type: FieldType, includeHidden = false): FieldWithIndex | undefined {
const fields = this.fieldByType[type];
const firstField = fields.find(field => !(field.config.custom && field.config.custom['Hidden']));
const firstField = fields.find(field => includeHidden || !field.config.custom?.hidden);
return firstField;
}
@@ -14,14 +14,15 @@ import {
TimeSeriesValue,
FieldDTO,
DataFrameDTO,
TIME_SERIES_FIELD_NAME,
TIME_SERIES_VALUE_FIELD_NAME,
TIME_SERIES_TIME_FIELD_NAME,
} from '../types/index';
import { isDateTime } from '../datetime/moment_wrapper';
import { ArrayVector } from '../vector/ArrayVector';
import { MutableDataFrame } from './MutableDataFrame';
import { SortedVector } from '../vector/SortedVector';
import { ArrayDataFrame } from './ArrayDataFrame';
import { getFieldTitle } from '../field/fieldState';
import { getFieldDisplayName } from '../field/fieldState';
function convertTableToDataFrame(table: TableData): DataFrame {
const fields = table.columns.map(c => {
@@ -71,13 +72,13 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
const fields = [
{
name: 'Time',
name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time,
config: {},
values: new ArrayVector<number>(times),
},
{
name: TIME_SERIES_FIELD_NAME,
name: TIME_SERIES_VALUE_FIELD_NAME,
type: FieldType.number,
config: {
unit: timeSeries.unit,
@@ -88,7 +89,7 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
];
if (timeSeries.title) {
(fields[1].config as FieldConfig).title = timeSeries.title;
(fields[1].config as FieldConfig).displayName = timeSeries.title;
}
return {
@@ -118,13 +119,13 @@ function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame {
name: graphSeries.label,
fields: [
{
name: graphSeries.label || TIME_SERIES_FIELD_NAME,
name: graphSeries.label || TIME_SERIES_VALUE_FIELD_NAME,
type: FieldType.number,
config: {},
values: x,
},
{
name: 'Time',
name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time,
config: {
unit: 'dateTimeAsIso',
@@ -332,7 +333,7 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
return {
alias: frame.name,
target: getFieldTitle(valueField, frame),
target: getFieldDisplayName(valueField, frame),
datapoints: rows,
unit: fields[0].config ? fields[0].config.unit : undefined,
refId: frame.refId,
@@ -63,10 +63,33 @@ const rangeOptions = [
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 },
];
const hiddenRangeOptions = [
{ from: 'now', to: 'now+1m', display: 'Next minute', section: 3 },
{ from: 'now', to: 'now+5m', display: 'Next 5 minutes', section: 3 },
{ from: 'now', to: 'now+15m', display: 'Next 15 minutes', section: 3 },
{ from: 'now', to: 'now+30m', display: 'Next 30 minutes', section: 3 },
{ from: 'now', to: 'now+1h', display: 'Next hour', section: 3 },
{ from: 'now', to: 'now+3h', display: 'Next 3 hours', section: 3 },
{ from: 'now', to: 'now+6h', display: 'Next 6 hours', section: 3 },
{ from: 'now', to: 'now+12h', display: 'Next 12 hours', section: 3 },
{ from: 'now', to: 'now+24h', display: 'Next 24 hours', section: 3 },
{ from: 'now', to: 'now+2d', display: 'Next 2 days', section: 0 },
{ from: 'now', to: 'now+7d', display: 'Next 7 days', section: 0 },
{ from: 'now', to: 'now+30d', display: 'Next 30 days', section: 0 },
{ from: 'now', to: 'now+90d', display: 'Next 90 days', section: 0 },
{ from: 'now', to: 'now+6M', display: 'Next 6 months', section: 0 },
{ from: 'now', to: 'now+1y', display: 'Next year', section: 0 },
{ from: 'now', to: 'now+2y', display: 'Next 2 years', section: 0 },
{ from: 'now', to: 'now+5y', display: 'Next 5 years', section: 0 },
];
const rangeIndex: any = {};
each(rangeOptions, (frame: any) => {
rangeIndex[frame.from + ' to ' + frame.to] = frame;
});
each(hiddenRangeOptions, (frame: any) => {
rangeIndex[frame.from + ' to ' + frame.to] = frame;
});
export function getRelativeTimesList(timepickerSettings: any, currentDisplay: any) {
const groups = groupBy(rangeOptions, (option: any) => {
@@ -188,8 +211,13 @@ export const describeTimeRangeAbbrevation = (range: TimeRange, timeZone?: TimeZo
return parsed ? timeZoneAbbrevation(parsed, { timeZone }) : '';
};
export const convertRawToRange = (raw: RawTimeRange): TimeRange => {
const from = dateTimeParse(raw.from, { roundUp: false });
const to = dateTimeParse(raw.to, { roundUp: true });
return { from, to, raw };
export const convertRawToRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => {
const from = dateTimeParse(raw.from, { roundUp: false, timeZone });
const to = dateTimeParse(raw.to, { roundUp: true, timeZone });
if (dateMath.isMathString(raw.from) || dateMath.isMathString(raw.to)) {
return { from, to, raw };
}
return { from, to, raw: { from, to } };
};
@@ -1,5 +1,18 @@
// List taken from https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
import { TimeZone } from '../types';
import { getTimeZone } from './common';
export const timeZoneFormatUserFriendly = (timeZone: TimeZone | undefined) => {
switch (getTimeZone({ timeZone })) {
case 'browser':
return 'Local browser time';
case 'utc':
return 'UTC';
default:
return timeZone;
}
};
// List taken from https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
export const getTimeZoneGroups = () => {
const europeZones = [
'Europe/Amsterdam',
@@ -32,7 +32,7 @@ describe('FieldDisplay', () => {
fieldConfig: {
overrides: [],
defaults: {
title: '$__cell_0 * $__field_name * $__series_name',
displayName: '$__cell_0 * $__field_name * $__series_name',
},
},
});
@@ -91,7 +91,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
let hitLimit = false;
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
const scopedVars: ScopedVars = {};
const defaultTitle = getTitleTemplate(calcs);
const defaultDisplayName = getTitleTemplate(calcs);
for (let s = 0; s < data.length && !hitLimit; s++) {
const series = data[s]; // Name is already set
@@ -109,7 +109,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
}
const config = field.config; // already set by the prepare task
const title = field.config.title ?? defaultTitle;
const displayName = field.config.displayName ?? defaultDisplayName;
const display =
field.display ??
@@ -121,7 +121,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
// Show all rows
if (reduceOptions.values) {
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0;
for (let j = 0; j < field.values.length; j++) {
// Add all the row variables
@@ -137,7 +137,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
}
const displayValue = display(field.values.get(j));
displayValue.title = replaceVariables(title, {
displayValue.title = replaceVariables(displayName, {
...field.state?.scopedVars, // series and field scoped vars
...scopedVars,
});
@@ -181,7 +181,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
for (const calc of calcs) {
scopedVars[VAR_CALC] = { value: calc, text: calc };
const displayValue = display(results[calc]);
displayValue.title = replaceVariables(title, {
displayValue.title = replaceVariables(displayName, {
...field.state?.scopedVars, // series and field scoped vars
...scopedVars,
});
@@ -209,7 +209,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
if (values.length === 0) {
values.push(createNoValuesFieldDisplay(options));
} else if (values.length === 1 && !fieldConfig.defaults.title) {
} else if (values.length === 1 && !fieldConfig.defaults.displayName) {
// Don't show title for single item
values[0].display.title = undefined;
}
@@ -19,7 +19,7 @@ import { Registry } from '../utils';
import { mockStandardProperties } from '../utils/tests/mockStandardProperties';
import { FieldMatcherID } from '../transformations';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { getFieldTitle } from './fieldState';
import { getFieldDisplayName } from './fieldState';
const property1 = {
id: 'custom.property1', // Match field properties
@@ -84,7 +84,7 @@ describe('applyFieldOverrides', () => {
matcher: { id: FieldMatcherID.numeric },
properties: [
{ id: 'decimals', value: 1 }, // Numeric
{ id: 'title', value: 'Kittens' }, // Text
{ id: 'displayName', value: 'Kittens' }, // Text
],
},
],
@@ -163,7 +163,7 @@ describe('applyFieldOverrides', () => {
dateFormat: '', // should be ignored
max: parseFloat('NOPE'), // should be ignored
min: null, // should alo be ignored!
title: 'newTitle',
displayName: 'newTitle',
};
const f: DataFrame = toDataFrame({
@@ -186,7 +186,7 @@ describe('applyFieldOverrides', () => {
expect(outField.config.min).toEqual(0);
expect(outField.config.max).toEqual(100);
expect(outField.config.unit).toEqual('ms');
expect(getFieldTitle(outField, f)).toEqual('newTitle');
expect(getFieldDisplayName(outField, f)).toEqual('newTitle');
});
it('will apply field overrides', () => {
@@ -210,7 +210,7 @@ describe('applyFieldOverrides', () => {
expect(config.unit).toEqual('xyz');
// The default value applied
expect(config.title).toEqual('Kittens');
expect(config.displayName).toEqual('Kittens');
// The override applied
expect(config.decimals).toEqual(1);
@@ -309,13 +309,13 @@ describe('setFieldConfigDefaults', () => {
describe('setDynamicConfigValue', () => {
it('applies dynamic config values', () => {
const config = {
title: 'test',
displayName: 'test',
};
setDynamicConfigValue(
config,
{
id: 'title',
id: 'displayName',
value: 'applied',
},
{
@@ -326,7 +326,7 @@ describe('setDynamicConfigValue', () => {
}
);
expect(config.title).toEqual('applied');
expect(config.displayName).toEqual('applied');
});
it('applies custom dynamic config values', () => {
@@ -379,7 +379,7 @@ describe('setDynamicConfigValue', () => {
it('removes properties', () => {
const config = {
title: 'title',
displayName: 'title',
custom: {
property3: {
nested: 1,
@@ -403,7 +403,7 @@ describe('setDynamicConfigValue', () => {
setDynamicConfigValue(
config,
{
id: 'title',
id: 'displayName',
value: undefined,
},
{
@@ -415,6 +415,6 @@ describe('setDynamicConfigValue', () => {
);
expect(config.custom.property3).toEqual({});
expect(config.title).toBeUndefined();
expect(config.displayName).toBeUndefined();
});
});
@@ -31,7 +31,7 @@ import { DataLinkBuiltInVars, locationUtil } from '../utils';
import { formattedValueToString } from '../valueFormats';
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
import { formatLabels } from '../utils/labels';
import { getFrameDisplayTitle, getFieldTitle } from './fieldState';
import { getFrameDisplayName, getFieldDisplayName } from './fieldState';
import { getTimeField } from '../dataframe/processDataFrame';
interface OverrideProps {
@@ -100,18 +100,18 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
return options.data.map((frame, index) => {
const scopedVars: ScopedVars = {
__series: { text: 'Series', value: { name: getFrameDisplayTitle(frame, index) } }, // might be missing
__series: { text: 'Series', value: { name: getFrameDisplayName(frame, index) } }, // might be missing
};
const fields: Field[] = frame.fields.map(field => {
// Config is mutable within this scope
const fieldScopedVars = { ...scopedVars };
const title = getFieldTitle(field, frame, options.data);
const displayName = getFieldDisplayName(field, frame, options.data);
fieldScopedVars['__field'] = {
text: 'Field',
value: {
name: title, // Generally appropriate (may include the series name if useful)
name: displayName, // Generally appropriate (may include the series name if useful)
labels: formatLabels(field.labels!),
label: field.labels,
},
@@ -119,8 +119,8 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
field.state = {
...field.state,
title: title,
scopedVars: fieldScopedVars,
displayName,
};
const config: FieldConfig = { ...field.config };
@@ -138,7 +138,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
// Find any matching rules and then override
for (const rule of override) {
if (rule.match(field)) {
if (rule.match(field, frame, options.data!)) {
for (const prop of rule.properties) {
// config.scopedVars is set already here
setDynamicConfigValue(config, prop, context);
@@ -194,7 +194,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
type,
state: {
...field.state,
title: null,
displayName: null,
},
};
@@ -1,5 +1,5 @@
import { DataFrame, TIME_SERIES_FIELD_NAME, FieldType } from '../types';
import { getFieldTitle } from './fieldState';
import { DataFrame, TIME_SERIES_VALUE_FIELD_NAME, FieldType } from '../types';
import { getFieldDisplayName } from './fieldState';
import { toDataFrame } from '../dataframe';
interface TitleScenario {
@@ -11,10 +11,10 @@ interface TitleScenario {
function checkScenario(scenario: TitleScenario): string {
const frame = scenario.frames[scenario.frameIndex ?? 0];
const field = frame.fields[scenario.fieldIndex ?? 0];
return getFieldTitle(field, frame, scenario.frames);
return getFieldDisplayName(field, frame, scenario.frames);
}
describe('Check field state calculations (title and id)', () => {
describe('Check field state calculations (displayName and id)', () => {
it('should use field name if no frame name', () => {
const title = checkScenario({
frames: [
@@ -92,23 +92,23 @@ describe('Check field state calculations (title and id)', () => {
expect(title).toEqual('{mode="B", server="Server A"}');
});
it('should use field name even when it is TIME_SERIES_FIELD_NAME if there are no labels', () => {
it('should use field name even when it is TIME_SERIES_VALUE_FIELD_NAME if there are no labels', () => {
const title = checkScenario({
frames: [
toDataFrame({
fields: [{ name: TIME_SERIES_FIELD_NAME, labels: {} }],
fields: [{ name: TIME_SERIES_VALUE_FIELD_NAME, labels: {} }],
}),
],
});
expect(title).toEqual('Value');
});
it('should use series name when field name is TIME_SERIES_FIELD_NAME and there are no labels ', () => {
it('should use series name when field name is TIME_SERIES_VALUE_FIELD_NAME and there are no labels ', () => {
const title = checkScenario({
frames: [
toDataFrame({
name: 'Series A',
fields: [{ name: TIME_SERIES_FIELD_NAME, labels: {} }],
fields: [{ name: TIME_SERIES_VALUE_FIELD_NAME, labels: {} }],
}),
],
});
+20 -20
View File
@@ -1,10 +1,10 @@
import { DataFrame, Field, TIME_SERIES_FIELD_NAME, FieldType } from '../types';
import { DataFrame, Field, TIME_SERIES_VALUE_FIELD_NAME, FieldType, TIME_SERIES_TIME_FIELD_NAME } from '../types';
import { formatLabels } from '../utils/labels';
/**
* Get an appropriate display title
*/
export function getFrameDisplayTitle(frame: DataFrame, index?: number) {
export function getFrameDisplayName(frame: DataFrame, index?: number) {
if (frame.name) {
return frame.name;
}
@@ -19,7 +19,7 @@ export function getFrameDisplayTitle(frame: DataFrame, index?: number) {
if (index === undefined) {
return frame.fields
.filter(f => f.type !== FieldType.time)
.map(f => getFieldTitle(f, frame))
.map(f => getFieldDisplayName(f, frame))
.join(', ');
}
@@ -30,39 +30,39 @@ export function getFrameDisplayTitle(frame: DataFrame, index?: number) {
return `Series (${index})`;
}
export function getFieldTitle(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string {
const existingTitle = field.state?.title;
export function getFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string {
const existingTitle = field.state?.displayName;
if (existingTitle) {
return existingTitle;
}
const title = calculateFieldTitle(field, frame, allFrames);
const displayName = calculateFieldDisplayName(field, frame, allFrames);
field.state = {
...field.state,
title,
displayName,
};
return title;
return displayName;
}
/**
* Get an appropriate display title. If the 'title' is set, use that
* Get an appropriate display name. If the 'title' is set, use that
*/
function calculateFieldTitle(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string {
const hasConfigTitle = field.config?.title && field.config?.title.length;
function calculateFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string {
const hasConfigTitle = field.config?.displayName && field.config?.displayName.length;
let title = hasConfigTitle ? field.config!.title! : field.name;
let displayName = hasConfigTitle ? field.config!.displayName! : field.name;
if (hasConfigTitle) {
return title;
return displayName;
}
// This is an ugly exception for time field
// For time series we should normally treat time field with same name
// But in case it has a join source we should handle it as normal field
if (field.type === FieldType.time && !field.labels) {
return title ?? 'Time';
return displayName ?? TIME_SERIES_TIME_FIELD_NAME;
}
let parts: string[] = [];
@@ -86,7 +86,7 @@ function calculateFieldTitle(field: Field, frame?: DataFrame, allFrames?: DataFr
frameNameAdded = true;
}
if (field.name && field.name !== TIME_SERIES_FIELD_NAME) {
if (field.name && field.name !== TIME_SERIES_VALUE_FIELD_NAME) {
parts.push(field.name);
}
@@ -106,7 +106,7 @@ function calculateFieldTitle(field: Field, frame?: DataFrame, allFrames?: DataFr
}
// if we have not added frame name and no labels, and field name = Value, we should add frame name
if (frame && !frameNameAdded && !labelsAdded && field.name === TIME_SERIES_FIELD_NAME) {
if (frame && !frameNameAdded && !labelsAdded && field.name === TIME_SERIES_VALUE_FIELD_NAME) {
if (frame.name && frame.name.length > 0) {
parts.push(frame.name);
frameNameAdded = true;
@@ -114,14 +114,14 @@ function calculateFieldTitle(field: Field, frame?: DataFrame, allFrames?: DataFr
}
if (parts.length) {
title = parts.join(' ');
displayName = parts.join(' ');
} else if (field.name) {
title = field.name;
displayName = field.name;
} else {
title = TIME_SERIES_FIELD_NAME;
displayName = TIME_SERIES_VALUE_FIELD_NAME;
}
return title;
return displayName;
}
/**
@@ -13,7 +13,7 @@ describe('getFieldDisplayValuesProxy', () => {
name: 'power',
values: [100, 200, 300],
config: {
title: 'The Power',
displayName: 'The Power',
},
},
{
@@ -54,7 +54,7 @@ describe('getFieldDisplayValuesProxy', () => {
expect(time2.toString()).toEqual(time.toString());
});
it('Lookup by name, index, or title', () => {
it('Lookup by name, index, or displayName', () => {
const p = getFieldDisplayValuesProxy(data, 2, {
theme: {} as GrafanaTheme,
});
@@ -29,7 +29,7 @@ export function getFieldDisplayValuesProxy(
}
if (!field) {
// 3. Match the title
field = frame.fields.find(f => key === f.config.title);
field = frame.fields.find(f => key === f.config.displayName);
}
if (!field) {
return undefined;
+1 -1
View File
@@ -7,4 +7,4 @@ export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
export { getFieldTitle, getFrameDisplayTitle } from './fieldState';
export { getFieldDisplayName, getFrameDisplayName } from './fieldState';
@@ -9,3 +9,4 @@ export {
TransformerUIProps,
standardTransformersRegistry,
} from './standardTransformersRegistry';
export { RegexpOrNamesMatcherOptions } from './matchers/nameMatcher';
@@ -16,7 +16,8 @@ describe('Field Type Matcher', () => {
it('finds numbers', () => {
for (const field of simpleSeriesWithTypes.fields) {
const matches = matcher.get(FieldType.number);
expect(matches(field)).toBe(field.type === FieldType.number);
const didMatch = matches(field, simpleSeriesWithTypes, [simpleSeriesWithTypes]);
expect(didMatch).toBe(field.type === FieldType.number);
}
});
});
@@ -1,4 +1,4 @@
import { Field, FieldType } from '../../types/dataFrame';
import { Field, FieldType, DataFrame } from '../../types/dataFrame';
import { FieldMatcherID } from './ids';
import { FieldMatcherInfo } from '../../types/transformations';
@@ -10,7 +10,7 @@ const fieldTypeMatcher: FieldMatcherInfo<FieldType> = {
defaultOptions: FieldType.number,
get: (type: FieldType) => {
return (field: Field) => {
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
return type === field.type;
};
},
@@ -18,6 +18,9 @@ export enum FieldMatcherID {
// With arguments
byType = 'byType',
byName = 'byName',
byNames = 'byNames',
byRegexp = 'byRegexp',
byRegexpOrNames = 'byRegexpOrNames',
// byIndex = 'byIndex',
// byLabel = 'byLabel',
}
@@ -2,20 +2,20 @@ import { getFieldMatcher } from '../matchers';
import { FieldMatcherID } from './ids';
import { toDataFrame } from '../../dataframe/processDataFrame';
describe('Field Name Matcher', () => {
describe('Field Name by Regexp Matcher', () => {
it('Match all with wildcard regex', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byName,
id: FieldMatcherID.byRegexp,
options: '/.*/',
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field)).toBe(true);
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true);
}
});
@@ -24,14 +24,14 @@ describe('Field Name Matcher', () => {
fields: [{ name: '12' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byName,
id: FieldMatcherID.byRegexp,
options: '/^\\d+$/',
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field)).toBe(true);
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true);
}
});
@@ -40,17 +40,269 @@ describe('Field Name Matcher', () => {
fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byName,
id: FieldMatcherID.byRegexp,
options: '/\\b(?:\\S+?\\.)+\\S+\\b$/',
};
const matcher = getFieldMatcher(config);
let resultCount = 0;
for (const field of seriesWithNames.fields) {
if (matcher(field)) {
if (matcher(field, seriesWithNames, [seriesWithNames])) {
resultCount++;
}
expect(resultCount).toBe(1);
}
});
});
describe('Field Name Matcher', () => {
it('Match only exact name', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byName,
options: 'C',
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
const didMatch = matcher(field, seriesWithNames, [seriesWithNames]);
expect(didMatch).toBe(field.name === 'C');
}
});
it('Match should respect letter case', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byName,
options: 'c',
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false);
}
});
it('Match none of the field names', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byName,
options: '',
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false);
}
});
});
describe('Field Multiple Names Matcher', () => {
it('Match only exact name', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byNames,
options: ['C'],
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
const didMatch = matcher(field, seriesWithNames, [seriesWithNames]);
expect(didMatch).toBe(field.name === 'C');
}
});
it('Match should respect letter case', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byNames,
options: ['c'],
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false);
}
});
it('Match none of the field names', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byNames,
options: [],
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false);
}
});
it('Match all of the field names', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byNames,
options: ['some.instance.path', '112', '13'],
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true);
}
});
});
describe('Field Regexp or Names Matcher', () => {
it('Match only exact name by name', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byRegexpOrNames,
options: {
names: ['C'],
},
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
const didMatch = matcher(field, seriesWithNames, [seriesWithNames]);
expect(didMatch).toBe(field.name === 'C');
}
});
it('Match all starting with AA', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byRegexpOrNames,
options: {
pattern: '/^AA/',
},
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
const didMatch = matcher(field, seriesWithNames, [seriesWithNames]);
expect(didMatch).toBe(field.name === 'AAA');
}
});
it('Match all starting with AA and C', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byRegexpOrNames,
options: {
pattern: '/^AA/',
names: ['C'],
},
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
const didMatch = matcher(field, seriesWithNames, [seriesWithNames]);
expect(didMatch).toBe(field.name === 'AAA' || field.name === 'C');
}
});
it('Match should respect letter case by name if not igored in pattern', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }],
});
const config = {
id: FieldMatcherID.byRegexpOrNames,
options: {
names: ['c'],
pattern: '/c/i',
},
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
const didMatch = matcher(field, seriesWithNames, [seriesWithNames]);
expect(didMatch).toBe(field.name === 'C');
}
});
it('Match none of the field names by name', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byRegexpOrNames,
options: {
names: [],
},
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false);
}
});
it('Match all of the field names by name', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byRegexpOrNames,
options: {
names: ['some.instance.path', '112', '13'],
},
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true);
}
});
it('Match all of the field names by regexp', () => {
const seriesWithNames = toDataFrame({
fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }],
});
const config = {
id: FieldMatcherID.byRegexpOrNames,
options: {
pattern: '/.*/',
},
};
const matcher = getFieldMatcher(config);
for (const field of seriesWithNames.fields) {
expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true);
}
});
});
@@ -1,35 +1,111 @@
import { Field, DataFrame } from '../../types/dataFrame';
import { FieldMatcherID, FrameMatcherID } from './ids';
import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations';
import { FieldMatcherInfo, FrameMatcherInfo, FieldMatcher } from '../../types/transformations';
import { stringToJsRegex } from '../../text/string';
import { getFieldTitle } from '../../field/fieldState';
import { getFieldDisplayName } from '../../field/fieldState';
export interface RegexpOrNamesMatcherOptions {
pattern?: string;
names?: string[];
}
// General Field matcher
const fieldNameMacher: FieldMatcherInfo<string> = {
const fieldNameMatcher: FieldMatcherInfo<string> = {
id: FieldMatcherID.byName,
name: 'Field Name',
description: 'match the field name',
defaultOptions: '/.*/',
defaultOptions: '',
get: (pattern: string) => {
let regex = new RegExp('');
try {
regex = stringToJsRegex(pattern);
} catch (e) {
console.error(e);
}
return (field: Field) => {
return regex.test(getFieldTitle(field) ?? '');
get: (name: string): FieldMatcher => {
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
return getFieldDisplayName(field, frame, allFrames) === name;
};
},
getOptionsDisplayText: (pattern: string) => {
return `Field name: ${pattern}`;
getOptionsDisplayText: (name: string) => {
return `Field name: ${name}`;
},
};
// General Field matcher
const frameNameMacher: FrameMatcherInfo<string> = {
const multipleFieldNamesMatcher: FieldMatcherInfo<string[]> = {
id: FieldMatcherID.byNames,
name: 'Field Names',
description: 'match any of the given the field names',
defaultOptions: [],
get: (names: string[]): FieldMatcher => {
const uniqueNames = new Set<string>(names ?? []);
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
return uniqueNames.has(getFieldDisplayName(field, frame, allFrames));
};
},
getOptionsDisplayText: (names: string[]): string => {
return `Field names: ${names.join(', ')}`;
},
};
const regexpFieldNameMatcher: FieldMatcherInfo<string> = {
id: FieldMatcherID.byRegexp,
name: 'Field Name by Regexp',
description: 'match the field name by a given regexp pattern',
defaultOptions: '/.*/',
get: (pattern: string): FieldMatcher => {
const regexp = patternToRegex(pattern);
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
const displayName = getFieldDisplayName(field, frame, allFrames);
return !!regexp && regexp.test(displayName);
};
},
getOptionsDisplayText: (pattern: string): string => {
return `Field name by pattern: ${pattern}`;
},
};
const regexpOrMultipleNamesMatcher: FieldMatcherInfo<RegexpOrNamesMatcherOptions> = {
id: FieldMatcherID.byRegexpOrNames,
name: 'Field Name by Regexp or Names',
description: 'match the field name by a given regexp pattern or given names',
defaultOptions: {
pattern: '/.*/',
names: [],
},
get: (options: RegexpOrNamesMatcherOptions): FieldMatcher => {
const regexpMatcher = regexpFieldNameMatcher.get(options?.pattern || '');
const namesMatcher = multipleFieldNamesMatcher.get(options?.names ?? []);
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
return namesMatcher(field, frame, allFrames) || regexpMatcher(field, frame, allFrames);
};
},
getOptionsDisplayText: (options: RegexpOrNamesMatcherOptions): string => {
const pattern = options?.pattern ?? '';
const names = options?.names?.join(',') ?? '';
return `Field name by pattern: ${pattern} or names: ${names}`;
},
};
const patternToRegex = (pattern?: string): RegExp | undefined => {
if (!pattern) {
return undefined;
}
try {
return stringToJsRegex(pattern);
} catch (error) {
console.log(error);
return undefined;
}
};
// General Frame matcher
const frameNameMatcher: FrameMatcherInfo<string> = {
id: FrameMatcherID.byName,
name: 'Frame Name',
description: 'match the frame name',
@@ -51,9 +127,9 @@ const frameNameMacher: FrameMatcherInfo<string> = {
* Registry Initalization
*/
export function getFieldNameMatchers(): FieldMatcherInfo[] {
return [fieldNameMacher];
return [fieldNameMatcher, regexpFieldNameMatcher, multipleFieldNamesMatcher, regexpOrMultipleNamesMatcher];
}
export function getFrameNameMatchers(): FrameMatcherInfo[] {
return [frameNameMacher];
return [frameNameMatcher];
}
@@ -13,26 +13,29 @@ const matchesTimeConfig: MatcherConfig = {
options: FieldType.time,
};
const both = [matchesNumberConfig, matchesTimeConfig];
const allFrames = [simpleSeriesWithTypes];
describe('Check Predicates', () => {
it('can not match both', () => {
const matches = fieldMatchers.get(MatcherID.allMatch).get(both);
for (const field of simpleSeriesWithTypes.fields) {
expect(matches(field)).toBe(false);
expect(matches(field, simpleSeriesWithTypes, allFrames)).toBe(false);
}
});
it('match either time or number', () => {
const matches = fieldMatchers.get(MatcherID.anyMatch).get(both);
for (const field of simpleSeriesWithTypes.fields) {
expect(matches(field)).toBe(field.type === FieldType.number || field.type === FieldType.time);
expect(matches(field, simpleSeriesWithTypes, allFrames)).toBe(
field.type === FieldType.number || field.type === FieldType.time
);
}
});
it('match not time', () => {
const matches = fieldMatchers.get(MatcherID.invertMatch).get(matchesTimeConfig);
for (const field of simpleSeriesWithTypes.fields) {
expect(matches(field)).toBe(field.type !== FieldType.time);
expect(matches(field, simpleSeriesWithTypes, allFrames)).toBe(field.type !== FieldType.time);
}
});
});
@@ -14,9 +14,9 @@ const anyFieldMatcher: FieldMatcherInfo<MatcherConfig[]> = {
const children = options.map(option => {
return getFieldMatcher(option);
});
return (field: Field) => {
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
for (const child of children) {
if (child(field)) {
if (child(field, frame, allFrames)) {
return true;
}
}
@@ -82,9 +82,9 @@ const allFieldsMatcher: FieldMatcherInfo<MatcherConfig[]> = {
const children = options.map(option => {
return getFieldMatcher(option);
});
return (field: Field) => {
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
for (const child of children) {
if (!child(field)) {
if (!child(field, frame, allFrames)) {
return false;
}
}
@@ -147,8 +147,8 @@ const notFieldMatcher: FieldMatcherInfo<MatcherConfig> = {
get: (option: MatcherConfig) => {
const check = getFieldMatcher(option);
return (field: Field) => {
return !check(field);
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
return !check(field, frame, allFrames);
};
},
@@ -10,6 +10,7 @@ import { organizeFieldsTransformer } from './transformers/organize';
import { seriesToColumnsTransformer } from './transformers/seriesToColumns';
import { renameFieldsTransformer } from './transformers/rename';
import { labelsToFieldsTransformer } from './transformers/labelsToFields';
import { ensureColumnsTransformer } from './transformers/ensureColumns';
export const standardTransformers = {
noopTransformer,
@@ -25,4 +26,5 @@ export const standardTransformers = {
seriesToColumnsTransformer,
renameFieldsTransformer,
labelsToFieldsTransformer,
ensureColumnsTransformer,
};
@@ -4,7 +4,7 @@ import { FieldType } from '../../types/dataFrame';
import { ReducerID } from '../fieldReducer';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { transformDataFrame } from '../transformDataFrame';
import { calculateFieldTransformer, CalculateFieldMode } from './calculateField';
import { calculateFieldTransformer, CalculateFieldMode, ReduceOptions } from './calculateField';
import { DataFrameView } from '../../dataframe';
import { BinaryOperationID } from '../../utils';
@@ -96,9 +96,9 @@ describe('calculateField transformer w/ timeseries', () => {
options: {
mode: CalculateFieldMode.ReduceRow,
reduce: {
include: 'B',
include: ['B'],
reducer: ReducerID.mean,
},
} as ReduceOptions,
replaceFields: true,
},
};
@@ -6,10 +6,11 @@ import { FieldMatcherID } from '../matchers/ids';
import { RowVector } from '../../vector/RowVector';
import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector';
import { doStandardCalcs } from '../fieldReducer';
import { seriesToColumnsTransformer } from './seriesToColumns';
import { getTimeField } from '../../dataframe/processDataFrame';
import defaults from 'lodash/defaults';
import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators';
import { ensureColumnsTransformer } from './ensureColumns';
import { getFieldDisplayName } from '../../field';
export enum CalculateFieldMode {
ReduceRow = 'reduceRow',
@@ -17,7 +18,7 @@ export enum CalculateFieldMode {
}
export interface ReduceOptions {
include?: string; // Assume all fields
include?: string[]; // Assume all fields
reducer: ReducerID;
nullValueMode?: NullValueMode;
}
@@ -68,22 +69,17 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
},
},
transformer: options => (data: DataFrame[]) => {
// Assume timeseries should first be joined by time
const timeFieldName = findConsistentTimeFieldName(data);
if (data.length > 1 && timeFieldName && options.timeSeries !== false) {
data = seriesToColumnsTransformer.transformer({
byField: timeFieldName,
})(data);
if (options && options.timeSeries !== false) {
data = ensureColumnsTransformer.transformer(null)(data);
}
const mode = options.mode ?? CalculateFieldMode.ReduceRow;
let creator: ValuesCreator | undefined = undefined;
if (mode === CalculateFieldMode.ReduceRow) {
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions));
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
} else if (mode === CalculateFieldMode.BinaryOperation) {
creator = getBinaryCreator(defaults(options.binary, defaultBinaryOptions));
creator = getBinaryCreator(defaults(options.binary, defaultBinaryOptions), data);
}
// Nothing configured
@@ -125,14 +121,14 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
},
};
function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): ValuesCreator {
let matcher = getFieldMatcher({
id: FieldMatcherID.numeric,
});
if (options.include && options.include.length) {
matcher = getFieldMatcher({
id: FieldMatcherID.byName,
id: FieldMatcherID.byNames,
options: options.include,
});
}
@@ -151,7 +147,7 @@ function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
// Find the columns that should be examined
const columns: Vector[] = [];
for (const field of frame.fields) {
if (matcher(field)) {
if (matcher(field, frame, allFrames)) {
columns.push(field.values);
}
}
@@ -176,13 +172,13 @@ function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
};
}
function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string): Vector | undefined {
function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string, allFrames: DataFrame[]): Vector | undefined {
if (!name) {
return undefined;
}
for (const f of frame.fields) {
if (f.name === name) {
if (name === getFieldDisplayName(f, frame, allFrames)) {
return f.values;
}
}
@@ -195,12 +191,12 @@ function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string): Vect
return undefined;
}
function getBinaryCreator(options: BinaryOptions): ValuesCreator {
function getBinaryCreator(options: BinaryOptions, allFrames: DataFrame[]): ValuesCreator {
const operator = binaryOperators.getIfExists(options.operator);
return (frame: DataFrame) => {
const left = findFieldValuesWithNameOrConstant(frame, options.left);
const right = findFieldValuesWithNameOrConstant(frame, options.right);
const left = findFieldValuesWithNameOrConstant(frame, options.left, allFrames);
const right = findFieldValuesWithNameOrConstant(frame, options.right, allFrames);
if (!left || !right || !operator) {
return (undefined as unknown) as Vector;
}
@@ -209,26 +205,6 @@ function getBinaryCreator(options: BinaryOptions): ValuesCreator {
};
}
/**
* Find the name for the time field used in all frames (if one exists)
*/
function findConsistentTimeFieldName(data: DataFrame[]): string | undefined {
let name: string | undefined = undefined;
for (const frame of data) {
const { timeField } = getTimeField(frame);
if (!timeField) {
return undefined; // Not timeseries
}
if (!name) {
name = timeField.name;
} else if (name !== timeField.name) {
// Second frame has a different time column?!
return undefined;
}
}
return name;
}
export function getNameFromOptions(options: CalculateFieldTransformerOptions) {
if (options.alias?.length) {
return options.alias;
@@ -0,0 +1,136 @@
import { DataTransformerID } from './ids';
import { toDataFrame } from '../../dataframe/processDataFrame';
import { FieldType } from '../../types/dataFrame';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { transformDataFrame } from '../transformDataFrame';
import { ensureColumnsTransformer } from './ensureColumns';
import { seriesToColumnsTransformer } from './seriesToColumns';
const seriesA = toDataFrame({
fields: [
{ name: 'TheTime', type: FieldType.time, values: [1000, 2000] },
{ name: 'A', type: FieldType.number, values: [1, 100] },
],
});
const seriesBC = toDataFrame({
fields: [
{ name: 'TheTime', type: FieldType.time, values: [1000, 2000] },
{ name: 'B', type: FieldType.number, values: [2, 200] },
{ name: 'C', type: FieldType.number, values: [3, 300] },
{ name: 'D', type: FieldType.string, values: ['first', 'second'] },
],
});
const seriesNoTime = toDataFrame({
fields: [
{ name: 'B', type: FieldType.number, values: [2, 200] },
{ name: 'C', type: FieldType.number, values: [3, 300] },
{ name: 'D', type: FieldType.string, values: ['first', 'second'] },
],
});
describe('ensureColumns transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([ensureColumnsTransformer, seriesToColumnsTransformer]);
});
it('will transform to columns if time field exists and multiple frames', () => {
const cfg = {
id: DataTransformerID.ensureColumns,
options: {},
};
const data = [seriesA, seriesBC];
const filtered = transformDataFrame([cfg], data);
expect(filtered.length).toEqual(1);
expect(filtered[0]).toMatchInlineSnapshot(`
Object {
"fields": Array [
Object {
"config": Object {},
"labels": undefined,
"name": "TheTime",
"type": "time",
"values": Array [
1000,
2000,
],
},
Object {
"config": Object {},
"labels": Object {},
"name": "A",
"type": "number",
"values": Array [
1,
100,
],
},
Object {
"config": Object {},
"labels": Object {},
"name": "B",
"type": "number",
"values": Array [
2,
200,
],
},
Object {
"config": Object {},
"labels": Object {},
"name": "C",
"type": "number",
"values": Array [
3,
300,
],
},
Object {
"config": Object {},
"labels": Object {},
"name": "D",
"type": "string",
"values": Array [
"first",
"second",
],
},
],
"meta": Object {
"transformations": Array [
"ensureColumns",
],
},
"name": undefined,
"refId": undefined,
}
`);
});
it('will not transform to columns if time field is missing for any of the series', () => {
const cfg = {
id: DataTransformerID.ensureColumns,
options: {},
};
const data = [seriesBC, seriesNoTime];
const filtered = transformDataFrame([cfg], data);
expect(filtered).toEqual(data);
});
it('will not transform to columns if only one series', () => {
const cfg = {
id: DataTransformerID.ensureColumns,
options: {},
};
const data = [seriesBC];
const filtered = transformDataFrame([cfg], data);
expect(filtered).toEqual(data);
});
});
@@ -0,0 +1,43 @@
import { seriesToColumnsTransformer } from './seriesToColumns';
import { DataFrame } from '../../types/dataFrame';
import { getTimeField } from '../../dataframe/processDataFrame';
import { DataTransformerInfo } from '../../types/transformations';
import { DataTransformerID } from './ids';
export const ensureColumnsTransformer: DataTransformerInfo = {
id: DataTransformerID.ensureColumns,
name: 'Ensure Columns Transformer',
description: 'Will check if current data frames is series or columns. If in series it will convert to columns.',
transformer: () => (data: DataFrame[]) => {
// Assume timeseries should first be joined by time
const timeFieldName = findConsistentTimeFieldName(data);
if (data.length > 1 && timeFieldName) {
return seriesToColumnsTransformer.transformer({
byField: timeFieldName,
})(data);
}
return data;
},
};
/**
* Find the name for the time field used in all frames (if one exists)
*/
function findConsistentTimeFieldName(data: DataFrame[]): string | undefined {
let name: string | undefined = undefined;
for (const frame of data) {
const { timeField } = getTimeField(frame);
if (!timeField) {
return undefined; // Not timeseries
}
if (!name) {
name = timeField.name;
} else if (name !== timeField.name) {
// Second frame has a different time column?!
return undefined;
}
}
return name;
}
@@ -34,15 +34,16 @@ export const filterFieldsTransformer: DataTransformerInfo<FilterOptions> = {
const fields: Field[] = [];
for (let i = 0; i < series.fields.length; i++) {
const field = series.fields[i];
if (exclude) {
if (exclude(field)) {
if (exclude(field, series, data)) {
continue;
}
if (!include) {
fields.push(field);
}
}
if (include && include(field)) {
if (include && include(field, series, data)) {
fields.push(field);
}
}
@@ -31,11 +31,13 @@ describe('filterByName transformer', () => {
});
describe('respects', () => {
it('inclusion', () => {
it('inclusion by pattern', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
include: ['^(startsWith)'],
include: {
pattern: '/^(startsWith)/',
},
},
};
@@ -44,11 +46,13 @@ describe('filterByName transformer', () => {
expect(filtered.fields[0].name).toBe('startsWithA');
});
it('exclusion', () => {
it('exclusion by pattern', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: ['^(startsWith)'],
exclude: {
pattern: '/^(startsWith)/',
},
},
};
@@ -57,12 +61,102 @@ describe('filterByName transformer', () => {
expect(filtered.fields[0].name).toBe('B');
});
it('inclusion and exclusion', () => {
it('inclusion and exclusion by pattern', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: ['^(startsWith)'],
include: [`^(B)$`],
exclude: { pattern: '/^(startsWith)/' },
include: { pattern: '/^(B)$/' },
},
};
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
expect(filtered.fields.length).toBe(1);
expect(filtered.fields[0].name).toBe('B');
});
it('inclusion by names', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
include: {
names: ['startsWithA', 'startsWithC'],
},
},
};
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
expect(filtered.fields.length).toBe(2);
expect(filtered.fields[0].name).toBe('startsWithA');
});
it('exclusion by names', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: {
names: ['startsWithA', 'startsWithC'],
},
},
};
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
expect(filtered.fields.length).toBe(2);
expect(filtered.fields[0].name).toBe('B');
});
it('inclusion and exclusion by names', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: { names: ['startsWithA', 'startsWithC'] },
include: { names: ['B'] },
},
};
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
expect(filtered.fields.length).toBe(1);
expect(filtered.fields[0].name).toBe('B');
});
it('inclusion by both', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
include: {
pattern: '/^(startsWith)/',
names: ['startsWithA'],
},
},
};
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
expect(filtered.fields.length).toBe(2);
expect(filtered.fields[0].name).toBe('startsWithA');
});
it('exclusion by both', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: {
pattern: '/^(startsWith)/',
names: ['startsWithA'],
},
},
};
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
expect(filtered.fields.length).toBe(2);
expect(filtered.fields[0].name).toBe('B');
});
it('inclusion and exclusion by both', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: { names: ['startsWithA', 'startsWithC'] },
include: { pattern: '/^(B)$/' },
},
};
@@ -1,11 +1,12 @@
import { DataTransformerID } from './ids';
import { filterFieldsTransformer, FilterOptions } from './filter';
import { DataTransformerInfo } from '../../types/transformations';
import { DataTransformerInfo, MatcherConfig } from '../../types/transformations';
import { FieldMatcherID } from '../matchers/ids';
import { FilterOptions, filterFieldsTransformer } from './filter';
import { RegexpOrNamesMatcherOptions } from '../matchers/nameMatcher';
export interface FilterFieldsByNameTransformerOptions {
include?: string[];
exclude?: string[];
include?: RegexpOrNamesMatcherOptions;
exclude?: RegexpOrNamesMatcherOptions;
}
export const filterFieldsByNameTransformer: DataTransformerInfo<FilterFieldsByNameTransformerOptions> = {
@@ -19,25 +20,33 @@ export const filterFieldsByNameTransformer: DataTransformerInfo<FilterFieldsByNa
* be applied, just return the input series
*/
transformer: (options: FilterFieldsByNameTransformerOptions) => {
const filterOptions: FilterOptions = {};
if (options.include) {
filterOptions.include = {
id: FieldMatcherID.byName,
options: options.include.length > 0 ? buildRegex(options.include) : '',
};
}
if (options.exclude) {
filterOptions.exclude = {
id: FieldMatcherID.byName,
options: options.exclude.length > 0 ? buildRegex(options.exclude) : '',
};
}
const filterOptions: FilterOptions = {
include: getMatcherConfig(options.include),
exclude: getMatcherConfig(options.exclude),
};
return filterFieldsTransformer.transformer(filterOptions);
},
};
const buildRegex = (regexs: string[]) => {
const include = regexs.map(s => `(${s})`).join('|');
return `/${include}/`;
const getMatcherConfig = (options?: RegexpOrNamesMatcherOptions): MatcherConfig | undefined => {
if (!options) {
return undefined;
}
const { names, pattern } = options;
if ((!Array.isArray(names) || names.length === 0) && !pattern) {
return undefined;
}
if (!pattern) {
return { id: FieldMatcherID.byNames, options: names };
}
if (!Array.isArray(names) || names.length === 0) {
return { id: FieldMatcherID.byRegexp, options: pattern };
}
return { id: FieldMatcherID.byRegexpOrNames, options };
};
@@ -1,18 +1,18 @@
export enum DataTransformerID {
// join = 'join', // Pick a field and merge all series based on that field
append = 'append', // Merge all series together
append = 'append',
// rotate = 'rotate', // Columns to rows
reduce = 'reduce', // Run calculations on fields
order = 'order', // order fields based on user configuration
organize = 'organize', // order, rename and filter based on user configuration
rename = 'rename', // rename field based on user configuration
calculateField = 'calculateField', // Run a reducer on the row
seriesToColumns = 'seriesToColumns', // former table transform timeseries_to_columns
labelsToFields = 'labelsToFields', // former table transform table
filterFields = 'filterFields', // Pick some fields (keep all frames)
filterFieldsByName = 'filterFieldsByName', // Pick fields with name matching regex (keep all frames)
filterFrames = 'filterFrames', // Pick some frames (keep all fields)
filterByRefId = 'filterByRefId', // Pick some frames by RefId
noop = 'noop', // Does nothing to the dataframe
reduce = 'reduce',
order = 'order',
organize = 'organize',
rename = 'rename',
calculateField = 'calculateField',
seriesToColumns = 'seriesToColumns',
labelsToFields = 'labelsToFields',
filterFields = 'filterFields',
filterFieldsByName = 'filterFieldsByName',
filterFrames = 'filterFrames',
filterByRefId = 'filterByRefId',
noop = 'noop',
ensureColumns = 'ensureColumns',
}
@@ -1,9 +1,9 @@
import { DataFrame, DataTransformerInfo, FieldType, Field } from '../../types';
import { DataTransformerID } from './ids';
import { MutableDataFrame } from '../../dataframe';
import { ArrayVector } from '../../vector';
import { filterFieldsTransformer } from './filter';
import { FieldMatcherID } from '..';
import { MutableField } from '../../dataframe';
export interface LabelsToFieldsOptions {}
type MapItem = { type: FieldType; values: Record<string, any>; isValue: boolean };
@@ -28,10 +28,10 @@ export const labelsToFieldsTransformer: DataTransformerInfo<LabelsToFieldsOption
}
const columnsMap = createColumnsMap(framesWithTimeField, framesWithoutTimeField);
const processed = createFields(columnsMap);
const fields = createFields(columnsMap);
const values: Record<string, any[]> = {};
const timeColumnItem = columnsMap[processed.fields[0].name];
const timeColumnItem = columnsMap[fields[0].name];
const seriesIndexStrings = Object.keys(timeColumnItem);
for (const seriesIndexString of seriesIndexStrings) {
const seriesItem = timeColumnItem[seriesIndexString];
@@ -41,9 +41,9 @@ export const labelsToFieldsTransformer: DataTransformerInfo<LabelsToFieldsOption
if (!values[timeValueString]) {
values[timeValueString] = [];
}
let row = new Array(processed.fields.length);
for (let index = 0; index < processed.fields.length; index++) {
const field = processed.fields[index];
let row = new Array(fields.length);
for (let index = 0; index < fields.length; index++) {
const field = fields[index];
const valueItem = columnsMap[field.name][seriesIndexString];
const value = valueItem ? valueItem.values[timeValueString] ?? null : null;
row[index] = value;
@@ -55,13 +55,18 @@ export const labelsToFieldsTransformer: DataTransformerInfo<LabelsToFieldsOption
const timestamps = Object.values(values);
for (const timestamp of timestamps) {
for (const row of timestamp) {
for (let fieldIndex = 0; fieldIndex < processed.fields.length; fieldIndex++) {
processed.fields[fieldIndex].values.add(row[fieldIndex]);
for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
fields[fieldIndex].values.add(row[fieldIndex]);
}
}
}
return [processed];
return [
{
fields,
length: fields[0].values.length,
},
];
},
};
@@ -145,9 +150,9 @@ function createColumnsMap(framesWithTimeField: DataFrame[], framesWithoutTimeFie
return map;
}
function createFields(columnsMap: Map) {
function createFields(columnsMap: Map): MutableField[] {
const columns = Object.keys(columnsMap);
const processed = new MutableDataFrame();
const fields: MutableField[] = [];
const valueColumns: string[] = [];
for (const column of columns) {
@@ -156,13 +161,13 @@ function createFields(columnsMap: Map) {
valueColumns.push(column);
continue;
}
processed.addField({ type: columnItem.type, values: new ArrayVector(), name: column });
fields.push({ type: columnItem.type, values: new ArrayVector(), name: column, config: {} });
}
for (const column of valueColumns) {
const columnItem = Object.values<MapItem>(columnsMap[column])[0];
processed.addField({ type: columnItem.type, values: new ArrayVector(), name: column });
fields.push({ type: columnItem.type, values: new ArrayVector(), name: column, config: {} });
}
return processed;
return fields;
}
@@ -50,19 +50,19 @@ describe('OrganizeFields Transformer', () => {
labels: undefined,
name: 'temperature',
state: {
title: 'temperature',
displayName: 'temperature',
},
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {
title: 'renamed_humidity',
displayName: 'renamed_humidity',
},
labels: undefined,
name: 'humidity',
state: {
title: 'renamed_humidity',
displayName: 'renamed_humidity',
},
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
@@ -105,11 +105,11 @@ describe('OrganizeFields Transformer', () => {
{
labels: undefined,
config: {
title: 'renamed_time',
displayName: 'renamed_time',
},
name: 'time',
state: {
title: 'renamed_time',
displayName: 'renamed_time',
},
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
@@ -119,7 +119,7 @@ describe('OrganizeFields Transformer', () => {
labels: undefined,
name: 'pressure',
state: {
title: 'pressure',
displayName: 'pressure',
},
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
@@ -29,7 +29,7 @@ export const organizeFieldsTransformer: DataTransformerInfo<OrganizeFieldsTransf
const rename = renameFieldsTransformer.transformer(options);
const order = orderFieldsTransformer.transformer(options);
const filter = filterFieldsByNameTransformer.transformer({
exclude: mapToExcludeArray(options.excludeByName),
exclude: { names: mapToExcludeArray(options.excludeByName) },
});
return (data: DataFrame[]) => rename(order(filter(data)));
@@ -65,25 +65,25 @@ describe('Reducer Transformer', () => {
name: 'first',
type: FieldType.number,
values: new ArrayVector([3, 10000.3, 1, 11000.1]),
config: { title: 'First' },
config: { displayName: 'First' },
},
{
name: 'min',
type: FieldType.number,
values: new ArrayVector([3, 10000.3, 1, 11000.1]),
config: { title: 'Min' },
config: { displayName: 'Min' },
},
{
name: 'max',
type: FieldType.number,
values: new ArrayVector([6, 10000.6, 7, 11000.7]),
config: { title: 'Max' },
config: { displayName: 'Max' },
},
{
name: 'last',
type: FieldType.number,
values: new ArrayVector([6, 10000.6, 7, 11000.7]),
config: { title: 'Last' },
config: { displayName: 'Last' },
},
];
@@ -111,25 +111,25 @@ describe('Reducer Transformer', () => {
name: 'first',
type: FieldType.number,
values: new ArrayVector([3, 1]),
config: { title: 'First' },
config: { displayName: 'First' },
},
{
name: 'min',
type: FieldType.number,
values: new ArrayVector([3, 1]),
config: { title: 'Min' },
config: { displayName: 'Min' },
},
{
name: 'max',
type: FieldType.number,
values: new ArrayVector([6, 7]),
config: { title: 'Max' },
config: { displayName: 'Max' },
},
{
name: 'last',
type: FieldType.number,
values: new ArrayVector([6, 7]),
config: { title: 'Last' },
config: { displayName: 'Last' },
},
];
@@ -157,25 +157,25 @@ describe('Reducer Transformer', () => {
name: 'first',
type: FieldType.number,
values: new ArrayVector([3, 10000.3]),
config: { title: 'First' },
config: { displayName: 'First' },
},
{
name: 'min',
type: FieldType.number,
values: new ArrayVector([3, 10000.3]),
config: { title: 'Min' },
config: { displayName: 'Min' },
},
{
name: 'max',
type: FieldType.number,
values: new ArrayVector([6, 10000.6]),
config: { title: 'Max' },
config: { displayName: 'Max' },
},
{
name: 'last',
type: FieldType.number,
values: new ArrayVector([6, 10000.6]),
config: { title: 'Last' },
config: { displayName: 'Last' },
},
];
@@ -203,25 +203,25 @@ describe('Reducer Transformer', () => {
name: 'first',
type: FieldType.number,
values: new ArrayVector([3]),
config: { title: 'First' },
config: { displayName: 'First' },
},
{
name: 'min',
type: FieldType.number,
values: new ArrayVector([3]),
config: { title: 'Min' },
config: { displayName: 'Min' },
},
{
name: 'max',
type: FieldType.number,
values: new ArrayVector([6]),
config: { title: 'Max' },
config: { displayName: 'Max' },
},
{
name: 'last',
type: FieldType.number,
values: new ArrayVector([6]),
config: { title: 'Last' },
config: { displayName: 'Last' },
},
];
@@ -59,7 +59,7 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
type: FieldType.other, // UNKNOWN until after we call the functions
values: values[values.length - 1],
config: {
title: info.name,
displayName: info.name,
// UNIT from original field?
},
});
@@ -72,7 +72,7 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
continue;
}
if (matcher(field)) {
if (matcher(field, series, data)) {
const results = reduceField({
field,
reducers,
@@ -41,36 +41,36 @@ describe('Rename Transformer', () => {
expect(renamed.fields).toEqual([
{
config: {
title: 'Total time',
displayName: 'Total time',
},
labels: undefined,
name: 'time',
state: {
title: 'Total time',
displayName: 'Total time',
},
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
{
config: {
title: 'how cold is it?',
displayName: 'how cold is it?',
},
labels: undefined,
name: 'temperature',
state: {
title: 'how cold is it?',
displayName: 'how cold is it?',
},
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {
title: 'Moistiness',
displayName: 'Moistiness',
},
name: 'humidity',
labels: undefined,
state: {
title: 'Moistiness',
displayName: 'Moistiness',
},
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
@@ -106,12 +106,12 @@ describe('Rename Transformer', () => {
expect(renamed.fields).toEqual([
{
config: {
title: 'ttl',
displayName: 'ttl',
},
name: 'time',
labels: undefined,
state: {
title: 'ttl',
displayName: 'ttl',
},
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
@@ -121,19 +121,19 @@ describe('Rename Transformer', () => {
labels: undefined,
name: 'pressure',
state: {
title: 'pressure',
displayName: 'pressure',
},
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {
title: 'hum',
displayName: 'hum',
},
labels: undefined,
name: 'humidity',
state: {
title: 'hum',
displayName: 'hum',
},
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
@@ -1,7 +1,7 @@
import { DataTransformerID } from './ids';
import { DataTransformerInfo } from '../../types/transformations';
import { DataFrame, Field } from '../../types/dataFrame';
import { getFieldTitle } from '../../field/fieldState';
import { getFieldDisplayName } from '../../field/fieldState';
export interface RenameFieldsTransformerOptions {
renameByName: Record<string, string>;
@@ -41,8 +41,8 @@ const createRenamer = (renameByName: Record<string, string>) => (frame: DataFram
}
return frame.fields.map(field => {
const title = getFieldTitle(field, frame);
const renameTo = renameByName[title];
const displayName = getFieldDisplayName(field, frame);
const renameTo = renameByName[displayName];
if (typeof renameTo !== 'string' || renameTo.length === 0) {
return field;
@@ -52,11 +52,11 @@ const createRenamer = (renameByName: Record<string, string>) => (frame: DataFram
...field,
config: {
...field.config,
title: renameTo,
displayName: renameTo,
},
state: {
...field.state,
title: renameTo,
displayName: renameTo,
},
};
});
@@ -46,7 +46,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'time',
state: {
title: 'time',
displayName: 'time',
},
type: FieldType.time,
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
@@ -56,7 +56,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'temperature',
state: {
title: 'temperature even',
displayName: 'temperature even',
},
type: FieldType.number,
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
@@ -66,7 +66,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'humidity',
state: {
title: 'humidity even',
displayName: 'humidity even',
},
type: FieldType.number,
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
@@ -76,7 +76,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'temperature',
state: {
title: 'temperature odd',
displayName: 'temperature odd',
},
type: FieldType.number,
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
@@ -86,7 +86,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'humidity',
state: {
title: 'humidity odd',
displayName: 'humidity odd',
},
type: FieldType.number,
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
@@ -109,7 +109,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'temperature',
state: {
title: 'temperature',
displayName: 'temperature',
},
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6, 11.1, 11.3, 11.5, 11.7]),
@@ -119,7 +119,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'time',
state: {
title: 'time even',
displayName: 'time even',
},
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000, null, null, null, null]),
@@ -129,7 +129,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'humidity',
state: {
title: 'humidity even',
displayName: 'humidity even',
},
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6, null, null, null, null]),
@@ -139,7 +139,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'time',
state: {
title: 'time odd',
displayName: 'time odd',
},
type: FieldType.time,
values: new ArrayVector([null, null, null, null, 1000, 3000, 5000, 7000]),
@@ -149,7 +149,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'humidity',
state: {
title: 'humidity odd',
displayName: 'humidity odd',
},
type: FieldType.number,
values: new ArrayVector([null, null, null, null, 11000.1, 11000.3, 11000.5, 11000.7]),
@@ -176,7 +176,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'time',
state: {
title: 'time',
displayName: 'time',
},
type: FieldType.time,
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
@@ -186,7 +186,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'temperature',
state: {
title: 'temperature even',
displayName: 'temperature even',
},
type: FieldType.number,
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
@@ -196,7 +196,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'humidity',
state: {
title: 'humidity even',
displayName: 'humidity even',
},
type: FieldType.number,
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
@@ -206,7 +206,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'temperature',
state: {
title: 'temperature odd',
displayName: 'temperature odd',
},
type: FieldType.number,
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
@@ -216,7 +216,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'humidity',
state: {
title: 'humidity odd',
displayName: 'humidity odd',
},
type: FieldType.number,
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
@@ -256,7 +256,7 @@ describe('SeriesToColumns Transformer', () => {
{
name: 'time',
state: {
title: 'time',
displayName: 'time',
},
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000]),
@@ -269,14 +269,14 @@ describe('SeriesToColumns Transformer', () => {
values: new ArrayVector([1, 3, 5, 7]),
config: {},
state: {
title: 'temperature temperature',
displayName: 'temperature temperature',
},
labels: { name: 'temperature' },
},
{
name: 'temperature',
state: {
title: 'temperature B',
displayName: 'temperature B',
},
type: FieldType.number,
values: new ArrayVector([2, 4, 6, 8]),
@@ -2,7 +2,7 @@ import { DataFrame, DataTransformerInfo, Field } from '../../types';
import { DataTransformerID } from './ids';
import { MutableDataFrame } from '../../dataframe';
import { ArrayVector } from '../../vector';
import { getFieldTitle } from '../../field/fieldState';
import { getFieldDisplayName } from '../../field/fieldState';
export interface SeriesToColumnsOptions {
byField?: string;
@@ -71,7 +71,7 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
resultFrame.addField(item.newField);
}
const keyFieldTitle = getFieldTitle(resultFrame.fields[0], resultFrame);
const keyFieldTitle = getFieldDisplayName(resultFrame.fields[0], resultFrame);
const byKeyField: { [key: string]: { [key: string]: any } } = {};
/*
@@ -92,7 +92,7 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
for (let fieldIndex = 0; fieldIndex < allFields.length; fieldIndex++) {
const { sourceField, keyField, newField } = allFields[fieldIndex];
const newFieldTitle = getFieldTitle(newField, resultFrame);
const newFieldTitle = getFieldDisplayName(newField, resultFrame);
for (let valueIndex = 0; valueIndex < sourceField.values.length; valueIndex++) {
const value = sourceField.values.get(valueIndex);
@@ -112,7 +112,7 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
for (let fieldIndex = 0; fieldIndex < resultFrame.fields.length; fieldIndex++) {
const field = resultFrame.fields[fieldIndex];
const otherColumnName = getFieldTitle(field, resultFrame);
const otherColumnName = getFieldDisplayName(field, resultFrame);
const value = byKeyField[keyValueAsString][otherColumnName] ?? null;
field.values.add(value);
}
@@ -126,7 +126,7 @@ function findKeyField(frame: DataFrame, matchTitle: string): Field | null {
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
const field = frame.fields[fieldIndex];
if (matchTitle === getFieldTitle(field)) {
if (matchTitle === getFieldDisplayName(field)) {
return field;
}
}
+3 -2
View File
@@ -15,7 +15,7 @@ export enum LoadingState {
Error = 'Error',
}
type PreferredVisualisationType = 'graph' | 'table';
export type PreferredVisualisationType = 'graph' | 'table';
export interface QueryResultMeta {
/** DatasSource Specific Values */
@@ -46,7 +46,7 @@ export interface QueryResultMeta {
}
export interface QueryResultMetaStat extends FieldConfig {
title: string;
displayName: string;
value: number;
}
@@ -96,6 +96,7 @@ export interface Column {
text: string; // For a Column, the 'text' is the field name
filterable?: boolean;
unit?: string;
custom?: Record<string, any>;
}
export interface TableData extends QueryResultBase {
+5 -4
View File
@@ -23,7 +23,7 @@ export enum FieldType {
* Plugins may extend this with additional properties. Something like series overrides
*/
export interface FieldConfig<TOptions extends object = any> {
title?: string; // The display value for this field. This supports template variables blank is auto
displayName?: string; // The display value for this field. This supports template variables blank is auto
filterable?: boolean;
// Numeric Options
@@ -82,7 +82,7 @@ export interface Field<T = any, V = Vector<T>> {
labels?: Labels;
/**
* Cached values with appropriate dispaly and id values
* Cached values with appropriate display and id values
*/
state?: FieldState | null;
@@ -106,7 +106,7 @@ export interface FieldState {
/**
* An appropriate name for the field (does not include frame info)
*/
title?: string | null;
displayName?: string | null;
/**
* Cache of reduced values
@@ -148,4 +148,5 @@ export interface DataFrameDTO extends QueryResultBase {
export interface FieldCalcs extends Record<string, any> {}
export const TIME_SERIES_FIELD_NAME = 'Value';
export const TIME_SERIES_VALUE_FIELD_NAME = 'Value';
export const TIME_SERIES_TIME_FIELD_NAME = 'Time';
@@ -305,12 +305,14 @@ export interface QueryEditorProps<
query: TQuery;
onRunQuery: () => void;
onChange: (value: TQuery) => void;
onBlur?: () => void;
/**
* Contains query response filtered by refId of QueryResultBase and possible query error
*/
data?: PanelData;
exploreMode?: ExploreMode;
exploreId?: any;
history?: HistoryItem[];
}
export enum DataSourceStatus {
@@ -340,6 +342,7 @@ export interface ExploreStartPageProps {
datasource?: DataSourceApi;
exploreMode: ExploreMode;
onClickExample: (query: DataQuery) => void;
exploreId?: any;
}
/**
@@ -1,6 +1,7 @@
import { eventFactory } from './utils';
import { DataQueryError, DataQueryResponseData } from './datasource';
import { AngularPanelMenuItem } from './panel';
import { DataFrame } from './dataFrame';
/** Payloads */
export interface PanelChangeViewPayload {
@@ -15,6 +16,7 @@ export const refresh = eventFactory('refresh');
export const componentDidMount = eventFactory('component-did-mount');
export const dataError = eventFactory<DataQueryError>('data-error');
export const dataReceived = eventFactory<DataQueryResponseData[]>('data-received');
export const dataFramesReceived = eventFactory<DataFrame[]>('data-frames-received');
export const dataSnapshotLoad = eventFactory<DataQueryResponseData[]>('data-snapshot-load');
export const editModeInitialized = eventFactory('init-edit-mode');
export const initPanelActions = eventFactory<AngularPanelMenuItem[]>('init-panel-actions');
+1
View File
@@ -12,6 +12,7 @@ export interface GrafanaThemeCommons {
md: string;
lg: string;
xl: string;
xxl: string;
};
typography: {
fontFamily: {
@@ -25,7 +25,7 @@ export interface DataTransformerConfig<TOptions = any> {
options: TOptions;
}
export type FieldMatcher = (field: Field) => boolean;
export type FieldMatcher = (field: Field, frame: DataFrame, allFrames: DataFrame[]) => boolean;
export type FrameMatcher = (frame: DataFrame) => boolean;
export interface FieldMatcherInfo<TOptions = any> extends RegistryItemWithOptions<TOptions> {
@@ -0,0 +1,31 @@
import { Registry } from './Registry';
import { FieldReducerInfo, fieldReducers, ReducerID } from '../transformations';
describe('Registry', () => {
describe('selectOptions', () => {
describe('when called with current', () => {
it('then order in select.current should be same as current', () => {
const list = fieldReducers.list();
const registry = new Registry<FieldReducerInfo>(() => list);
const current = [ReducerID.step, ReducerID.mean, ReducerID.allIsZero, ReducerID.first, ReducerID.delta];
const select = registry.selectOptions(current);
expect(select.current).toEqual([
{ description: 'Minimum interval between values', label: 'Step', value: 'step' },
{ description: 'Average Value', label: 'Mean', value: 'mean' },
{ description: 'All values are zero', label: 'All Zeros', value: 'allIsZero' },
{ description: 'First Value', label: 'First', value: 'first' },
{ description: 'Cumulative change in value', label: 'Delta', value: 'delta' },
]);
});
describe('when called without current', () => {
it('then it should return an empty array', () => {
const list = fieldReducers.list();
const registry = new Registry<FieldReducerInfo>(() => list);
const select = registry.selectOptions();
expect(select.current).toEqual([]);
});
});
});
});
});
+10 -4
View File
@@ -84,10 +84,10 @@ export class Registry<T extends RegistryItem> {
current: [],
} as RegistrySelectInfo;
const currentIds: any = {};
const currentOptions: Record<string, SelectableValue<string>> = {};
if (current) {
for (const id of current) {
currentIds[id] = true;
currentOptions[id] = {};
}
}
@@ -106,10 +106,16 @@ export class Registry<T extends RegistryItem> {
};
select.options.push(option);
if (currentIds[ext.id]) {
select.current.push(option);
if (currentOptions[ext.id]) {
currentOptions[ext.id] = option;
}
}
if (current) {
// this makes sure we preserve the order of ids
select.current = Object.values(currentOptions);
}
return select;
}
@@ -4,6 +4,7 @@ import { getDataFrameRow } from '../dataframe/processDataFrame';
// Test with local CSV files
import fs from 'fs';
import { toDataFrameDTO } from '../dataframe/processDataFrame';
import { MutableDataFrame } from '../dataframe';
describe('read csv', () => {
it('should get X and y', () => {
@@ -88,3 +89,30 @@ describe('write csv', () => {
expect(fields.map(f => f.name).join(',')).toEqual('a,b,c'); // the names
});
});
describe('DataFrame to CSV', () => {
it('should escape double quotes in the field names', () => {
const dataFrame = new MutableDataFrame({
fields: [
{ name: 'Time', values: [1589455688623] },
// As we have traceId in message already this will shadow it.
{
name: 'Value',
values: ['1234'],
labels: {
label1: 'value1',
label2: 'value1',
},
},
],
});
const csv = toCSV([dataFrame]);
expect(csv).toMatchInlineSnapshot(`
"\\"Time\\",\\"{label1=\\"\\"value1\\"\\", label2=\\"\\"value1\\"\\"}\\"
1589455688623,1234
"
`);
});
});
+2 -1
View File
@@ -7,6 +7,7 @@ import isNumber from 'lodash/isNumber';
import { DataFrame, Field, FieldType, FieldConfig } from '../types';
import { guessFieldTypeFromValue } from '../dataframe/processDataFrame';
import { MutableDataFrame } from '../dataframe/MutableDataFrame';
import { getFieldDisplayName } from '../field';
export enum CSVHeaderStyle {
full,
@@ -289,7 +290,7 @@ export function toCSV(data: DataFrame[], config?: CSVConfig): string {
if (i > 0) {
csv += config.delimiter;
}
csv += fields[i].name;
csv += `"${getFieldDisplayName(fields[i], series).replace(/"/g, '""')}"`;
}
csv += config.newline;
}
@@ -3,10 +3,10 @@ import { ThresholdsMode } from '../../types';
export const mockStandardProperties = () => {
const title = {
id: 'title',
path: 'title',
name: 'Title',
description: "Field's title",
id: 'displayName',
path: 'displayName',
name: 'Display name',
description: "Field's display name",
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
@@ -0,0 +1,55 @@
import { currency } from './symbolFormatters';
describe('currency', () => {
const symbol = '@';
describe('when called without asSuffix', () => {
const fmtFunc = currency(symbol);
it.each`
value | expectedSuffix | expectedText
${999} | ${''} | ${'999'}
${1000} | ${'K'} | ${'1'}
${1000000} | ${'M'} | ${'1'}
${1000000000} | ${'B'} | ${'1'}
${1000000000000} | ${'T'} | ${'1'}
${1000000000000000} | ${undefined} | ${'NA'}
${-1000000000000} | ${'T'} | ${'-1'}
${-1000000000} | ${'B'} | ${'-1'}
${-1000000} | ${'M'} | ${'-1'}
${-1000} | ${'K'} | ${'-1'}
${-999} | ${''} | ${'-999'}
`('when called with value:{$value}', ({ value, expectedSuffix, expectedText }) => {
const { prefix, suffix, text } = fmtFunc(value);
expect(prefix).toEqual(symbol);
expect(suffix).toEqual(expectedSuffix);
expect(text).toEqual(expectedText);
});
});
describe('when called with asSuffix', () => {
const fmtFunc = currency(symbol, true);
it.each`
value | expectedSuffix | expectedText
${999} | ${'@'} | ${'999'}
${1000} | ${'K@'} | ${'1'}
${1000000} | ${'M@'} | ${'1'}
${1000000000} | ${'B@'} | ${'1'}
${1000000000000} | ${'T@'} | ${'1'}
${1000000000000000} | ${undefined} | ${'NA'}
${-1000000000000} | ${'T@'} | ${'-1'}
${-1000000000} | ${'B@'} | ${'-1'}
${-1000000} | ${'M@'} | ${'-1'}
${-1000} | ${'K@'} | ${'-1'}
${-999} | ${'@'} | ${'-999'}
`('when called with value:{$value}', ({ value, expectedSuffix, expectedText }) => {
const { prefix, suffix, text } = fmtFunc(value);
expect(prefix).toEqual(undefined);
expect(suffix).toEqual(expectedSuffix);
expect(text).toEqual(expectedText);
});
});
});
@@ -10,7 +10,7 @@ export function currency(symbol: string, asSuffix?: boolean): ValueFormatter {
}
const scaled = scaler(size, decimals, scaledDecimals);
if (asSuffix) {
scaled.suffix = symbol;
scaled.suffix = scaled.suffix !== undefined ? `${scaled.suffix}${symbol}` : undefined;
} else {
scaled.prefix = symbol;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "7.0.0-beta.3",
"version": "7.0.0",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
@@ -18,6 +18,7 @@ export const Components = {
Panel: {
title: (title: string) => `Panel header title item ${title}`,
headerItems: (item: string) => `Panel header item ${item}`,
containerByTitle: (title: string) => `Panel container title ${title}`,
},
Visualization: {
Graph: {
@@ -28,6 +29,9 @@ export const Components = {
legendItemAlias: (name: string) => `gpl alias ${name}`,
showLegendSwitch: 'gpl show legend',
},
xAxis: {
labels: () => 'div.flot-x-axis > div.flot-tick-label',
},
},
},
},
@@ -49,6 +53,7 @@ export const Components = {
close: Pages.Dashboard.Toolbar.toolbarItems('Close options pane'),
open: Pages.Dashboard.Toolbar.toolbarItems('Open options pane'),
select: 'Panel editor option pane select',
tab: (title: string) => `Panel editor option pane tab ${title}`,
},
// not sure about the naming *DataPane*
DataPane: {
@@ -109,6 +114,7 @@ export const Components = {
Select: {
option: 'Select option',
input: () => 'input[id*="react-select-"]',
singleValue: () => 'div[class*="-singleValue"]',
},
FieldConfigEditor: {
content: 'Field config editor content',
@@ -119,4 +125,7 @@ export const Components = {
FolderPicker: {
container: 'Folder picker select container',
},
TimeZonePicker: {
container: 'Time zone picker select container',
},
};
+1 -1
View File
@@ -1,5 +1,5 @@
# Grafana End-to-End Test library
> **@grafana/e2e is currently in ALPHA**. Core API is unstable and can be a subject of breaking changes!
> **@grafana/e2e is currently in BETA**.
This package contains an API wrapper built on top of [Cypress](https://www.cypress.io) that simplifies creating end-to-end tests for Grafana. More information can be found [here](https://github.com/grafana/grafana/blob/master/contribute/style-guides/e2e.md).
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "7.0.0-beta.3",
"version": "7.0.0",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -44,7 +44,7 @@
"types": "src/index.ts",
"dependencies": {
"@cypress/webpack-preprocessor": "4.1.3",
"@grafana/e2e-selectors": "7.0.0-beta.3",
"@grafana/e2e-selectors": "7.0.0",
"@grafana/tsconfig": "^1.0.0-rc1",
"blink-diff": "1.0.13",
"commander": "5.0.0",
+2 -2
View File
@@ -1,5 +1,5 @@
# Grafana Runtime library
> **@grafana/runtime is currently in ALPHA**. Core API is unstable and can be a subject of breaking changes!
> **@grafana/runtime is currently in BETA**.
This package allows access to grafana services. It requires Grafana to be running already and the functions to be imported as externals.
This package allows access to grafana services. It requires Grafana to be running already and the functions to be imported as externals.
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "7.0.0-beta.3",
"version": "7.0.0",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -23,8 +23,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "7.0.0-beta.3",
"@grafana/ui": "7.0.0-beta.3",
"@grafana/data": "7.0.0",
"@grafana/ui": "7.0.0",
"systemjs": "0.20.19",
"systemjs-plugin-css": "0.1.37"
},
+55 -20
View File
@@ -1,7 +1,7 @@
> **WARNING: @grafana/toolkit is currently in ALPHA**. The core API is unstable and can be a subject of breaking changes!
> **WARNING: @grafana/toolkit is currently in BETA**.
# grafana-toolkit
grafana-toolkit is a CLI that enables efficient development of Grafana plugins. We want to help our community focus on the core value of their plugins rather than all the setup required to develop them.
## Getting started
@@ -35,13 +35,15 @@ Follow the steps below to start using grafana-toolkit in your existing plugin.
```
3. Create `.prettierrc.js` file in the root dir of your plugin and paste the code below:
```js
module.exports = {
...require("./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json"),
...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'),
};
```
4. In your `package.json` file add following scripts:
```json
"scripts": {
"build": "grafana-toolkit plugin:build",
@@ -52,7 +54,8 @@ module.exports = {
```
## Usage
With grafana-toolkit, we give you a CLI that addresses common tasks performed when working on Grafana plugin:
With grafana-toolkit, we give you a CLI that addresses common tasks performed when working on Grafana plugin:
- `grafana-toolkit plugin:create`
- `grafana-toolkit plugin:dev`
@@ -60,6 +63,7 @@ With grafana-toolkit, we give you a CLI that addresses common tasks performed w
- `grafana-toolkit plugin:build`
### Create your plugin
`grafana-toolkit plugin:create plugin-name`
This command creates a new Grafana plugin from template.
@@ -67,27 +71,31 @@ This command creates a new Grafana plugin from template.
If `plugin-name` is provided, then the template is downloaded to `./plugin-name` directory. Otherwise, it will be downloaded to the current directory.
### Develop your plugin
`grafana-toolkit plugin:dev`
This command creates a development build that's easy to play with and debug using common browser tooling.
Available options:
- `-w`, `--watch` - run development task in a watch mode
### Test your plugin
`grafana-toolkit plugin:test`
This command runs Jest against your codebase.
Available options:
- `--watch` - Runs tests in interactive watch mode.
- `--coverage` - Reports code coverage.
- `-u`, `--updateSnapshot` - Performs snapshots update.
- `--testNamePattern=<regex>` - Runs test with names that match provided regex (https://jestjs.io/docs/en/cli#testnamepattern-regex).
- `--testPathPattern=<regex>` - Runs test with paths that match provided regex (https://jestjs.io/docs/en/cli#testpathpattern-regex).
### Build your plugin
`grafana-toolkit plugin:build`
This command creates a production-ready build of your plugin.
@@ -95,18 +103,23 @@ This command creates a production-ready build of your plugin.
## FAQ
### Which version of grafana-toolkit should I use?
See [Grafana packages versioning guide](https://github.com/grafana/grafana/blob/master/packages/README.md#versioning).
### What tools does grafana-toolkit use?
grafana-toolkit comes with TypeScript, ESLint, Prettier, Jest, CSS and SASS support.
### How to start using grafana-toolkit in my plugin?
See [Updating your plugin to use grafana-toolkit](#updating-your-plugin-to-use-grafana-toolkit).
### Can I use TypeScript to develop Grafana plugins?
Yes! grafana-toolkit supports TypeScript by default.
### How can I test my plugin?
grafana-toolkit comes with Jest as a test runner.
Internally at Grafana we use Enzyme. If you are developing React plugin and you want to configure Enzyme as a testing utility, then you need to configure `enzyme-adapter-react`. To do so, create `<YOUR_PLUGIN_DIR>/config/jest-setup.ts` file that will provide necessary setup. Copy the following code into that file to get Enzyme working with React:
@@ -121,13 +134,16 @@ configure({ adapter: new Adapter() });
You can also set up Jest with shims of your needs by creating `jest-shim.ts` file in the same directory: `<YOUR_PLUGIN_DIR_>/config/jest-shim.ts`
### Can I provide custom setup for Jest?
You can provide custom Jest configuration with a `package.json` file. For more details, see [Jest docs](https://jest-bot.github.io/jest/docs/configuration.html).
Currently we support following Jest configuration properties:
- [`snapshotSerializers`](https://jest-bot.github.io/jest/docs/configuration.html#snapshotserializers-array-string)
- [`moduleNameMapper`](https://jestjs.io/docs/en/configuration#modulenamemapper-object-string-string)
### How can I customize Webpack rules or plugins?
You can provide your own `webpack.config.js` file that exports a `getWebpackConfig` function. We recommend that you extend the standard configuration, but you are free to create your own:
```js
@@ -135,27 +151,28 @@ const CustomPlugin = require('custom-plugin');
module.exports.getWebpackConfig = (config, options) => ({
...config,
plugins: [
...config.plugins,
new CustomPlugin()
]
plugins: [...config.plugins, new CustomPlugin()],
});
```
### How can I style my plugin?
We support pure CSS, SASS, and CSS-in-JS approach (via [Emotion](https://emotion.sh/)).
#### Single CSS or SASS file
Create your CSS or SASS file and import it in your plugin entry point (typically `module.ts`):
```ts
import 'path/to/your/css_or_sass'
import 'path/to/your/css_or_sass';
```
The styles will be injected via `style` tag during runtime.
> Note that imported static assets will be inlined as base64 URIs. *This can be subject of change in the future!*
> Note that imported static assets will be inlined as base64 URIs. _This can be subject of change in the future!_
#### Theme-specific stylesheets
If you want to provide different stylesheets for dark/light theme, then create `dark.[css|scss]` and `light.[css|scss]` files in the `src/styles` directory of your plugin. grafana-toolkit generates theme-specific stylesheets that are stored in `dist/styles` directory.
In order for Grafana to pick up your theme stylesheets, you need to use `loadPluginCss` from `@grafana/runtime` package. Typically you would do that in the entry point of your plugin:
@@ -174,7 +191,8 @@ You must add `@grafana/runtime` to your plugin dependencies by running `yarn add
> Note that in this case static files (png, svg, json, html) are all copied to dist directory when the plugin is bundled. Relative paths to those files does not change!
#### Emotion
Starting from Grafana 6.2 *our suggested way* for styling plugins is by using [Emotion](https://emotion.sh). It's a CSS-in-JS library that we use internally at Grafana. The biggest advantage of using Emotion is that you can access Grafana Theme variables.
Starting from Grafana 6.2 _our suggested way_ for styling plugins is by using [Emotion](https://emotion.sh). It's a CSS-in-JS library that we use internally at Grafana. The biggest advantage of using Emotion is that you can access Grafana Theme variables.
To start using Emotion, you first must add it to your plugin dependencies:
@@ -185,21 +203,29 @@ To start using Emotion, you first must add it to your plugin dependencies:
Then, import `css` function from Emotion:
```ts
import { css } from 'emotion'
import { css } from 'emotion';
```
Now you are ready to implement your styles:
```tsx
const MyComponent = () => {
return <div className={css`background: red;`} />
}
return (
<div
className={css`
background: red;
`}
/>
);
};
```
To learn more about using Grafana theme please refer to [Theme usage guide](https://github.com/grafana/grafana/blob/master/style_guides/themes.md#react)
> We do not support Emotion's `css` prop. Use className instead!
### Can I adjust TypeScript configuration to suit my needs?
Yes! However, it's important that your `tsconfig.json` file contains the following lines:
```json
@@ -214,50 +240,59 @@ Yes! However, it's important that your `tsconfig.json` file contains the followi
```
### Can I adjust ESLint configuration to suit my needs?
grafana-toolkit comes with [default config for ESLint](https://github.com/grafana/grafana/blob/master/packages/grafana-toolkit/src/config/eslint.plugin.json). For now, there is now way to customise ESLint config.
### How is Prettier integrated into grafana-toolkit workflow?
When building plugin with [`grafana-toolkit plugin:build`](#building-plugin) task, grafana-toolkit performs Prettier check. If the check detects any Prettier issues, the build will not pass. To avoid such situation we suggest developing plugin with [`grafana-toolkit plugin:dev --watch`](#developing-plugin) task running. This task tries to fix Prettier issues automatically.
### My editor does not respect Prettier config, what should I do?
In order for your editor to pick up our Prettier config you need to create `.prettierrc.js` file in the root directory of your plugin with following content:
```js
module.exports = {
...require("./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json"),
...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'),
};
```
### How do I add third-party dependencies that are not npm packages?
Put them in the `static` directory in the root of your project. The `static` directory is copied when the plugin is built.
### I am getting this message when I run yarn install: `Request failed \"404 Not Found\"`
If you are using version `canary`, this error occurs because a `canary` release unpublishes previous versions leaving `yarn.lock` outdated. Remove `yarn.lock` and run `yarn install` again.
### I am getting this message when I run my plugin: `Unable to dynamically transpile ES module A loader plugin needs to be configured via SystemJS.config({ transpiler: 'transpiler-module' }).`
This error occurs when you bundle your plugin using the `grafana-toolkit plugin:dev` task and your code comments include ES2016 code.
There are two issues at play:
* The `grafana-toolkit plugin:dev` task does not remove comments from your bundled package.
* Grafana does not support [ES modules](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/).
- The `grafana-toolkit plugin:dev` task does not remove comments from your bundled package.
- Grafana does not support [ES modules](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/).
If your comments include ES2016 code, then SystemJS v0.20.19, which Grafana uses internally to load plugins, interprets your code as an ESM and fails.
To fix this error, remove the ES2016 code from your comments.
## Contribute to grafana-toolkit
You can contribute to grafana-toolkit by helping develop it or by debugging it.
### Develop grafana-toolkit
Typically plugins should be developed using the `@grafana/toolkit` installed from npm. However, when working on the toolkit, you might want to use the local version. Follow the steps below to develop with a local version:
1. Clone [Grafana repository](https://github.com/grafana/grafana).
2. Navigate to the directory you have cloned Grafana repo to and then run `yarn install --pure-lockfile`.
3. Navigate to `<GRAFANA_DIR>/packages/grafana-toolkit` and then run `yarn link`.
2. Navigate to the directory where your plugin code is and then run `npx grafana-toolkit plugin:dev --yarnlink`. This adds all dependencies required by grafana-toolkit to your project, as well as link your local grafana-toolkit version to be used by the plugin.
4. Navigate to the directory where your plugin code is and then run `npx grafana-toolkit plugin:dev --yarnlink`. This adds all dependencies required by grafana-toolkit to your project, as well as link your local grafana-toolkit version to be used by the plugin.
### Debug grafana-toolkit
To debug grafana-toolkit you can use standard [NodeJS debugging methods](https://nodejs.org/de/docs/guides/debugging-getting-started/#enable-inspector) (`node --inspect`, `node --inspect-brk`).
To run grafana-toolkit in a debugging session use the following command in the toolkit's directory:
@@ -8,7 +8,7 @@ let includeInternalScripts = false;
const isLinkedMode = () => {
// In circleci we are in linked mode. Detect by using the circle working directory,
// rather than the present working directory.
const pwd = process.env.CIRCLE_WORKING_DIRECTORY || process.env.PWD;
const pwd = process.env.CIRCLE_WORKING_DIRECTORY || process.env.PWD || process.cwd();
if (path.basename(pwd) === 'grafana-toolkit') {
return true;
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "7.0.0-beta.3",
"version": "7.0.0",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -29,10 +29,10 @@
"dependencies": {
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.0",
"@grafana/data": "7.0.0-beta.3",
"@grafana/data": "7.0.0",
"@grafana/eslint-config": "^1.0.0-rc1",
"@grafana/tsconfig": "^1.0.0-rc1",
"@grafana/ui": "7.0.0-beta.3",
"@grafana/ui": "7.0.0",
"@types/command-exists": "^1.2.0",
"@types/execa": "^0.9.0",
"@types/expect-puppeteer": "3.3.1",
@@ -5,22 +5,63 @@ interface CherryPickOptions {
enterprise: boolean;
}
const cherryPickRunner: TaskRunner<CherryPickOptions> = async ({ enterprise }) => {
const githubClient = new GithubClient({ enterprise });
const client = githubClient.client;
// https://github.com/lisposter/github-pagination/blob/master/lib/octopage.js
const pagingParser = (linkStr: string): { prev?: string; next?: string; last?: string; first?: string } => {
return linkStr
.split(',')
.map(rel => {
//@ts-ignore
return rel.split(';').map((curr, idx) => {
if (idx === 0) {
//@ts-ignore
return /[^_]page=(\d+)/.exec(curr)[1];
}
if (idx === 1) {
//@ts-ignore
return /rel="(.+)"/.exec(curr)[1];
}
});
})
.reduce(function(obj, curr, i) {
//@ts-ignore
obj[curr[1]] = curr[0];
return obj;
}, {});
};
const res = await client.get('/issues', {
const getIssues = async (client: any, page: string) => {
const result = await client.get('/issues', {
params: {
state: 'closed',
per_page: 100,
labels: 'cherry-pick needed',
sort: 'closed',
direction: 'asc',
page,
},
});
let data = result.data;
if (!result.headers.link) {
return data;
}
const pages = pagingParser(result.headers.link);
if (pages.next) {
const nextPage = await getIssues(client, pages.next);
data = data.concat(nextPage);
}
return data;
};
const cherryPickRunner: TaskRunner<CherryPickOptions> = async ({ enterprise }) => {
const githubClient = new GithubClient({ enterprise });
const client = githubClient.client;
const results = await getIssues(client, '1');
// sort by closed date ASC
res.data.sort((a: any, b: any) => {
results.sort((a: any, b: any) => {
return new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime();
});
@@ -30,13 +71,17 @@ const cherryPickRunner: TaskRunner<CherryPickOptions> = async ({ enterprise }) =
console.log('Printing PRs with cherry-pick-needed, in ASC merge date order');
console.log('--------------------------------------------------------------------');
for (const item of res.data) {
for (const item of results) {
if (!item.milestone) {
console.log(item.number + ' missing milestone!');
continue;
}
const issueDetails = await client.get(item.pull_request.url);
if (!issueDetails.data.merged) {
continue;
}
console.log(`* ${item.title}, (#${item.number}), merge-sha: ${issueDetails.data.merge_commit_sha}`);
commands += `git cherry-pick -x ${issueDetails.data.merge_commit_sha}\n`;
}
+1 -1
View File
@@ -41,7 +41,7 @@ addParameters({
theme: GrafanaDark,
showPanel: true,
showRoots: true,
panelPosition: 'bottom',
panelPosition: 'right',
showNav: true,
isFullscreen: false,
isToolshown: true,
+65 -23
View File
@@ -1,15 +1,18 @@
* **Forms**: Remove Forms namespace [BREAKING]. Will cause all `Forms` imports to stop working. See migration guide below. [#24378](https://github.com/grafana/grafana/pull/24378), [@tskarhed](https://github.com/tskarhed)
# 7.0.0-beta.2 (2020-05-07)
### Bug Fixes
* **Dashboard**: Fix for folder picker menu not being visible outside modal when saving dashboard. [#24296](https://github.com/grafana/grafana/pull/24296), [@tskarhed](https://github.com/tskarhed)
* **Select**: Fixes so component loses focus on selecting value or pressing outside of input. [#24008](https://github.com/grafana/grafana/pull/24008), [@mckn](https://github.com/mckn)
- **Dashboard**: Fix for folder picker menu not being visible outside modal when saving dashboard. [#24296](https://github.com/grafana/grafana/pull/24296), [@tskarhed](https://github.com/tskarhed)
- **Select**: Fixes so component loses focus on selecting value or pressing outside of input. [#24008](https://github.com/grafana/grafana/pull/24008), [@mckn](https://github.com/mckn)
# 7.0.0-beta.1 (2020-04-28)
## Breaking changes
### @grafana/ui forms migration notice
In Grafana 7 we have migrated from our old form components to `LegacyForms` namespace. The new components were previously available under the `Forms` namespace.
All the following components were moved to the LegacyForms namespace, and some replaced with the new form components:
@@ -27,12 +30,15 @@ All the following components were moved to the LegacyForms namespace, and some r
One exception to this is `FormLabel`, which has been renamed to `InlineFormLabel`.
If you were previously using the legacy form styles and your plugin is breaking, change from this:
```jsx
import { Switch } from '@grafana/ui';
…
<Switch .../>
```
To this:
```jsx
import { LegacyForms } from '@grafana/ui';
…
@@ -46,7 +52,9 @@ import { Forms } from '@grafana/ui';
…
<Forms.Switch ... />
```
To this:
```jsx
import { Switch} from '@grafana/ui';
…
@@ -55,41 +63,75 @@ import { Switch} from '@grafana/ui';
To see the new form components visit [our Storybook](https://developers.grafana.com/ui)
### Create custom value with Select
Previously the only thing you had to do to enable creating a custom value with Select was to add the `allowCustomValue` prop. Now you also have to add a `onCreateOption` handler.
Before:
```jsx
import { Select } from '@grafana/ui';
...
<Select
...
allowCustomValue
/>
```
After:
```jsx
import { Select } from '@grafana/ui';
...
<Select
...
allowCustomValue
onCreateOption={(customValue) => {
// Do things with the customValue
}}
/>
```
### Features / Enhancements
* **@grafana/ui**: Create Icon component and replace icons. [#23402](https://github.com/grafana/grafana/pull/23402), [@ivanahuckova](https://github.com/ivanahuckova)
* **@grafana/ui**: Create slider component. [#22275](https://github.com/grafana/grafana/pull/22275), [@ivanahuckova](https://github.com/ivanahuckova)
* **@grafana/ui**: Remove ColorPallete component. [#23592](https://github.com/grafana/grafana/pull/23592), [@ivanahuckova](https://github.com/ivanahuckova)
* **Components**: IconButton. [#23510](https://github.com/grafana/grafana/pull/23510), [@torkelo](https://github.com/torkelo)
* **Docs**: Adding API reference documentation support for the packages libraries. [#21931](https://github.com/grafana/grafana/pull/21931), [@mckn](https://github.com/mckn)
* **Migration**: Add old Input to legacy namespace. [#23286](https://github.com/grafana/grafana/pull/23286), [@tskarhed](https://github.com/tskarhed)
* **Migration**: Final components to LegacyForms. [#23707](https://github.com/grafana/grafana/pull/23707), [@tskarhed](https://github.com/tskarhed)
* **Migration**: Move Switch from Forms namespace. [#23386](https://github.com/grafana/grafana/pull/23386), [@tskarhed](https://github.com/tskarhed)
* **Migration**: Move last components from Forms namespace. [#23556](https://github.com/grafana/grafana/pull/23556), [@tskarhed](https://github.com/tskarhed)
* **Migration**: Remove Button from Forms namespace. [#23105](https://github.com/grafana/grafana/pull/23105), [@tskarhed](https://github.com/tskarhed)
* **Migration**: TextArea from Forms namespace. [#23436](https://github.com/grafana/grafana/pull/23436), [@tskarhed](https://github.com/tskarhed)
* **grafana/ui**: Add basic horizontal and vertical layout components. [#22303](https://github.com/grafana/grafana/pull/22303), [@dprokop](https://github.com/dprokop)
- **@grafana/ui**: Create Icon component and replace icons. [#23402](https://github.com/grafana/grafana/pull/23402), [@ivanahuckova](https://github.com/ivanahuckova)
- **@grafana/ui**: Create slider component. [#22275](https://github.com/grafana/grafana/pull/22275), [@ivanahuckova](https://github.com/ivanahuckova)
- **@grafana/ui**: Remove ColorPallete component. [#23592](https://github.com/grafana/grafana/pull/23592), [@ivanahuckova](https://github.com/ivanahuckova)
- **Components**: IconButton. [#23510](https://github.com/grafana/grafana/pull/23510), [@torkelo](https://github.com/torkelo)
- **Docs**: Adding API reference documentation support for the packages libraries. [#21931](https://github.com/grafana/grafana/pull/21931), [@mckn](https://github.com/mckn)
- **Migration**: Add old Input to legacy namespace. [#23286](https://github.com/grafana/grafana/pull/23286), [@tskarhed](https://github.com/tskarhed)
- **Migration**: Final components to LegacyForms. [#23707](https://github.com/grafana/grafana/pull/23707), [@tskarhed](https://github.com/tskarhed)
- **Migration**: Move Switch from Forms namespace. [#23386](https://github.com/grafana/grafana/pull/23386), [@tskarhed](https://github.com/tskarhed)
- **Migration**: Move last components from Forms namespace. [#23556](https://github.com/grafana/grafana/pull/23556), [@tskarhed](https://github.com/tskarhed)
- **Migration**: Remove Button from Forms namespace. [#23105](https://github.com/grafana/grafana/pull/23105), [@tskarhed](https://github.com/tskarhed)
- **Migration**: TextArea from Forms namespace. [#23436](https://github.com/grafana/grafana/pull/23436), [@tskarhed](https://github.com/tskarhed)
- **grafana/ui**: Add basic horizontal and vertical layout components. [#22303](https://github.com/grafana/grafana/pull/22303), [@dprokop](https://github.com/dprokop)
### Bug Fixes
* **@grafana/ui**: Fix time range when only partial datetime is provided. [#23122](https://github.com/grafana/grafana/pull/23122), [@ivanahuckova](https://github.com/ivanahuckova)
- **@grafana/ui**: Fix time range when only partial datetime is provided. [#23122](https://github.com/grafana/grafana/pull/23122), [@ivanahuckova](https://github.com/ivanahuckova)
# 6.6.0-beta.1.0 (2020-01-20)
### Features / Enhancements
* **Forms**: introduce RadioButtonGroup. [#20828](https://github.com/grafana/grafana/pull/20828), [@dprokop](https://github.com/dprokop)
* **grafana/ui**: ConfirmModal component. [#20965](https://github.com/grafana/grafana/pull/20965), [@alexanderzobnin](https://github.com/alexanderzobnin)
* **grafana/ui**: Create Tabs component. [#21328](https://github.com/grafana/grafana/pull/21328), [@peterholmberg](https://github.com/peterholmberg)
* **grafana/ui**: New table component. [#20991](https://github.com/grafana/grafana/pull/20991), [@peterholmberg](https://github.com/peterholmberg)
* **grafana/ui**: New updated time picker. [#20931](https://github.com/grafana/grafana/pull/20931), [@mckn](https://github.com/mckn)
- **Forms**: introduce RadioButtonGroup. [#20828](https://github.com/grafana/grafana/pull/20828), [@dprokop](https://github.com/dprokop)
- **grafana/ui**: ConfirmModal component. [#20965](https://github.com/grafana/grafana/pull/20965), [@alexanderzobnin](https://github.com/alexanderzobnin)
- **grafana/ui**: Create Tabs component. [#21328](https://github.com/grafana/grafana/pull/21328), [@peterholmberg](https://github.com/peterholmberg)
- **grafana/ui**: New table component. [#20991](https://github.com/grafana/grafana/pull/20991), [@peterholmberg](https://github.com/peterholmberg)
- **grafana/ui**: New updated time picker. [#20931](https://github.com/grafana/grafana/pull/20931), [@mckn](https://github.com/mckn)
### Bug Fixes
* **API**: Optionally list expired API keys. [#20468](https://github.com/grafana/grafana/pull/20468), [@papagian](https://github.com/papagian)
* **grafana/ui**: Do not build grafana/ui in strict mode as it depends on non-strict libs. [#21319](https://github.com/grafana/grafana/pull/21319), [@dprokop](https://github.com/dprokop)
- **API**: Optionally list expired API keys. [#20468](https://github.com/grafana/grafana/pull/20468), [@papagian](https://github.com/papagian)
- **grafana/ui**: Do not build grafana/ui in strict mode as it depends on non-strict libs. [#21319](https://github.com/grafana/grafana/pull/21319), [@dprokop](https://github.com/dprokop)
# 6.0.0-alpha.0 (2019-02-22)
Version update to 6.0.0 to keep @grafana/ui version in sync with [Grafana](https://github.com/grafana/grafana)
# 1.0.0-alpha.0 (2019-02-21)
First public release
First public release
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "7.0.0-beta.3",
"version": "7.0.0",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -28,8 +28,8 @@
},
"dependencies": {
"@emotion/core": "^10.0.27",
"@grafana/data": "7.0.0-beta.3",
"@grafana/e2e-selectors": "7.0.0-beta.3",
"@grafana/data": "7.0.0",
"@grafana/e2e-selectors": "7.0.0",
"@grafana/slate-react": "0.22.9-grafana",
"@grafana/tsconfig": "^1.0.0-rc1",
"@iconscout/react-unicons": "^1.0.0",
@@ -9,7 +9,7 @@ import tinycolor from 'tinycolor2';
import { css } from 'emotion';
import { HorizontalGroup } from '..';
export type BadgeColor = 'blue' | 'red' | 'green' | 'orange';
export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple';
export interface BadgeProps {
text: string;
@@ -7,7 +7,9 @@ import { Button } from './Button';
## Primary
Used for "call to action".
Used for "call to action", i.e. triggering the main action. There should never be more than one on a page. If you need multiple buttons for different actions, decide which action is the most important and make that the primary `Button`. All other `Button` components should be secondary.
If there is no primary action, all `Button` components should be secondary.
<Preview>
<div>
@@ -25,7 +27,10 @@ Used for "call to action".
## Secondary
The secondary button, used for "cancel" or aborting.
The secondary `Button` is the default button style and can trigger various actions. How it is used depends on its surroundings:
1. When next to the primary `Button`, the Secondary style can for example be used for "Cancel" or "Abort" actions.
2. When there is no main important action on a given page, all `Button` components should use the secondary style.
<Preview>
<div>
@@ -43,7 +48,7 @@ The secondary button, used for "cancel" or aborting.
## Destructive
Used for removing or deleting entities.
Used for triggering a removing or deleting action. Because of its dominant coloring, it should be used sparingly. If you need multiple Destructive `Button` components in one view, we recommend using a secondary `Button` or Link variant instead and only use the Destructive variant to double confirm.
<Preview>
<div>
@@ -63,6 +63,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, scollableContent: boolean)
flex-grow: 1;
overflow: ${!scollableContent ? 'hidden' : 'auto'};
z-index: 0;
height: 100%;
`,
};
});
@@ -5,6 +5,27 @@ import { Checkbox } from "./Checkbox";
# Checkbox
### When to use
Checked represents true, un-checked represent false. So you can use them to select a binary option or multiple options in a set. `Checkbox` can be used in groups, where the single checkboxes have no dependencies. That means that selecting one doesn’t affect any other `Checkbox`. When adding a description to your `Checkbox`, write positive statements so that "checked" means "yes" and not "no". That way, you can avoid confusion.
**DO:** [ ] Hide options
**DON'T:** [ ] Do not show options
Checkboxes typically only trigger changes after sending a form. If your component should trigger a change immediately, it's better to use a toggle switch instead. Furthermore, checkboxes are not mutually exclusive. That means that selecting one will not disable the others or impact them in any other way. If you want to offer a mutually exclusive choice, use `RadioButtonGroup` or a `Select` dropdown.
**DO:**
Show series
[ ] A-series
[ ] B-series
[ ] C-series
**DON'T:**
Show only
[ ] A-series
[ ] B-series
[ ] C-series
### Usage
```jsx
@@ -5,9 +5,13 @@ import { RadioButtonGroup } from "./RadioButtonGroup";
# RadioButtonGroup
`RadioButtonGroup` is used for selecting single value from multiple options.
`RadioButtonGroup` is used to select a single value from multiple mutually exclusive options.
Use `RadioButtonGroup` if there are up to four options available. Otherwise use Select component.
### When to use
Use `RadioButtonGroup` for mutually exclusive selections if there are up to four options available. This is because the `RadioButtonGroup` cannot have more than one row and should still accommodate small resolutions. For a mutually exclusive selection of more than four options, use `Select` component.
Radio buttons can only exist in this type of group. If you want one single option, it's better to use `Switch` instead. To offer multiple choices within the same group or context which are not mutually exclusive, use `Checkbox` instead.
### Usage
@@ -117,6 +117,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, surface: SurfaceType, size
`,
icon: css`
margin-bottom: 0;
vertical-align: baseline;
display: flex;
`,
};
@@ -9,6 +9,7 @@ interface FeatureInfoBox extends Omit<InfoBoxProps, 'branded' | 'title' | 'urlTi
title: string;
featureState?: FeatureState;
}
export const FeatureInfoBox = React.memo(
React.forwardRef<HTMLDivElement, FeatureInfoBox>(({ title, featureState, ...otherProps }, ref) => {
const theme = useTheme();
@@ -136,8 +136,8 @@ const getStyles = stylesFactory(
align-items: ${align};
&:last-child {
margin-bottom: 0;
margin-right: 0;
margin-bottom: ${orientation === Orientation.Vertical && 0};
margin-right: ${orientation === Orientation.Horizontal && 0};
}
`,
};
@@ -83,8 +83,10 @@ class UnThemedLogDetails extends PureComponent<Props> {
return (
row.dataFrame.fields
.map((field, index) => ({ ...field, index }))
// Remove Id which we use for react key and entry field which we are showing as the log message.
.filter((field, index) => 'id' !== field.name && row.entryFieldIndex !== index)
// Remove Id which we use for react key and entry field which we are showing as the log message. Also remove hidden fields.
.filter(
(field, index) => !('id' === field.name || row.entryFieldIndex === index || field.config.custom?.hidden)
)
// Filter out fields without values. For example in elastic the fields are parsed from the document which can
// have different structure per row and so the dataframe is pretty sparse.
.filter(field => {
@@ -106,6 +108,10 @@ class UnThemedLogDetails extends PureComponent<Props> {
);
});
/**
* Returns all fields for log row which consists of fields we parse from the message itself and any derived fields
* setup in data source config.
*/
getAllFields = memoizeOne((row: LogRowModel) => {
const fields = this.parseMessage(row.entry);
const derivedFields = this.getDerivedFields(row);
@@ -121,18 +127,10 @@ class UnThemedLogDetails extends PureComponent<Props> {
}
return acc;
}, {} as { [key: string]: FieldDef });
const allFields = Object.values(fieldsMap);
allFields.sort((fieldA, fieldB) => {
if (fieldA.links?.length && !fieldB.links?.length) {
return -1;
}
allFields.sort(sortFieldsLinkFirst);
if (!fieldA.links?.length && fieldB.links?.length) {
return 1;
}
return fieldA.key > fieldB.key ? 1 : fieldA.key < fieldB.key ? -1 : 0;
});
return allFields;
});
@@ -233,5 +231,15 @@ class UnThemedLogDetails extends PureComponent<Props> {
}
}
function sortFieldsLinkFirst(fieldA: FieldDef, fieldB: FieldDef) {
if (fieldA.links?.length && !fieldB.links?.length) {
return -1;
}
if (!fieldA.links?.length && fieldB.links?.length) {
return 1;
}
return fieldA.key > fieldB.key ? 1 : fieldA.key < fieldB.key ? -1 : 0;
}
export const LogDetails = withTheme(UnThemedLogDetails);
LogDetails.displayName = 'LogDetails';
@@ -102,6 +102,11 @@ const LogRowContextGroupHeader: React.FunctionComponent<LogRowContextGroupHeader
const theme = useContext(ThemeContext);
const { header } = getLogRowContextStyles(theme);
const onClickLoadMore = (event: React.SyntheticEvent) => {
event.stopPropagation();
onLoadMoreContext();
};
return (
<div className={header}>
<span
@@ -120,7 +125,7 @@ const LogRowContextGroupHeader: React.FunctionComponent<LogRowContextGroupHeader
cursor: pointer;
}
`}
onClick={() => onLoadMoreContext()}
onClick={onClickLoadMore}
>
Load 10 more
</span>
@@ -1,5 +1,8 @@
import React from 'react';
import { FieldType, LogRowModel, MutableDataFrame, Labels, LogLevel, DataQueryResponse } from '@grafana/data';
import { getRowContexts } from './LogRowContextProvider';
import { getRowContexts, LogRowContextProvider } from './LogRowContextProvider';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
describe('getRowContexts', () => {
describe('when called with a DataFrame and results are returned', () => {
@@ -84,6 +87,85 @@ describe('getRowContexts', () => {
});
});
describe('LogRowContextProvider', () => {
describe('when requesting longer context', () => {
it('can request more log lines', async () => {
const firstResult = new MutableDataFrame({
refId: 'B',
fields: [
{ name: 'ts', type: FieldType.time, values: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] },
{
name: 'line',
type: FieldType.string,
values: ['10', '9', '8', '7', '6', '5', '4', '3', '2', '1'],
labels: {},
},
{
name: 'id',
type: FieldType.string,
values: ['10', '9', '8', '7', '6', '5', '4', '3', '2', '1'],
labels: {},
},
],
});
const secondResult = new MutableDataFrame({
refId: 'B',
fields: [
{ name: 'ts', type: FieldType.time, values: [14, 13, 12] },
{ name: 'line', type: FieldType.string, values: ['14', '13', '12'], labels: {} },
{ name: 'id', type: FieldType.string, values: ['14', '13', '12'], labels: {} },
],
});
let called = false;
const getRowContextMock = (row: LogRowModel, options?: any): Promise<DataQueryResponse> => {
if (!called) {
called = true;
return Promise.resolve({ data: [firstResult] });
}
return Promise.resolve({ data: [secondResult] });
};
let wrapper: any;
await act(async () => {
wrapper = await mount(
<LogRowContextProvider row={row} getRowContext={getRowContextMock}>
{({ result, errors, hasMoreContextRows, updateLimit, limit }) => {
return (
<div>
<div className="result">
<p className="result-before">{result.before?.toString()}</p>
<p className="result-after">{result.after?.toString()}</p>
</div>
<div className="errors">
<p className="errors-before">{errors.before}</p>
<p className="errors-after">{errors.after}</p>
</div>
<div className="hasMoreContextRows">
<p className="hasMoreContextRows-before">{String(hasMoreContextRows.before)}</p>
<p className="hasMoreContextRows-after">{String(hasMoreContextRows.after)}</p>
</div>
<div className="limit">{limit}</div>
<button className="updateLimit" onClick={updateLimit}>
Update limit
</button>
</div>
);
}}
</LogRowContextProvider>
);
});
expect(wrapper.find('.hasMoreContextRows-before').text()).toBe('true');
expect(wrapper.find('.hasMoreContextRows-after').text()).toBe('true');
expect(wrapper.find('.limit').text()).toBe('10');
await act(async () => wrapper.find('.updateLimit').simulate('click'));
expect(wrapper.find('.limit').text()).toBe('20');
expect(wrapper.find('.hasMoreContextRows-before').text()).toBe('true');
expect(wrapper.find('.hasMoreContextRows-after').text()).toBe('false');
});
});
});
const row: LogRowModel = {
entryFieldIndex: 0,
rowIndex: 0,
@@ -37,6 +37,7 @@ interface LogRowContextProviderProps {
errors: LogRowContextQueryErrors;
hasMoreContextRows: HasMoreContextRows;
updateLimit: () => void;
limit: number;
}) => JSX.Element;
}
@@ -160,11 +161,19 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
let hasMoreLogsBefore = true,
hasMoreLogsAfter = true;
if (currentResult && currentResult.data[0].length === value.data[0].length) {
const currentResultBefore = currentResult?.data[0][0];
const currentResultAfter = currentResult?.data[1][0];
const valueBefore = value.data[0][0];
const valueAfter = value.data[1][0];
// checks if there are more log rows in a given direction
// if after fetching additional rows the length of result is the same,
// we can assume there are no logs in that direction within a given time range
if (currentResult && (!valueBefore || currentResultBefore.length === valueBefore.length)) {
hasMoreLogsBefore = false;
}
if (currentResult && currentResult.data[1].length === value.data[1].length) {
if (currentResult && (!valueAfter || currentResultAfter.length === valueAfter.length)) {
hasMoreLogsAfter = false;
}
@@ -189,5 +198,6 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
},
hasMoreContextRows,
updateLimit: () => setLimit(limit + 10),
limit,
});
};
@@ -1,32 +1,26 @@
import React from 'react';
import React, { memo, useMemo, useCallback } from 'react';
import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
import { FieldMatcherID, fieldMatchers, getFieldTitle } from '@grafana/data';
import { FieldMatcherID, fieldMatchers, getFieldDisplayName, SelectableValue, DataFrame } from '@grafana/data';
import { Select } from '../Select/Select';
export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> {
render() {
const { data, options, onChange } = this.props;
const names: Set<string> = new Set();
export const FieldNameMatcherEditor = memo<MatcherUIProps<string>>(props => {
const { data, options } = props;
const names = useFieldDisplayNames(data);
const selectOptions = useSelectOptions(names);
for (const frame of data) {
for (const field of frame.fields) {
names.add(getFieldTitle(field, frame, data));
const onChange = useCallback(
(selection: SelectableValue<string>) => {
if (!selection.value || !names.has(selection.value)) {
return;
}
}
if (options) {
names.add(options);
}
const selectOptions = Array.from(names).map(n => ({
value: n,
label: n,
}));
const selectedOption = selectOptions.find(v => v.value === options);
return props.onChange(selection.value);
},
[names, props.onChange]
);
return (
<Select allowCustomValue value={selectedOption} options={selectOptions} onChange={o => onChange(o.value!)} />
);
}
}
const selectedOption = selectOptions.find(v => v.value === options);
return <Select value={selectedOption} options={selectOptions} onChange={onChange} />;
});
export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
id: FieldMatcherID.byName,
@@ -35,3 +29,26 @@ export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
name: 'Filter by field',
description: 'Set properties for fields matching the name',
};
const useFieldDisplayNames = (data: DataFrame[]): Set<string> => {
return useMemo(() => {
const names: Set<string> = new Set();
for (const frame of data) {
for (const field of frame.fields) {
names.add(getFieldDisplayName(field, frame, data));
}
}
return names;
}, [data]);
};
const useSelectOptions = (displayNames: Set<string>): Array<SelectableValue<string>> => {
return useMemo(() => {
return Array.from(displayNames).map(n => ({
value: n,
label: n,
}));
}, [displayNames]);
};
@@ -29,6 +29,7 @@ export interface QueryFieldProps {
onRunQuery?: () => void;
onBlur?: () => void;
onChange?: (value: string) => void;
onRichValueChange?: (value: Value) => void;
onClick?: (event: Event, editor: CoreEditor, next: () => any) => any;
onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
@@ -68,10 +69,12 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
// Base plugins
this.plugins = [
NewlinePlugin(),
// SuggestionsPlugin and RunnerPlugin need to be before NewlinePlugin
// because they override Enter behavior
SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion }),
ClearPlugin(),
RunnerPlugin({ handler: this.runOnChangeAndRunQuery }),
NewlinePlugin(),
ClearPlugin(),
SelectionShortcutsPlugin(),
IndentationPlugin(),
ClipboardPlugin(),
@@ -121,6 +124,9 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
onChange = (value: Value, runQuery?: boolean) => {
const documentChanged = value.document !== this.state.value.document;
const prevValue = this.state.value;
if (this.props.onRichValueChange) {
this.props.onRichValueChange(value);
}
// Update local state with new value and optionally change value upstream.
this.setState({ value }, () => {
@@ -22,6 +22,7 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme) => {
display: flex;
align-items: center;
flex-direction: row;
flex-shrink: 0;
white-space: nowrap;
cursor: pointer;
border-left: 2px solid transparent;
@@ -2,6 +2,9 @@ import { SelectableValue } from '@grafana/data';
import React from 'react';
export type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
export type InputActionMeta = {
action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close';
};
export interface SelectCommonProps<T> {
allowCustomValue?: boolean;
@@ -39,7 +42,7 @@ export interface SelectCommonProps<T> {
onCloseMenu?: () => void;
/** allowCustomValue must be enabled. Function decides what to do with that custom value. */
onCreateOption?: (value: string) => void;
onInputChange?: (label: string) => void;
onInputChange?: (value: string, actionMeta: InputActionMeta) => void;
onKeyDown?: (event: React.KeyboardEvent) => void;
onOpenMenu?: () => void;
openMenuOnFocus?: boolean;
@@ -156,6 +156,29 @@ describe('sharedSingleStatMigrationHandler', () => {
`);
});
it('Rename title to displayName', () => {
const panel = {
options: {
fieldOptions: {
stat: 'last',
decimals: 5,
defaults: {
title: 'newTitle',
min: 0,
max: 100,
mappings: [],
},
override: {},
},
},
title: 'Usage',
type: 'bargauge',
};
sharedSingleStatMigrationHandler(panel as any);
expect((panel as any).fieldConfig.defaults.displayName).toBe('newTitle');
});
it('change from angular singlestat with no enabled gauge', () => {
const old: any = {
angular: {
@@ -202,6 +202,15 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
delete options.fieldOptions;
}
if (previousVersion < 7.1) {
// move title to displayName
const oldTitle = (panel.fieldConfig.defaults as any).title;
if (oldTitle !== undefined && oldTitle !== null) {
panel.fieldConfig.defaults.displayName = oldTitle;
delete (panel.fieldConfig.defaults as any).title;
}
}
return options as SingleStatBaseOptions;
}
@@ -14,6 +14,7 @@ interface Props {
allowMultiple?: boolean;
defaultStat?: string;
className?: string;
menuPlacement?: 'auto' | 'bottom' | 'top';
}
export class StatsPicker extends PureComponent<Props> {
@@ -62,7 +63,7 @@ export class StatsPicker extends PureComponent<Props> {
};
render() {
const { stats, allowMultiple, defaultStat, placeholder, className } = this.props;
const { stats, allowMultiple, defaultStat, placeholder, className, menuPlacement } = this.props;
const select = fieldReducers.selectOptions(stats);
return (
@@ -75,6 +76,7 @@ export class StatsPicker extends PureComponent<Props> {
options={select.options}
placeholder={placeholder}
onChange={this.onSelectionChange}
menuPlacement={menuPlacement}
/>
);
}
@@ -7,18 +7,14 @@ export const DefaultCell: FC<TableCellProps> = props => {
const { field, cell, tableStyles, row } = props;
let link: LinkModel<any> | undefined;
if (!field.display) {
return null;
}
const displayValue = field.display(cell.value);
const displayValue = field.display ? field.display(cell.value) : cell.value;
if (field.getLinks) {
link = field.getLinks({
valueRowIndex: row.index,
})[0];
}
const value = formattedValueToString(displayValue);
const value = field.display ? formattedValueToString(displayValue) : displayValue;
return (
<div className={tableStyles.tableCell}>
@@ -21,7 +21,7 @@ export const JSONViewCell: FC<TableCellProps> = props => {
return (
<div className={cx(txt, tableStyles.tableCell)}>
<Tooltip placement="auto" content={content} theme={'info'}>
<span>{displayValue}</span>
<div className={tableStyles.overflow}>{displayValue}</div>
</Tooltip>
</div>
);
@@ -1,4 +1,4 @@
import React, { FC, memo, useMemo, useCallback } from 'react';
import React, { FC, memo, useCallback, useMemo } from 'react';
import { DataFrame, Field } from '@grafana/data';
import {
Cell,
@@ -6,10 +6,10 @@ import {
HeaderGroup,
useAbsoluteLayout,
useResizeColumns,
useSortBy,
useTable,
UseResizeColumnsState,
useSortBy,
UseSortByState,
useTable,
} from 'react-table';
import { FixedSizeList } from 'react-window';
import { getColumns, getTextAlign } from './utils';
@@ -73,14 +73,9 @@ export const Table: FC<Props> = memo((props: Props) => {
if (!data.fields.length) {
return [];
}
// Check if an array buffer already exists
const buffer = (data.fields[0].values as any).buffer;
if (Array.isArray(buffer) && buffer.length === data.length) {
return buffer;
}
// For arrow tables, the `toArray` implementation is expensive and akward *especially* for timestamps
// as we only use this to fake the length of our data set for react-table we need to make sure we always return an array
// filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in
// https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585
return Array(data.length).fill(0);
}, [data]);
@@ -17,6 +17,7 @@ export interface TableStyles {
row: string;
theme: GrafanaTheme;
resizeHandle: string;
overflow: string;
}
export const getTableStyles = stylesFactory(
@@ -92,6 +93,10 @@ export const getTableStyles = stylesFactory(
white-space: nowrap;
overflow: hidden;
`,
overflow: css`
overflow: hidden;
text-overflow: ellipsis;
`,
resizeHandle: css`
label: resizeHandle;
cursor: col-resize !important;
@@ -7,6 +7,7 @@ export interface TableFieldOptions {
width: number;
align: FieldTextAlignment;
displayMode: TableCellDisplayMode;
hidden?: boolean;
}
export enum TableCellDisplayMode {
@@ -1,5 +1,5 @@
import { TextAlignProperty } from 'csstype';
import { DataFrame, Field, FieldType, getFieldTitle } from '@grafana/data';
import { DataFrame, Field, FieldType, getFieldDisplayName } from '@grafana/data';
import { Column } from 'react-table';
import { DefaultCell } from './DefaultCell';
import { BarGaugeCell } from './BarGaugeCell';
@@ -38,10 +38,13 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
const columns: Column[] = [];
let fieldCountWithoutWidth = data.fields.length;
for (let fieldIndex = 0; fieldIndex < data.fields.length; fieldIndex++) {
const field = data.fields[fieldIndex];
for (const [fieldIndex, field] of data.fields.entries()) {
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;
if (fieldTableOptions.hidden) {
continue;
}
if (fieldTableOptions.width) {
availableWidth -= fieldTableOptions.width;
fieldCountWithoutWidth -= 1;
@@ -51,7 +54,7 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
columns.push({
Cell,
id: fieldIndex.toString(),
Header: getFieldTitle(field, data),
Header: getFieldDisplayName(field, data),
accessor: (row: any, i: number) => {
return field.values.get(i);
},
@@ -30,7 +30,7 @@ export const Tab = React.forwardRef<HTMLLIElement, TabProps>(
onChangeTab();
}
}}
aria-label={selectors.components.Tab.title(label)}
aria-label={otherProps['aria-label'] || selectors.components.Tab.title(label)}
ref={ref}
>
{icon && <Icon name={icon} />}
@@ -8,6 +8,7 @@ import {
dateTimeFormat,
dateTimeParse,
rangeUtil,
RawTimeRange,
} from '@grafana/data';
import { TimePickerCalendar } from './TimePickerCalendar';
import { Field } from '../../Forms/Field';
@@ -59,10 +60,8 @@ export const TimeRangeForm: React.FC<Props> = props => {
return;
}
const timeRange = rangeUtil.convertRawToRange({
from: dateTimeParse(from.value, { timeZone }),
to: dateTimeParse(to.value, { timeZone }),
});
const raw: RawTimeRange = { from: from.value, to: to.value };
const timeRange = rangeUtil.convertRawToRange(raw, timeZone);
props.onApply(timeRange);
}, [from, to, roundup, timeZone]);
@@ -69,7 +69,7 @@ const Options: React.FC<Props> = ({ options, value, onSelect, timeZone }) => {
key={keyForOption(option, index)}
value={option}
selected={isEqual(option, value)}
onSelect={option => onSelect(mapOptionToTimeRange(option))}
onSelect={option => onSelect(mapOptionToTimeRange(option, timeZone))}
/>
))}
</div>
@@ -1,7 +1,7 @@
import { TimeOption, TimeRange, TimeZone, rangeUtil, dateTimeFormat, dateTimeFormatISO } from '@grafana/data';
export const mapOptionToTimeRange = (option: TimeOption): TimeRange => {
return rangeUtil.convertRawToRange({ from: option.from, to: option.to });
export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone);
};
export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => {
@@ -13,7 +13,7 @@ import { stylesFactory } from '../../themes/stylesFactory';
import { withTheme, useTheme } from '../../themes/ThemeContext';
// Types
import { isDateTime, rangeUtil, GrafanaTheme, dateTimeFormatWithAbbrevation } from '@grafana/data';
import { isDateTime, rangeUtil, GrafanaTheme, dateTimeFormat, timeZoneFormatUserFriendly } from '@grafana/data';
import { TimeRange, TimeOption, TimeZone, dateMath } from '@grafana/data';
import { Themeable } from '../../types';
@@ -206,13 +206,21 @@ const ZoomOutTooltip = () => (
</>
);
const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => (
<>
{dateTimeFormatWithAbbrevation(timeRange.from, { timeZone })}
<div className="text-center">to</div>
{dateTimeFormatWithAbbrevation(timeRange.to, { timeZone })}
</>
);
const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => {
const theme = useTheme();
const styles = getLabelStyles(theme);
return (
<>
{dateTimeFormat(timeRange.from, { timeZone })}
<div className="text-center">to</div>
{dateTimeFormat(timeRange.to, { timeZone })}
<div className="text-center">
<span className={styles.utc}>{timeZoneFormatUserFriendly(timeZone)}</span>
</div>
</>
);
};
const TimePickerButtonLabel = memo<Props>(({ hideText, value, timeZone }) => {
const theme = useTheme();
@@ -0,0 +1,46 @@
import React from 'react';
import { mount } from 'enzyme';
import { Typeahead, State } from './Typeahead';
import { TypeaheadItem } from './TypeaheadItem';
import { CompletionItemKind } from '../../types';
describe('Typeahead', () => {
const completionItemGroups = [{ label: 'my group', items: [{ label: 'first item' }] }];
describe('when closed', () => {
it('renders nothing when no items given', () => {
const component = mount(<Typeahead origin="test" groupedItems={[]} />);
expect(component.find('.typeahead')).toHaveLength(0);
});
it('renders nothing when items given', () => {
const component = mount(<Typeahead origin="test" groupedItems={completionItemGroups} />);
expect(component.find('.typeahead')).toHaveLength(0);
});
});
describe('when open', () => {
it('renders given items and nothing is selected', () => {
const component = mount(<Typeahead origin="test" groupedItems={completionItemGroups} isOpen />);
expect(component.find('.typeahead')).toHaveLength(1);
const items = component.find(TypeaheadItem);
expect(items).toHaveLength(2);
expect(items.get(0).props.item.kind).toEqual(CompletionItemKind.GroupTitle);
expect(items.get(0).props.isSelected).toBeFalsy();
expect(items.get(1).props.item.label).toEqual('first item');
expect(items.get(1).props.isSelected).toBeFalsy();
});
});
it('selected the first non-group item on moving to first item', () => {
const component = mount(<Typeahead origin="test" groupedItems={completionItemGroups} isOpen />);
expect(component.find('.typeahead')).toHaveLength(1);
let items = component.find(TypeaheadItem);
expect(items).toHaveLength(2);
expect((component.state() as State).typeaheadIndex).toBe(null);
(component.instance() as Typeahead).moveMenuIndex(1);
expect((component.state() as State).typeaheadIndex).toBe(1);
component.setProps({});
items = component.find(TypeaheadItem);
expect(items.get(0).props.isSelected).toBeFalsy();
expect(items.get(1).props.isSelected).toBeTruthy();
});
});
@@ -20,13 +20,13 @@ interface Props {
isOpen?: boolean;
}
interface State {
export interface State {
allItems: CompletionItem[];
listWidth: number;
listHeight: number;
itemHeight: number;
hoveredItem: number | null;
typeaheadIndex: number;
typeaheadIndex: number | null;
}
export class Typeahead extends React.PureComponent<Props, State> {
@@ -34,7 +34,14 @@ export class Typeahead extends React.PureComponent<Props, State> {
context!: React.ContextType<typeof ThemeContext>;
listRef = createRef<FixedSizeList>();
state: State = { hoveredItem: null, typeaheadIndex: 1, allItems: [], listWidth: -1, listHeight: -1, itemHeight: -1 };
state: State = {
hoveredItem: null,
typeaheadIndex: null,
allItems: [],
listWidth: -1,
listHeight: -1,
itemHeight: -1,
};
componentDidMount = () => {
if (this.props.menuRef) {
@@ -63,7 +70,12 @@ export class Typeahead extends React.PureComponent<Props, State> {
};
componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>) => {
if (prevState.typeaheadIndex !== this.state.typeaheadIndex && this.listRef && this.listRef.current) {
if (
this.state.typeaheadIndex !== null &&
prevState.typeaheadIndex !== this.state.typeaheadIndex &&
this.listRef &&
this.listRef.current
) {
if (this.state.typeaheadIndex === 1) {
this.listRef.current.scrollToItem(0); // special case for handling the first group label
return;
@@ -75,7 +87,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
const allItems = flattenGroupItems(this.props.groupedItems);
const longestLabel = calculateLongestLabel(allItems);
const { listWidth, listHeight, itemHeight } = calculateListSizes(this.context, allItems, longestLabel);
this.setState({ listWidth, listHeight, itemHeight, allItems });
this.setState({ listWidth, listHeight, itemHeight, allItems, typeaheadIndex: null });
}
};
@@ -95,7 +107,8 @@ export class Typeahead extends React.PureComponent<Props, State> {
const itemCount = this.state.allItems.length;
if (itemCount) {
// Select next suggestion
let newTypeaheadIndex = modulo(this.state.typeaheadIndex + moveAmount, itemCount);
const typeaheadIndex = this.state.typeaheadIndex || 0;
let newTypeaheadIndex = modulo(typeaheadIndex + moveAmount, itemCount);
if (this.state.allItems[newTypeaheadIndex].kind === CompletionItemKind.GroupTitle) {
newTypeaheadIndex = modulo(newTypeaheadIndex + moveAmount, itemCount);
@@ -110,7 +123,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
};
insertSuggestion = () => {
if (this.props.onSelectSuggestion) {
if (this.props.onSelectSuggestion && this.state.typeaheadIndex !== null) {
this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
}
};
@@ -144,6 +157,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
const { allItems, listWidth, listHeight, itemHeight, hoveredItem, typeaheadIndex } = this.state;
const showDocumentation = hoveredItem || typeaheadIndex;
const documentationItem = allItems[hoveredItem ? hoveredItem : typeaheadIndex || 0];
return (
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
@@ -169,7 +183,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
return (
<TypeaheadItem
onClickItem={() => (this.props.onSelectSuggestion ? this.props.onSelectSuggestion(item) : {})}
isSelected={allItems[typeaheadIndex] === item}
isSelected={typeaheadIndex === null ? false : allItems[typeaheadIndex] === item}
item={item}
prefix={prefix}
style={style}
@@ -181,9 +195,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
</FixedSizeList>
</ul>
{showDocumentation && (
<TypeaheadInfo height={listHeight} item={allItems[hoveredItem ? hoveredItem : typeaheadIndex]} />
)}
{showDocumentation && <TypeaheadInfo height={listHeight} item={documentationItem} />}
</Portal>
);
}
@@ -17,6 +17,7 @@ interface ValuePickerProps<T> {
variant?: ButtonVariant;
size?: ComponentSize;
isFullWidth?: boolean;
menuPlacement?: 'auto' | 'bottom' | 'top';
}
export function ValuePicker<T>({
@@ -27,6 +28,7 @@ export function ValuePicker<T>({
variant,
size = 'sm',
isFullWidth = true,
menuPlacement,
}: ValuePickerProps<T>) {
const [isPicking, setIsPicking] = useState(false);
@@ -50,6 +52,7 @@ export function ValuePicker<T>({
setIsPicking(false);
onChange(value);
}}
menuPlacement={menuPlacement}
/>
)}
</>

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