Merge branch 'master' into lukas/log_context_fixes

This commit is contained in:
Lukas Siatka
2020-05-07 00:02:05 +02:00
303 changed files with 18930 additions and 3614 deletions
+13 -3
View File
@@ -54,7 +54,7 @@ commands:
- run:
name: "Install Grafana build pipeline tool"
command: |
VERSION=0.4.3
VERSION=0.4.4
curl -fLO https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v${VERSION}/grabpl
chmod +x grabpl
mv grabpl /tmp
@@ -192,7 +192,13 @@ jobs:
- run:
name: Build internal Grafana plug-ins
command: |
/tmp/grabpl build-plugins --jobs 2 --edition << parameters.edition >>
if [[ -n "$CIRCLE_PR_NUMBER" ]]; then
# This is a forked PR, so don't sign as it requires an API secret
/tmp/grabpl build-plugins --jobs 2 --edition << parameters.edition >>
else
export GRAFANA_API_KEY=$GRAFANA_COM_API_KEY
/tmp/grabpl build-plugins --jobs 2 --edition << parameters.edition >> --sign --signing-admin
fi
- run:
name: Move artifacts
command: |
@@ -622,8 +628,12 @@ jobs:
if [[ $CIRCLE_BRANCH == "chore/test-release-pipeline" ]]; then
# We're testing the release pipeline
/tmp/grabpl publish-docker --jobs 4 --edition << parameters.edition >> --ubuntu=<< parameters.ubuntu >> --dry-run
else
elif [[ -n $CIRCLE_TAG ]]; then
# This is a release
/tmp/grabpl publish-docker --jobs 4 --edition << parameters.edition >> --ubuntu=<< parameters.ubuntu >>
else
# TODO: Don't ignore errors, temporary workaround until we fix #22955
/tmp/grabpl publish-docker --jobs 4 --edition << parameters.edition >> --ubuntu=<< parameters.ubuntu >> || echo Publishing failed!
fi
- run:
name: CI job failed
+16 -18
View File
@@ -1,20 +1,3 @@
# Golang build container
FROM golang:1.14.2-alpine3.11 as go-builder
RUN apk add --no-cache gcc g++
WORKDIR $GOPATH/src/github.com/grafana/grafana
COPY go.mod go.sum ./
RUN go mod verify
COPY pkg pkg
COPY build.go package.json ./
RUN go run build.go build
# Node build container
FROM node:12.16.3-alpine3.11 as js-builder
WORKDIR /usr/src/app/
@@ -33,7 +16,22 @@ COPY emails emails
ENV NODE_ENV production
RUN ./node_modules/.bin/grunt build
# Final container
FROM golang:1.14.2-alpine3.11 as go-builder
RUN apk add --no-cache gcc g++
WORKDIR $GOPATH/src/github.com/grafana/grafana
COPY go.mod go.sum ./
RUN go mod verify
COPY pkg pkg
COPY build.go package.json ./
RUN go run build.go build
# Final stage
FROM alpine:3.11
LABEL maintainer="Grafana team <hello@grafana.com>"
+13 -13
View File
@@ -1,16 +1,3 @@
FROM golang:1.14.2 AS go-builder
WORKDIR /src/grafana
COPY go.mod go.sum ./
RUN go mod verify
COPY build.go package.json ./
COPY pkg pkg/
RUN go run build.go build
FROM node:12.16.3-slim AS js-builder
WORKDIR /usr/src/app/
@@ -29,6 +16,19 @@ COPY emails emails
ENV NODE_ENV production
RUN ./node_modules/.bin/grunt build
FROM golang:1.14.2 AS go-builder
WORKDIR /src/grafana
COPY go.mod go.sum ./
RUN go mod verify
COPY build.go package.json ./
COPY pkg pkg/
RUN go run build.go build
FROM ubuntu:20.04
LABEL maintainer="Grafana team <hello@grafana.com>"
+2
View File
@@ -695,6 +695,8 @@ disable_sanitize_html = false
[plugins]
enable_alpha = false
app_tls_skip_verify_insecure = false
# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature.
allow_loading_unsigned_plugins =
#################################### Grafana Image Renderer Plugin ##########################
[plugin.grafana-image-renderer]
+2
View File
@@ -684,6 +684,8 @@
[plugins]
;enable_alpha = false
;app_tls_skip_verify_insecure = false
# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature.
;allow_loading_unsigned_plugins =
#################################### Grafana Image Renderer Plugin ##########################
[plugin.grafana-image-renderer]
@@ -33,6 +33,7 @@ The following sections provide general guidelines on topics specific to Grafana
- Use: The panel opens. Grafana opens the panel.
* Do not use an ampersand (&) as an abbreviation for "and."
- **Exceptions:** If an ampersand is used in the Grafana UI, then match the UI.
* Avoid using internal slang and jargon in technical documentation.
### File naming conventions
@@ -112,7 +113,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
### Word usage
Grafana products has some words, abbreviations, and slang particular to this discourse community.
Grafana products has some words, abbreviations, and terms particular to the Grafana discourse community.
#### checkout, check out
@@ -141,6 +142,10 @@ Two words, not one
* Correct, but passive voice: Your list of active alarms is displayed.
* Incorrect: The list of active alarms displays.
#### drawer
Do not use. This is developer jargon that refers to a UI panel. Refer to the panel or feature by its proper name.
#### intro, introduction
"Introduction" is the preferred word. Use "intro" if there are space constraints (like on the side menu) or you are specifically trying for a less formal, more conversational tone.
+39 -1
View File
@@ -8,7 +8,7 @@ Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/jav
- [Table of Contents](#table-of-contents)
- [Basic rules](#basic-rules)
- [Naming conventions](#naming-conventions)
- [Files and directories naming conventions](#files-and-directories-naming-conventions)
- [File and directory naming conventions](#file-and-directory-naming-conventions)
- [Code organization](#code-organization)
- [Exports](#exports)
- [Comments](#comments)
@@ -172,6 +172,44 @@ const CONSTANT_VALUE = "This string won't change";
_SASS styles are deprecated. Please migrate to Emotion whenever you need to modify SASS styles._
### Typing
In general, you should let Typescript infer the types so that there's no need to explicitly define type for each variable.
There are some exceptions to this:
```typescript
// Typescript needs to know type of arrays or objects otherwise it would infer it as array of any
// bad
const stringArray = [];
// good
const stringArray: string[] = [];
```
Specify function return types explicitly in new code. This improves readability by being able to tell what a function returns just by looking at the signature. It also prevents errors when a function's return type is broader than expected by the author.
> Note: We don't have linting for this enabled because of lots of old code that needs to be fixed first.
```typescript
// bad
function transform(value?: string) {
if (!value) {
return undefined
}
return applyTransform(value)
};
// good
function transform(value?: string): TransformedValue | undefined {
if (!value) {
return undefined
}
return applyTransform(value)
};
```
### File and directory naming conventions
Name files according to the primary export:
+8 -1
View File
@@ -39,4 +39,11 @@ Some of the blocks support dynamic change of the image version used in the Docke
make devenv sources=postgres,openldap,grafana postgres_version=9.2 grafana_version=6.7.0-beta1
```
Note: The grafana block is pre-configured with the dev-datasources and dashboards.
### Notes per block
#### Grafana
The grafana block is pre-configured with the dev-datasources and dashboards.
#### Jaeger
Jaeger block runs both Jaeger and Loki container. Loki container sends traces to Jaeger and also logs its own logs into itself so it is possible to setup derived field for traceID from Loki to Jaeger. You need to install a docker plugin for the self logging to work, without it the container won't start. See https://github.com/grafana/loki/tree/master/cmd/docker-driver#plugin-installation for installation instructions.
+24
View File
@@ -246,5 +246,29 @@ datasources:
access: proxy
url: http://localhost:3100
editable: false
jsonData:
derivedFields:
- name: "traceID"
matcherRegex: "traceID=(\\w+)"
url: "$${__value.raw}"
datasourceUid: gdev-jaeger
- name: "traceID"
matcherRegex: "traceID=(\\w+)"
url: "$${__value.raw}"
datasourceUid: gdev-zipkin
- name: gdev-jaeger
type: jaeger
uid: gdev-jaeger
access: proxy
url: http://localhost:16686
editable: false
- name: gdev-zipkin
type: zipkin
uid: gdev-zipkin
access: proxy
url: http://localhost:9411
editable: false
@@ -10,6 +10,11 @@
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
# For this to work you need to install the logging driver see https://github.com/grafana/loki/tree/master/cmd/docker-driver#plugin-installation
logging:
driver: loki
options:
loki-url: "http://localhost:3100/loki/api/v1/push"
# Optional jaeger tracing
environment:
- JAEGER_AGENT_HOST=jaeger
+3 -1
View File
@@ -1,9 +1,11 @@
.PHONY: docs docs-test
IMAGE = grafana/docs-base:latest
IMAGE = grafana/docs-base:latest
docs:
docker pull ${IMAGE}
docker run -v $(shell pwd)/sources:/hugo/content/docs/grafana/latest -p 3002:3002 --rm -it $(IMAGE) /bin/bash -c 'make server'
docs-test:
docker pull ${IMAGE}
docker run -v $(shell pwd)/sources:/hugo/content/docs/grafana/latest --rm -it $(IMAGE) /bin/bash -c 'make prod'
+3 -3
View File
@@ -45,11 +45,11 @@ aliases = ["/docs/grafana/v1.1", "/docs/grafana/latest/guides/reference/admin",
## Guides
<div class="nav-cards">
<a href="{{< relref "guides/what-is-grafana.md" >}}" class="nav-cards__item nav-cards__item--guide">
<a href="{{< relref "getting-started/what-is-grafana.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>What is Grafana?</h4>
<p>Get an overview of Grafana's key features.</p>
</a>
<a href="{{< relref "guides/getting_started.md" >}}" class="nav-cards__item nav-cards__item--guide">
<a href="{{< relref "getting-started/getting-started.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Getting started</h4>
<p>Learn the basics of using Grafana.</p>
</a>
@@ -57,7 +57,7 @@ aliases = ["/docs/grafana/v1.1", "/docs/grafana/latest/guides/reference/admin",
<h4>Configure Grafana</h4>
<p>Review the configuration and setup options.</p>
</a>
<a href="{{< relref "guides/timeseries.md" >}}" class="nav-cards__item nav-cards__item--guide">
<a href="{{< relref "getting-started/timeseries.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Intro to time series</h4>
<p>Learn about time series data.</p>
</a>
@@ -164,7 +164,7 @@ libXcomposite libXdamage libXtst cups libXScrnSaver pango atk adwaita-cursor-the
### Certificate signed by internal certificate authorities
In many cases Grafana, runs on internal servers and uses certificates that have not been signed by a CA ([Certificate Authority](https://en.wikipedia.org/wiki/Certificate_authority)) known to Chrome, and therefore cannot be validated. Chrome internally uses NSS ([Network Security Services](https://en.wikipedia.org/wiki/Network_Security_Services)) for cryptogtraphic operations such as the validation of certificates.
In many cases, Grafana runs on internal servers and uses certificates that have not been signed by a CA ([Certificate Authority](https://en.wikipedia.org/wiki/Certificate_authority)) known to Chrome, and therefore cannot be validated. Chrome internally uses NSS ([Network Security Services](https://en.wikipedia.org/wiki/Network_Security_Services)) for cryptogtraphic operations such as the validation of certificates.
If you are using the Grafana Image Renderer with a Grafana server that uses a certificate signed by such a custom CA (for example a company-internal CA), rendering images will fail and you will see messages like this in the Grafana log:
+1 -1
View File
@@ -11,7 +11,7 @@ weight = 1
# Alerting Engine and Rules Guide
Alerting in Grafana allows you to attach rules to your dashboard panels. When you save the dashboard
Alerting in Grafana allows you to attach rules to your dashboard panels. When you save the dashboard,
Grafana will extract the alert rules into a separate alert rule storage and schedule them for evaluation.
{{< imgbox max-width="40%" img="/img/docs/v4/drag_handles_gif.gif" caption="Alerting overview" >}}
+7
View File
@@ -0,0 +1,7 @@
+++
title = "Dashboards"
type = "docs"
[menu.docs]
identifier = "dashboards"
weight = 4
+++
+5 -1
View File
@@ -1,7 +1,7 @@
+++
title = "Grafana Enterprise"
description = "Grafana Enterprise overview"
keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise"]
keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise", "insights", "reporting"]
type = "docs"
[menu.docs]
name = "Grafana Enterprise"
@@ -58,6 +58,10 @@ Supported auth providers:
[White labeling]({{< relref "white-labeling.md" >}}) allows you to replace the Grafana brand and logo with your own corporate brand and logo. You can also change footer links to point to your custom resources.
## Usage insights
[Usage insights]({{< relref "usage-insights.md" >}}) allow you to understand how your Grafana instance is used. You can see who is looking at a dashboard, how often a dashboard is seen, and which dashboards are prone to errors. You'll also be able to discover what are the least and the most used dashboards.
## Enterprise plugins
With a Grafana Enterprise license, you get access to premium plugins, including:
+56 -2
View File
@@ -39,10 +39,45 @@ Currently only Organization Admins can create reports.
1. Select the layout option for generated report: **Portrait** or **Landscape**.
1. Enter scheduling information. Options vary depending on the frequency you select.
1. **Save** the report.
1. **Send test mail** after saving the report to verify that the whole configuration is working as expected.
{{< docs-imagebox img="/img/docs/enterprise/reports_create_new.png" max-width="500px" class="docs-image--no-shadow" >}}
## Send test mail
### Scheduling
Scheduled reports can be sent on a weekly, daily, or hourly basis. You may also disable scheduling for when you either want to pause a report or send it via the API.
All scheduling indicates when the reporting service will start rendering the dashboard. It can take a few minutes to render a dashboard with a lot of panels.
#### Hourly
Hourly reports are generated once per hour. All fields are required.
* **At minute -** The number of minutes after full hour when the report should be generated.
* **Time zone -** Time zone to determine the offset of the full hour. Does not currently change the time in the rendered report.
#### Daily
Daily reports are generated once per day. All fields are required.
* **Time -** Time of day in 24 hours format when the report should be sent.
* **Time zone -** Time zone for the **Time** field.
#### Weekly
Weekly reports are generated once per week. All fields are required.
* **Day -** Weekday which the report should be sent on.
* **Time -** Time of day in 24 hours format when the report should be sent.
* **Time zone -** Time zone for the **Time** field.
#### Never
> Only available in Grafana Enterprise v7.0+.
Reports which are scheduled to never be sent have no parameter and will not be sent to the scheduler. They may be manually generated from the **Send test email** prompt or via the API.
### Send test mail
> Only available in Grafana Enterprise v7.0+.
@@ -55,9 +90,17 @@ The last saved version of the report will be sent to selected emails. You can us
{{< docs-imagebox img="/img/docs/enterprise/reports_send_test_mail.png" max-width="500px" class="docs-image--no-shadow" >}}
## Send report via the API
You can send reports programmatically with the [send report]({{< relref "../http_api/reporting.md#send-report" >}}) endpoint in the [HTTP APIs]({{< relref "../http_api" >}}).
## Rendering configuration
When Grafana generates a report, it will render each panel separately and then put them together in a PDF file. You can configure the per-panel rendering request timeout and the maximum number of concurrent calls to the rendering service. These options are available in the [configuration]({{< relref "../installation/configuration.md">}}) file.
When generating reports, each panel renders separately before being collected in a PDF. The per panel rendering timeout and number of concurrently rendered panels can be configured.
To modify the panels' clarity you can set a scale factor for the rendered images. A higher scale factor is more legible but will increase the file size of the generated PDF.
These options are available in the [configuration]({{< relref "../installation/configuration.md">}}) file.
```ini
[reporting]
@@ -65,8 +108,19 @@ When Grafana generates a report, it will render each panel separately and then p
rendering_timeout = 10s
# Set maximum number of concurrent calls to the rendering service
concurrent_render_limit = 4
# Set the scale factor for rendering images. 2 is enough for monitor resolutions
# 4 would be better for printed material. Setting a higher value affects performance and memory
image_scale_factor = 2
```
## Report time range
Reports use the saved time range of the dashboard. Changing the time range of the report is done by saving a modified time range to the dashboard.
The page header of the report displays the time range for the dashboard's data queries. Dashboards set to use the browser's time zone will use the time zone on the Grafana server.
If the time zone is set differently between your Grafana server and its remote image renderer, then the time ranges in the report might be different between the page header and the time axes in the panels. We advise always setting the time zone to UTC for dashboards when using a remote renderer to avoid this.
## Troubleshoot reporting
To troubleshoot and get more log information, enable debug logging in the configuration file. Refer to [Configuration]({{< relref "../installation/configuration.md#filters" >}}) for more information.
+57
View File
@@ -0,0 +1,57 @@
+++
title = "Usage-insights"
description = "Usage-insights"
keywords = ["grafana", "usage-insights", "enterprise"]
aliases = ["/docs/grafana/latest/enterprise/usage-insights/"]
type = "docs"
[menu.docs]
name = "Usage-insights"
parent = "enterprise"
weight = 700
+++
# Usage insights
Usage insights allow you to have a better understanding of how your Grafana instance is used. The collected data are the number of:
- Dashboard views (aggregated and per user)
- Data source errors
- Data source queries
> Only available in Grafana Enterprise v7.0+.
## Presence indicator
The presence indicator is visible to all signed-in users on all dashboards. It shows the avatars of users who interacted with the dashboard recently (last 10 minutes by default). You can see the user's name by hovering your cursor over the user's avatar. The avatars come from [Gravatar](https://gravatar.com) based on the user's email.
When more users are active on a dashboard than can fit in the presence indicator section, click on the `+X` icon that opens [dashboard insights]({{< relref "#dashboard-insights" >}}) to see more details about recent user activity.
{{< docs-imagebox img="/img/docs/enterprise/presence_indicators.png" max-width="400px" class="docs-image--no-shadow" >}}
You can choose your own definition of "recent" by setting it in the [configuration]({{< relref "../installation/configuration.md">}}) file.
```ini
[analytics.views]
# Set age for recent active users
recent_users_age = 10m
```
## Dashboard insights
You can see dashboard usage information by clicking on the `Dashboard insights` button in the top bar.
{{< docs-imagebox img="/img/docs/enterprise/dashboard_insights_button.png" max-width="400px" class="docs-image--no-shadow" >}}
It shows two kinds of information:
- **Stats:** Shows the daily query count and error count for the last 30 days.
- **Users & activity:** Shows the daily view count for the last 30 days; last activities on the dashboard and recent users (with a limit of 20).
{{< docs-imagebox img="/img/docs/enterprise/dashboard_insights_stats.png" max-width="400px" class="docs-image--no-shadow" >}}{{< docs-imagebox img="/img/docs/enterprise/dashboard_insights_users.png" max-width="400px" class="docs-image--no-shadow" >}}
## Improved dashboard search
In the search view, you can sort dashboards using these insights data. It helps you find unused or broken dashboards or discover most viewed ones.
{{< docs-imagebox img="/img/docs/enterprise/improved_search.png" max-width="650px" class="docs-image--no-shadow" >}}
+3 -2
View File
@@ -189,14 +189,15 @@ datasources:
jsonData:
maxLines: 1000
derivedFields:
# Field with internal link pointing to datasource in Grafana
# Field with internal link pointing to data source in Grafana.
# Right now, Grafana supports only Jaeger and Zipkin data sources as link targets.
- datasourceUid: my_jaeger_uid
matcherRegex: "traceID=(\\w+)"
name: TraceID
# url will be interpreted as query for the datasource
url: "$${__value.raw}"
# Field with external link
# Field with external link.
- matcherRegex: "traceID=(\\w+)"
name: TraceID
url: "http://localhost:16686/trace/$${__value.raw}"
@@ -32,7 +32,7 @@ To access Prometheus settings, click the **Configuration** (gear) icon, then cli
## Prometheus query editor
Open a graph in edit mode by click the title > Edit (or by pressing `e` key while hovering over panel).
Open a graph in edit mode by clicking the title > Edit (or by pressing `e` key while hovering over panel).
{{< docs-imagebox img="/img/docs/v45/prometheus_query_editor_still.png"
animated-gif="/img/docs/v45/prometheus_query_editor.gif" >}}
@@ -227,7 +227,7 @@ SLO queries use the same [alignment period functionality as metric queries]({{<
## Templating
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
Instead of hard-coding things like server, application and sensor name in your metric queries you can use variables in their place.
Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns make it easy to change the data
being displayed in your dashboard.
@@ -19,7 +19,7 @@ This makes it much easier to verify functionality since the data can be shared v
The `TestData DB` data source is not enabled by default. To enable it:
1. In the **Configuration** menu (small gear on the left side of the screen, click **Data Sources**.
1. In the **Configuration** menu (small gear on the left side of the screen), click **Data Sources**.
2. Click **Add Data Source**.
3. Search and click `TestData DB`.
4. Click **Save & Test** to enable it.
+1 -3
View File
@@ -51,8 +51,6 @@ You can close the newly created query by clicking on the Close Split button.
## Query history
> BETA: Query history is a beta feature.
Query history is a list of queries that you have used in Explore. The history is local to your browser and is not shared with others. To open and interact with your history, click the **Query history** button in Explore.
### View query history
@@ -124,7 +122,7 @@ On the left-hand side of the query field is a `Metrics` button, clicking on this
The Query field supports autocomplete for metric names, function and works mostly the same way as the standard Prometheus query editor. Press the enter key to execute a query.
The autocomplete menu can be trigger by pressing Ctrl+Space. The Autocomplete menu contains a new History section with a list of recently executed queries.
The autocomplete menu can be triggered by pressing Ctrl+Space. The Autocomplete menu contains a new History section with a list of recently executed queries.
Suggestions can appear under the query field - click on them to update your query with the suggested change.
+9
View File
@@ -0,0 +1,9 @@
+++
title = "Getting started"
type = "docs"
[menu.docs]
name = "Getting started"
identifier = "getting-started"
weight = 100
+++
@@ -3,12 +3,12 @@ title = "Getting started"
description = "Guide for getting started with Grafana"
keywords = ["grafana", "intro", "guide", "started"]
type = "docs"
aliases = ["/docs/grafana/latest/guides/gettingstarted"]
aliases = ["/docs/grafana/latest/guides/gettingstarted","/docs/grafana/latest/guides/getting_started"]
[menu.docs]
name = "Getting started"
identifier = "getting_started_guide"
parent = "guides"
weight = 100
weight = 200
+++
# Getting started
@@ -3,16 +3,17 @@ title = "Glossary"
description = "Grafana glossary"
keywords = ["grafana", "intro", "glossary", "dictionary"]
type = "docs"
aliases = ["/docs/grafana/latest/guides/glossary"]
[menu.docs]
name = "Glossary"
identifier = "glossary"
parent = "guides"
weight = 400
weight = 500
+++
# Glossary
This topic lists words and abbreviations that commonly used in the Grafana documentation and community.
This topic lists words and abbreviations that are commonly used in the Grafana documentation and community.
<table>
<tr>
@@ -0,0 +1,58 @@
+++
title = "Histograms and heatmaps"
description = "An introduction to histograms and heatmaps"
keywords = ["grafana", "heatmap", "panel", "documentation", "histogram"]
type = "docs"
[menu.docs]
name = "intro-to-histograms"
parent = "panels"
weight = 400
+++
# Introduction to histograms and heatmaps
A histogram is a graphical representation of the distribution of numerical data. It groups values into buckets
(sometimes also called bins) and then counts how many values fall into each bucket.
Instead of graphing the actual values, histograms graph the buckets. Each bar represents a bucket,
and the bar height represents the frequency (such as count) of values that fell into that bucket's interval.
## Histogram example
This histogram shows the value distribution of a couple of time series. You can easily see that
most values land between 240-300 with a peak between 260-280.
![](/img/docs/v43/heatmap_histogram.png)
Histograms only look at _value distributions_ over a specific time range. The problem with histograms is you cannot see any trend or changes in the distribution over time.
This is where heatmaps become useful.
## Heatmaps
A _heatmap_ is like a histogram, but over time where each time slice represents its own histogram. Instead of using bar height as a representation of frequency, it uses cells and colors the cell proportional to the number of values in the bucket.
In this example, you can clearly see what values are more common and how they trend over time.
![](/img/docs/v43/heatmap_histogram_over_time.png)
## Pre-bucketed data
There are a number of data sources supporting histogram over time like Elasticsearch (by using a Histogram bucket
aggregation) or Prometheus (with [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) metric type
and *Format as* option set to Heatmap). But generally, any data source could be used if it meets the requirements:
returns series with names representing bucket bound or returns series sorted by the bound in ascending order.
## Raw data vs aggregated
If you use the heatmap with regular time series data (not pre-bucketed), then it's important to keep in mind that your data
is often already aggregated by your time series backend. Most time series queries do not return raw sample data
but include a group by time interval or maxDataPoints limit coupled with an aggregation function (usually average).
This all depends on the time range of your query of course. But the important point is to know that the histogram bucketing
that Grafana performs might be done on already aggregated and averaged data. To get more accurate heatmaps it is better
to do the bucketing during metric collection or store the data in Elasticsearch, or in the other data source which
supports doing histogram bucketing on the raw data.
If you remove or lower the group by time (or raise maxDataPoints) in your query to return more data points your heatmap will be
more accurate but this can also be very CPU and memory taxing for your browser and could cause hangs and crashes if the number of
data points becomes unreasonably large.
@@ -4,6 +4,7 @@ description = "Introduction to time series"
keywords = ["grafana", "intro", "guide", "concepts", "timeseries"]
type = "docs"
[menu.docs]
aliases = ["/docs/grafana/latest/guides/timeseries"]
name = "Time series"
identifier = "time_series"
parent = "guides"
@@ -4,6 +4,7 @@ description = "Overview of Grafana and features"
keywords = ["grafana", "intro", "guide", "started"]
type = "docs"
[menu.docs]
aliases = ["/docs/grafana/latest/guides/what-is-grafana"]
name = "What is Grafana?"
identifier = "what_is_grafana"
parent = "guides"
@@ -12,11 +13,11 @@ weight = 200
# What is Grafana?
This topic provides a high-level look at Grafana, the Grafana process, and Grafana features. It's a good place to start if you want to learn more about Grafana software. To jump right in, refer to [Getting started]({{< relref "getting_started.md" >}}).
This topic provides a high-level look at Grafana, the Grafana process, and Grafana features. It's a good place to start if you want to learn more about Grafana software. To jump right in, refer to [Getting started]({{< relref "getting-started.md" >}}).
Grafana is open source visualization and analytics software. It allows you to query, visualize, alert on, and explore your metrics no matter where they are stored. In plain English, it provides you with tools to turn your time-series database (TSDB) data into beautiful graphs and visualizations.
After creating a dashboard like you do in [Getting started]({{< relref "getting_started.md" >}}), there are many possible things you might do next. It all depends on your needs and your use case.
After creating a dashboard like you do in [Getting started]({{< relref "getting-started.md" >}}), there are many possible things you might do next. It all depends on your needs and your use case.
For example, if you want to view weather data and statistics about your smart home, then you might create a playlist. If you are the administrator for a corporation and are managing Grafana for multiple teams, then you might need to set up provisioning and authentication.
+4 -2
View File
@@ -76,11 +76,13 @@ Up until now the overrides were available only for Graph and Table panel(via Col
This feature enables even more powerful visualizations and fine grained control over how the data is displayed.
## Panel inspect and export to CSV
## Inspect panels and export data to CSV
{{< docs-imagebox img="/img/docs/v70/panel_edit_export_raw_data.png" max-width="800px" class="docs-image--right" caption="Panel Edit - Export raw data to CSV" >}}
Another new feature of Grafana 7.0 is Panel inspect. Inspect allows you to view the raw data for any Grafana panel as well as export that data to a CSV file. With Panel inspect you will also be able to perform simple raw data transformations like join, view query stats or detailed execution data.
Another new feature of Grafana 7.0 is the panel inspector. Inspect allows you to view the raw data for any Grafana panel as well as export that data to a CSV file. With Panel inspect you will also be able to perform simple raw data transformations like join, view query stats or detailed execution data.
Learn more about this feature in [Inspect a panel]({{< relref "../panels/inspect-panel.md" >}})
<div class="clearfix"></div>
+1
View File
@@ -43,3 +43,4 @@ dashboards, creating users and updating data sources.
* [Data Source Permissions API]({{< relref "datasource_permissions.md" >}})
* [External Group Sync API]({{< relref "external_group_sync.md" >}})
* [Reporting API]({{< relref "reporting.md" >}})
+70
View File
@@ -0,0 +1,70 @@
+++
title = "Reporting API"
description = "Grafana Enterprise APIs"
keywords = ["grafana", "enterprise", "api", "reporting"]
aliases = ["/docs/grafana/latest/http_api/reporting/"]
type = "docs"
[menu.docs]
name = "Reporting API"
parent = "http_api"
+++
# Reporting API
This API allows you to interact programmatically with the [Reporting]({{< relref "../enterprise/reporting.md" >}}) feature.
> Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../enterprise" >}}).
## Send a report
> Only available in Grafana Enterprise v7.0+.
> This API endpoint is experimental and may be deprecated in a future release. On deprecation, a migration strategy will be provided and the endpoint will remain functional until the next major release of Grafana.
`POST /api/reports/email`
Generate and send a report. This API waits for the report to be generated before returning. We recommend that you set the client's timeout to at least 60 seconds.
### Example request
```http
POST /api/reports/email HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"id":"3",
"useEmailsFromReport": true
}
```
### JSON Body Schema
Field name | Data type | Description
---------- | ---- | -----------
id | string | ID of the report to send. It is the same as in the URL when editing a report, not to be confused with the ID of the dashboard. Required.
emails | string | Comma-separated list of emails to which to send the report to. Overrides the emails from the report. Required if **useEmailsFromReport** is not present.
useEmailsFromReport | boolean | Send the report to the emails specified in the report. Required if **emails** is not present.
### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 29
{"message":"Report was sent"}
```
### Status Codes
Code | Description
---- | -----------
200 | Report was sent.
400 | Bad request (invalid json, missing content-type, missing or invalid fields, etc.).
401 | Authentication failed, refer to [Authentication API]({{< relref "../http_api/auth.md" >}}).
403 | User is authenticated but is not authorized to generate the report.
404 | Report not found.
500 | Unexpected error or server misconfiguration. Refer to body and/or server logs for more details.
+1 -1
View File
@@ -7,7 +7,7 @@ aliases = ["/docs/grafana/latest/installation/installation/", "/docs/grafana/v2.
[menu.docs]
name = "Installation"
identifier = "installation"
weight = 1
weight = 200
+++
## Install Grafana
@@ -840,6 +840,10 @@ is false. This settings was introduced in Grafana v6.0.
Set to true if you want to test alpha plugins that are not yet ready for general usage.
### allow_loading_unsigned_plugins
Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature.
## [feature_toggles]
### enable
+1 -1
View File
@@ -164,7 +164,7 @@ Start Grafana by running:
## Next steps
Refer to the [Getting Started]({{< relref "../guides/getting_started/" >}}) guide for information about logging in, setting up data sources, and so on.
Refer to the [Getting Started]({{< relref "../getting-started/getting-started/" >}}) guide for information about logging in, setting up data sources, and so on.
## Configure Grafana
+1 -1
View File
@@ -217,7 +217,7 @@ chown -R grafana:grafana /usr/share/grafana
## Next steps
Refer to the [Getting Started]({{< relref "../guides/getting_started/" >}}) guide for information about logging in, setting up data sources, and so on.
Refer to the [Getting Started]({{< relref "../getting-started/getting-started/" >}}) guide for information about logging in, setting up data sources, and so on.
## Configure Docker image
+2 -2
View File
@@ -135,7 +135,7 @@ sudo rpm -i --nodeps <local rpm package>
### Install from binary .tar.gz file
Download the latest [`.tar.gz` file](https://grafana.com/grafana/download?platform=linux) and extract it. The files extract into a folder named after the Grafana version that you downloaded. This folder contains all files required to run Grafana. There are no init scripts or install scripts in this package.
Download the latest [`.tar.gz` file](https://grafana.com/grafana/download?platform=linux) and extract it. The files are extracted into a folder named after the Grafana version that you downloaded. This folder contains all files required to run Grafana. There are no init scripts or install scripts in this package.
```bash
wget <tar.gz package url>
@@ -202,7 +202,7 @@ Start Grafana by running:
## Next steps
Refer to the [Getting Started]({{< relref "../guides/getting_started/" >}}) guide for information about logging in, setting up data sources, and so on.
Refer to the [Getting Started]({{< relref "../getting-started/getting-started/" >}}) guide for information about logging in, setting up data sources, and so on.
## Configure Grafana
+4 -4
View File
@@ -31,19 +31,19 @@ You can either use the Windows installer or you can install a standalone Windows
1. Click **Download the installer**.
1. Open and run the installer.
To run Grafana, open your browser and go to the Grafana port (http://localhost:3000/ is default) and then follow the instructions in [Getting Started]({{< relref "../guides/getting_started/" >}}).
To run Grafana, open your browser and go to the Grafana port (http://localhost:3000/ is default) and then follow the instructions in [Getting Started]({{< relref "../getting-started/getting-started/" >}}).
## Install standalone Windows binary
1. Click **Download the zip file**.
**Important:** After you've downloaded the zip file and before extracting it, make sure to open the properties for that file (right-click **Properties**) and select the `unblock` check box and then click `Ok`.
2. Extract this folder to anywhere you want Grafana to run from.
1. Extract this folder to anywhere you want Grafana to run from.
3. Start Grafana by executing `grafana-server.exe`, located in the `bin` directory, preferably from the command line. If you want to run Grafana as a Windows service, then download
1. Start Grafana by executing `grafana-server.exe`, located in the `bin` directory, preferably from the command line. If you want to run Grafana as a Windows service, then download
[NSSM](https://nssm.cc/). It is very easy to add Grafana as a Windows service using that tool.
To run Grafana, open your browser and go to the Grafana port (http://localhost:3000/ is default) and then follow the instructions in [Getting Started]({{< relref "../guides/getting_started/" >}}).
To run Grafana, open your browser and go to the Grafana port (http://localhost:3000/ is default) and then follow the instructions in [Getting Started]({{< relref "../getting-started/getting-started/" >}}).
> **Note:** The default Grafana port is `3000`. This port might require extra permissions on Windows. If it does not appear in the default port, you can try changing to a different port.
>
+15 -7
View File
@@ -1,14 +1,16 @@
- name: Getting started
link: /guides/
link: /getting-started/
children:
- name: What is Grafana?
link: /guides/what-is-grafana/
link: /getting-started/what-is-grafana/
- name: Getting started
link: /guides/getting_started/
link: /getting-started/getting-started/
- name: Intro to time series
link: /guides/timeseries/
link: /getting-started/timeseries/
- name: Intro to histograms
link: /getting-started/intro-histograms/
- name: Glossary
link: /guides/glossary/
link: /getting-started/glossary/
- name: Installation
link: /installation/
children:
@@ -265,16 +267,18 @@
link: /enterprise/enhanced_ldap/
- name: Reporting
link: /enterprise/reporting/
- name: Export dashboard as PDF
link: /enterprise/export-pdf/
- name: SAML authentication
link: /enterprise/saml/
- name: Team sync
link: /enterprise/team-sync/
- name: White labeling
link: /enterprise/white-labeling/
- name: Usage insights
link: /enterprise/usage-insights/
- name: License expiration
link: /enterprise/license-expiration/
- name: Export dashboard as PDF
link: /enterprise/export-pdf/
- name: Plugins
link: /plugins/
children:
@@ -285,6 +289,8 @@
children:
- link: /plugins/developing/development/
name: Developer Guide
- link: /plugins/developing/dataframe/
name: Introduction to data frames
- link: /plugins/developing/code-styleguide/
name: Plugin Code Styleguide
- link: /plugins/developing/plugin-review-guidelines/
@@ -346,6 +352,8 @@
link: /http_api/playlist/
- name: Preferences API
link: /http_api/preferences/
- name: Reporting API
link: /http_api/reporting/
- name: Snapshot API
link: /http_api/snapshot/
- name: Teams API
+7
View File
@@ -0,0 +1,7 @@
+++
title = "Panels"
type = "docs"
[menu.docs]
identifier = "panels"
weight = 4
+++
+87
View File
@@ -0,0 +1,87 @@
+++
title = "Inspect a panel"
type = "docs"
[menu.docs]
identifier = "inspect-a-panel"
parent = "panels"
weight = 400
+++
# Inspect a panel
> **Note:** This documentation refers to a feature only available in Grafana 7.0 beta.
The panel inspector helps you understand and troubleshoot your panels. You can inspect the raw data for any Grafana panel, export that data to a comma-separated values (CSV) file, view query requests, and export panel and data JSON.
## Panel inspector UI
The panel inspector displays Inspect: <NameOfPanelBeingInspected> at the top of the pane. Click the arrow in the upper right corner to expand or reduce the pane.
The panel inspector consists of four tabs:
* **Data tab -** Shows the raw data returned by the query with transformations applied. Field options such as overrides and value mappings are not applied.
* **Stats tab -** Shows how long your query takes and how much it returns.
* **JSON tab -** Allows you to view and copy the panel JSON, panel data JSON, and data frame structure JSON. This is useful if you are provisioning or administering Grafana.
* **Query tab -** Shows you the requests to the server sent when Grafana queries the data source.
> **Note:** Not all panel types include all four tabs. For example, dashboard list panels do not have raw data to inspect, so they do not display the Stats, Data, or Query tabs.
## Panel inspector tasks
Tasks you can perform in the panel inspector are described below.
### Open the panel inspector
You can inspect any panel that you can view.
1. In Grafana, navigate to the dashboard that contains the panel you want to inspect.
1. Click the title of the panel you want to inspect and then click **Inspect**.
Or
Hover your cursor over the panel title and then press **i**.
The panel inspector pane opens on the right side of the screen.
### Inspect raw query results
View raw query results in a table. This is the data returned by the query with transformations applied and before the panel applies field configurations or overrides.
1. Open the panel inspector and then click the **Data** tab or in the panel menu click **Inspect > Data**.
2. If your panel contains multiple queries or queries multiple nodes, then you have additional options.
* **Select result -** Choose which result set data you want to view.
* **Transform data**
* **Join by time -** View raw data from all your queries at once, one result set per column. Click a column heading to reorder the data.
### Download raw query results as CSV
Grafana generates a CSV file in your default browser download location. You can open it in the viewer of your choice.
1. Open the panel inspector.
1. Inspect the raw query results as described above. Adjust settings until you see the raw data that you want to export.
1. Click **Download CSV**.
### Inspect query performance
The Stats tab displays statistics that tell you how long your query takes, how many queries you send, and the number of rows returned. This information can help you troubleshoot your queries, especially if any of the numbers is unexpectedly high or low.
1. Open the panel inspector.
1. Navigate to the Stats tab.
Statistics are displayed in read-only format.
### View panel JSON model
Explore and export panel, panel data, and data frame JSON models.
1. Open the panel inspector and then click the **JSON** tab or in the panel menu click **Inspect > Panel JSON**.
1. In Select source, choose one of the following options:
* **Panel JSON -** Displays a JSON object representing the panel.
* **Panel data -** Displays a JSON object representing the data that was passed to the panel.
* **DataFrame structure -** Displays the raw result set with transformations, field configuration, and overrides configuration applied.
1. You can expand or collapse portions of the JSON to explore it, or you can click **Copy to clipboard** and paste the JSON in another application.
### View raw request and response to data source
1. Open the panel inspector and then click the **Query** tab or in the panel menu click **Inspect > Query**.
1. Click **Refresh**.
Grafana sends a query to the server to collect information and then displays the result. You can now drill down on specific portions of the query, expand or collapse all of it, or copy the data to the clipboard to use in other applications.
+75
View File
@@ -0,0 +1,75 @@
title = "Thresholds"
type = "docs"
[menu.docs]
identifier = "thresholds"
parent = "panels"
weight = 300
+++
# Thresholds
Thresholds set the color of either the value text or the background depending on conditions that you define.
You can define thresholds one of two ways:
* **Absolute** thresholds are defined based on a number. For example, 80 on a scale of 1 to 150.
* **Percentage** thresholds are defined relative to minimum and maximums. For example, 80 percent.
You can apply thresholds to the following visualizations:
* Stat
* Gauge
* Bar gauge
* Table
* Graph
## Default thresholds
On visualizations that support it, Grafana sets default threshold values of:
* 80 = red
* Base = green
* Mode = Absolute
The **Base** value represents minus infinity. It is generally the “good” color.
## Add a threshold
You can add as many thresholds to a panel as you want. Grafana automatically sorts thresholds from highest value to lowest.
> **Note:** These instructions apply only to the Stat, Gauge, Bar gauge, and Table visualizations.
1. Navigate to the panel you want to add a threshold to.
1. Click the **Field** tab.
1. Click **Add threshold**.
1. Grafana adds a threshold with suggested numerical and color values.
1. Accept the recommendations or edit the new threshold.
* **Edit color:** Click the color dot you wish to change and then select a new color.
* **Edit number:** Click the number you wish to change and then enter a new number.
* **Thresholds mode -** Click the mode to change it for all thresholds on this panel.
1. Click **Save** to save the changes in the dashboard.
## Add a threshold to a Graph panel
In the Graph panel visualization, thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when the graph crosses a particular threshold.
1. Navigate to the graph panel you want to add a threshold to.
1. On the Panel tab, click **Thresholds**.
1. Click **Add threshold**.
1. Fill in as many fields as you want. Only the **T1** fields are required.
* **T1 -** Both values are required to display a threshold.
* **lt** or **gt** - Select **lt** for less than or **gt** for greater than to indicate what the threshold applies to.
* **Value -** Enter a threshold value. Grafana draws a threshold line along the Y-axis at that value.
* **Color -** Choose a condition that corresponds to a color, or define your own color.
* **custom -** You define the fill color and line color.
* **critical -** Fill and line color are red.
* **warning -** Fill and line color are yellow.
* **ok -** Fill and line color are green.
* **Fill -** Controls whether the threshold fill is displayed.
* **Line -** Controls whether the threshold line is displayed.
* **Y-Axis -** Choose **left** or **right**.
1. Click **Save** to save the changes in the dashboard.
## Delete a threshold
1. Navigate to the panel you want to add a threshold to.
1. Click the **Field** tab. (Or **Panel** tab for a graph panel.)
1. Click the trash can icon next to the threshold you want to remove.
1. Click **Save** to save the changes in the dashboard.
+17 -4
View File
@@ -5,14 +5,15 @@ type = "docs"
name = "Data frames"
parent = "developing"
weight = 9
draft = true
+++
## Introduction to data frames
Grafana supports a variety of different data sources, each with its own data model. To make this possible, Grafana consolidates the query results from each of these data sources into one, unified data structure called _data frames_.
Grafana supports a variety of different data sources, each with its own data model. To make this possible, Grafana consolidates the query results from each of these data sources into one unified data structure called a _data frame_.
Data frames were introduced in Grafana 7.0 to replace the Time series and Table structures with a more generic data structure that can support a wider range of data types.
The data frame structure is a concept that's borrowed from data analysis tools like the [R programming language](https://www.r-project.org), and [Pandas](https://pandas.pydata.org/).
> Data frames are available in Grafana 7.0+, and replaced the Time series and Table structures with a more generic data structure that can support a wider range of data types.
This document gives an overview of the data frame structure, and of how data is handled within Grafana.
@@ -60,6 +61,18 @@ Each field has three values, and each value in a field must share the same type.
One restriction on data frames is that all fields in the frame must be of the same length to be a valid data frame.
#### Field configuration
Each field in a data frame contains optional information about the values in the field, such as units, scaling, and so on.
By adding field configurations to a data frame, Grafana can configure visualizations automatically. For example, you could configure Grafana to automatically set the unit provided by the data source.
### Transformations
Along with the type information, field configs enables _data transformations_ within Grafana.
A data transformation is any function that accepts a data frame as input, and returns another data frame as output. By using data frames in your plugin, you get a range of transformations for free.
### Data frames as time series
A data frame with at least one time field is considered a _time series_.
@@ -113,7 +126,7 @@ Dimensions: 2 fields by 2 rows
The wide format can typically be used when multiple time series are collected by the same process. In this case, every measurement is made at the same interval and will therefore share the same time values.
### Long format
#### Long format
Some data sources return data in a _long_ format (also called _narrow_ format). This is common format returned by, for example, SQL databases.
@@ -26,7 +26,7 @@ An alternative syntax (that might be deprecated in the future) is `[[var_name:op
## CSV
Formats multi-value variable as a comma-separated string.
Formats variables with multiple values as a comma-separated string.
```bash
servers = ['test1', 'test2']
@@ -36,7 +36,7 @@ Interpolation result: 'test1,test2'
## Distributed - OpenTSDB
Formats multi-value variable in custom format for OpenTSDB.
Formats variables with multiple values in custom format for OpenTSDB.
```bash
servers = ['test1', 'test2']
@@ -56,7 +56,7 @@ Interpolation result: '"test1","test2"'
## Glob - Graphite
Formats multi-value variable into a glob (for Graphite queries).
Formats variables with multiple values into a glob (for Graphite queries).
```bash
servers = ['test1', 'test2']
@@ -66,7 +66,7 @@ Interpolation result: '{test1,test2}'
## JSON
Formats multi-value variable as a comma-separated string.
Formats variables with multiple values as a comma-separated string.
```bash
servers = ['test1', 'test2']
@@ -76,7 +76,7 @@ Interpolation result: '["test1", "test2"]'
## Lucene - Elasticsearch
Formats multi-value variable in Lucene format for Elasticsearch.
Formats variables with multiple values in Lucene format for Elasticsearch.
```bash
servers = ['test1', 'test2']
@@ -96,7 +96,7 @@ Interpolation result: 'foo%28%29bar%20BAZ%2Ctest2'
## Pipe
Formats multi-value variable into a pipe-separated string.
Formats variables with multiple values into a pipe-separated string.
```bash
servers = ['test1.', 'test2']
@@ -104,9 +104,19 @@ String to interpolate: '${servers:pipe}'
Interpolation result: 'test1.|test2'
```
## Raw
Turns off data source-specific formatting, such as single quotes in an SQL query.
```bash
servers = ['test1.', 'test2']
String to interpolate: '${var_name:raw}'
Interpolation result: '{test.1,test2}'
```
## Regex
Formats multi-value variable into a regex string.
Formats variables with multiple values into a regex string.
```bash
servers = ['test1.', 'test2']
@@ -48,7 +48,7 @@ Panel titles and metric queries can refer to variables using two different synta
Example: apps.frontend.[[server]].requests.count
Before queries are sent to your data source the query is _interpolated_, meaning the variable is replaced with its current value. During
interpolation the variable value might be _escaped_ in order to conform to the syntax of the query language and where it is used.
interpolation, the variable value might be _escaped_ in order to conform to the syntax of the query language and where it is used.
For example, a variable used in a regex expression in an InfluxDB or Prometheus query will be regex escaped. Read the data source specific
documentation topic for details on value escaping during interpolation.
@@ -66,7 +66,7 @@ Variables are listed in dropdown lists across the top of the screen. Select diff
To see variable settings, navigate to **Dashboard Settings > Variables**. Click a variable in the list to see its settings.
Templates are in the query portion of panels. Queries with text that starts with `$` are templates. Not all panels will have template queries.
Variables can be used in titles, descriptions, text panels, and queries. Queries with text that starts with `$` are templates. Not all panels will have template queries.
## Variable types
+422 -325
View File
@@ -1,74 +1,171 @@
import { e2e } from '@grafana/e2e';
// This test should really be broken into several smaller tests
e2e.scenario({
describeName: 'Variables',
itName: 'Query Variables CRUD',
addScenarioDataSource: true,
addScenarioDashBoard: true,
skipScenario: false,
scenario: () => {
e2e.getScenarioContext().then(({ lastAddedDashboardUid }: any) => {
// skipped scenario helper because of some perf issue upgrading cypress to 4.5.0 and splitted the whole test into smaller
// several it functions. Very important to keep the order of these it functions because they have dependency in the order
// https://github.com/cypress-io/cypress/issues/5987
// https://github.com/cypress-io/cypress/issues/6023#issuecomment-574031655
describe('Variables', () => {
let lastUid = '';
let lastData = '';
let variables: VariablesData[] = [
{ name: 'query1', query: '*', label: 'query1-label', options: ['All', 'A', 'B', 'C'], selectedOption: 'A' },
{
name: 'query2',
query: '$query1.*',
label: 'query2-label',
options: ['All', 'AA', 'AB', 'AC'],
selectedOption: 'AA',
},
{
name: 'query3',
query: '$query1.$query2.*',
label: 'query3-label',
options: ['All', 'AAA', 'AAB', 'AAC'],
selectedOption: 'AAA',
},
];
beforeEach(() => {
e2e.flows.login('admin', 'admin');
if (!lastUid || !lastData) {
e2e.flows.addDataSource();
e2e.flows.addDashboard();
} else {
e2e.setScenarioContext({ lastAddedDataSource: lastData, lastAddedDashboardUid: lastUid });
}
e2e.getScenarioContext().then(({ lastAddedDashboardUid, lastAddedDataSource }: any) => {
e2e.flows.openDashboard(lastAddedDashboardUid);
lastUid = lastAddedDashboardUid;
lastData = lastAddedDataSource;
});
});
it(`asserts defaults`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
assertDefaultsForNewVariable();
});
e2e.pages.Dashboard.Settings.General.sectionItems('General').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
variables.forEach((variable, index) => {
it(`creates variable ${variable.name}`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
let queryVariables: QueryVariableData[] = [
{
name: 'query1',
query: '*',
label: 'query1-label',
options: ['All', 'A', 'B', 'C'],
selectedOption: 'A',
},
{
name: 'query2',
query: '$query1.*',
label: 'query2-label',
options: ['All', 'AA', 'AB', 'AC'],
selectedOption: 'AA',
},
{
name: 'query3',
query: '$query1.$query2.*',
label: 'query3-label',
options: ['All', 'AAA', 'AAB', 'AAC'],
selectedOption: 'AAA',
},
];
if (index === 0) {
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
} else {
e2e.pages.Dashboard.Settings.Variables.List.newButton().click();
}
assertAdding3dependantQueryVariablesScenario(queryVariables);
const { name, label, query, options, selectedOption } = variable;
e2e.getScenarioContext().then(({ lastAddedDataSource }: any) => {
createQueryVariable({
dataSourceName: lastAddedDataSource,
name,
label,
query,
options,
selectedOption,
});
});
// assert select updates
assertSelects(queryVariables);
e2e.pages.Dashboard.Settings.General.saveDashBoard()
.should('be.visible')
.click();
e2e.pages.SaveDashboardModal.save()
.should('be.visible')
.click();
e2e.flows.assertSuccessNotification();
// assert that duplicate works
queryVariables = assertDuplicateItem(queryVariables);
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
});
});
// assert that delete works
queryVariables = assertDeleteItem(queryVariables);
it(`asserts submenus`, () => {
assertVariableLabelsAndComponents(variables);
});
// assert that update works
queryVariables = assertUpdateItem(queryVariables);
it(`asserts variable table`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
// assert that move down works
queryVariables = assertMoveDownItem(queryVariables);
assertVariableTable(variables);
});
// assert that move up works
assertMoveUpItem(queryVariables);
},
it(`asserts variable selects`, () => {
assertSelects(variables);
});
it(`asserts duplicate variable`, () => {
// mutates variables
variables = assertDuplicateItem(variables);
e2e.flows.saveDashboard();
});
it(`asserts delete variable`, () => {
// mutates variables
variables = assertDeleteItem(variables);
e2e.flows.saveDashboard();
});
it(`asserts update variable`, () => {
// mutates variables
variables = assertUpdateItem(variables);
e2e.components.BackButton.backArrow()
.should('be.visible')
.should('be.visible')
.click();
e2e.flows.saveDashboard();
});
it(`asserts move variable down`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
// mutates variables
variables = assertMoveDownItem(variables);
e2e.flows.saveDashboard();
});
it(`asserts move variable up`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
// mutates variables
assertMoveUpItem(variables);
});
});
interface VariablesData {
name: string;
query: string;
label: string;
options: string[];
selectedOption: string;
}
interface CreateQueryVariableArguments extends VariablesData {
dataSourceName: string;
}
const assertDefaultsForNewVariable = () => {
logSection('Asserting defaults for new variable');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().within(input => {
expect(input.attr('placeholder')).equals('name');
expect(input.val()).equals('');
@@ -88,18 +185,11 @@ const assertDefaultsForNewVariable = () => {
.should('have.text', '');
});
e2e()
.window()
.then((win: any) => {
const chainer = 'have.text';
const value = '';
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().within(select => {
e2e()
.get('option:selected')
.should(chainer, value);
});
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().within(select => {
e2e()
.get('option:selected')
.should('have.text', '');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect().within(select => {
@@ -133,32 +223,16 @@ const assertDefaultsForNewVariable = () => {
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist');
logSection('Asserting defaults for new variable, OK!');
};
interface CreateQueryVariableArguments extends QueryVariableData {
dataSourceName: string;
}
const createQueryVariable = ({ name, label, dataSourceName, query }: CreateQueryVariableArguments) => {
logSection('Creating a Query Variable with', { name, label, dataSourceName, query });
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().type(name);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label);
e2e()
.window()
.then((win: any) => {
const text = `${dataSourceName}`;
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.select(text)
.blur();
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.select(`${dataSourceName}`)
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
.within(input => {
expect(input.attr('placeholder')).equals('metric name or tags query');
expect(input.val()).equals('');
})
.type(query)
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist');
@@ -181,10 +255,34 @@ const createQueryVariable = ({ name, label, dataSourceName, query }: CreateQuery
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.addButton().click();
logSection('Creating a Query Variable with required, OK!');
};
const assertVariableTableRow = ({ name, query }: QueryVariableData, index: number, length: number) => {
const assertVariableLabelAndComponent = ({ label, options, selectedOption }: VariablesData) => {
e2e.pages.Dashboard.SubMenu.submenuItemLabels(label).should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(selectedOption)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown().should('be.visible');
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(options[optionIndex]).should('be.visible');
}
};
const assertVariableLabelsAndComponents = (args: VariablesData[]) => {
e2e.pages.Dashboard.SubMenu.submenuItem().should('have.length', args.length);
for (let index = 0; index < args.length; index++) {
e2e.pages.Dashboard.SubMenu.submenuItem()
.eq(index)
.within(() => {
e2e()
.get('label')
.contains(args[index].name);
});
assertVariableLabelAndComponent(args[index]);
}
};
const assertVariableTableRow = ({ name, query }: VariablesData, index: number, length: number) => {
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields(name)
.should('exist')
.contains(name);
@@ -201,8 +299,7 @@ const assertVariableTableRow = ({ name, query }: QueryVariableData, index: numbe
e2e.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(name).should('exist');
};
const assertVariableTable = (args: QueryVariableData[]) => {
logSection('Asserting variable table with', args);
const assertVariableTable = (args: VariablesData[]) => {
e2e.pages.Dashboard.Settings.Variables.List.table()
.should('be.visible')
.within(() => {
@@ -214,90 +311,197 @@ const assertVariableTable = (args: QueryVariableData[]) => {
for (let index = 0; index < args.length; index++) {
assertVariableTableRow(args[index], index, args.length);
}
logSection('Asserting variable table, Ok');
};
const assertVariableLabelAndComponent = ({ label, options, selectedOption }: QueryVariableData) => {
e2e.pages.Dashboard.SubMenu.submenuItemLabels(label).should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(selectedOption)
const assertSelects = (variables: VariablesData[]) => {
// Values in submenus should be
// query1: [A] query2: [AA] query3: [AAA]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown().should('be.visible');
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(options[optionIndex]).should('be.visible');
}
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar().click();
// Values in submenus should be
// query1: [B] query2: [All] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 2);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
// Values in submenus should be
// query1: [B] query2: [BB] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0);
// Values in submenus should be
// query1: [B] query2: [BB] query3: [BBB]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BB + BC] query3: [BBB]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB + BCC')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BB + BC] query3: [BBB + BCC]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BA] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')
.should('be.visible')
.should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')
.should('be.visible')
.should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')
.should('be.visible')
.should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 2);
// Values in submenus should be
// query1: [A] query2: [All] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [A] query2: [AA] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0);
};
const assertVariableLabelsAndComponents = (args: QueryVariableData[]) => {
logSection('Asserting variable components and labels');
e2e.pages.Dashboard.SubMenu.submenuItem().should('have.length', args.length);
for (let index = 0; index < args.length; index++) {
e2e.pages.Dashboard.SubMenu.submenuItem()
.eq(index)
.within(() => {
e2e()
.get('label')
.contains(args[index].name);
});
assertVariableLabelAndComponent(args[index]);
}
logSection('Asserting variable components and labels, Ok');
};
const assertAdding3dependantQueryVariablesScenario = (queryVariables: QueryVariableData[]) => {
// This creates 3 variables where 2 depends on 1 and 3 depends on 2 and for each added variable
// we assert that the variable looks ok in the variable list and that it looks ok in the submenu in dashboard
for (let queryVariableIndex = 0; queryVariableIndex < queryVariables.length; queryVariableIndex++) {
const { name, label, query, options, selectedOption } = queryVariables[queryVariableIndex];
const asserts = queryVariables.slice(0, queryVariableIndex + 1);
e2e.getScenarioContext().then(({ lastAddedDataSource }: any) => {
createQueryVariable({
dataSourceName: lastAddedDataSource,
name,
label,
query,
options,
selectedOption,
});
});
assertVariableTable(asserts);
e2e.pages.Dashboard.Settings.General.saveDashBoard().click();
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.components.BackButton.backArrow().click();
assertVariableLabelsAndComponents(asserts);
if (queryVariableIndex < queryVariables.length - 1) {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.newButton().click();
}
}
};
interface QueryVariableData {
name: string;
query: string;
label: string;
options: string[];
selectedOption: string;
}
const logSection = (message: string, args?: any) => {
e2e().logToConsole('');
e2e().logToConsole(message, args);
e2e().logToConsole('===============================================================================');
};
const assertDuplicateItem = (queryVariables: QueryVariableData[]) => {
logSection('Asserting variable duplicate');
const itemToDuplicate = queryVariables[1];
const assertDuplicateItem = (variables: VariablesData[]) => {
const itemToDuplicate = variables[1];
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(itemToDuplicate.name)
@@ -308,10 +512,10 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => {
.within(() => {
e2e()
.get('tbody > tr')
.should('have.length', queryVariables.length + 1);
.should('have.length', variables.length + 1);
});
const newItem = { ...itemToDuplicate, name: `copy_of_${itemToDuplicate.name}` };
assertVariableTableRow(newItem, queryVariables.length - 1, queryVariables.length);
assertVariableTableRow(newItem, variables.length - 1, variables.length);
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields(newItem.name).click();
newItem.label = `copy_of_${itemToDuplicate.label}`;
@@ -323,7 +527,9 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => {
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(newItem.label).should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(newItem.selectedOption)
@@ -335,14 +541,11 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => {
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(newItem.options[optionIndex]).should('be.visible');
}
logSection('Asserting variable duplicate, OK!');
return [...queryVariables, newItem];
return [...variables, newItem];
};
const assertDeleteItem = (queryVariables: QueryVariableData[]) => {
logSection('Asserting variable delete');
const itemToDelete = queryVariables[1];
const assertDeleteItem = (variables: VariablesData[]) => {
const itemToDelete = variables[1];
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
@@ -352,26 +555,26 @@ const assertDeleteItem = (queryVariables: QueryVariableData[]) => {
.within(() => {
e2e()
.get('tbody > tr')
.should('have.length', queryVariables.length - 1);
.should('have.length', variables.length - 1);
});
e2e.pages.Dashboard.Settings.General.saveDashBoard().click();
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(itemToDelete.label).should('not.exist');
logSection('Asserting variable delete, OK!');
return queryVariables.filter(item => item.name !== itemToDelete.name);
return variables.filter(item => item.name !== itemToDelete.name);
};
const assertUpdateItem = (data: QueryVariableData[]) => {
const queryVariables = [...data];
const assertUpdateItem = (data: VariablesData[]) => {
const variables = [...data];
// updates an item to a constant variable instead
const itemToUpdate = queryVariables[1];
const itemToUpdate = variables[1];
let updatedItem = {
...itemToUpdate,
name: `update_of_${itemToUpdate.name}`,
@@ -381,8 +584,7 @@ const assertUpdateItem = (data: QueryVariableData[]) => {
selectedOption: 'undefined',
};
logSection('Asserting variable update');
queryVariables[1] = updatedItem;
variables[1] = updatedItem;
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
@@ -406,33 +608,29 @@ const assertUpdateItem = (data: QueryVariableData[]) => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().select('');
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(updatedItem.query);
e2e.components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
e2e()
.window()
.then((win: any) => {
queryVariables[1].selectedOption = 'A constant';
assertVariableLabelAndComponent(queryVariables[1]);
});
variables[1].selectedOption = 'A constant';
assertVariableLabelAndComponent(variables[1]);
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
assertVariableTableRow(queryVariables[1], 1, queryVariables.length);
assertVariableTableRow(variables[1], 1, variables.length);
queryVariables[1].selectedOption = 'A constant';
variables[1].selectedOption = 'A constant';
logSection('Asserting variable update, OK!');
return queryVariables;
return variables;
};
const assertMoveDownItem = (data: QueryVariableData[]) => {
logSection('Asserting variable move down');
const queryVariables = [...data];
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(queryVariables[0].name).click();
const temp = { ...queryVariables[0] };
queryVariables[0] = { ...queryVariables[1] };
queryVariables[1] = temp;
const assertMoveDownItem = (data: VariablesData[]) => {
const variables = [...data];
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(variables[0].name).click();
const temp = { ...variables[0] };
variables[0] = { ...variables[1] };
variables[1] = temp;
e2e.pages.Dashboard.Settings.Variables.List.table().within(() => {
e2e()
.get('tbody > tr')
@@ -441,11 +639,11 @@ const assertMoveDownItem = (data: QueryVariableData[]) => {
e2e()
.get('td')
.eq(0)
.contains(queryVariables[0].name);
.contains(variables[0].name);
e2e()
.get('td')
.eq(1)
.contains(queryVariables[0].query);
.contains(variables[0].query);
});
e2e()
.get('tbody > tr')
@@ -454,130 +652,29 @@ const assertMoveDownItem = (data: QueryVariableData[]) => {
e2e()
.get('td')
.eq(0)
.contains(queryVariables[1].name);
.contains(variables[1].name);
e2e()
.get('td')
.eq(1)
.contains(queryVariables[1].query);
.contains(variables[1].query);
});
});
e2e.components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
assertVariableLabelsAndComponents(queryVariables);
assertVariableLabelsAndComponents(variables);
logSection('Asserting variable move down, OK!');
return queryVariables;
return variables;
};
const assertSelects = (queryVariables: QueryVariableData[]) => {
// Values in submenus should be
// query1: [A] query2: [AA] query3: [AAA]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
// Values in submenus should be
// query1: [B] query2: [All] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 2);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
// Values in submenus should be
// query1: [B] query2: [BB] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0);
// Values in submenus should be
// query1: [B] query2: [BB] query3: [BBB]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC').should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BB + BC] query3: [BBB]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB + BCC').should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BB + BC] query3: [BBB + BCC]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BA').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BA] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 2);
// Values in submenus should be
// query1: [A] query2: [All] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 1);
// Values in submenus should be
// query1: [A] query2: [AA] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').click();
e2e.pages.Dashboard.Toolbar.navBar().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA').should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0);
};
const assertMoveUpItem = (data: QueryVariableData[]) => {
logSection('Asserting variable move up');
const queryVariables = [...data];
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(queryVariables[1].name).click();
const temp = { ...queryVariables[0] };
queryVariables[0] = { ...queryVariables[1] };
queryVariables[1] = temp;
const assertMoveUpItem = (data: VariablesData[]) => {
const variables = [...data];
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(variables[1].name).click();
const temp = { ...variables[0] };
variables[0] = { ...variables[1] };
variables[1] = temp;
e2e.pages.Dashboard.Settings.Variables.List.table().within(() => {
e2e()
.get('tbody > tr')
@@ -586,11 +683,11 @@ const assertMoveUpItem = (data: QueryVariableData[]) => {
e2e()
.get('td')
.eq(0)
.contains(queryVariables[0].name);
.contains(variables[0].name);
e2e()
.get('td')
.eq(1)
.contains(queryVariables[0].query);
.contains(variables[0].query);
});
e2e()
.get('tbody > tr')
@@ -599,19 +696,19 @@ const assertMoveUpItem = (data: QueryVariableData[]) => {
e2e()
.get('td')
.eq(0)
.contains(queryVariables[1].name);
.contains(variables[1].name);
e2e()
.get('td')
.eq(1)
.contains(queryVariables[1].query);
.contains(variables[1].query);
});
});
e2e.components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
assertVariableLabelsAndComponents(queryVariables);
assertVariableLabelsAndComponents(variables);
logSection('Asserting variable move up, OK!');
return queryVariables;
return variables;
};
+1 -1
View File
@@ -30,7 +30,7 @@ require (
github.com/gorilla/websocket v1.4.1
github.com/gosimple/slug v1.4.2
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
github.com/grafana/grafana-plugin-sdk-go v0.57.0
github.com/grafana/grafana-plugin-sdk-go v0.60.0
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd
github.com/hashicorp/go-plugin v1.2.2
github.com/hashicorp/go-version v1.1.0
+4 -2
View File
@@ -135,8 +135,8 @@ github.com/gosimple/slug v1.4.2 h1:jDmprx3q/9Lfk4FkGZtvzDQ9Cj9eAmsjzeQGp24PeiQ=
github.com/gosimple/slug v1.4.2/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 h1:SPdxCL9BChFTlyi0Khv64vdCW4TMna8+sxL7+Chx+Ag=
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4/go.mod h1:nc0XxBzjeGcrMltCDw269LoWF9S8ibhgxolCdA1R8To=
github.com/grafana/grafana-plugin-sdk-go v0.57.0 h1:eJnAjUh50uGimDCyK9gA2tzFzMntL6j3vWiSfTNj10g=
github.com/grafana/grafana-plugin-sdk-go v0.57.0/go.mod h1:cJqtJv4uuWOB3SAUToTMa8yQoNxEtF07CabXHwsr5fc=
github.com/grafana/grafana-plugin-sdk-go v0.60.0 h1:9IhNRdoaRi2RQflmNQHv2VuqXRGigpLacoYHHXZ+XBU=
github.com/grafana/grafana-plugin-sdk-go v0.60.0/go.mod h1:V3mq3GjPrIOc4K9jOm3mP5scJQgx9NJaYQ3cUlxm8vE=
github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 h1:0IKlLyQ3Hs9nDaiK5cSHAGmcQEIC8l2Ts1u6x5Dfrqg=
github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
@@ -212,6 +212,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+3
View File
@@ -14,4 +14,7 @@ module.exports = {
setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'],
snapshotSerializers: ['enzyme-to-json/serializer'],
globals: { 'ts-jest': { isolatedModules: true } },
moduleNameMapper: {
'\\.svg': '<rootDir>/public/test/mocks/svg.ts',
},
};
+2 -4
View File
@@ -53,12 +53,10 @@
},
"lint-staged": {
"*.{ts,tsx,json,scss}": [
"prettier --write",
"git add"
"prettier --write"
],
"*pkg/**/*.go": [
"gofmt -w -s",
"git add"
"gofmt -w -s"
]
},
"devDependencies": {
@@ -1,51 +1,11 @@
import fs from 'fs';
import path from 'path';
import { resultsToDataFrames, grafanaDataFrameToArrowTable, arrowTableToDataFrame } from './ArrowDataFrame';
import { grafanaDataFrameToArrowTable, arrowTableToDataFrame } from './ArrowDataFrame';
import { toDataFrameDTO, toDataFrame } from './processDataFrame';
import { FieldType } from '../types';
import { Table } from 'apache-arrow';
/* eslint-disable */
const resp = {
results: {
'': {
refId: '',
dataframes: [
'QVJST1cxAACsAQAAEAAAAAAACgAOAAwACwAEAAoAAAAUAAAAAAAAAQMACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAOD+//8IAAAADAAAAAIAAABHQwAABQAAAHJlZklkAAAAAP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAAlAAAAAQAAACG////FAAAAGAAAABgAAAAAAADAWAAAAACAAAALAAAAAQAAABQ////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAAB0////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAAAAABm////AAACAAAAAAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAbAAAAHQAAAAAAAoBdAAAAAIAAAA0AAAABAAAANz///8IAAAAEAAAAAQAAAB0aW1lAAAAAAQAAAB0eXBlAAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAC8AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAA0AAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAA0AAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAIAAAANAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAFp00e2XHFQAIo158ZccVAPqoiH1lxxUA7K6yfmXHFQDetNx/ZccVANC6BoFlxxUAwsAwgmXHFQC0xlqDZccVAKbMhIRlxxUAmNKuhWXHFQCK2NiGZccVAHzeAohlxxUAbuQsiWXHFQAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAMAAQAAALgBAAAAAAAAwAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAABQAAAAAgAAACgAAAAEAAAA4P7//wgAAAAMAAAAAgAAAEdDAAAFAAAAcmVmSWQAAAAA////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAIAAACUAAAABAAAAIb///8UAAAAYAAAAGAAAAAAAAMBYAAAAAIAAAAsAAAABAAAAFD///8IAAAAEAAAAAYAAABudW1iZXIAAAQAAAB0eXBlAAAAAHT///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAAAAAAGb///8AAAIAAAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABsAAAAdAAAAAAACgF0AAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABAAAAHRpbWUAAAAABAAAAHR5cGUAAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAANgBAABBUlJPVzE=',
'QVJST1cxAAC8AQAAEAAAAAAACgAOAAwACwAEAAoAAAAUAAAAAAAAAQMACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAND+//8IAAAADAAAAAIAAABHQgAABQAAAHJlZklkAAAA8P7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAApAAAAAQAAAB2////FAAAAGgAAABoAAAAAAADAWgAAAACAAAALAAAAAQAAABA////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAABk////CAAAABQAAAAJAAAAR0Itc2VyaWVzAAAABAAAAG5hbWUAAAAAAAAAAF7///8AAAIACQAAAEdCLXNlcmllcwASABgAFAATABIADAAAAAgABAASAAAAFAAAAGwAAAB0AAAAAAAKAXQAAAACAAAANAAAAAQAAADc////CAAAABAAAAAEAAAAdGltZQAAAAAEAAAAdHlwZQAAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAvAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAANAAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAANAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAABoAAAAAAAAAAAAAAACAAAADQAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAAAAAAAAAAAAAAAABadNHtlxxUACKNefGXHFQD6qIh9ZccVAOyusn5lxxUA3rTcf2XHFQDQugaBZccVAMLAMIJlxxUAtMZag2XHFQCmzISEZccVAJjSroVlxxUAitjYhmXHFQB83gKIZccVAG7kLIllxxUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAADIAQAAAAAAAMAAAAAAAAAA0AAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAND+//8IAAAADAAAAAIAAABHQgAABQAAAHJlZklkAAAA8P7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAApAAAAAQAAAB2////FAAAAGgAAABoAAAAAAADAWgAAAACAAAALAAAAAQAAABA////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAABk////CAAAABQAAAAJAAAAR0Itc2VyaWVzAAAABAAAAG5hbWUAAAAAAAAAAF7///8AAAIACQAAAEdCLXNlcmllcwASABgAFAATABIADAAAAAgABAASAAAAFAAAAGwAAAB0AAAAAAAKAXQAAAACAAAANAAAAAQAAADc////CAAAABAAAAAEAAAAdGltZQAAAAAEAAAAdHlwZQAAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA6AEAAEFSUk9XMQ==',
],
series: [] as any[],
tables: null as any,
frames: null as any,
},
},
};
/* eslint-enable */
describe('GEL Utils', () => {
test('should parse output with dataframe', () => {
const frames = resultsToDataFrames(resp);
for (const frame of frames) {
console.log('Frame', frame.refId);
for (const field of frame.fields) {
console.log(' > ', field.name, field.labels);
console.log(' (values)= ', field.values.toArray());
}
}
const norm = frames.map(f => toDataFrameDTO(f));
expect(norm).toMatchSnapshot();
});
test('processEmptyResults', () => {
const frames = resultsToDataFrames({
results: { '': { refId: '', meta: null, series: null, tables: null, dataframes: null } },
});
expect(frames.length).toEqual(0);
});
});
describe('Read/Write arrow Table to DataFrame', () => {
test('should parse output with dataframe', () => {
const frame = toDataFrame({
@@ -161,20 +161,3 @@ export function grafanaDataFrameToArrowTable(data: DataFrame): Table {
}
return table;
}
export function resultsToDataFrames(rsp: any): DataFrame[] {
if (rsp === undefined || rsp.results === undefined) {
return [];
}
const results = rsp.results as Array<{ dataframes: string[] }>;
const frames: DataFrame[] = Object.values(results).flatMap(res => {
if (!res.dataframes) {
return [];
}
return res.dataframes.map((b: string) => arrowTableToDataFrame(base64StringToArrowTable(b)));
});
return frames;
}
@@ -1,108 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GEL Utils should parse output with dataframe 1`] = `
Array [
Object {
"fields": Array [
Object {
"config": Object {},
"labels": undefined,
"name": "Time",
"type": "time",
"values": Array [
1569334575000,
1569334580000,
1569334585000,
1569334590000,
1569334595000,
1569334600000,
1569334605000,
1569334610000,
1569334615000,
1569334620000,
1569334625000,
1569334630000,
1569334635000,
],
},
Object {
"config": Object {},
"labels": undefined,
"name": "",
"type": "number",
"values": Array [
3,
3,
3,
5,
5,
5,
3,
3,
3,
5,
5,
5,
3,
],
},
],
"meta": undefined,
"name": undefined,
"refId": "GC",
},
Object {
"fields": Array [
Object {
"config": Object {},
"labels": undefined,
"name": "Time",
"type": "time",
"values": Array [
1569334575000,
1569334580000,
1569334585000,
1569334590000,
1569334595000,
1569334600000,
1569334605000,
1569334610000,
1569334615000,
1569334620000,
1569334625000,
1569334630000,
1569334635000,
],
},
Object {
"config": Object {},
"labels": undefined,
"name": "GB-series",
"type": "number",
"values": Array [
0,
0,
0,
2,
2,
2,
0,
0,
0,
2,
2,
2,
0,
],
},
],
"meta": undefined,
"name": undefined,
"refId": "GB",
},
]
`;
exports[`Read/Write arrow Table to DataFrame should read all types 1`] = `
Object {
"fields": Array [
@@ -7,6 +7,7 @@ import {
DataFrame,
DisplayValue,
DisplayValueAlignmentFactors,
Field,
FieldConfig,
FieldConfigSource,
FieldType,
@@ -80,6 +81,7 @@ export interface FieldDisplay {
colIndex?: number; // The field column index
rowIndex?: number; // only filled in when the value is from a row (ie, not a reduction)
getLinks?: () => LinkModel[];
hasLinks: boolean;
}
export interface GetFieldDisplayValuesOptions {
@@ -168,6 +170,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
valueRowIndex: j,
})
: () => [],
hasLinks: hasLinks(field),
});
if (values.length >= limit) {
@@ -210,6 +213,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
calculatedValue: displayValue,
})
: () => [],
hasLinks: hasLinks(field),
});
}
}
@@ -227,6 +231,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
return values;
};
export function hasLinks(field: Field): boolean {
return field.config?.links?.length ? field.config.links.length > 0 : false;
}
export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): DisplayValueAlignmentFactors {
const info: DisplayValueAlignmentFactors = {
title: '',
@@ -287,6 +295,7 @@ function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): Fiel
numeric: 0,
color: display.color,
},
hasLinks: false,
};
}
@@ -46,6 +46,7 @@ export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
let max = Number.MIN_VALUE;
const reducers = [ReducerID.min, ReducerID.max];
for (const frame of data) {
for (const field of frame.fields) {
if (field.type === FieldType.number) {
@@ -7,7 +7,12 @@ export { FilterFieldsByNameTransformerOptions } from './transformers/filterByNam
export { FilterFramesByRefIdTransformerOptions } from './transformers/filterByRefId';
export { SeriesToColumnsOptions } from './transformers/seriesToColumns';
export { ReduceTransformerOptions } from './transformers/reduce';
export { CalculateFieldTransformerOptions } from './transformers/calculateField';
export { LabelsToFieldsOptions } from './transformers/labelsToFields';
export {
CalculateFieldTransformerOptions,
CalculateFieldMode,
getResultFieldNameForCalculateFieldTransformerOptions,
} from './transformers/calculateField';
export { OrganizeFieldsTransformerOptions } from './transformers/organize';
export { createOrderFieldsComparer } from './transformers/order';
export { transformDataFrame } from './transformDataFrame';
@@ -4,19 +4,27 @@ import { FieldType } from '../../types/dataFrame';
import { ReducerID } from '../fieldReducer';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { transformDataFrame } from '../transformDataFrame';
import { calculateFieldTransformer } from './calculateField';
import { calculateFieldTransformer, CalculateFieldMode } from './calculateField';
import { DataFrameView } from '../../dataframe';
import { BinaryOperationID } from '../../utils';
const seriesToTestWith = toDataFrame({
const seriesA = toDataFrame({
fields: [
{ name: 'A', type: FieldType.time, values: [1000, 2000] },
{ name: 'B', type: FieldType.number, values: [1, 100] },
{ name: 'C', type: FieldType.number, values: [2, 200] },
{ 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'] },
],
});
describe('calculateField transformer', () => {
describe('calculateField transformer w/ timeseries', () => {
beforeAll(() => {
mockTransformationsRegistry([calculateFieldTransformer]);
});
@@ -25,28 +33,30 @@ describe('calculateField transformer', () => {
const cfg = {
id: DataTransformerID.calculateField,
options: {
// defautls to sum
// defaults to `sum` ReduceRow
alias: 'The Total',
},
};
const filtered = transformDataFrame([cfg], [seriesToTestWith])[0];
const filtered = transformDataFrame([cfg], [seriesA, seriesBC])[0];
const rows = new DataFrameView(filtered).toArray();
expect(rows).toMatchInlineSnapshot(`
Array [
Object {
"A": 1000,
"B": 1,
"C": 2,
"D": "first",
"The Total": 3,
"A {0}": 1,
"B {1}": 2,
"C {1}": 3,
"D {1}": "first",
"The Total": 6,
"TheTime": 1000,
},
Object {
"A": 2000,
"B": 100,
"C": 200,
"D": "second",
"The Total": 300,
"A {0}": 100,
"B {1}": 200,
"C {1}": 300,
"D {1}": "second",
"The Total": 600,
"TheTime": 2000,
},
]
`);
@@ -56,20 +66,25 @@ describe('calculateField transformer', () => {
const cfg = {
id: DataTransformerID.calculateField,
options: {
reducer: ReducerID.mean,
mode: CalculateFieldMode.ReduceRow,
reduce: {
reducer: ReducerID.mean,
},
replaceFields: true,
},
};
const filtered = transformDataFrame([cfg], [seriesToTestWith])[0];
const filtered = transformDataFrame([cfg], [seriesA, seriesBC])[0];
const rows = new DataFrameView(filtered).toArray();
expect(rows).toMatchInlineSnapshot(`
Array [
Object {
"Mean": 1.5,
"Mean": 2,
"TheTime": 1000,
},
Object {
"Mean": 150,
"Mean": 200,
"TheTime": 2000,
},
]
`);
@@ -79,21 +94,86 @@ describe('calculateField transformer', () => {
const cfg = {
id: DataTransformerID.calculateField,
options: {
reducer: ReducerID.mean,
mode: CalculateFieldMode.ReduceRow,
reduce: {
include: 'B',
reducer: ReducerID.mean,
},
replaceFields: true,
include: 'B',
},
};
const filtered = transformDataFrame([cfg], [seriesToTestWith])[0];
const filtered = transformDataFrame([cfg], [seriesBC])[0];
const rows = new DataFrameView(filtered).toArray();
expect(rows).toMatchInlineSnapshot(`
Array [
Object {
"Mean": 1,
"Mean": 2,
"TheTime": 1000,
},
Object {
"Mean": 100,
"Mean": 200,
"TheTime": 2000,
},
]
`);
});
it('binary math', () => {
const cfg = {
id: DataTransformerID.calculateField,
options: {
mode: CalculateFieldMode.BinaryOperation,
binary: {
left: 'B',
operation: BinaryOperationID.Add,
right: 'C',
},
replaceFields: true,
},
};
const filtered = transformDataFrame([cfg], [seriesBC])[0];
const rows = new DataFrameView(filtered).toArray();
expect(rows).toMatchInlineSnapshot(`
Array [
Object {
"B + C": 5,
"TheTime": 1000,
},
Object {
"B + C": 500,
"TheTime": 2000,
},
]
`);
});
it('field + static number', () => {
const cfg = {
id: DataTransformerID.calculateField,
options: {
mode: CalculateFieldMode.BinaryOperation,
binary: {
left: 'B',
operation: BinaryOperationID.Add,
right: '2',
},
replaceFields: true,
},
};
const filtered = transformDataFrame([cfg], [seriesBC])[0];
const rows = new DataFrameView(filtered).toArray();
expect(rows).toMatchInlineSnapshot(`
Array [
Object {
"B + 2": 4,
"TheTime": 1000,
},
Object {
"B + 2": 202,
"TheTime": 2000,
},
]
`);
@@ -4,79 +4,248 @@ import { ReducerID, fieldReducers } from '../fieldReducer';
import { getFieldMatcher } from '../matchers';
import { FieldMatcherID } from '../matchers/ids';
import { RowVector } from '../../vector/RowVector';
import { ArrayVector } from '../../vector';
import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector';
import { doStandardCalcs } from '../fieldReducer';
import { seriesToColumnsTransformer } from './seriesToColumns';
import { getTimeField } from '../../dataframe';
import defaults from 'lodash/defaults';
import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators';
export interface CalculateFieldTransformerOptions {
reducer: ReducerID;
export enum CalculateFieldMode {
ReduceRow = 'reduceRow',
BinaryOperation = 'binary',
}
interface ReduceOptions {
include?: string; // Assume all fields
alias?: string; // The output field name
replaceFields?: boolean;
reducer: ReducerID;
nullValueMode?: NullValueMode;
}
interface BinaryOptions {
left: string;
operator: BinaryOperationID;
right: string;
}
const defaultReduceOptions: ReduceOptions = {
reducer: ReducerID.sum,
};
const defaultBinaryOptions: BinaryOptions = {
left: '',
operator: BinaryOperationID.Add,
right: '',
};
export interface CalculateFieldTransformerOptions {
// True/False or auto
timeSeries?: boolean;
mode: CalculateFieldMode; // defaults to 'reduce'
// Only one should be filled
reduce?: ReduceOptions;
binary?: BinaryOptions;
// Remove other fields
replaceFields?: boolean;
// Output field properties
alias?: string; // The output field name
// TODO: config?: FieldConfig; or maybe field overrides? since the UI exists
}
type ValuesCreator = (data: DataFrame) => Vector;
export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransformerOptions> = {
id: DataTransformerID.calculateField,
name: 'Add field from calculation',
description: 'Use the row values to calculate a new field',
defaultOptions: {
reducer: ReducerID.sum,
mode: CalculateFieldMode.ReduceRow,
reduce: {
reducer: ReducerID.sum,
},
},
transformer: options => (data: DataFrame[]) => {
let matcher = getFieldMatcher({
id: FieldMatcherID.numeric,
});
if (options.include && options.include.length) {
matcher = getFieldMatcher({
id: FieldMatcherID.byName,
options: options.include,
});
// 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);
}
const info = fieldReducers.get(options.reducer);
if (!info) {
throw new Error(`Unknown reducer: ${options.reducer}`);
const mode = options.mode ?? CalculateFieldMode.ReduceRow;
let creator: ValuesCreator | undefined = undefined;
if (mode === CalculateFieldMode.ReduceRow) {
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions));
} else if (mode === CalculateFieldMode.BinaryOperation) {
creator = getBinaryCreator(defaults(options.binary, defaultBinaryOptions));
}
// Nothing configured
if (!creator) {
return data;
}
const reducer = info.reduce ?? doStandardCalcs;
const ignoreNulls = options.nullValueMode === NullValueMode.Ignore;
const nullAsZero = options.nullValueMode === NullValueMode.AsZero;
return data.map(frame => {
// Find the columns that should be examined
const columns: Vector[] = [];
frame.fields.forEach(field => {
if (matcher(field)) {
columns.push(field.values);
}
});
// Prepare a "fake" field for the row
const iter = new RowVector(columns);
const row: Field = {
name: 'temp',
values: iter,
type: FieldType.number,
config: {},
};
const vals: number[] = [];
for (let i = 0; i < frame.length; i++) {
iter.rowIndex = i;
row.calcs = undefined; // bust the cache (just in case)
const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer];
vals.push(val);
// delegate field creation to the specific function
const values = creator!(frame);
if (!values) {
return frame;
}
const field = {
name: options.alias || info.name,
name: getResultFieldNameForCalculateFieldTransformerOptions(options),
type: FieldType.number,
config: {},
values: new ArrayVector(vals),
values,
};
let fields: Field[] = [];
// Replace all fields with the single field
if (options.replaceFields) {
const { timeField } = getTimeField(frame);
if (timeField && options.timeSeries !== false) {
fields = [timeField, field];
} else {
fields = [field];
}
} else {
fields = [...frame.fields, field];
}
return {
...frame,
fields: options.replaceFields ? [field] : [...frame.fields, field],
fields,
};
});
},
};
function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
let matcher = getFieldMatcher({
id: FieldMatcherID.numeric,
});
if (options.include && options.include.length) {
matcher = getFieldMatcher({
id: FieldMatcherID.byName,
options: options.include,
});
}
const info = fieldReducers.get(options.reducer);
if (!info) {
throw new Error(`Unknown reducer: ${options.reducer}`);
}
const reducer = info.reduce ?? doStandardCalcs;
const ignoreNulls = options.nullValueMode === NullValueMode.Ignore;
const nullAsZero = options.nullValueMode === NullValueMode.AsZero;
return (frame: DataFrame) => {
// Find the columns that should be examined
const columns: Vector[] = [];
for (const field of frame.fields) {
if (matcher(field)) {
columns.push(field.values);
}
}
// Prepare a "fake" field for the row
const iter = new RowVector(columns);
const row: Field = {
name: 'temp',
values: iter,
type: FieldType.number,
config: {},
};
const vals: number[] = [];
for (let i = 0; i < frame.length; i++) {
iter.rowIndex = i;
row.calcs = undefined; // bust the cache (just in case)
const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer];
vals.push(val);
}
return new ArrayVector(vals);
};
}
function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string): Vector | undefined {
if (!name) {
return undefined;
}
for (const f of frame.fields) {
if (f.name === name) {
return f.values;
}
}
const v = parseFloat(name);
if (!isNaN(v)) {
return new ConstantVector(v, frame.length);
}
return undefined;
}
function getBinaryCreator(options: BinaryOptions): ValuesCreator {
const operator = binaryOperators.getIfExists(options.operator);
return (frame: DataFrame) => {
const left = findFieldValuesWithNameOrConstant(frame, options.left);
const right = findFieldValuesWithNameOrConstant(frame, options.right);
if (!left || !right || !operator) {
return (undefined as unknown) as Vector;
}
return new BinaryOperationVector(left, right, operator.operation);
};
}
/**
* 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 getResultFieldNameForCalculateFieldTransformerOptions(options: CalculateFieldTransformerOptions) {
if (options.alias?.length) {
return options.alias;
}
if (options.mode === CalculateFieldMode.BinaryOperation) {
const { binary } = options;
return `${binary?.left ?? ''} ${binary?.operator ?? ''} ${binary?.right ?? ''}`;
}
if (options.mode === CalculateFieldMode.ReduceRow) {
const r = fieldReducers.getIfExists(options.reduce?.reducer);
if (r) {
return r.name;
}
}
return 'math';
}
@@ -18,7 +18,7 @@ export interface ReduceTransformerOptions {
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
id: DataTransformerID.reduce,
name: 'Reduce',
description: 'Reduce all rows to a single row and concatenate all results',
description: 'Reduce all rows or data points to a single value using a function like max, min, mean or last',
defaultOptions: {
reducers: [ReducerID.max],
},
+9
View File
@@ -74,3 +74,12 @@ export class AppPlugin<T = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
}
}
}
/**
* Defines life cycle of a feature
* @internal
*/
export enum FeatureState {
alpha = 'alpha',
beta = 'beta',
}
+2
View File
@@ -8,6 +8,7 @@ import { DataFrame } from './dataFrame';
*/
export enum LogLevel {
emerg = 'critical',
fatal = 'critical',
alert = 'critical',
crit = 'critical',
critical = 'critical',
@@ -17,6 +18,7 @@ export enum LogLevel {
eror = 'error',
error = 'error',
info = 'info',
information = 'info',
notice = 'info',
dbug = 'debug',
debug = 'debug',
@@ -0,0 +1,39 @@
import { RegistryItem, Registry } from './Registry';
export enum BinaryOperationID {
Add = '+',
Subtract = '-',
Divide = '/',
Multiply = '*',
}
export type BinaryOperation = (left: number, right: number) => number;
interface BinaryOperatorInfo extends RegistryItem {
operation: BinaryOperation;
}
export const binaryOperators = new Registry<BinaryOperatorInfo>(() => {
return [
{
id: BinaryOperationID.Add,
name: 'Add',
operation: (a: number, b: number) => a + b,
},
{
id: BinaryOperationID.Subtract,
name: 'Subtract',
operation: (a: number, b: number) => a - b,
},
{
id: BinaryOperationID.Multiply,
name: 'Multiply',
operation: (a: number, b: number) => a * b,
},
{
id: BinaryOperationID.Divide,
name: 'Divide',
operation: (a: number, b: number) => a / b,
},
];
});
+1 -1
View File
@@ -17,7 +17,7 @@ describe('read csv', () => {
const rows = 4;
expect(series.length).toBe(rows);
// Make sure everythign it padded properly
// Make sure everything is padded properly
for (const field of series.fields) {
expect(field.values.length).toBe(rows);
}
+9
View File
@@ -0,0 +1,9 @@
/**
* Enumeration of documentation topics
* @internal
*/
export enum DocsId {
Transformations,
FieldConfig,
FieldConfigOverrides,
}
+2
View File
@@ -8,6 +8,7 @@ export * from './labels';
export * from './object';
export * from './namedColorsPalette';
export * from './series';
export * from './binaryOperators';
export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders';
export { getMappedValue } from './valueMappings';
@@ -15,3 +16,4 @@ export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
export { locationUtil } from './location';
export { urlUtil, UrlQueryMap, UrlQueryValue } from './url';
export { DataLinkBuiltInVars } from './dataLinks';
export { DocsId } from './docs';
+1 -1
View File
@@ -15,7 +15,7 @@ describe('getLoglevel()', () => {
});
it('returns no log level on when level is part of a word', () => {
expect(getLogLevel('this is information')).toBe(LogLevel.unknown);
expect(getLogLevel('who warns us')).toBe(LogLevel.unknown);
});
it('returns same log level for long and short version', () => {
@@ -1,11 +1,14 @@
import { ArrayVector } from './ArrayVector';
import { ScaledVector } from './ScaledVector';
import { BinaryOperationVector } from './BinaryOperationVector';
import { ConstantVector } from './ConstantVector';
import { binaryOperators, BinaryOperationID } from '../utils/binaryOperators';
describe('ScaledVector', () => {
it('should support multiply operations', () => {
const source = new ArrayVector([1, 2, 3, 4]);
const scale = 2.456;
const v = new ScaledVector(source, scale);
const operation = binaryOperators.get(BinaryOperationID.Multiply).operation;
const v = new BinaryOperationVector(source, new ConstantVector(scale, source.length), operation);
expect(v.length).toEqual(source.length);
// expect(v.push(10)).toEqual(source.length); // not implemented
for (let i = 0; i < 10; i++) {
@@ -0,0 +1,23 @@
import { Vector } from '../types/vector';
import { vectorToArray } from './vectorToArray';
import { BinaryOperation } from '../utils/binaryOperators';
export class BinaryOperationVector implements Vector<number> {
constructor(private left: Vector<number>, private right: Vector<number>, private operation: BinaryOperation) {}
get length(): number {
return this.left.length;
}
get(index: number): number {
return this.operation(this.left.get(index), this.right.get(index));
}
toArray(): number[] {
return vectorToArray(this);
}
toJSON(): number[] {
return vectorToArray(this);
}
}
@@ -1,22 +0,0 @@
import { Vector } from '../types/vector';
import { vectorToArray } from './vectorToArray';
export class ScaledVector implements Vector<number> {
constructor(private source: Vector<number>, private scale: number) {}
get length(): number {
return this.source.length;
}
get(index: number): number {
return this.source.get(index) * this.scale;
}
toArray(): number[] {
return vectorToArray(this);
}
toJSON(): number[] {
return vectorToArray(this);
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ export * from './AppendedVectors';
export * from './ArrayVector';
export * from './CircularVector';
export * from './ConstantVector';
export * from './ScaledVector';
export * from './BinaryOperationVector';
export * from './SortedVector';
export { vectorator } from './FunctionalVector';
+2 -2
View File
@@ -26,7 +26,7 @@
"docsExtract": "mkdir -p ../../reports/docs && api-extractor run 2>&1 | tee ../../reports/docs/$(basename $(pwd)).log",
"lint": "eslint cypress/ src/ --ext=.js,.ts,.tsx",
"open": "cypress open",
"start": "cypress run",
"start": "cypress run --headless --browser chrome",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
@@ -48,7 +48,7 @@
"@grafana/tsconfig": "^1.0.0-rc1",
"blink-diff": "1.0.13",
"commander": "5.0.0",
"cypress": "3.7.0",
"cypress": "4.5.0",
"execa": "4.0.0",
"ts-loader": "6.2.1",
"typescript": "3.7.5",
+1
View File
@@ -34,6 +34,7 @@
"@rollup/plugin-node-resolve": "7.1.1",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/systemjs": "^0.20.6",
"@types/jest": "23.3.14",
"lodash": "4.17.15",
"pretty-format": "25.1.0",
"rollup": "2.0.6",
+1
View File
@@ -9,3 +9,4 @@ export * from './types';
export { loadPluginCss, SystemJS, PluginCssOptions } from './utils/plugin';
export { reportMetaAnalytics } from './utils/analytics';
export { DataSourceWithBackend, HealthCheckResult, HealthStatus } from './utils/DataSourceWithBackend';
export { toDataQueryError, toDataQueryResponse } from './utils/queryResponse';
@@ -4,3 +4,4 @@ export * from './dataSourceSrv';
export * from './LocationSrv';
export * from './EchoSrv';
export * from './templateSrv';
export * from './legacyAngularInjector';
@@ -0,0 +1,23 @@
import { auto } from 'angular';
let singleton: auto.IInjectorService;
/**
* Used during startup by Grafana to temporarily expose the angular injector to
* pure javascript plugins using {@link getLegacyAngularInjector}.
*
* @internal
*/
export const setLegacyAngularInjector = (instance: auto.IInjectorService) => {
singleton = instance;
};
/**
* WARNING: this function provides a temporary way for plugins to access anything in the
* angular injector. While the migration from angular to react continues, there are a few
* options that do not yet have good alternatives. Note that use of this function will
* be removed in the future.
*
* @beta
*/
export const getLegacyAngularInjector = (): auto.IInjectorService => singleton;
@@ -1,4 +1,4 @@
import { VariableModel } from '@grafana/data';
import { VariableModel, ScopedVars } from '@grafana/data';
/**
* Via the TemplateSrv consumers get access to all the available template variables
@@ -8,7 +8,15 @@ import { VariableModel } from '@grafana/data';
* @public
*/
export interface TemplateSrv {
/**
* List the dashboard variables
*/
getVariables(): VariableModel[];
/**
* Replace the values within the target string. See also {@link InterpolateFunction}
*/
replace(target: string, scopedVars?: ScopedVars, format?: string | Function): string;
}
let singletonInstance: TemplateSrv;
@@ -9,6 +9,7 @@ import {
import { Observable, from } from 'rxjs';
import { config } from '..';
import { getBackendSrv } from '../services';
import { toDataQueryResponse } from './queryResponse';
const ExpressionDatasourceID = '__expr__';
@@ -94,7 +95,11 @@ export class DataSourceWithBackend<
requestId,
})
.then((rsp: any) => {
return this.toDataQueryResponse(rsp?.data);
return toDataQueryResponse(rsp);
})
.catch(err => {
err.isHandled = true; // Avoid extra popup warning
return toDataQueryResponse(err);
});
return from(req);
@@ -109,16 +114,6 @@ export class DataSourceWithBackend<
return query;
}
/**
* This makes the arrow library loading async.
*/
async toDataQueryResponse(rsp: any): Promise<DataQueryResponse> {
const { resultsToDataFrames } = await import(
/* webpackChunkName: "apache-arrow-util" */ '@grafana/data/src/dataframe/ArrowDataFrame'
);
return { data: resultsToDataFrames(rsp) };
}
/**
* Make a GET request to the datasource resource path
*/
@@ -0,0 +1,165 @@
import { toDataFrameDTO } from '@grafana/data';
import { toDataQueryResponse } from './queryResponse';
/* eslint-disable */
const resp = {
data: {
results: {
GC: {
dataframes: [
'QVJST1cxAACsAQAAEAAAAAAACgAOAAwACwAEAAoAAAAUAAAAAAAAAQMACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAOD+//8IAAAADAAAAAIAAABHQwAABQAAAHJlZklkAAAAAP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAAlAAAAAQAAACG////FAAAAGAAAABgAAAAAAADAWAAAAACAAAALAAAAAQAAABQ////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAAB0////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAAAAABm////AAACAAAAAAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAbAAAAHQAAAAAAAoBdAAAAAIAAAA0AAAABAAAANz///8IAAAAEAAAAAQAAAB0aW1lAAAAAAQAAAB0eXBlAAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAC8AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAA0AAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAA0AAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAIAAAANAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAFp00e2XHFQAIo158ZccVAPqoiH1lxxUA7K6yfmXHFQDetNx/ZccVANC6BoFlxxUAwsAwgmXHFQC0xlqDZccVAKbMhIRlxxUAmNKuhWXHFQCK2NiGZccVAHzeAohlxxUAbuQsiWXHFQAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAMAAQAAALgBAAAAAAAAwAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAABQAAAAAgAAACgAAAAEAAAA4P7//wgAAAAMAAAAAgAAAEdDAAAFAAAAcmVmSWQAAAAA////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAIAAACUAAAABAAAAIb///8UAAAAYAAAAGAAAAAAAAMBYAAAAAIAAAAsAAAABAAAAFD///8IAAAAEAAAAAYAAABudW1iZXIAAAQAAAB0eXBlAAAAAHT///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAAAAAAGb///8AAAIAAAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABsAAAAdAAAAAAACgF0AAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABAAAAHRpbWUAAAAABAAAAHR5cGUAAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAANgBAABBUlJPVzE=',
],
frames: null as any,
},
},
},
};
const resWithError = {
data: {
results: {
A: {
error: 'Hello Error',
series: null,
tables: null,
dataframes: [
'QVJST1cxAAD/////WAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAJwAAAADAAAATAAAACgAAAAEAAAAPP///wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABc////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAHz///8IAAAANAAAACoAAAB7Im5vdGljZXMiOlt7InNldmVyaXR5IjoyLCJ0ZXh0IjoiVGV4dCJ9XX0AAAQAAABtZXRhAAAAAAEAAAAYAAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAAA0wAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABwAAAG51bWJlcnMABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgAHAAAAbnVtYmVycwAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADAwAKABgADAAIAAQACgAAABQAAAA4AAAAAgAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAA8D8AAAAAAAAIQBAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAABoAQAAAAAAAJAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAnAAAAAMAAABMAAAAKAAAAAQAAAA8////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAFz///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAfP///wgAAAA0AAAAKgAAAHsibm90aWNlcyI6W3sic2V2ZXJpdHkiOjIsInRleHQiOiJUZXh0In1dfQAABAAAAG1ldGEAAAAAAQAAABgAAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAADTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAHAAAAbnVtYmVycwAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAACAAcAAABudW1iZXJzAIABAABBUlJPVzE=',
],
},
},
},
};
const emptyResults = {
data: { '': { refId: '', meta: null, series: null, tables: null, dataframes: null } },
};
/* eslint-enable */
describe('GEL Utils', () => {
test('should parse output with dataframe', () => {
const res = toDataQueryResponse(resp);
const frames = res.data;
for (const frame of frames) {
expect(frame.refId).toEqual('GC');
}
const norm = frames.map(f => toDataFrameDTO(f));
expect(norm).toMatchInlineSnapshot(`
Array [
Object {
"fields": Array [
Object {
"config": Object {},
"labels": undefined,
"name": "Time",
"type": "time",
"values": Array [
1569334575000,
1569334580000,
1569334585000,
1569334590000,
1569334595000,
1569334600000,
1569334605000,
1569334610000,
1569334615000,
1569334620000,
1569334625000,
1569334630000,
1569334635000,
],
},
Object {
"config": Object {},
"labels": undefined,
"name": "",
"type": "number",
"values": Array [
3,
3,
3,
5,
5,
5,
3,
3,
3,
5,
5,
5,
3,
],
},
],
"meta": undefined,
"name": undefined,
"refId": "GC",
},
]
`);
});
test('processEmptyResults', () => {
const frames = toDataQueryResponse(emptyResults).data;
expect(frames.length).toEqual(0);
});
test('resultWithError', () => {
// Generated from:
// qdr.Responses[q.GetRefID()] = backend.DataResponse{
// Error: fmt.Errorf("an Error: %w", fmt.Errorf("another error")),
// Frames: data.Frames{
// {
// Fields: data.Fields{data.NewField("numbers", nil, []float64{1, 3})},
// Meta: &data.FrameMeta{
// Notices: []data.Notice{
// {
// Severity: data.NoticeSeverityError,
// Text: "Text",
// },
// },
// },
// },
// },
// }
const res = toDataQueryResponse(resWithError);
expect(res.error).toMatchInlineSnapshot(`
Object {
"message": "Hello Error",
"refId": "A",
}
`);
const norm = res.data.map(f => toDataFrameDTO(f));
expect(norm).toMatchInlineSnapshot(`
Array [
Object {
"fields": Array [
Object {
"config": Object {},
"labels": undefined,
"name": "numbers",
"type": "number",
"values": Array [
1,
3,
],
},
],
"meta": Object {
"notices": Array [
Object {
"severity": 2,
"text": "Text",
},
],
},
"name": undefined,
"refId": "A",
},
]
`);
});
});
@@ -0,0 +1,96 @@
import {
DataQueryResponse,
arrowTableToDataFrame,
base64StringToArrowTable,
KeyValue,
LoadingState,
DataQueryError,
} from '@grafana/data';
interface DataResponse {
error?: string;
refId?: string;
dataframes?: string[];
// series: null,
// tables: null,
}
/**
* Parse the results from `/api/ds/query
*/
export function toDataQueryResponse(res: any): DataQueryResponse {
const rsp: DataQueryResponse = { data: [], state: LoadingState.Done };
if (res.data?.results) {
const results: KeyValue = res.data.results;
for (const refId of Object.keys(results)) {
const dr = results[refId] as DataResponse;
if (dr) {
if (dr.error) {
if (!rsp.error) {
rsp.error = {
refId,
message: dr.error,
};
rsp.state = LoadingState.Error;
}
}
if (dr.dataframes) {
for (const b64 of dr.dataframes) {
try {
const t = base64StringToArrowTable(b64);
const f = arrowTableToDataFrame(t);
if (!f.refId) {
f.refId = refId;
}
rsp.data.push(f);
} catch (err) {
rsp.state = LoadingState.Error;
rsp.error = toDataQueryError(err);
}
}
}
}
}
}
// When it is not an OK response, make sure the error gets added
if (res.status && res.status !== 200) {
if (rsp.state !== LoadingState.Error) {
rsp.state = LoadingState.Error;
}
if (!rsp.error) {
rsp.error = toDataQueryError(res);
}
}
return rsp;
}
/**
* Convert an object into a DataQueryError -- if this is an HTTP response,
* it will put the correct values in the error filds
*/
export function toDataQueryError(err: any): DataQueryError {
const error = (err || {}) as DataQueryError;
if (!error.message) {
if (typeof err === 'string' || err instanceof String) {
return { message: err } as DataQueryError;
}
let message = 'Query error';
if (error.message) {
message = error.message;
} else if (error.data && error.data.message) {
message = error.data.message;
} else if (error.data && error.data.error) {
message = error.data.error;
} else if (error.status) {
message = `Query error: ${error.status} ${error.statusText}`;
}
error.message = message;
}
return error;
}
+6 -1
View File
@@ -11,5 +11,10 @@
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/sanitize-url.d.ts"]
"include": [
"src/**/*.ts*",
"../../public/app/types/jquery/*.ts",
"../../public/app/types/sanitize-url.d.ts",
"../../public/app/types/svg.d.ts"
]
}
@@ -1,8 +1,12 @@
#!/bin/bash
set -eo pipefail
source ./common.sh
output=$(docker build . | tee /dev/tty)
hash=$(echo "$output" | tail -1 | sed -ne "s/^Successfully built \(.*\)/\1/p")
docker tag "$hash" $DOCKER_IMAGE_NAME:latest
docker push $DOCKER_IMAGE_NAME:latest
if [ ${#hash} -gt 0 ]; then
docker tag "$hash" $DOCKER_IMAGE_NAME:latest
docker push $DOCKER_IMAGE_NAME:latest
fi
@@ -1,4 +1,5 @@
#!/bin/sh
set -eo pipefail
source "./deploy-common.sh"
# Make libgcc compatible
@@ -27,10 +28,10 @@ get_file "https://github.com/golangci/golangci-lint/releases/download/v1.23.7/$f
"/tmp/$filename" \
"34df1794a2ea8e168b3c98eed3cc0f3e13ed4cba735e4e40ef141df5c41bc086"
untar_file "/tmp/$filename"
chmod 755 /usr/local/bin/golangci-lint
ln -s /usr/local/golangci-lint-1.23.7-linux-amd64/golangci-lint /usr/local/bin/golangci-lint
ln -s /usr/local/go/bin/go /usr/local/bin/go
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt
chmod 755 /usr/local/bin/golangci-lint
# Install dependencies
apk add fontconfig zip jq
@@ -38,9 +39,12 @@ apk add fontconfig zip jq
# Install code climate
get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \
"/usr/local/bin/cc-test-reporter" \
"38f2442892027f61a07f52c845818750261b2ba58bffb043a582495339d37c05"
"b4138199aa755ebfe171b57cc46910b13258ace5fbc4eaa099c42607cd0bff32"
chmod +x /usr/local/bin/cc-test-reporter
wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.4.4/grabpl"
chmod +x /usr/local/bin/grabpl
apk add git
# Install Mage
mkdir -pv /tmp/mage $HOME/go/bin
@@ -56,10 +60,6 @@ for file in $(ls $HOME/go/bin); do
mv -v $HOME/go/bin/$file /usr/local/bin/$file
done
ls -l /usr/local/bin
revive --help
file /usr/local/bin/revive
# Cleanup after yourself
/bin/rm -rf /tmp/mage
/bin/rm -rf $HOME/go
@@ -1,8 +1,12 @@
#!/bin/bash
set -eo pipefail
source ./common.sh
output=$(docker build . | tee /dev/tty)
hash=$(echo "$output" | tail -1 | sed -ne "s/^Successfully built \(.*\)/\1/p")
docker tag "$hash" $DOCKER_IMAGE_NAME:latest
docker push $DOCKER_IMAGE_NAME:latest
if [ ${#hash} -gt 0 ]; then
docker tag "$hash" $DOCKER_IMAGE_NAME:latest
docker push $DOCKER_IMAGE_NAME:latest
fi
@@ -1,4 +1,6 @@
#!/bin/bash
set -eo pipefail
source "/etc/profile"
source "./deploy-slim.sh"
source "./deploy-common.sh"
@@ -11,7 +13,6 @@ wget -O - "https://nodejs.org/dist/v12.16.2/node-${NODEVER}.tar.xz" | tar Jvxf -
pushd /tmp/node-${NODEVER}
/bin/rm -f CHANGELOG.md README.md LICENSE
/bin/cp -r * /usr/local
/bin/cp -r .??* /usr/local
popd
/bin/rm -rf /tmp/node-${NODEVER}
@@ -32,17 +33,20 @@ get_file "https://github.com/golangci/golangci-lint/releases/download/v1.23.7/$f
"/tmp/$filename" \
"34df1794a2ea8e168b3c98eed3cc0f3e13ed4cba735e4e40ef141df5c41bc086"
untar_file "/tmp/$filename"
chmod 755 /usr/local/bin/golangci-lint
ln -s /usr/local/golangci-lint-1.23.7-linux-amd64/golangci-lint /usr/local/bin/golangci-lint
ln -s /usr/local/go/bin/go /usr/local/bin/go
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt
chmod 755 /usr/local/bin/golangci-lint
# Install code climate
get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \
"/usr/local/bin/cc-test-reporter" \
"38f2442892027f61a07f52c845818750261b2ba58bffb043a582495339d37c05"
"b4138199aa755ebfe171b57cc46910b13258ace5fbc4eaa099c42607cd0bff32"
chmod 755 /usr/local/bin/cc-test-reporter
wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.4.4/grabpl"
chmod +x /usr/local/bin/grabpl
# Install Mage
mkdir -pv /tmp/mage $HOME/go/bin
git clone https://github.com/magefile/mage.git /tmp/mage
@@ -24,9 +24,12 @@ apt-get update -y && apt-get install -y adduser libfontconfig1 locate && /bin/rm
# Install code climate
get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \
"/usr/local/bin/cc-test-reporter" \
"38f2442892027f61a07f52c845818750261b2ba58bffb043a582495339d37c05"
"b4138199aa755ebfe171b57cc46910b13258ace5fbc4eaa099c42607cd0bff32"
chmod +x /usr/local/bin/cc-test-reporter
wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.4.4/grabpl"
chmod +x /usr/local/bin/grabpl
# Install Mage
mkdir -pv /tmp/mage $HOME/go/bin
git clone https://github.com/magefile/mage.git /tmp/mage
@@ -40,4 +43,4 @@ mv $HOME/go/bin/mage /usr/local/bin
sudo -u circleci ./deploy-user.sh
# Get the size down
/bin/rm -rf /var/lib/apt/lists
/bin/rm -rf /var/lib/apt/lists
@@ -5,7 +5,6 @@ import chalk from 'chalk';
import { startTask } from './tasks/core.start';
import { changelogTask } from './tasks/changelog';
import { cherryPickTask } from './tasks/cherrypick';
import { manifestTask } from './tasks/manifest';
import { precommitTask } from './tasks/precommit';
import { templateTask } from './tasks/template';
import { pluginBuildTask } from './tasks/plugin.build';
@@ -236,14 +235,6 @@ export const run = (includeInternalScripts = false) => {
await execTask(pluginUpdateTask)({});
});
// Test the manifest creation
program
.command('manifest')
.description('create a manifest file in the cwd')
.action(async cmd => {
await execTask(manifestTask)({ folder: process.cwd() });
});
program.on('command:*', () => {
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
process.exit(1);
@@ -1,76 +0,0 @@
import { getFilesForManifest, convertSha1SumsToManifest } from './manifest';
describe('Manifest', () => {
it('should collect file paths', () => {
const info = getFilesForManifest(__dirname);
expect(info).toMatchInlineSnapshot(`
Array [
"changelog.ts",
"cherrypick.ts",
"closeMilestone.ts",
"component.create.ts",
"core.start.ts",
"manifest.test.ts",
"manifest.ts",
"nodeVersionChecker.ts",
"package.build.ts",
"plugin/bundle.managed.ts",
"plugin/bundle.ts",
"plugin/create.ts",
"plugin/tests.ts",
"plugin.build.ts",
"plugin.ci.ts",
"plugin.create.ts",
"plugin.dev.ts",
"plugin.tests.ts",
"plugin.update.ts",
"plugin.utils.ts",
"precommit.ts",
"searchTestDataSetup.ts",
"task.ts",
"template.ts",
"toolkit.build.ts",
]
`);
});
it('should convert a sha1 sum to manifest structure', () => {
const sha1output = `7df059597099bb7dcf25d2a9aedfaf4465f72d8d LICENSE
4ebed28a02dc029719296aa847bffcea8eb5b9ff README.md
4493f107eb175b085f020c1afea04614232dc0fd gfx_sheets_darwin_amd64
d8b05884e3829d1389a9c0e4b79b0aba8c19ca4a gfx_sheets_linux_amd64
88f33db20182e17c72c2823fe3bed87d8c45b0fd gfx_sheets_windows_amd64.exe
e6d8f6704dbe85d5f032d4e8ba44ebc5d4a68c43 img/config-page.png
63d79d0e0f9db21ea168324bd4e180d6892b9d2b img/dashboard.png
7ea6295954b24be55b27320af2074852fb088fa1 img/graph.png
262f2bfddb004c7ce567042e8096f9e033c9b1bd img/query-editor.png
f134ab85caff88b59ea903c5491c6a08c221622f img/sheets.svg
40b8c38cea260caed3cdc01d6e3c1eca483ab5c1 module.js
3c04068eb581f73a262a2081f4adca2edbb14edf module.js.map
bfcae42976f0feca58eed3636655bce51702d3ed plugin.json`;
const manifest = convertSha1SumsToManifest(sha1output);
expect(manifest).toMatchInlineSnapshot(`
Object {
"files": Object {
"LICENSE": "7df059597099bb7dcf25d2a9aedfaf4465f72d8d",
"README.md": "4ebed28a02dc029719296aa847bffcea8eb5b9ff",
"gfx_sheets_darwin_amd64": "4493f107eb175b085f020c1afea04614232dc0fd",
"gfx_sheets_linux_amd64": "d8b05884e3829d1389a9c0e4b79b0aba8c19ca4a",
"gfx_sheets_windows_amd64.exe": "88f33db20182e17c72c2823fe3bed87d8c45b0fd",
"img/config-page.png": "e6d8f6704dbe85d5f032d4e8ba44ebc5d4a68c43",
"img/dashboard.png": "63d79d0e0f9db21ea168324bd4e180d6892b9d2b",
"img/graph.png": "7ea6295954b24be55b27320af2074852fb088fa1",
"img/query-editor.png": "262f2bfddb004c7ce567042e8096f9e033c9b1bd",
"img/sheets.svg": "f134ab85caff88b59ea903c5491c6a08c221622f",
"module.js": "40b8c38cea260caed3cdc01d6e3c1eca483ab5c1",
"module.js.map": "3c04068eb581f73a262a2081f4adca2edbb14edf",
"plugin.json": "bfcae42976f0feca58eed3636655bce51702d3ed",
},
"plugin": "<?>",
"version": "<?>",
}
`);
});
});
@@ -1,100 +0,0 @@
import { Task, TaskRunner } from './task';
import fs from 'fs';
import path from 'path';
import execa from 'execa';
import { ManifestInfo } from '../../plugins/types';
interface ManifestOptions {
folder: string;
}
export function getFilesForManifest(root: string, work?: string, acc?: string[]): string[] {
if (!acc) {
acc = [];
}
let abs = work ?? root;
const files = fs.readdirSync(abs);
files.forEach(file => {
const f = path.join(abs, file);
const stat = fs.statSync(f);
if (stat.isDirectory()) {
acc = getFilesForManifest(root, f, acc);
} else {
const idx = f.lastIndexOf('.');
if (idx > 0) {
// Don't hash images
const suffix = f.substring(idx + 1).toLowerCase();
if (suffix === 'png' || suffix == 'gif' || suffix === 'svg') {
return;
}
}
acc!.push(f.substring(root.length + 1).replace('\\', '/'));
}
});
return acc;
}
export function convertSha1SumsToManifest(sums: string): ManifestInfo {
const files: Record<string, string> = {};
for (const line of sums.split(/\r?\n/)) {
const idx = line.indexOf(' ');
if (idx > 0) {
const hash = line.substring(0, idx).trim();
const path = line.substring(idx + 1).trim();
files[path] = hash;
}
}
return {
plugin: '<?>',
version: '<?>',
files,
};
}
const manifestRunner: TaskRunner<ManifestOptions> = async ({ folder }) => {
const GRAFANA_API_KEY = process.env.GRAFANA_API_KEY;
if (!GRAFANA_API_KEY) {
console.log('Plugin signing requires a grafana API key');
}
const filename = 'MANIFEST.txt';
const files = getFilesForManifest(folder).filter(f => f !== filename);
// Run sha1sum
const originalDir = __dirname;
process.chdir(folder);
const { stdout } = await execa('sha1sum', files);
process.chdir(originalDir);
// Send the manifest to grafana API
const manifest = convertSha1SumsToManifest(stdout);
const outputPath = path.join(folder, filename);
const pluginPath = path.join(folder, 'plugin.json');
const plugin = require(pluginPath);
const url = `https://grafana.com/api/plugins/${plugin.id}/ci/sign`;
manifest.plugin = plugin.id;
manifest.version = plugin.info.version;
console.log('Request Signature:', url, manifest);
const axios = require('axios');
try {
const info = await axios.post(url, manifest, {
headers: { Authorization: 'Bearer ' + GRAFANA_API_KEY },
responseType: 'arraybuffer',
});
if (info.status === 200) {
console.log('OK: ', info.data);
const buffer = new Buffer(info.data, 'binary');
fs.writeFileSync(outputPath, buffer);
} else {
console.warn('Error: ', info);
console.log('Saving the unsigned manifest');
fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
}
} catch (err) {
console.log('ERROR Fetching response', err);
}
};
export const manifestTask = new Task<ManifestOptions>('Build Manifest', manifestRunner);
@@ -3,9 +3,11 @@ import execa = require('execa');
import * as fs from 'fs';
// @ts-ignore
import * as path from 'path';
import { resolve as resolvePath } from 'path';
import chalk from 'chalk';
import { useSpinner } from '../utils/useSpinner';
import { Task, TaskRunner } from './task';
import globby from 'globby';
let distDir: string, cwd: string;
@@ -68,6 +70,7 @@ const preparePackage = async (pkg: any) => {
const moveFiles = () => {
const files = ['README.md', 'CHANGELOG.md', 'index.js'];
// @ts-ignore
return useSpinner<void>(`Moving ${files.join(', ')} files`, async () => {
const promises = files.map(file => {
@@ -86,6 +89,26 @@ const moveFiles = () => {
})();
};
const moveStaticFiles = async (pkg: any, cwd: string) => {
if (pkg.name.endsWith('/ui')) {
const staticFiles = await globby(resolvePath(process.cwd(), 'src/**/*.+(png|svg|gif|jpg)'));
return useSpinner<void>(`Moving static files`, async () => {
const promises = staticFiles.map(file => {
return new Promise((resolve, reject) => {
fs.copyFile(file, `${cwd}/compiled/${file.replace(`${cwd}/src`, '')}`, (err: any) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
});
await Promise.all(promises);
})();
}
};
interface PackageBuildOptions {
scope: string;
}
@@ -107,6 +130,7 @@ const buildTaskRunner: TaskRunner<PackageBuildOptions> = async ({ scope }) => {
await clean();
await compile();
await moveStaticFiles(pkg, cwd);
await rollup();
await preparePackage(pkg);
await moveFiles();
@@ -19,8 +19,6 @@ import {
} from '../../plugins/env';
import { agregateWorkflowInfo, agregateCoverageInfo, agregateTestInfo } from '../../plugins/workflow';
import { PluginPackageDetails, PluginBuildReport } from '../../plugins/types';
import { manifestTask } from './manifest';
import { execTask } from '../utils/execTask';
import rimrafCallback from 'rimraf';
import { promisify } from 'util';
const rimraf = promisify(rimrafCallback);
@@ -165,7 +163,7 @@ const packagePluginRunner: TaskRunner<PluginCIOptions> = async () => {
// Write a manifest.txt file in the dist folder
try {
await execTask(manifestTask)({ folder: distContentDir });
await execa('grabpl', ['build-plugin-manifest', distContentDir]);
} catch (err) {
console.warn(`Error signing manifest: ${distContentDir}`, err);
}
+6 -1
View File
@@ -6,5 +6,10 @@
},
"exclude": ["../dist", "../node_modules"],
"extends": "../tsconfig.json",
"include": ["../src/**/*.ts", "../src/**/*.tsx", "../../../public/app/types/sanitize-url.d.ts"]
"include": [
"../src/**/*.ts",
"../src/**/*.tsx",
"../../../public/app/types/sanitize-url.d.ts",
"../../../public/app/types/svg.d.ts"
]
}
+1
View File
@@ -71,6 +71,7 @@
},
"devDependencies": {
"@rollup/plugin-commonjs": "11.0.2",
"@rollup/plugin-image": "2.0.4",
"@rollup/plugin-node-resolve": "7.1.1",
"@storybook/addon-actions": "5.3.17",
"@storybook/addon-docs": "5.3.17",
+2
View File
@@ -1,5 +1,6 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import image from '@rollup/plugin-image';
// import sourceMaps from 'rollup-plugin-sourcemaps';
import { terser } from 'rollup-plugin-terser';
@@ -71,6 +72,7 @@ const buildCjsPackage = ({ env }) => {
}),
resolve(),
// sourceMaps(),
image(),
env === 'production' && terser(),
],
};
@@ -0,0 +1,35 @@
import React from 'react';
import { boolean, text, select } from '@storybook/addon-knobs';
import { Badge, BadgeColor } from './Badge';
export default {
title: 'Data Display/Badge',
component: Badge,
decorators: [],
parameters: {
docs: {},
},
};
export const basic = () => {
const badgeColor = select<BadgeColor>(
'Badge color',
{
Red: 'red',
Green: 'green',
Blue: 'blue',
Orange: 'orange',
},
'blue'
);
const withIcon = boolean('With icon', true);
const tooltipText = text('Tooltip text', '');
return (
<Badge
text={'Badge label'}
color={badgeColor}
icon={withIcon ? 'rocket' : undefined}
tooltip={tooltipText.trim() === '' ? undefined : tooltipText}
/>
);
};
@@ -0,0 +1,90 @@
import React from 'react';
import { Icon } from '../Icon/Icon';
import { useTheme } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes/stylesFactory';
import { IconName } from '../../types';
import { Tooltip } from '../Tooltip/Tooltip';
import { getColorFromHexRgbOrName, GrafanaTheme } from '@grafana/data';
import tinycolor from 'tinycolor2';
import { css } from 'emotion';
import { HorizontalGroup } from '..';
export type BadgeColor = 'blue' | 'red' | 'green' | 'orange';
export interface BadgeProps {
text: string;
color: BadgeColor;
icon?: IconName;
tooltip?: string;
}
export const Badge = React.memo<BadgeProps>(({ icon, color, text, tooltip }) => {
const theme = useTheme();
const styles = getStyles(theme, color);
const badge = (
<div className={styles.wrapper}>
<HorizontalGroup align="center" spacing="xs">
{icon && <Icon name={icon} size="sm" />}
<span>{text}</span>
</HorizontalGroup>
</div>
);
return tooltip ? (
<Tooltip content={tooltip} placement="auto">
{badge}
</Tooltip>
) : (
badge
);
});
Badge.displayName = 'Badge';
const getStyles = stylesFactory((theme: GrafanaTheme, color: BadgeColor) => {
let sourceColor = getColorFromHexRgbOrName(color);
let borderColor = '';
let bgColor = '';
let textColor = '';
if (theme.isDark) {
bgColor = tinycolor(sourceColor)
.darken(38)
.toString();
borderColor = tinycolor(sourceColor)
.darken(25)
.toString();
textColor = tinycolor(sourceColor)
.lighten(45)
.toString();
} else {
bgColor = tinycolor(sourceColor)
.lighten(30)
.toString();
borderColor = tinycolor(sourceColor)
.lighten(15)
.toString();
textColor = tinycolor(sourceColor)
.darken(40)
.toString();
}
return {
wrapper: css`
font-size: ${theme.typography.size.sm};
display: inline-flex;
padding: 1px 4px;
border-radius: 3px;
margin-top: 6px;
background: ${bgColor};
border: 1px solid ${borderColor};
color: ${textColor};
> span {
position: relative;
top: 1px;
margin-left: 2px;
}
`,
};
});
@@ -1,5 +1,4 @@
import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs';
import { text, select, number, color } from '@storybook/addon-knobs';
import { BigValue, BigValueColorMode, BigValueGraphMode } from './BigValue';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
@@ -8,51 +7,45 @@ const getKnobs = () => {
return {
value: text('value', '$5022'),
title: text('title', 'Total Earnings'),
colorMode: select('Color mode', [BigValueColorMode.Value, BigValueColorMode.Background], BigValueColorMode.Value),
graphMode: select('Graph mode', [BigValueGraphMode.Area, BigValueGraphMode.Line], BigValueGraphMode.Area),
width: number('Width', 400, { range: true, max: 800, min: 200 }),
height: number('Height', 300, { range: true, max: 800, min: 200 }),
color: color('Value color', 'red'),
};
};
const BigValueStories = storiesOf('Visualizations/BigValue', module);
export default {
title: 'Visualizations/BigValue',
component: BigValue,
decorators: [withCenteredStory],
};
BigValueStories.addDecorator(withCenteredStory);
export const basic = () => {
const { value, title, colorMode, graphMode, height, width, color } = getKnobs();
interface StoryOptions {
colorMode: BigValueColorMode;
graphMode: BigValueGraphMode;
width?: number;
height?: number;
noSparkline?: boolean;
}
function addStoryForMode(options: StoryOptions) {
BigValueStories.add(`Color: ${options.colorMode}`, () => {
const { value, title } = getKnobs();
return renderComponentWithTheme(BigValue, {
width: options.width || 400,
height: options.height || 300,
colorMode: options.colorMode,
graphMode: options.graphMode,
value: {
text: value,
numeric: 5022,
color: 'red',
title,
},
sparkline: {
minX: 0,
maxX: 5,
data: [
[0, 10],
[1, 20],
[2, 15],
[3, 25],
[4, 5],
[5, 10],
],
},
});
return renderComponentWithTheme(BigValue, {
width: width,
height: height,
colorMode: colorMode,
graphMode: graphMode,
value: {
text: value,
numeric: 5022,
color: color,
title,
},
sparkline: {
minX: 0,
maxX: 5,
data: [
[0, 10],
[1, 20],
[2, 15],
[3, 25],
[4, 5],
[5, 10],
],
},
});
}
addStoryForMode({ colorMode: BigValueColorMode.Value, graphMode: BigValueGraphMode.Area });
addStoryForMode({ colorMode: BigValueColorMode.Background, graphMode: BigValueGraphMode.Line });
};
@@ -5,15 +5,16 @@ import { linkModelToContextMenuItems } from '../../utils/dataLinks';
import { css } from 'emotion';
interface DataLinksContextMenuProps {
children: (props: { openMenu?: React.MouseEventHandler<HTMLElement>; targetClassName?: string }) => JSX.Element;
links?: () => LinkModel[];
children: (props: DataLinksContextMenuApi) => JSX.Element;
links: () => LinkModel[];
}
export interface DataLinksContextMenuApi {
openMenu?: React.MouseEventHandler<HTMLElement>;
targetClassName?: string;
}
export const DataLinksContextMenu: React.FC<DataLinksContextMenuProps> = ({ children, links }) => {
if (!links) {
return children({});
}
const getDataLinksContextMenuItems = () => {
return [{ items: linkModelToContextMenuItems(links), label: 'Data links' }];
};
@@ -9,16 +9,16 @@ interface DataLinkEditorModalContentProps {
index: number;
data: DataFrame[];
suggestions: VariableSuggestion[];
onChange: (index: number, ink: DataLink) => void;
onClose: () => void;
onSave: (index: number, ink: DataLink) => void;
onCancel: (index: number) => void;
}
export const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = ({
link,
index,
suggestions,
onChange,
onClose,
onSave,
onCancel,
}) => {
const [dirtyLink, setDirtyLink] = useState(link);
return (
@@ -35,13 +35,12 @@ export const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = (
<HorizontalGroup>
<Button
onClick={() => {
onChange(index, dirtyLink);
onClose();
onSave(index, dirtyLink);
}}
>
Save
</Button>
<Button variant="secondary" onClick={() => onClose()}>
<Button variant="secondary" onClick={() => onCancel(index)}>
Cancel
</Button>
</HorizontalGroup>
@@ -18,26 +18,40 @@ interface DataLinksInlineEditorProps {
export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ links, onChange, suggestions, data }) => {
const theme = useTheme();
const [editIndex, setEditIndex] = useState<number | null>(null);
const [isNew, setIsNew] = useState(false);
const styles = getDataLinksInlineEditorStyles(theme);
const linksSafe: DataLink[] = links ?? [];
const isEditing = editIndex !== null && linksSafe[editIndex] !== undefined;
const isEditing = editIndex !== null;
const onDataLinkChange = (index: number, link: DataLink) => {
if (isNew) {
if (link.title.trim() === '' && link.url.trim() === '') {
setIsNew(false);
setEditIndex(null);
return;
} else {
setEditIndex(null);
setIsNew(false);
}
}
const update = cloneDeep(linksSafe);
update[index] = link;
onChange(update);
setEditIndex(null);
};
const onDataLinkAdd = () => {
let update = cloneDeep(linksSafe);
setEditIndex(update.length);
setIsNew(true);
};
update.push({
title: '',
url: '',
});
setEditIndex(update.length - 1);
onChange(update);
const onDataLinkCancel = (index: number) => {
if (isNew) {
setIsNew(false);
}
setEditIndex(null);
};
const onDataLinkRemove = (index: number) => {
@@ -72,15 +86,15 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
title="Edit link"
isOpen={true}
onDismiss={() => {
setEditIndex(null);
onDataLinkCancel(editIndex);
}}
>
<DataLinkEditorModalContent
index={editIndex}
link={linksSafe[editIndex]}
link={isNew ? { title: '', url: '' } : linksSafe[editIndex]}
data={data}
onChange={onDataLinkChange}
onClose={() => setEditIndex(null)}
onSave={onDataLinkChange}
onCancel={onDataLinkCancel}
suggestions={suggestions}
/>
</Modal>
@@ -38,6 +38,7 @@ export function Form<T>({
<form
className={css`
max-width: ${maxWidth}px;
width: 100%;
`}
onSubmit={handleSubmit(onSubmit)}
>

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