mirror of https://github.com/grafana/grafana.git
Merge branch 'master' into cli/watch-sass-var
This commit is contained in:
commit
d19616a16b
|
@ -1,4 +1,5 @@
|
|||
* Link the PR to an issue for new features
|
||||
* Follow the contribution guidelines in [`CONTRIBUTING.md`](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md)
|
||||
* Rebase your PR if it gets out of sync with master
|
||||
* Include `closes #<issue>` or a link to the issue in the description
|
||||
|
||||
**REMOVE THE TEXT ABOVE BEFORE CREATING THE PULL REQUEST**
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.git
|
||||
.github
|
||||
dist/
|
||||
pkg/
|
||||
node_modules
|
||||
public/vendor/
|
||||
vendor/
|
||||
|
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -1,5 +1,14 @@
|
|||
# 6.0.0-beta3 (unreleased)
|
||||
|
||||
### Minor
|
||||
* **CLI**: Grafana CLI should preserve permissions for backend binaries for Linux and Darwin [#15500](https://github.com/grafana/grafana/issues/15500)
|
||||
* **Alerting**: Allow image rendering 90 percent of alertTimeout [#15395](https://github.com/grafana/grafana/pull/15395)
|
||||
|
||||
### Bug fixes
|
||||
* **Influxdb**: Add support for alerting on InfluxDB queries that use the non_negative_difference function [#15415](https://github.com/grafana/grafana/issues/15415), thx [@kiran3394](https://github.com/kiran3394)
|
||||
* **Alerting**: Fix percent_diff calculation when points are nulls [#15443](https://github.com/grafana/grafana/issues/15443), thx [@max-neverov](https://github.com/max-neverov)
|
||||
* **Alerting**: Fixed handling of alert urls with true flags [#15454](https://github.com/grafana/grafana/issues/15454)
|
||||
|
||||
# 6.0.0-beta2 (2019-02-11)
|
||||
|
||||
### New Features
|
||||
|
@ -20,20 +29,19 @@
|
|||
* **Login**: Anonymous usage stats for token auth [#15288](https://github.com/grafana/grafana/issues/15288)
|
||||
* **AzureMonitor**: improve autocomplete for Log Analytics and App Insights editor [#15131](https://github.com/grafana/grafana/issues/15131)
|
||||
* **LDAP**: Fix IPA/FreeIPA v4.6.4 does not allow LDAP searches with empty attributes [#14432](https://github.com/grafana/grafana/issues/14432)
|
||||
* **Provisioning**: Allow testing data sources that were added by config [#12164](https://github.com/grafana/grafana/issues/12164)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* **Internal Metrics** Edition has been added to the build_info metric. This will break any Graphite queries using this metric. Edition will be a new label for the Prometheus metric. [#15363](https://github.com/grafana/grafana/pull/15363)
|
||||
|
||||
### 6.0.0-beta1 fixes
|
||||
### Bug fixes
|
||||
|
||||
* **Postgres**: Fix default port not added when port not configured [#15189](https://github.com/grafana/grafana/issues/15189)
|
||||
* **Alerting**: Fixes crash bug when alert notifier folders are missing [#15295](https://github.com/grafana/grafana/issues/15295)
|
||||
* **Dashboard**: Fix save provisioned dashboard modal [#15219](https://github.com/grafana/grafana/pull/15219)
|
||||
* **Dashboard**: Fix having a long query in prometheus dashboard query editor blocks 30% of the query field when on OSX and having native scrollbars [#15122](https://github.com/grafana/grafana/issues/15122)
|
||||
* **Explore**: Fix issue with wrapping on long queries [#15222](https://github.com/grafana/grafana/issues/15222)
|
||||
* **Explore**: Fix cut & paste adds newline before and after selection [#15223](https://github.com/grafana/grafana/issues/15223)
|
||||
* **Dataproxy**: Fix global datasource proxy timeout not added to correct http client [#15258](https://github.com/grafana/grafana/issues/15258) [#5699](https://github.com/grafana/grafana/issues/5699)
|
||||
* **Gauge**: Fix issue with gauge requests being cancelled [#15366](https://github.com/grafana/grafana/issues/15366)
|
||||
* **Gauge**: Accept decimal inputs for thresholds [#15372](https://github.com/grafana/grafana/issues/15372)
|
||||
* **UI**: Fix error caused by named colors that are not part of named colors palette [#15373](https://github.com/grafana/grafana/issues/15373)
|
||||
* **Search**: Bug pressing special regexp chars in input fields [#12972](https://github.com/grafana/grafana/issues/12972)
|
||||
* **Permissions**: No need to have edit permissions to be able to "Save as" [#13066](https://github.com/grafana/grafana/issues/13066)
|
||||
|
||||
# 6.0.0-beta1 (2019-01-30)
|
||||
|
||||
|
@ -82,6 +90,13 @@
|
|||
* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
|
||||
* **Annotations**: Fix creating annotation when graph panel has no data points position the popup outside viewport [#13765](https://github.com/grafana/grafana/issues/13765), thx [@banjeremy](https://github.com/banjeremy)
|
||||
* **Piechart/Flot**: Fixes multiple piechart instances with donut bug [#15062](https://github.com/grafana/grafana/pull/15062)
|
||||
* **Postgres**: Fix default port not added when port not configured [#15189](https://github.com/grafana/grafana/issues/15189)
|
||||
* **Alerting**: Fixes crash bug when alert notifier folders are missing [#15295](https://github.com/grafana/grafana/issues/15295)
|
||||
* **Dashboard**: Fix save provisioned dashboard modal [#15219](https://github.com/grafana/grafana/pull/15219)
|
||||
* **Dashboard**: Fix having a long query in prometheus dashboard query editor blocks 30% of the query field when on OSX and having native scrollbars [#15122](https://github.com/grafana/grafana/issues/15122)
|
||||
* **Explore**: Fix issue with wrapping on long queries [#15222](https://github.com/grafana/grafana/issues/15222)
|
||||
* **Explore**: Fix cut & paste adds newline before and after selection [#15223](https://github.com/grafana/grafana/issues/15223)
|
||||
* **Dataproxy**: Fix global datasource proxy timeout not added to correct http client [#15258](https://github.com/grafana/grafana/issues/15258) [#5699](https://github.com/grafana/grafana/issues/5699)
|
||||
|
||||
### Breaking changes
|
||||
* **Text Panel**: The text panel does no longer by default allow unsantizied HTML. [#4117](https://github.com/grafana/grafana/issues/4117). This means that if you have text panels with scripts tags they will no longer work as before. To enable unsafe javascript execution in text panels enable the settings `disable_sanitize_html` under the section `[panels]` in your Grafana ini file, or set env variable `GF_PANELS_DISABLE_SANITIZE_HTML=true`.
|
||||
|
|
|
@ -8,49 +8,47 @@ Contributions take the form of pull requests that will be reviewed by the core t
|
|||
|
||||
* If you have a trivial fix or improvement, go ahead and create a pull request.
|
||||
|
||||
* If you plan to do something more involved, discuss your idea on the respective [issue](https://github.com/grafana/grafana/issues) or create a [new issue](https://github.com/grafana/grafana/issues/new) if it does not exist. This will avoid unnecessary work and surely give you and us a good deal of inspiration.
|
||||
* If you plan to do something more involved, discuss your idea on the respective [issue](https://github.com/grafana/grafana/issues) or create a [new issue](https://github.com/grafana/grafana/issues/new) if it does not exist. This will avoid unnecessary work and surely give you and us a good deal of inspiration.
|
||||
|
||||
* Sign our [CLA](http://docs.grafana.org/contribute/cla/).
|
||||
|
||||
* For changes in the backend, follow the style guides used in Go [Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) and Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style)
|
||||
|
||||
## Steps to Contribute
|
||||
|
||||
Should you wish to work on a GitHub issue, check first if it is not already assigned to someone. If it is free, you claim it by commenting on the issue that you want to work on it. This is to prevent duplicated efforts from contributors on the same issue.
|
||||
|
||||
Please check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are good for getting started. If you have questions about one of the issues, with or without the tag, please comment on them and one of the core team or the original poster will clarify it.
|
||||
Please check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) and [`help wanted`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) labels to find issues that are good for getting started. If you have questions about one of the issues, with or without the tag, please comment on them and one of the core team or the original poster will clarify it.
|
||||
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
Follow the setup guide in README.md
|
||||
|
||||
### Rebuild frontend assets on source change
|
||||
```
|
||||
yarn watch
|
||||
```
|
||||
|
||||
### Rerun tests on source change
|
||||
```
|
||||
yarn jest
|
||||
```
|
||||
|
||||
### Run tests for backend assets before commit
|
||||
```
|
||||
test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
|
||||
```
|
||||
|
||||
### Run tests for frontend assets before commit
|
||||
```
|
||||
yarn test
|
||||
go test -v ./pkg/...
|
||||
```
|
||||
To setup a local development environment we recommend reading [Building Grafana from source](http://docs.grafana.org/project/building_from_source/)
|
||||
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
* Branch from the master branch and, if needed, rebase to the current master branch before submitting your pull request. If it doesn't merge cleanly with master you may be asked to rebase your changes.
|
||||
|
||||
* Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
|
||||
|
||||
* If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a comment.
|
||||
|
||||
* Add tests relevant to the fixed bug or new feature.
|
||||
|
||||
### Pull requests with new features
|
||||
Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
|
||||
|
||||
Make sure to include `closes #<issue>` or `fixes #<issue>` in the pull request description.
|
||||
|
||||
### Pull requests with bug fixes
|
||||
Please make all changes in one commit if possible. Include `closes #12345` in bottom of the commit message.
|
||||
A commit message for a bug fix should look something like this.
|
||||
|
||||
```
|
||||
avoid infinite loop in the dashboard provisioner
|
||||
|
||||
if one dashboard with an uid is refered to by two
|
||||
provsioners each provisioner overwrite each other.
|
||||
filling up dashboard_versions quite fast if using
|
||||
default settings.
|
||||
|
||||
closes #12864
|
||||
```
|
||||
|
||||
If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged.
|
|
@ -11,11 +11,11 @@ datasources:
|
|||
- name: gdev-prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
isDefault: true
|
||||
url: http://localhost:9090
|
||||
|
||||
- name: gdev-testdata
|
||||
type: testdata
|
||||
isDefault: true
|
||||
|
||||
- name: gdev-influxdb
|
||||
type: influxdb
|
||||
|
|
|
@ -7,6 +7,13 @@ services:
|
|||
- "80:80"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
logging:
|
||||
driver: "fluentd"
|
||||
options:
|
||||
tag: nginx
|
||||
|
||||
db:
|
||||
image: mysql:5.6
|
||||
|
@ -66,6 +73,8 @@ services:
|
|||
# - GF_DATABASE_SSL_MODE=disable
|
||||
# - GF_SESSION_PROVIDER=postgres
|
||||
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
|
||||
- GF_SERVER_ROUTER_LOGGING=true
|
||||
- GF_LOG_CONSOLE_FORMAT=json
|
||||
- GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
|
||||
- GF_AUTH_TOKEN_ROTATION_INTERVAL_MINUTES=2
|
||||
ports:
|
||||
|
@ -73,6 +82,10 @@ services:
|
|||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
logging:
|
||||
driver: "fluentd"
|
||||
options:
|
||||
tag: grafana
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.4.2
|
||||
|
@ -82,3 +95,21 @@ services:
|
|||
- VIRTUAL_HOST=prometheus.loc
|
||||
ports:
|
||||
- 9090
|
||||
|
||||
loki:
|
||||
image: grafana/loki:master
|
||||
environment:
|
||||
- VIRTUAL_HOST=loki.loc
|
||||
ports:
|
||||
- 3100
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
|
||||
fluentd:
|
||||
image: grafana/fluent-plugin-loki:master
|
||||
volumes:
|
||||
- ./fluentd/fluentd.conf:/fluentd/etc/fluentd.conf
|
||||
links:
|
||||
- loki
|
||||
ports:
|
||||
- "24224:24224"
|
||||
- "24224:24224/udp"
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
<source>
|
||||
@type forward
|
||||
port 24224
|
||||
bind 0.0.0.0
|
||||
</source>
|
||||
|
||||
<filter grafana>
|
||||
@type parser
|
||||
<parse>
|
||||
@type json
|
||||
json_parser json
|
||||
</parse>
|
||||
replace_invalid_sequence true
|
||||
emit_invalid_record_to_error false
|
||||
key_name log
|
||||
reserve_data true
|
||||
remove_key_name_field true
|
||||
</filter>
|
||||
|
||||
<filter nginx>
|
||||
@type parser
|
||||
format /^.*m(?<host>[^ ]*) (?<remote>[^ ]*) \- \- \[(?<time>[^\]]*)\] \"(?<method>\S+)(?: +(?<path>[^\"]*) +\S*)?\" (?<code>[^ ]*) (?<size>[^ ]*)(?: \"(?<referer>[^\"]*)\" \"(?<agent>[^\"]*)\")?$/
|
||||
time_format %d/%b/%Y:%H:%M:%S %z
|
||||
key_name log
|
||||
reserve_data true
|
||||
remove_key_name_field true
|
||||
</filter>
|
||||
|
||||
<filter **>
|
||||
@type record_transformer
|
||||
remove_keys "source,t"
|
||||
</filter>
|
||||
|
||||
<match grafana>
|
||||
@type copy
|
||||
<store>
|
||||
@type stdout
|
||||
output_type json
|
||||
</store>
|
||||
<store>
|
||||
@type loki
|
||||
url "http://loki:3100"
|
||||
extra_labels {"app":"grafana"}
|
||||
label_keys "container_name,container_id,logger"
|
||||
flush_interval 10s
|
||||
flush_at_shutdown true
|
||||
buffer_chunk_limit 1m
|
||||
</store>
|
||||
</match>
|
||||
|
||||
<filter nginx>
|
||||
@type record_transformer
|
||||
<record>
|
||||
lvl "info"
|
||||
</record>
|
||||
</filter>
|
||||
|
||||
<match nginx>
|
||||
@type copy
|
||||
<store>
|
||||
@type stdout
|
||||
output_type json
|
||||
</store>
|
||||
<store>
|
||||
@type loki
|
||||
url "http://loki:3100"
|
||||
extra_labels {"app":"nginx"}
|
||||
label_keys "container_name,container_id"
|
||||
flush_interval 10s
|
||||
flush_at_shutdown true
|
||||
buffer_chunk_limit 1m
|
||||
</store>
|
||||
</match>
|
|
@ -8,4 +8,9 @@ datasources:
|
|||
jsonData:
|
||||
timeInterval: 10s
|
||||
queryTimeout: 30s
|
||||
httpMethod: POST
|
||||
httpMethod: POST
|
||||
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
|
|
|
@ -36,4 +36,12 @@ scrape_configs:
|
|||
- 'mysqld-exporter'
|
||||
type: 'A'
|
||||
port: 9104
|
||||
refresh_interval: 10s
|
||||
|
||||
- job_name: 'loki'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'loki'
|
||||
type: 'A'
|
||||
port: 3100
|
||||
refresh_interval: 10s
|
|
@ -211,6 +211,8 @@ allowed_organizations =
|
|||
|
||||
## Set up OAuth2 with non-compliant providers
|
||||
|
||||
> Only available in Grafana v6.0 and above.
|
||||
|
||||
Some OAuth2 providers might not support `client_id` and `client_secret` passed via Basic Authentication HTTP header, which
|
||||
results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be
|
||||
send via POST body, which can be enabled via the following settings:
|
||||
|
|
|
@ -38,7 +38,7 @@ provider (listed above). There is also options for allowing self sign up.
|
|||
|
||||
### Login and short-lived tokens
|
||||
|
||||
> The followung applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration.
|
||||
> The following applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration.
|
||||
|
||||
Grafana are using short-lived tokens as a mechanism for verifying authenticated users.
|
||||
These short-lived tokens are rotated each `token_rotation_interval_minutes` for an active authenticated user.
|
||||
|
|
|
@ -13,7 +13,6 @@ weight = 5
|
|||
|
||||
Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes.
|
||||
|
||||
|
||||
## Querying
|
||||
|
||||
The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization.
|
||||
|
@ -28,6 +27,7 @@ The following datasources are officially supported:
|
|||
* [InfluxDB]({{< relref "influxdb.md" >}})
|
||||
* [OpenTSDB]({{< relref "opentsdb.md" >}})
|
||||
* [Prometheus]({{< relref "prometheus.md" >}})
|
||||
* [Loki]({{< relref "loki.md" >}})
|
||||
* [MySQL]({{< relref "mysql.md" >}})
|
||||
* [Postgres]({{< relref "postgres.md" >}})
|
||||
* [Microsoft SQL Server (MSSQL)]({{< relref "mssql.md" >}})
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
+++
|
||||
title = "Using Loki in Grafana"
|
||||
description = "Guide for using Loki in Grafana"
|
||||
keywords = ["grafana", "loki", "logging", "guide"]
|
||||
type = "docs"
|
||||
aliases = ["/datasources/loki"]
|
||||
[menu.docs]
|
||||
name = "Loki"
|
||||
parent = "datasources"
|
||||
weight = 11
|
||||
+++
|
||||
|
||||
# Using Loki in Grafana
|
||||
|
||||
> BETA: Querying Loki data requires Grafana's Explore section.
|
||||
> Grafana v6.x comes with Explore enabled by default.
|
||||
> In Grafana v5.3.x and v5.4.x. you need to enable Explore manually.
|
||||
> Viewing Loki data in dashboard panels is not supported yet, but is being worked on.
|
||||
|
||||
Grafana ships with built-in support for Loki, Grafana's log aggregation system.
|
||||
Just add it as a datasource and you are ready to query your log data in [Explore](/features/explore).
|
||||
|
||||
## Adding the data source to Grafana
|
||||
|
||||
1. Open Grafana and make sure you are logged in.
|
||||
2. In the side menu under the `Configuration` link you should find a link named `Data Sources`.
|
||||
3. Click the `Add data source` button at the top.
|
||||
4. Select `Loki` from the list of data sources.
|
||||
|
||||
> NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization.
|
||||
|
||||
| Name | Description |
|
||||
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| _Name_ | The datasource name. This is how you refer to the datasource in panels, queries, and Explore. |
|
||||
| _Default_ | Default datasource means that it will be pre-selected for new panels. |
|
||||
| _URL_ | The URL of the Loki instance, e.g., `http://localhost:3100` |
|
||||
| _Maximum lines_ | Upper limit for number of log lines returned by Loki (default is 1000). Decrease if your browser is sluggish when displaying logs in Explore. |
|
||||
|
||||
## Querying Logs
|
||||
|
||||
Querying and displaying log data from Loki is available via [Explore](/features/explore).
|
||||
Select the Loki data source, and then enter a log query to display your logs.
|
||||
|
||||
> Viewing Loki data in dashboard panels is not supported yet, but is being worked on.
|
||||
|
||||
### Log Queries
|
||||
|
||||
A log query consists of two parts: **log stream selector**, and a **search expression**. For performance reasons you need to start by choosing a log stream by selecting a log label.
|
||||
|
||||
The Logs Explorer (the `Log labels` button) next to the query field shows a list of labels of available log streams. An alternative way to write a query is to use the query field's autocomplete - you start by typing a left curly brace `{` and the autocomplete menu will suggest a list of labels. Press the `enter` key to execute the query.
|
||||
|
||||
Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
|
||||
|
||||
<div class="medium-6 columns">
|
||||
<video width="800" height="500" controls>
|
||||
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
### Log Stream Selector
|
||||
|
||||
For the label part of the query expression, wrap it in curly braces `{}` and then use the key value syntax for selecting labels. Multiple label expressions are separated by a comma:
|
||||
|
||||
`{app="mysql",name="mysql-backup"}`
|
||||
|
||||
The following label matching operators are currently supported:
|
||||
|
||||
* `=` exactly equal.
|
||||
* `!=` not equal.
|
||||
* `=~` regex-match.
|
||||
* `!~` do not regex-match.
|
||||
|
||||
Examples:
|
||||
|
||||
* `{name=~"mysql.+"}`
|
||||
* `{name!~"mysql.+"}`
|
||||
|
||||
The [same rules that apply for Prometheus Label Selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#instant-vector-selectors) apply for Loki Log Stream Selectors.
|
||||
|
||||
Another way to add a label selector, is in the table section, clicking on the **Filter** button beside a label will add the label to the query expression. This even works for multiple queries and will the label selector to each query.
|
||||
|
||||
### Search Expression
|
||||
|
||||
After writing the Log Stream Selector, you can filter the results further by writing a search expression. The search expression can be just text or a regex expression.
|
||||
|
||||
Example queries:
|
||||
|
||||
* `{job="mysql"} error`
|
||||
* `{name="kafka"} tsdb-ops.*io:2003`
|
||||
* `{instance=~"kafka-[23]",name="kafka"} kafka.server:type=ReplicaManager`
|
||||
|
||||
## Templating
|
||||
|
||||
Template variables are not yet supported by Loki.
|
||||
|
||||
## Annotations
|
||||
|
||||
Annotations are not yet supported by Loki.
|
||||
|
||||
## Configure the Datasource with Provisioning
|
||||
|
||||
You can set up the datasource via config files with Grafana's provisioning system.
|
||||
You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
|
||||
|
||||
Here is an example:
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Loki
|
||||
type: loki
|
||||
url: http://localhost:3100
|
||||
jsonData:
|
||||
maxLines: 1000
|
||||
```
|
|
@ -67,9 +67,9 @@ The autocomplete menu can be trigger by pressing Ctrl + Space. The Autocomplete
|
|||
|
||||
Suggestions can appear under the query field - click on them to update your query with the suggested change.
|
||||
|
||||
- For counters (monotonously increasing metrics), a rate function will be suggested.
|
||||
- For buckets, a histogram function will be suggested.
|
||||
- For recording rules, possible to expand the rules.
|
||||
* For counters (monotonously increasing metrics), a rate function will be suggested.
|
||||
* For buckets, a histogram function will be suggested.
|
||||
* For recording rules, possible to expand the rules.
|
||||
|
||||
### Table Filters
|
||||
|
||||
|
@ -79,6 +79,8 @@ Click on the filter button <span title="Filter for label" class="logs-label__ico
|
|||
|
||||
For Grafana 6.0, the first log integration is for the new open source log aggregation system from Grafana Labs - [Loki](https://github.com/grafana/loki). Loki is designed to be very cost effective, as it does not index the contents of the logs, but rather a set of labels for each log stream. The logs from Loki are queried in a similar way to querying with label selectors in Prometheus. It uses labels to group log streams which can be made to match up with your Prometheus labels. Read more about Grafana Loki [here](https://github.com/grafana/loki) or the Grafana Labs hosted variant: [Grafana Cloud Logs](https://grafana.com/loki).
|
||||
|
||||
See the [Loki's data source documentation](../datasources/loki) on how to query for log data.
|
||||
|
||||
### Switching from Metrics to Logs
|
||||
|
||||
If you switch from a Prometheus query to a logs query (you can do a split first to have your metrics and logs side by side) then it will keep the labels from your query that exist in the logs and use those to query the log streams. For example, the following Prometheus query:
|
||||
|
@ -91,67 +93,18 @@ after switching to the Logs datasource, the query changes to:
|
|||
|
||||
This will return a chunk of logs in the selected time range that can be grepped/text searched.
|
||||
|
||||
### Log Queries
|
||||
|
||||
A log query consists of two parts: **log stream selector**, and a **search expression**. For performance reasons you need to start by choosing a log stream by selecting a log label.
|
||||
|
||||
The Logs Explorer (the `Log labels` button) next to the query field shows a list of labels of available log streams. An alternative way to write a query is to use the query field's autocomplete - you start by typing a left curly brace `{` and the autocomplete menu will suggest a list of labels. Press the `enter` key to execute the query.
|
||||
|
||||
Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
|
||||
|
||||
<div class="medium-6 columns">
|
||||
<video width="800" height="500" controls>
|
||||
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
#### Log Stream Selector
|
||||
|
||||
For the label part of the query expression, wrap it in curly braces `{}` and then use the key value syntax for selecting labels. Multiple label expressions are separated by a comma:
|
||||
|
||||
`{app="mysql",name="mysql-backup"}`
|
||||
|
||||
The following label matching operators are currently supported:
|
||||
|
||||
- `=` exactly equal.
|
||||
- `!=` not equal.
|
||||
- `=~` regex-match.
|
||||
- `!~` do not regex-match.
|
||||
|
||||
Examples:
|
||||
|
||||
- `{name=~"mysql.+"}`
|
||||
- `{name!~"mysql.+"}`
|
||||
|
||||
The [same rules that apply for Prometheus Label Selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#instant-vector-selectors) apply for Loki Log Stream Selectors.
|
||||
|
||||
Another way to add a label selector, is in the table section, clicking on the **Filter** button beside a label will add the label to the query expression. This even works for multiple queries and will the label selector to each query.
|
||||
|
||||
#### Search Expression
|
||||
|
||||
After writing the Log Stream Selector, you can filter the results further by writing a search expression. The search expression can be just text or a regex expression.
|
||||
|
||||
Example queries:
|
||||
|
||||
- `{job="mysql"} error`
|
||||
- `{name="kafka"} tsdb-ops.*io:2003`
|
||||
- `{instance=~"kafka-[23]",name="kafka"} kafka.server:type=ReplicaManager`
|
||||
|
||||
### Deduping
|
||||
|
||||
Log data can be very repetitive and Explore can help by hiding duplicate log lines. There are a few different deduplication algorithms that you can use:
|
||||
|
||||
- `exact` Exact matches are done on the whole line, except for date fields.
|
||||
- `numbers` Matches on the line after stripping out numbers (durations, IP addresses etc.).
|
||||
- `signature` The most aggressive deduping - strips all letters and numbers, and matches on the remaining whitespace and punctuation.
|
||||
* `exact` Exact matches are done on the whole line, except for date fields.
|
||||
* `numbers` Matches on the line after stripping out numbers (durations, IP addresses etc.).
|
||||
* `signature` The most aggressive deduping - strips all letters and numbers, and matches on the remaining whitespace and punctuation.
|
||||
|
||||
### Timestamp, Local time and Labels
|
||||
|
||||
There are some other check boxes under the logging graph apart from the Deduping options.
|
||||
|
||||
- Timestamp: shows/hides the Timestamp column
|
||||
- Local time: shows/hides the Local time column
|
||||
- Labels: shows/hides the label filters column
|
||||
* Timestamp: shows/hides the Timestamp column
|
||||
* Local time: shows/hides the Local time column
|
||||
* Labels: shows/hides the label filters column
|
||||
|
|
|
@ -27,6 +27,7 @@ The main highlights are:
|
|||
- [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource
|
||||
- [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins.
|
||||
- [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker.
|
||||
- [Removal of user session storage]({{< relref "#easier-to-deploy-improved-security" >}}) makes Grafana easier to deploy & improves security.
|
||||
|
||||
## Explore
|
||||
|
||||
|
@ -113,30 +114,42 @@ will be shared closer to or just after release.
|
|||
{{< docs-imagebox img="/img/docs/v60/react_panels.png" max-width="600px" caption="React Panel" >}}
|
||||
<br />
|
||||
|
||||
### Google Stackdriver Datasource
|
||||
## Google Stackdriver Datasource
|
||||
|
||||
Built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) is officially released in Grafana 6.0. Beta support was added in Grafana 5.3 and we have added lots of improvements since then.
|
||||
|
||||
To get started read the guide: [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/).
|
||||
|
||||
### Azure Monitor Datasource
|
||||
## Azure Monitor Datasource
|
||||
|
||||
One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon Cloudwatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource will get alerting support for the official 6.0 release.
|
||||
|
||||
The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics.
|
||||
|
||||
### Provisioning support for alert notifiers
|
||||
## Provisioning support for alert notifiers
|
||||
|
||||
Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance.
|
||||
|
||||
### Auth and session token improvements
|
||||
## Easier to deploy & improved security
|
||||
|
||||
The previous session storage implementation in Grafana was causing problems in larger HA setups due to too many write requests to the database. The remember me token also have several security issues which is why we decided to rewrite auth middleware in Grafana and remove the session storage since most operations using the session storage could be rewritten to use cookies or data already made available earlier in the request.
|
||||
If you are using `Auth proxy` for authentication the session storage will still be used but our goal is to remove this ASAP as well.
|
||||
Grafana 6.0 removes the need of configuring and setup of additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a
|
||||
high availability setup and/or if you're using a stateless user session storage like Redis, Memcache, Postgres or MySQL.
|
||||
|
||||
This release will force all users to log in again since their previous token is not valid anymore.
|
||||
Instead of user sessions a solution based on short-lived tokens that are rotated frequently have been implemented. This also replaces the old "remember me cookie"
|
||||
solution, which allowed a user to be logged in between browser sessions, and which have been subject to several security holes throughout the years.
|
||||
Read more about the short-lived token solution and how to configure it [here](/auth/overview/#login-and-short-lived-tokens).
|
||||
|
||||
### Named Colors
|
||||
> Please note that due to these changes, all users will be required to login upon next visit after upgrade.
|
||||
|
||||
Besides these changes we have also made security improvements regarding Cross-Site Request Forgery (CSRF) and Cross-site Scripting (XSS) vulnerabilities:
|
||||
|
||||
* Cookies are per default using the [SameSite](/installation/configuration/#cookie-samesite) attribute to protect against CSRF attacks
|
||||
* Script tags in text panels are per default [disabled](/installation/configuration/#disable-sanitize-html) to protect against XSS attacks
|
||||
|
||||
> If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured
|
||||
but our goal is to remove this requirements in a near future.
|
||||
|
||||
## Named Colors
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
|
||||
|
||||
|
@ -148,12 +161,16 @@ Named colors also enables Grafana to adapt colors to the current theme.
|
|||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
### Other features
|
||||
## Other features
|
||||
|
||||
- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics.
|
||||
- Support for Google Hangouts Chat alert notifications
|
||||
- New built in template variables for the current time range in `$__from` and `$__to`
|
||||
|
||||
## Upgrading
|
||||
|
||||
See [upgrade notes](/installation/upgrading/#upgrading-to-v6-0).
|
||||
|
||||
## Changelog
|
||||
|
||||
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list of new features, changes, and bug fixes.
|
||||
|
|
|
@ -160,6 +160,13 @@ The path to the directory where the front end files (HTML, JS, and CSS
|
|||
files). Default to `public` which is why the Grafana binary needs to be
|
||||
executed with working directory set to the installation path.
|
||||
|
||||
### enable_gzip
|
||||
|
||||
Set this option to `true` to enable HTTP compression, this can improve
|
||||
transfer speed and bandwidth utilization. It is recommended that most
|
||||
users set it to `true`. By default it is set to `false` for compatibility
|
||||
reasons.
|
||||
|
||||
### cert_file
|
||||
|
||||
Path to the certificate file (if `protocol` is set to `https`).
|
||||
|
@ -594,7 +601,7 @@ Default setting for new alert rules. Defaults to categorize error and timeouts a
|
|||
|
||||
Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
|
||||
# concurrent_render_limit
|
||||
### concurrent_render_limit
|
||||
|
||||
> Available in 5.3 and above
|
||||
|
||||
|
|
|
@ -117,3 +117,34 @@ One of the database migrations included in this release will update all annotati
|
|||
We've got one report where using systemd, PostgreSQL and a large amount of annotations (table size 1645mb) took 8-20 minutes for the database migration to complete. However, the grafana-server process was killed after 90 seconds by systemd. Any database migration queries in progress when systemd kills the grafana-server process continues to execute in database until finished.
|
||||
|
||||
If you're using systemd and have a large amount of annotations consider temporary adjusting the systemd `TimeoutStartSec` setting to something high like `30m` before upgrading.
|
||||
|
||||
## Upgrading to v6.0
|
||||
|
||||
If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitzied HTML.
|
||||
Read more [here](/installation/configuration/#disable-sanitize-html) about this new setting.
|
||||
|
||||
### Authentication and security
|
||||
|
||||
If your using Grafana's builtin, LDAP (without Auth Proxy) or OAuth authentication all users will be required to login upon the next visit after the upgrade.
|
||||
|
||||
If you have `cookie_secure` set to `true` in the `session` section you probably want to change the `cookie_secure` to `true` in the `security` section as well. Ending up with a configuration like this:
|
||||
|
||||
```ini
|
||||
[session]
|
||||
cookie_secure = true
|
||||
|
||||
[security]
|
||||
cookie_secure = true
|
||||
```
|
||||
|
||||
The `login_remember_days`, `cookie_username` and `cookie_remember_name` settings in the `security` section are no longer being used so they're safe to remove.
|
||||
|
||||
If you have `login_remember_days` configured to 0 (zero) you should change your configuration to this to accomplish similar behavior, i.e. a logged in user will maximum be logged in for 1 day until being forced to login again:
|
||||
|
||||
```ini
|
||||
[auth]
|
||||
login_maximum_inactive_lifetime_days = 1
|
||||
login_maximum_lifetime_days = 1
|
||||
```
|
||||
|
||||
The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings.
|
|
@ -38,22 +38,81 @@ documentation article for details on value escaping during interpolation.
|
|||
|
||||
### Advanced Formatting Options
|
||||
|
||||
> Only available in Grafana v5.1+.
|
||||
|
||||
The formatting of the variable interpolation depends on the data source but there are some situations where you might want to change the default formatting. For example, the default for the MySql datasource is to join multiple values as comma-separated with quotes: `'server01','server02'`. In some cases you might want to have a comma-separated string without quotes: `server01,server02`. This is now possible with the advanced formatting options.
|
||||
|
||||
Syntax: `${var_name:option}`
|
||||
|
||||
Filter Option | Example | Raw | Interpolated | Description
|
||||
------------ | ------------- | ------------- | ------------- | -------------
|
||||
`glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | (Default) Formats multi-value variable into a glob (for Graphite queries)
|
||||
`regex` | ${servers:regex} | `'test.', 'test2'` | <code>(test\.|test2)</code> | Formats multi-value variable into a regex string
|
||||
`pipe` | ${servers:pipe} | `'test.', 'test2'` | <code>test.|test2</code> | Formats multi-value variable into a pipe-separated string
|
||||
`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
|
||||
`json`| ${servers:json} | `'test1', 'test2'` | `["test1","test2"]` | Formats multi-value variable as a JSON string
|
||||
`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
|
||||
`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
|
||||
`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
|
||||
#### Glob
|
||||
Formats multi-value variable into a glob (for Graphite queries).
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:glob}'
|
||||
Interpolation result: '{test1,test2}'
|
||||
```
|
||||
|
||||
### Regex
|
||||
Formats multi-value variable into a regex string.
|
||||
|
||||
```bash
|
||||
servers = ['test1.', 'test2']
|
||||
String to interpolate: '${servers:regex}'
|
||||
Interpolation result: '(test\.|test2)'
|
||||
```
|
||||
|
||||
### Pipe
|
||||
Formats multi-value variable into a pipe-separated string.
|
||||
|
||||
```bash
|
||||
servers = ['test1.', 'test2']
|
||||
String to interpolate: '${servers:pipe}'
|
||||
Interpolation result: 'test.|test2'
|
||||
```
|
||||
|
||||
### Csv
|
||||
Formats multi-value variable as a comma-separated string.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:csv}'
|
||||
Interpolation result: 'test,test2'
|
||||
```
|
||||
|
||||
### Json
|
||||
Formats multi-value variable as a comma-separated string.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:json}'
|
||||
Interpolation result: '["test1", "test2"]'
|
||||
```
|
||||
|
||||
### Distributed
|
||||
Formats multi-value variable in custom format for OpenTSDB.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:distributed}'
|
||||
Interpolation result: 'test1,servers=test2'
|
||||
```
|
||||
|
||||
### Lucene
|
||||
Formats multi-value variable in lucene format for Elasticsearch.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:lucene}'
|
||||
Interpolation result: '("test1" OR "test2")'
|
||||
```
|
||||
|
||||
### Percentencode
|
||||
Formats single & multi valued variables for use in URL parameters.
|
||||
|
||||
```bash
|
||||
servers = ['foo()bar BAZ', 'test2']
|
||||
String to interpolate: '${servers:lucene}'
|
||||
Interpolation result: 'foo%28%29bar%20BAZ%2Ctest2'
|
||||
```
|
||||
|
||||
Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ Setting up Grafana for high availability is fairly simple. It comes down to two
|
|||
2. Decide how to store session data.
|
||||
|
||||
<div class="text-center">
|
||||
<img src="/img/docs/tutorials/grafana-high-availability.png" max-width= "800px" class="center"></img>
|
||||
<img src="/img/docs/tutorials/grafana-high-availability.png" max-width= "800px" class="center" />
|
||||
</div>
|
||||
|
||||
## Configure multiple servers to use the same database
|
||||
|
@ -24,8 +24,14 @@ First, you need to do is to setup MySQL or Postgres on another server and config
|
|||
You can find the configuration for doing that in the [[database]]({{< relref "configuration.md" >}}#database) section in the grafana config.
|
||||
Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on for the database you're using.
|
||||
|
||||
## Alerting
|
||||
|
||||
Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
|
||||
|
||||
## User sessions
|
||||
|
||||
> Beginning with Grafana v6.0 and above the following only applies when using [Auth Proxy Authentication](/auth/auth-proxy/).
|
||||
|
||||
The second thing to consider is how to deal with user sessions and how to configure your load balancer in front of Grafana.
|
||||
Grafana supports two ways of storing session data: locally on disk or in a database/cache-server.
|
||||
If you want to store sessions on disk you can use `sticky sessions` in your load balancer. If you prefer to store session data in a database/cache-server
|
||||
|
@ -41,6 +47,4 @@ If you use MySQL/Postgres for session storage, you first need a table to store t
|
|||
|
||||
For Grafana itself it doesn't really matter if you store the session data on disk or database/redis/memcache. But we recommend using a database/redis/memcache since it makes it easier manage the grafana servers.
|
||||
|
||||
## Alerting
|
||||
|
||||
Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support distributing the alert rule execution between servers. That might be added in the future but right now prefer to keep it simple.
|
||||
|
|
12
package.json
12
package.json
|
@ -87,7 +87,7 @@
|
|||
"postcss-browser-reporter": "^0.5.0",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"postcss-reporter": "^5.0.0",
|
||||
"prettier": "1.9.2",
|
||||
"prettier": "1.16.4",
|
||||
"react-hot-loader": "^4.3.6",
|
||||
"react-test-renderer": "^16.5.0",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
|
@ -128,7 +128,8 @@
|
|||
"jest": "jest --notify --watch",
|
||||
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||
"storybook": "cd packages/grafana-ui && yarn storybook",
|
||||
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts"
|
||||
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
|
||||
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\""
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
@ -136,11 +137,7 @@
|
|||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*.scss": [
|
||||
"*.{ts,tsx,json,scss}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
|
@ -164,6 +161,7 @@
|
|||
"angular-native-dragdrop": "1.2.2",
|
||||
"angular-route": "1.6.6",
|
||||
"angular-sanitize": "1.6.6",
|
||||
"ansicolor": "1.1.78",
|
||||
"baron": "^3.0.3",
|
||||
"brace": "^0.10.0",
|
||||
"classnames": "^2.2.6",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { SeriesColorPicker, ColorPicker } from './ColorPicker';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
|
|
@ -30,7 +30,7 @@ export const warnAboutColorPickerPropsDeprecation = (componentName: string, prop
|
|||
|
||||
export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
popover: React.ComponentType<T>,
|
||||
displayName = 'ColorPicker',
|
||||
displayName = 'ColorPicker'
|
||||
) => {
|
||||
return class ColorPicker extends Component<T, any> {
|
||||
static displayName = displayName;
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('ColorPickerPopover', () => {
|
|||
|
||||
describe('rendering', () => {
|
||||
it('should render provided color as selected if color provided by name', () => {
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
|
||||
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
|
||||
|
||||
|
@ -25,7 +25,9 @@ describe('ColorPickerPopover', () => {
|
|||
});
|
||||
|
||||
it('should render provided color as selected if color provided by hex', () => {
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />);
|
||||
const wrapper = mount(
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />
|
||||
);
|
||||
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
|
||||
|
||||
|
@ -46,7 +48,11 @@ describe('ColorPickerPopover', () => {
|
|||
|
||||
it('should pass hex color value to onChange prop by default', () => {
|
||||
wrapper = mount(
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={getTheme(GrafanaThemeType.Light)} />
|
||||
<ColorPickerPopover
|
||||
color={BasicGreen.variants.dark}
|
||||
onChange={onChangeSpy}
|
||||
theme={getTheme(GrafanaThemeType.Light)}
|
||||
/>
|
||||
);
|
||||
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
|
||||
|
||||
|
|
|
@ -119,4 +119,3 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import { getTheme } from '../../themes';
|
|||
import { GrafanaThemeType } from '../../types';
|
||||
|
||||
describe('NamedColorsPalette', () => {
|
||||
|
||||
const BasicGreen = getColorDefinitionByName('green');
|
||||
|
||||
describe('theme support for named colors', () => {
|
||||
|
@ -23,13 +22,15 @@ describe('NamedColorsPalette', () => {
|
|||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
|
||||
wrapper.unmount();
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />);
|
||||
wrapper = mount(
|
||||
<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />
|
||||
);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
|
||||
});
|
||||
|
||||
it('should render dar variant of provided color when theme not provided', () => {
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProp
|
|||
},
|
||||
};
|
||||
|
||||
|
||||
const pointerColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.dark3,
|
||||
|
|
|
@ -37,4 +37,4 @@
|
|||
@include gradient-horizontal($scrollbarBackground, $scrollbarBackground2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
export interface Props {
|
||||
children: JSX.Element | string;
|
||||
}
|
||||
|
||||
const EmptySearchResult: FC<Props> = ({ children }) => {
|
||||
return <div className="empty-search-result">{children}</div>;
|
||||
};
|
||||
|
||||
export { EmptySearchResult };
|
|
@ -0,0 +1,8 @@
|
|||
.empty-search-result {
|
||||
border-left: 3px solid $info-box-border-color;
|
||||
background-color: $empty-list-cta-bg;
|
||||
padding: $spacer;
|
||||
min-width: 350px;
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: $spacer * 2;
|
||||
}
|
|
@ -31,7 +31,7 @@ export const FormLabel: FunctionComponent<Props> = ({
|
|||
<label className={classes} {...rest} htmlFor={htmlFor}>
|
||||
{children}
|
||||
{tooltip && (
|
||||
<Tooltip placement="top" content={tooltip} theme={"info"}>
|
||||
<Tooltip placement="top" content={tooltip} theme={'info'}>
|
||||
<div className="gf-form-help-icon gf-form-help-icon--right-normal">
|
||||
<i className="fa fa-info-circle" />
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ const setup = (propOverrides?: object) => {
|
|||
width: 300,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
theme: getTheme()
|
||||
theme: getTheme(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
@ -84,9 +84,9 @@ describe('Get thresholds formatted', () => {
|
|||
it('should get the correct formatted values when thresholds are added', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Themeable } from '../../index';
|
|||
type TimeSeriesValue = string | number | null;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
decimals: number;
|
||||
decimals?: number | null;
|
||||
height: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
|
@ -98,16 +98,15 @@ export class Gauge extends PureComponent<Props> {
|
|||
getFormattedThresholds() {
|
||||
const { maxValue, minValue, thresholds, theme } = this.props;
|
||||
|
||||
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
|
||||
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
|
||||
const lastThreshold = thresholds[thresholds.length - 1];
|
||||
|
||||
return [
|
||||
...thresholdsSortedByIndex.map(threshold => {
|
||||
...thresholds.map(threshold => {
|
||||
if (threshold.index === 0) {
|
||||
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
|
||||
}
|
||||
|
||||
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
|
||||
const previousThreshold = thresholds[threshold.index - 1];
|
||||
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
|
||||
}),
|
||||
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
|
||||
|
@ -116,9 +115,9 @@ export class Gauge extends PureComponent<Props> {
|
|||
|
||||
getFontScale(length: number): number {
|
||||
if (length > 12) {
|
||||
return FONT_SCALE - length * 5 / 120;
|
||||
return FONT_SCALE - (length * 5) / 120;
|
||||
}
|
||||
return FONT_SCALE - length * 5 / 105;
|
||||
return FONT_SCALE - (length * 5) / 105;
|
||||
}
|
||||
|
||||
draw() {
|
||||
|
|
|
@ -22,7 +22,7 @@ export class Graph extends PureComponent<GraphProps> {
|
|||
showBars: false,
|
||||
};
|
||||
|
||||
element: HTMLElement | null;
|
||||
element: HTMLElement | null = null;
|
||||
|
||||
componentDidUpdate() {
|
||||
this.draw();
|
||||
|
|
|
@ -6,10 +6,5 @@ interface Props {
|
|||
}
|
||||
|
||||
export const PanelOptionsGrid: SFC<Props> = ({ children }) => {
|
||||
|
||||
return (
|
||||
<div className="panel-options-grid">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className="panel-options-grid">{children}</div>;
|
||||
};
|
||||
|
|
|
@ -61,7 +61,9 @@ interface AsyncProps {
|
|||
export const MenuList = (props: any) => {
|
||||
return (
|
||||
<components.MenuList {...props}>
|
||||
<CustomScrollbar autoHide={false} autoHeightMax="inherit">{props.children}</CustomScrollbar>
|
||||
<CustomScrollbar autoHide={false} autoHeightMax="inherit">
|
||||
{props.children}
|
||||
</CustomScrollbar>
|
||||
</components.MenuList>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface State {
|
|||
|
||||
export class Switch extends PureComponent<Props, State> {
|
||||
state = {
|
||||
id: _.uniqueId(),
|
||||
id: _.uniqueId('check-'),
|
||||
};
|
||||
|
||||
internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
|
@ -29,18 +29,20 @@ export class Switch extends PureComponent<Props, State> {
|
|||
render() {
|
||||
const { labelClass = '', switchClass = '', label, checked, transparent, className } = this.props;
|
||||
|
||||
const labelId = `check-${this.state.id}`;
|
||||
const labelId = this.state.id;
|
||||
const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
|
||||
const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
|
||||
|
||||
return (
|
||||
<label htmlFor={labelId} className={`gf-form gf-form-switch-container ${className}`}>
|
||||
{label && <div className={labelClassName}>{label}</div>}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<span className="gf-form-switch__slider" />
|
||||
</div>
|
||||
</label>
|
||||
<div className="gf-form-switch-container-react">
|
||||
<label htmlFor={labelId} className={`gf-form gf-form-switch-container ${className || ''}`}>
|
||||
{label && <div className={labelClassName}>{label}</div>}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<span className="gf-form-switch__slider" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React, { ChangeEvent } from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
onChange: jest.fn(),
|
||||
thresholds: [],
|
||||
|
@ -11,12 +10,26 @@ const setup = (propOverrides?: object) => {
|
|||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
||||
const wrapper = mount(<ThresholdsEditor {...props} />);
|
||||
const instance = wrapper.instance() as ThresholdsEditor;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render with base threshold', () => {
|
||||
const { wrapper } = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should add a base threshold if missing', () => {
|
||||
const instance = setup();
|
||||
const { instance } = setup();
|
||||
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||
});
|
||||
|
@ -24,7 +37,7 @@ describe('Initialization', () => {
|
|||
|
||||
describe('Add threshold', () => {
|
||||
it('should not add threshold at index 0', () => {
|
||||
const instance = setup();
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onAddThreshold(0);
|
||||
|
||||
|
@ -32,32 +45,32 @@ describe('Add threshold', () => {
|
|||
});
|
||||
|
||||
it('should add threshold', () => {
|
||||
const instance = setup();
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onAddThreshold(1);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add another threshold above a first', () => {
|
||||
const instance = setup({
|
||||
const { instance } = setup({
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
|
||||
});
|
||||
|
||||
instance.onAddThreshold(2);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add another threshold between first and second index', () => {
|
||||
const instance = setup({
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
|
@ -68,10 +81,10 @@ describe('Add threshold', () => {
|
|||
instance.onAddThreshold(2);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 3, value: 75, color: '#6ED0E0' },
|
||||
{ index: 2, value: 62.5, color: '#EF843C' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 62.5, color: '#EF843C' },
|
||||
{ index: 3, value: 75, color: '#6ED0E0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -83,7 +96,7 @@ describe('Remove threshold', () => {
|
|||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const instance = setup({ thresholds });
|
||||
const { instance } = setup({ thresholds });
|
||||
|
||||
instance.onRemoveThreshold(thresholds[0]);
|
||||
|
||||
|
@ -96,9 +109,7 @@ describe('Remove threshold', () => {
|
|||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const instance = setup({
|
||||
thresholds,
|
||||
});
|
||||
const { instance } = setup({ thresholds });
|
||||
|
||||
instance.onRemoveThreshold(thresholds[1]);
|
||||
|
||||
|
@ -116,7 +127,7 @@ describe('change threshold value', () => {
|
|||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const instance = setup({ thresholds });
|
||||
const { instance } = setup({ thresholds });
|
||||
|
||||
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
|
||||
|
||||
|
@ -126,7 +137,7 @@ describe('change threshold value', () => {
|
|||
});
|
||||
|
||||
it('should update value', () => {
|
||||
const instance = setup();
|
||||
const { instance } = setup();
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
|
@ -150,24 +161,24 @@ describe('change threshold value', () => {
|
|||
});
|
||||
|
||||
describe('on blur threshold value', () => {
|
||||
it('should resort rows and update indexes', () => {
|
||||
const instance = setup();
|
||||
it.only('should resort rows and update indexes', () => {
|
||||
const { instance } = setup();
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 78, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
|
||||
instance.state = {
|
||||
instance.setState({
|
||||
thresholds,
|
||||
};
|
||||
});
|
||||
|
||||
instance.onBlur();
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 2, value: 78, color: '#EAB839' },
|
||||
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||
{ index: 2, value: 78, color: '#EAB839' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import { Threshold } from '../../types';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
import { ColorPicker } from '..';
|
||||
import { PanelOptionsGroup } from '..';
|
||||
import { colors } from '../../utils';
|
||||
import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui';
|
||||
|
||||
|
@ -54,16 +54,16 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
|
||||
|
||||
// Set a color
|
||||
const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
|
||||
const color = colors.filter(c => !newThresholds.some(t => t.color === c))[0];
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: this.sortThresholds([
|
||||
...newThresholds,
|
||||
{
|
||||
color,
|
||||
index,
|
||||
value: value as number,
|
||||
color,
|
||||
},
|
||||
]),
|
||||
},
|
||||
|
@ -137,10 +137,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||
onBlur = () => {
|
||||
this.setState(prevState => {
|
||||
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||
let index = sortThresholds.length - 1;
|
||||
let index = 0;
|
||||
sortThresholds.forEach(t => {
|
||||
t.index = index--;
|
||||
t.index = index++;
|
||||
});
|
||||
|
||||
return { thresholds: sortThresholds };
|
||||
});
|
||||
|
||||
|
@ -153,12 +154,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||
|
||||
sortThresholds = (thresholds: Threshold[]) => {
|
||||
return thresholds.sort((t1, t2) => {
|
||||
return t2.value - t1.value;
|
||||
return t1.value - t2.value;
|
||||
});
|
||||
};
|
||||
|
||||
renderInput = (threshold: Threshold) => {
|
||||
const value = threshold.index === 0 ? 'Base' : threshold.value;
|
||||
return (
|
||||
<div className="thresholds-row-input-inner">
|
||||
<span className="thresholds-row-input-inner-arrow" />
|
||||
|
@ -169,51 +169,60 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="thresholds-row-input-inner-value">
|
||||
<input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||
value={value}
|
||||
onBlur={this.onBlur}
|
||||
readOnly={threshold.index === 0}
|
||||
/>
|
||||
</div>
|
||||
{threshold.index > 0 && (
|
||||
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
|
||||
<i className="fa fa-times" />
|
||||
{threshold.index === 0 && (
|
||||
<div className="thresholds-row-input-inner-value">
|
||||
<input type="text" value="Base" readOnly />
|
||||
</div>
|
||||
)}
|
||||
{threshold.index > 0 && (
|
||||
<>
|
||||
<div className="thresholds-row-input-inner-value">
|
||||
<input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||
value={threshold.value}
|
||||
onBlur={this.onBlur}
|
||||
readOnly={threshold.index === 0}
|
||||
/>
|
||||
</div>
|
||||
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
|
||||
<i className="fa fa-times" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
{thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||
<div
|
||||
className="thresholds-row-add-button"
|
||||
onClick={() => this.onAddThreshold(threshold.index + 1)}
|
||||
>
|
||||
<i className="fa fa-plus" />
|
||||
{thresholds
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((threshold, index) => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||
<div
|
||||
className="thresholds-row-add-button"
|
||||
onClick={() => this.onAddThreshold(threshold.index + 1)}
|
||||
>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
}
|
||||
|
||||
.thresholds-row-input {
|
||||
margin-top: 49px;
|
||||
margin-top: 44px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render with base threshold 1`] = `
|
||||
<ContextConsumer>
|
||||
<Component />
|
||||
</ContextConsumer>
|
||||
`;
|
|
@ -17,12 +17,7 @@ const transitionStyles: { [key: string]: object } = {
|
|||
exiting: { opacity: 0, transitionDelay: '500ms' },
|
||||
};
|
||||
|
||||
export type RenderPopperArrowFn = (
|
||||
props: {
|
||||
arrowProps: PopperArrowProps;
|
||||
placement: string;
|
||||
}
|
||||
) => JSX.Element;
|
||||
export type RenderPopperArrowFn = (props: { arrowProps: PopperArrowProps; placement: string }) => JSX.Element;
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
show: boolean;
|
||||
|
@ -35,8 +30,16 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
|||
|
||||
class Popper extends PureComponent<Props> {
|
||||
render() {
|
||||
const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props;
|
||||
const { content } = this.props;
|
||||
const {
|
||||
content,
|
||||
show,
|
||||
placement,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
className,
|
||||
wrapperClassName,
|
||||
renderArrow,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
|
@ -50,7 +53,7 @@ class Popper extends PureComponent<Props> {
|
|||
// TODO: move modifiers config to popper controller
|
||||
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
|
||||
>
|
||||
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
||||
{({ ref, style, placement, arrowProps }) => {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
|
@ -65,11 +68,7 @@ class Popper extends PureComponent<Props> {
|
|||
className={`${wrapperClassName}`}
|
||||
>
|
||||
<div className={className}>
|
||||
{typeof content === 'string'
|
||||
? content
|
||||
: React.cloneElement(content, {
|
||||
updatePopperPosition: scheduleUpdate,
|
||||
})}
|
||||
{typeof content === 'string' ? content : React.cloneElement(content)}
|
||||
{renderArrow &&
|
||||
renderArrow({
|
||||
arrowProps,
|
||||
|
|
|
@ -21,7 +21,7 @@ export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) =
|
|||
onMouseEnter={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
referenceElement={tooltipTriggerRef.current}
|
||||
wrapperClassName='popper'
|
||||
wrapperClassName="popper"
|
||||
className={popperBackgroundClassName}
|
||||
renderArrow={({ arrowProps, placement }) => (
|
||||
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
|
||||
|
|
|
@ -82,15 +82,15 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
|||
|
||||
return (
|
||||
<PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
key={`${valueMapping.text}-${index}`}
|
||||
valueMapping={valueMapping}
|
||||
updateValueMapping={this.updateGauge}
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
key={`${valueMapping.text}-${index}`}
|
||||
valueMapping={valueMapping}
|
||||
updateValueMapping={this.updateGauge}
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@
|
|||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||
@import 'ColorPicker/ColorPicker';
|
||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
@import "FormField/FormField";
|
||||
@import 'EmptySearchResult/EmptySearchResult';
|
||||
@import 'FormField/FormField';
|
||||
|
|
|
@ -23,3 +23,4 @@ export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
|||
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
export { Switch } from './Switch/Switch';
|
||||
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
||||
|
|
|
@ -129,7 +129,6 @@ $page-header-border-color: $dark-9;
|
|||
$divider-border-color: $gray-1;
|
||||
|
||||
// Graphite Target Editor
|
||||
$tight-form-bg: $dark-3; //remove after merge!!!
|
||||
$tight-form-func-bg: $dark-9;
|
||||
$tight-form-func-highlight-bg: $dark-10;
|
||||
|
||||
|
@ -157,7 +156,6 @@ $scrollbarBorder: $dark-10;
|
|||
|
||||
// Tables
|
||||
// -------------------------
|
||||
$table-bg: transparent; // remove after merge!!!
|
||||
$table-bg-accent: $dark-6; // for striping
|
||||
$table-border: $dark-6; // table and cell border
|
||||
|
||||
|
|
|
@ -115,7 +115,6 @@ $page-header-border-color: $gray-4;
|
|||
$divider-border-color: $gray-2;
|
||||
|
||||
// Graphite Target Editor
|
||||
$tight-form-bg: $dark-3; //remove after merge!!!
|
||||
$tight-form-func-bg: $gray-5;
|
||||
$tight-form-func-highlight-bg: $gray-6;
|
||||
|
||||
|
@ -143,7 +142,6 @@ $scrollbarBorder: $gray-3;
|
|||
|
||||
// Tables
|
||||
// -------------------------
|
||||
$table-bg: transparent; // remove after merge!!!
|
||||
$table-bg-accent: $gray-5; // for striping
|
||||
$table-border: $gray-3; // table and cell border
|
||||
|
||||
|
|
|
@ -21,12 +21,31 @@ $spacer: 1rem !default;
|
|||
$spacer-x: $spacer !default;
|
||||
$spacer-y: $spacer !default;
|
||||
$spacers: (
|
||||
0: (x: 0, y: 0),
|
||||
1: (x: $spacer-x, y: $spacer-y),
|
||||
2: (x: ($spacer-x * 1.5), y: ($spacer-y * 1.5)),
|
||||
3: (x: ($spacer-x * 3), y: ($spacer-y * 3))
|
||||
)
|
||||
!default;
|
||||
0: (
|
||||
x: 0,
|
||||
y: 0,
|
||||
),
|
||||
1: (
|
||||
x: $spacer-x,
|
||||
y: $spacer-y,
|
||||
),
|
||||
2: (
|
||||
x: (
|
||||
$spacer-x * 1.5,
|
||||
),
|
||||
y: (
|
||||
$spacer-y * 1.5,
|
||||
),
|
||||
),
|
||||
3: (
|
||||
x: (
|
||||
$spacer-x * 3,
|
||||
),
|
||||
y: (
|
||||
$spacer-y * 3,
|
||||
),
|
||||
),
|
||||
) !default;
|
||||
$border-width: 1px !default;
|
||||
|
||||
// Grid breakpoints
|
||||
|
@ -34,13 +53,24 @@ $border-width: 1px !default;
|
|||
// Define the minimum and maximum dimensions at which your layout will change,
|
||||
// adapting to different screen sizes, for use in media queries.
|
||||
|
||||
$grid-breakpoints: (xs: 0, sm: 544px, md: 768px, lg: 992px, xl: 1200px) !default;
|
||||
$grid-breakpoints: (
|
||||
xs: 0,
|
||||
sm: 544px,
|
||||
md: 768px,
|
||||
lg: 992px,
|
||||
xl: 1200px,
|
||||
) !default;
|
||||
|
||||
// Grid containers
|
||||
//
|
||||
// Define the maximum width of .container for different screen sizes.
|
||||
// Define the maximum width of \`.container\` for different screen sizes.
|
||||
|
||||
$container-max-widths: (sm: 576px, md: 720px, lg: 940px, xl: 1080px) !default;
|
||||
$container-max-widths: (
|
||||
sm: 576px,
|
||||
md: 720px,
|
||||
lg: 940px,
|
||||
xl: 1080px,
|
||||
) !default;
|
||||
|
||||
// Grid columns
|
||||
//
|
||||
|
@ -54,9 +84,9 @@ $enable-flex: true;
|
|||
// Typography
|
||||
// -------------------------
|
||||
|
||||
$font-family-sans-serif: ${theme.typography.fontFamily.sansSerif};
|
||||
$font-family-serif: ${theme.typography.fontFamily.serif};
|
||||
$font-family-monospace: ${theme.typography.fontFamily.monospace};
|
||||
$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
$font-family-serif: Georgia, 'Times New Roman', Times, serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
$font-family-base: $font-family-sans-serif !default;
|
||||
|
||||
$font-size-root: 14px !default;
|
||||
|
@ -107,7 +137,7 @@ $line-height-sm: 1.5 !default;
|
|||
|
||||
$border-radius: 3px !default;
|
||||
$border-radius-lg: 5px !default;
|
||||
$border-radius-sm: 2px!default;
|
||||
$border-radius-sm: 2px !default;
|
||||
|
||||
// Page
|
||||
|
||||
|
@ -121,7 +151,7 @@ $link-hover-decoration: none !default;
|
|||
|
||||
// Tables
|
||||
//
|
||||
// Customizes the table component with basic values, each used across all table variations.
|
||||
// Customizes the \`.table\` component with basic values, each used across all table variations.
|
||||
|
||||
$table-cell-padding: 4px 10px !default;
|
||||
|
||||
|
@ -150,12 +180,9 @@ $gf-form-input-height: 35px;
|
|||
$cursor-disabled: not-allowed !default;
|
||||
|
||||
// Form validation icons
|
||||
$form-icon-success: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")
|
||||
!default;
|
||||
$form-icon-warning: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")
|
||||
!default;
|
||||
$form-icon-danger: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")
|
||||
!default;
|
||||
$form-icon-success: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E") !default;
|
||||
$form-icon-warning: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E") !default;
|
||||
$form-icon-danger: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E") !default;
|
||||
|
||||
// Z-index master list
|
||||
// -------------------------
|
||||
|
@ -198,19 +225,38 @@ $panel-margin: 10px;
|
|||
$dashboard-padding: $panel-margin * 2;
|
||||
$panel-horizontal-padding: 10;
|
||||
$panel-vertical-padding: 5;
|
||||
$panel-padding: 0px $panel-horizontal-padding+0px $panel-vertical-padding+0px $panel-horizontal-padding+0px;
|
||||
$panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
|
||||
|
||||
// tabs
|
||||
$tabs-padding: 10px 15px 9px;
|
||||
|
||||
$external-services: (
|
||||
github: (bgColor: #464646, borderColor: #393939, icon: ''),
|
||||
gitlab: (bgColor: #fc6d26, borderColor: #e24329, icon: ''),
|
||||
google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''),
|
||||
grafanacom: (bgColor: #262628, borderColor: #393939, icon: ''),
|
||||
oauth: (bgColor: #262628, borderColor: #393939, icon: '')
|
||||
)
|
||||
!default;
|
||||
github: (
|
||||
bgColor: #464646,
|
||||
borderColor: #393939,
|
||||
icon: '',
|
||||
),
|
||||
gitlab: (
|
||||
bgColor: #fc6d26,
|
||||
borderColor: #e24329,
|
||||
icon: '',
|
||||
),
|
||||
google: (
|
||||
bgColor: #e84d3c,
|
||||
borderColor: #b83e31,
|
||||
icon: '',
|
||||
),
|
||||
grafanacom: (
|
||||
bgColor: #262628,
|
||||
borderColor: #393939,
|
||||
icon: '',
|
||||
),
|
||||
oauth: (
|
||||
bgColor: #262628,
|
||||
borderColor: #393939,
|
||||
icon: '',
|
||||
),
|
||||
) !default;
|
||||
|
||||
:export {
|
||||
panelhorizontalpadding: $panel-horizontal-padding;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import tinycolor from 'tinycolor2';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import defaultTheme from './default';
|
||||
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ let themeMock: ((name?: string) => GrafanaTheme) | null;
|
|||
|
||||
export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme);
|
||||
|
||||
export const mockTheme = (mock: (name: string) => GrafanaTheme) => {
|
||||
export const mockTheme = (mock: (name?: string) => GrafanaTheme) => {
|
||||
themeMock = mock;
|
||||
return () => {
|
||||
themeMock = null;
|
||||
|
|
|
@ -40,12 +40,10 @@ describe('Theme variable variant selector', () => {
|
|||
it('return dark theme variant if no theme given', () => {
|
||||
const theme = lightThemeMock;
|
||||
|
||||
const selectedValue = selectThemeVariant(
|
||||
{
|
||||
dark: theme.color.red,
|
||||
light: theme.color.green,
|
||||
}
|
||||
);
|
||||
const selectedValue = selectThemeVariant({
|
||||
dark: theme.color.red,
|
||||
light: theme.color.green,
|
||||
});
|
||||
|
||||
expect(selectedValue).toBe(lightThemeMock.color.red);
|
||||
});
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { GrafanaThemeType } from '../types/theme';
|
||||
import { GrafanaThemeType } from '../types/theme';
|
||||
|
||||
type VariantDescriptor = {
|
||||
[key in GrafanaThemeType]: string | number;
|
||||
};
|
||||
type VariantDescriptor = { [key in GrafanaThemeType]: string | number };
|
||||
|
||||
export const selectThemeVariant = (variants: VariantDescriptor, currentTheme?: GrafanaThemeType) => {
|
||||
return variants[currentTheme || GrafanaThemeType.Dark];
|
||||
|
|
|
@ -29,6 +29,16 @@ export interface DataQuery {
|
|||
datasource?: string | null;
|
||||
}
|
||||
|
||||
export interface DataQueryError {
|
||||
data?: {
|
||||
message?: string;
|
||||
error?: string;
|
||||
};
|
||||
message?: string;
|
||||
status?: string;
|
||||
statusText?: string;
|
||||
}
|
||||
|
||||
export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
|
||||
timezone: string;
|
||||
range: TimeRange;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
export * from './data';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ComponentClass } from 'react';
|
||||
import { TimeSeries, LoadingState, TableData } from './data';
|
||||
import { TimeRange } from './time';
|
||||
|
||||
|
@ -19,11 +20,29 @@ export interface PanelData {
|
|||
tableData?: TableData;
|
||||
}
|
||||
|
||||
export interface PanelOptionsProps<T = any> {
|
||||
export interface PanelEditorProps<T = any> {
|
||||
options: T;
|
||||
onChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export class ReactPanelPlugin<TOptions = any> {
|
||||
panel: ComponentClass<PanelProps<TOptions>>;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
defaults?: TOptions;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
setDefaults(defaults: TOptions) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PanelSize {
|
||||
width: number;
|
||||
height: number;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ComponentClass } from 'react';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { ReactPanelPlugin } from './panel';
|
||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
|
||||
|
||||
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||
|
@ -81,9 +81,7 @@ export interface PluginExports {
|
|||
|
||||
// Panel plugin
|
||||
PanelCtrl?: any;
|
||||
Panel?: ComponentClass<PanelProps>;
|
||||
PanelOptions?: ComponentClass<PanelOptionsProps>;
|
||||
PanelDefaults?: any;
|
||||
reactPanel: ReactPanelPlugin;
|
||||
}
|
||||
|
||||
export interface PluginMeta {
|
||||
|
|
|
@ -51,7 +51,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
|
|||
}
|
||||
|
||||
if (currentValue !== null && typeof currentValue !== 'number') {
|
||||
throw {message: 'Time series contains non number values'};
|
||||
throw { message: 'Time series contains non number values' };
|
||||
}
|
||||
|
||||
// Due to missing values we could have different timeStep all along the series
|
||||
|
|
|
@ -15,12 +15,7 @@ const ThemableStory: React.FunctionComponent<{}> = ({ children }) => {
|
|||
GrafanaThemeType.Dark
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={getTheme(themeKnob)}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
|
||||
);
|
||||
return <ThemeContext.Provider value={getTheme(themeKnob)}>{children}</ThemeContext.Provider>;
|
||||
};
|
||||
|
||||
// Temporary solution. When we update to Storybook V5 we will be able to pass data from decorator to story
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { toFixed } from './valueFormats';
|
||||
import { toFixed, DecimalCount } from './valueFormats';
|
||||
|
||||
export function toPercent(size: number, decimals: number) {
|
||||
export function toPercent(size: number, decimals: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
return toFixed(size, decimals) + '%';
|
||||
}
|
||||
|
||||
export function toPercentUnit(size: number, decimals: number) {
|
||||
export function toPercentUnit(size: number, decimals: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
return toFixed(100 * size, decimals) + '%';
|
||||
}
|
||||
|
||||
export function toHex0x(value: number, decimals: number) {
|
||||
export function toHex0x(value: number, decimals: DecimalCount) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ export function toHex0x(value: number, decimals: number) {
|
|||
return '0x' + hexString;
|
||||
}
|
||||
|
||||
export function toHex(value: number, decimals: number) {
|
||||
export function toHex(value: number, decimals: DecimalCount) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -34,9 +34,9 @@ export function toHex(value: number, decimals: number) {
|
|||
.toUpperCase();
|
||||
}
|
||||
|
||||
export function sci(value: number, decimals: number) {
|
||||
export function sci(value: number, decimals: DecimalCount) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toExponential(decimals);
|
||||
return value.toExponential(decimals as number);
|
||||
}
|
||||
|
|
|
@ -191,6 +191,7 @@ export const getCategories = (): ValueFormatCategory[] => [
|
|||
{ name: 'Litre/hour', id: 'litreh', fn: toFixedUnit('l/h') },
|
||||
{ name: 'Litre/min (l/min)', id: 'flowlpm', fn: toFixedUnit('l/min') },
|
||||
{ name: 'milliLitre/min (mL/min)', id: 'flowmlpm', fn: toFixedUnit('mL/min') },
|
||||
{ name: 'Lux (lx)', id: 'lux', fn: toFixedUnit('lux') },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -306,7 +307,7 @@ export const getCategories = (): ValueFormatCategory[] => [
|
|||
{ name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') },
|
||||
{ name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') },
|
||||
{ name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') },
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Volume',
|
||||
|
@ -318,5 +319,5 @@ export const getCategories = (): ValueFormatCategory[] => [
|
|||
{ name: 'cubic decimetre', id: 'dm3', fn: toFixedUnit('dm³') },
|
||||
{ name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') },
|
||||
],
|
||||
}
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { toFixed, toFixedScaled } from './valueFormats';
|
||||
import { toFixed, toFixedScaled, DecimalCount } from './valueFormats';
|
||||
import moment from 'moment';
|
||||
|
||||
interface IntervalsInSeconds {
|
||||
|
@ -27,7 +27,7 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
|
|||
[Interval.Millisecond]: 0.001,
|
||||
};
|
||||
|
||||
export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ export function toNanoSeconds(size: number, decimals: number, scaledDecimals: nu
|
|||
}
|
||||
}
|
||||
|
||||
export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export function toMicroSeconds(size: number, decimals: number, scaledDecimals: n
|
|||
}
|
||||
}
|
||||
|
||||
export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -83,22 +83,29 @@ export function toMilliSeconds(size: number, decimals: number, scaledDecimals: n
|
|||
return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
|
||||
}
|
||||
|
||||
export function toSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount {
|
||||
if (value1 !== null && value1 !== undefined && value2 !== null && value2 !== undefined) {
|
||||
return value1 - value2;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Less than 1 µs, divide in ns
|
||||
if (Math.abs(size) < 0.000001) {
|
||||
return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns');
|
||||
return toFixedScaled(size * 1e9, decimals, trySubstract(scaledDecimals, decimals), -9, ' ns');
|
||||
}
|
||||
// Less than 1 ms, divide in µs
|
||||
if (Math.abs(size) < 0.001) {
|
||||
return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs');
|
||||
return toFixedScaled(size * 1e6, decimals, trySubstract(scaledDecimals, decimals), -6, ' µs');
|
||||
}
|
||||
// Less than 1 second, divide in ms
|
||||
if (Math.abs(size) < 1) {
|
||||
return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms');
|
||||
return toFixedScaled(size * 1e3, decimals, trySubstract(scaledDecimals, decimals), -3, ' ms');
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 60) {
|
||||
|
@ -120,7 +127,7 @@ export function toSeconds(size: number, decimals: number, scaledDecimals: number
|
|||
return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
|
||||
}
|
||||
|
||||
export function toMinutes(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -138,7 +145,7 @@ export function toMinutes(size: number, decimals: number, scaledDecimals: number
|
|||
}
|
||||
}
|
||||
|
||||
export function toHours(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -154,7 +161,7 @@ export function toHours(size: number, decimals: number, scaledDecimals: number)
|
|||
}
|
||||
}
|
||||
|
||||
export function toDays(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -168,13 +175,15 @@ export function toDays(size: number, decimals: number, scaledDecimals: number) {
|
|||
}
|
||||
}
|
||||
|
||||
export function toDuration(size: number, decimals: number, timeScale: Interval): string {
|
||||
export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): string {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (size === 0) {
|
||||
return '0 ' + timeScale + 's';
|
||||
}
|
||||
|
||||
if (size < 0) {
|
||||
return toDuration(-size, decimals, timeScale) + ' ago';
|
||||
}
|
||||
|
@ -189,14 +198,22 @@ export function toDuration(size: number, decimals: number, timeScale: Interval):
|
|||
{ long: Interval.Second },
|
||||
{ long: Interval.Millisecond },
|
||||
];
|
||||
|
||||
// convert $size to milliseconds
|
||||
// intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
|
||||
size *= INTERVALS_IN_SECONDS[timeScale] * 1000;
|
||||
|
||||
const strings = [];
|
||||
|
||||
// after first value >= 1 print only $decimals more
|
||||
let decrementDecimals = false;
|
||||
for (let i = 0; i < units.length && decimals >= 0; i++) {
|
||||
let decimalsCount = 0;
|
||||
|
||||
if (decimals !== null || decimals !== undefined) {
|
||||
decimalsCount = decimals as number;
|
||||
}
|
||||
|
||||
for (let i = 0; i < units.length && decimalsCount >= 0; i++) {
|
||||
const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000;
|
||||
const value = size / interval;
|
||||
if (value >= 1 || decrementDecimals) {
|
||||
|
@ -205,14 +222,14 @@ export function toDuration(size: number, decimals: number, timeScale: Interval):
|
|||
const unit = units[i].long + (floor !== 1 ? 's' : '');
|
||||
strings.push(floor + ' ' + unit);
|
||||
size = size % interval;
|
||||
decimals--;
|
||||
decimalsCount--;
|
||||
}
|
||||
}
|
||||
|
||||
return strings.join(', ');
|
||||
}
|
||||
|
||||
export function toClock(size: number, decimals?: number) {
|
||||
export function toClock(size: number, decimals?: DecimalCount) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -257,11 +274,11 @@ export function toClock(size: number, decimals?: number) {
|
|||
return format ? `${hours}:${moment.utc(size).format(format)}` : hours;
|
||||
}
|
||||
|
||||
export function toDurationInMilliseconds(size: number, decimals: number) {
|
||||
export function toDurationInMilliseconds(size: number, decimals: DecimalCount) {
|
||||
return toDuration(size, decimals, Interval.Millisecond);
|
||||
}
|
||||
|
||||
export function toDurationInSeconds(size: number, decimals: number) {
|
||||
export function toDurationInSeconds(size: number, decimals: DecimalCount) {
|
||||
return toDuration(size, decimals, Interval.Second);
|
||||
}
|
||||
|
||||
|
@ -276,19 +293,19 @@ export function toDurationInHoursMinutesSeconds(size: number) {
|
|||
return strings.join(':');
|
||||
}
|
||||
|
||||
export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) {
|
||||
export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount) {
|
||||
return toSeconds(size, decimals, scaledDecimals);
|
||||
}
|
||||
|
||||
export function toClockMilliseconds(size: number, decimals: number) {
|
||||
export function toClockMilliseconds(size: number, decimals: DecimalCount) {
|
||||
return toClock(size, decimals);
|
||||
}
|
||||
|
||||
export function toClockSeconds(size: number, decimals: number) {
|
||||
export function toClockSeconds(size: number, decimals: DecimalCount) {
|
||||
return toClock(size * 1000, decimals);
|
||||
}
|
||||
|
||||
export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
|
||||
export function dateTimeAsIso(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
|
||||
const time = isUtc ? moment.utc(value) : moment(value);
|
||||
|
||||
if (moment().isSame(value, 'day')) {
|
||||
|
@ -297,7 +314,7 @@ export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: n
|
|||
return time.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
|
||||
export function dateTimeAsUS(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
|
||||
const time = isUtc ? moment.utc(value) : moment(value);
|
||||
|
||||
if (moment().isSame(value, 'day')) {
|
||||
|
@ -306,7 +323,7 @@ export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: nu
|
|||
return time.format('MM/DD/YYYY h:mm:ss a');
|
||||
}
|
||||
|
||||
export function dateTimeFromNow(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
|
||||
export function dateTimeFromNow(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
|
||||
const time = isUtc ? moment.utc(value) : moment(value);
|
||||
return time.fromNow();
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { scaledUnits } from './valueFormats';
|
||||
import { scaledUnits, DecimalCount } from './valueFormats';
|
||||
|
||||
export function currency(symbol: string) {
|
||||
const units = ['', 'K', 'M', 'B', 'T'];
|
||||
const scaler = scaledUnits(1000, units);
|
||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
||||
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { getCategories } from './categories';
|
||||
|
||||
type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string;
|
||||
export type DecimalCount = number | null | undefined;
|
||||
|
||||
interface ValueFormat {
|
||||
export type ValueFormatter = (
|
||||
value: number,
|
||||
decimals?: DecimalCount,
|
||||
scaledDecimals?: DecimalCount,
|
||||
isUtc?: boolean
|
||||
) => string;
|
||||
|
||||
export interface ValueFormat {
|
||||
name: string;
|
||||
id: string;
|
||||
fn: ValueFormatter;
|
||||
|
@ -22,7 +29,7 @@ let categories: ValueFormatCategory[] = [];
|
|||
const index: ValueFormatterIndex = {};
|
||||
let hasBuiltIndex = false;
|
||||
|
||||
export function toFixed(value: number, decimals?: number): string {
|
||||
export function toFixed(value: number, decimals?: DecimalCount): string {
|
||||
if (value === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -50,20 +57,24 @@ export function toFixed(value: number, decimals?: number): string {
|
|||
|
||||
export function toFixedScaled(
|
||||
value: number,
|
||||
decimals: number,
|
||||
scaledDecimals: number,
|
||||
additionalDecimals: number,
|
||||
ext: string
|
||||
decimals?: DecimalCount,
|
||||
scaledDecimals?: DecimalCount,
|
||||
additionalDecimals?: DecimalCount,
|
||||
ext?: string
|
||||
) {
|
||||
if (scaledDecimals === null) {
|
||||
return toFixed(value, decimals) + ext;
|
||||
} else {
|
||||
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
|
||||
if (scaledDecimals) {
|
||||
if (additionalDecimals) {
|
||||
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
|
||||
} else {
|
||||
return toFixed(value, scaledDecimals) + ext;
|
||||
}
|
||||
}
|
||||
|
||||
return toFixed(value, decimals) + ext;
|
||||
}
|
||||
|
||||
export function toFixedUnit(unit: string) {
|
||||
return (size: number, decimals: number) => {
|
||||
export function toFixedUnit(unit: string): ValueFormatter {
|
||||
return (size: number, decimals?: DecimalCount) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -75,7 +86,7 @@ export function toFixedUnit(unit: string) {
|
|||
// numeric factor. Repeatedly scales the value down by the factor until it is
|
||||
// less than the factor in magnitude, or the end of the array is reached.
|
||||
export function scaledUnits(factor: number, extArray: string[]) {
|
||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
||||
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -92,7 +103,7 @@ export function scaledUnits(factor: number, extArray: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
if (steps > 0 && scaledDecimals !== null) {
|
||||
if (steps > 0 && scaledDecimals !== null && scaledDecimals !== undefined) {
|
||||
decimals = scaledDecimals + 3 * steps;
|
||||
}
|
||||
|
||||
|
@ -100,17 +111,17 @@ export function scaledUnits(factor: number, extArray: string[]) {
|
|||
};
|
||||
}
|
||||
|
||||
export function locale(value: number, decimals: number) {
|
||||
export function locale(value: number, decimals: DecimalCount) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: decimals });
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number });
|
||||
}
|
||||
|
||||
export function simpleCountUnit(symbol: string) {
|
||||
const units = ['', 'K', 'M', 'B', 'T'];
|
||||
const scaler = scaledUnits(1000, units);
|
||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
||||
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"rootDirs": [".", "stories"],
|
||||
"module": "esnext",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"typeRoots": ["./node_modules/@types", "types"],
|
||||
"skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
|
|||
"info": panel.Info,
|
||||
"hideFromList": panel.HideFromList,
|
||||
"sort": getPanelSort(panel.Id),
|
||||
"dataFormats": panel.DataFormats,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
|||
Text: "Explore",
|
||||
Id: "explore",
|
||||
SubTitle: "Explore your data",
|
||||
Icon: "fa fa-rocket",
|
||||
Icon: "gicon gicon-explore",
|
||||
Url: setting.AppSubUrl + "/explore",
|
||||
})
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ func installCommand(c CommandLine) error {
|
|||
return InstallPlugin(pluginToInstall, version, c)
|
||||
}
|
||||
|
||||
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
|
||||
// and then extracts the zip into the plugins directory.
|
||||
func InstallPlugin(pluginName, version string, c CommandLine) error {
|
||||
pluginFolder := c.PluginDirectory()
|
||||
downloadURL := c.PluginURL()
|
||||
|
@ -152,6 +154,10 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
return extractFiles(body, pluginName, filePath)
|
||||
}
|
||||
|
||||
func extractFiles(body []byte, pluginName string, filePath string) error {
|
||||
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -161,12 +167,18 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
|||
|
||||
if zf.FileInfo().IsDir() {
|
||||
err := os.Mkdir(newFile, 0777)
|
||||
if PermissionsError(err) {
|
||||
if permissionsError(err) {
|
||||
return fmt.Errorf(permissionsDeniedMessage, newFile)
|
||||
}
|
||||
} else {
|
||||
dst, err := os.Create(newFile)
|
||||
if PermissionsError(err) {
|
||||
fileMode := zf.Mode()
|
||||
|
||||
if strings.HasSuffix(newFile, "_linux_amd64") || strings.HasSuffix(newFile, "_darwin_amd64") {
|
||||
fileMode = os.FileMode(0755)
|
||||
}
|
||||
|
||||
dst, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||
if permissionsError(err) {
|
||||
return fmt.Errorf(permissionsDeniedMessage, newFile)
|
||||
}
|
||||
|
||||
|
@ -184,6 +196,6 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func PermissionsError(err error) bool {
|
||||
func permissionsError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "permission denied")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
@ -37,3 +39,42 @@ func TestFoldernameReplacement(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractFiles(t *testing.T) {
|
||||
Convey("Should preserve file permissions for plugin backend binaries for linux and darwin", t, func() {
|
||||
err := os.RemoveAll("testdata/fake-plugins-dir")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = os.MkdirAll("testdata/fake-plugins-dir", 0774)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
body, err := ioutil.ReadFile("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = extractFiles(body, "grafana-simple-json-datasource", "testdata/fake-plugins-dir")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
//File in zip has permissions 777
|
||||
fileInfo, err := os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_darwin_amd64")
|
||||
So(err, ShouldBeNil)
|
||||
So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
|
||||
|
||||
//File in zip has permission 664
|
||||
fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_linux_amd64")
|
||||
So(err, ShouldBeNil)
|
||||
So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
|
||||
|
||||
//File in zip has permission 644
|
||||
fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe")
|
||||
So(err, ShouldBeNil)
|
||||
So(fileInfo.Mode().String(), ShouldEqual, "-rw-r--r--")
|
||||
|
||||
//File in zip has permission 755
|
||||
fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/non-plugin-binary")
|
||||
So(err, ShouldBeNil)
|
||||
So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x")
|
||||
|
||||
err = os.RemoveAll("testdata/fake-plugins-dir")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -4,6 +4,7 @@ import "encoding/json"
|
|||
|
||||
type PanelPlugin struct {
|
||||
FrontendPluginBase
|
||||
DataFormats []string `json:"dataFormats"`
|
||||
}
|
||||
|
||||
func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
|
@ -15,6 +16,10 @@ func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if p.DataFormats == nil {
|
||||
p.DataFormats = []string{"time_series", "table"}
|
||||
}
|
||||
|
||||
Panels[p.Id] = p
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -95,52 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
|||
}
|
||||
}
|
||||
case "diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
value = first - points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
allNull, value = calculateDiff(series, allNull, value, diff)
|
||||
case "percent_diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
val := (first - points[i][0].Float64) / points[i][0].Float64 * 100
|
||||
value = math.Abs(val)
|
||||
break
|
||||
}
|
||||
}
|
||||
allNull, value = calculateDiff(series, allNull, value, percentDiff)
|
||||
case "count_non_null":
|
||||
for _, v := range series.Points {
|
||||
if v[0].Valid {
|
||||
|
@ -163,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
|||
func NewSimpleReducer(typ string) *SimpleReducer {
|
||||
return &SimpleReducer{Type: typ}
|
||||
}
|
||||
|
||||
func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) {
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
if i >= 1 {
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
val := fn(first, points[i][0].Float64)
|
||||
value = math.Abs(val)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return allNull, value
|
||||
}
|
||||
|
||||
var diff = func(newest, oldest float64) float64 {
|
||||
return newest - oldest
|
||||
}
|
||||
|
||||
var percentDiff = func(newest, oldest float64) float64 {
|
||||
return (newest - oldest) / oldest * 100
|
||||
}
|
||||
|
|
|
@ -143,6 +143,18 @@ func TestSimpleReducer(t *testing.T) {
|
|||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
|
||||
Convey("diff with only nulls", func() {
|
||||
reducer := NewSimpleReducer("diff")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("percent_diff one point", func() {
|
||||
result := testReducer("percent_diff", 40)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
|
@ -157,6 +169,18 @@ func TestSimpleReducer(t *testing.T) {
|
|||
result := testReducer("percent_diff", 30, 40, 40)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
|
||||
Convey("percent_diff with only nulls", func() {
|
||||
reducer := NewSimpleReducer("percent_diff")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
|
|||
return c.dashboardRef, nil
|
||||
}
|
||||
|
||||
const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d"
|
||||
const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d"
|
||||
|
||||
func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if c.IsTestRun {
|
||||
|
|
|
@ -3,6 +3,7 @@ package alerting
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||
|
@ -126,7 +127,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
|||
renderOpts := rendering.Opts{
|
||||
Width: 1000,
|
||||
Height: 500,
|
||||
Timeout: alertTimeout / 2,
|
||||
Timeout: time.Duration(float64(alertTimeout) * 0.9),
|
||||
OrgId: context.Rule.OrgId,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
ConcurrentLimit: setting.AlertingRenderLimit,
|
||||
|
|
|
@ -52,6 +52,6 @@ func (srv *UserAuthTokenService) deleteExpiredTokens(maxInactiveLifetime, maxLif
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
srv.log.Info("cleanup of expired auth tokens done", "count", affected)
|
||||
srv.log.Debug("cleanup of expired auth tokens done", "count", affected)
|
||||
return affected, err
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ func init() {
|
|||
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
|
||||
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
|
||||
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
|
||||
renders["non_negative_difference"] = QueryDefinition{Renderer: functionRenderer}
|
||||
|
||||
renders["holt_winters"] = QueryDefinition{
|
||||
Renderer: functionRenderer,
|
||||
|
|
|
@ -24,6 +24,7 @@ func TestInfluxdbQueryPart(t *testing.T) {
|
|||
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
|
||||
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
|
||||
{mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`},
|
||||
{mode: "non_negative_difference", params: []string{}, input: "max(value)", expected: `non_negative_difference(max(value))`},
|
||||
}
|
||||
|
||||
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
|
||||
|
|
|
@ -27,7 +27,7 @@ class ErrorBoundary extends Component<Props, State> {
|
|||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
errorInfo: errorInfo,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
|
||||
const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
|
||||
|
||||
export const escapeStringForRegex = (value: string) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const newValue = specialChars.reduce(
|
||||
(escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar),
|
||||
value
|
||||
);
|
||||
|
||||
return newValue;
|
||||
};
|
||||
|
||||
export const unEscapeStringFromRegex = (value: string) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const newValue = specialChars.reduce(
|
||||
(escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar),
|
||||
value
|
||||
);
|
||||
|
||||
return newValue;
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
value: string | undefined;
|
||||
placeholder?: string;
|
||||
labelClassName?: string;
|
||||
inputClassName?: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const FilterInput = forwardRef<HTMLInputElement, Props>((props, ref) => (
|
||||
<label className={props.labelClassName}>
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className={props.inputClassName}
|
||||
value={unEscapeStringFromRegex(props.value)}
|
||||
onChange={event => props.onChange(escapeStringForRegex(event.target.value))}
|
||||
placeholder={props.placeholder ? props.placeholder : null}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
));
|
|
@ -9,42 +9,49 @@ interface Props {
|
|||
newGrafanaVersion: string;
|
||||
}
|
||||
|
||||
export const Footer: FC<Props> = React.memo(({appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion}) => {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="text-center">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://docs.grafana.org" target="_blank">
|
||||
<i className="fa fa-file-code-o" /> Docs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://grafana.com/services/support" target="_blank">
|
||||
<i className="fa fa-support" /> Support Plans
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://community.grafana.com/" target="_blank">
|
||||
<i className="fa fa-comments-o" /> Community
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://grafana.com" target="_blank">{appName}</a> <span>v{buildVersion} (commit: {buildCommit})</span>
|
||||
</li>
|
||||
{newGrafanaVersionExists && (
|
||||
export const Footer: FC<Props> = React.memo(
|
||||
({ appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion }) => {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="text-center">
|
||||
<ul>
|
||||
<li>
|
||||
<Tooltip placement="auto" content={newGrafanaVersion}>
|
||||
<a href="https://grafana.com/get" target="_blank">
|
||||
New version available!
|
||||
</a>
|
||||
</Tooltip>
|
||||
<a href="http://docs.grafana.org" target="_blank">
|
||||
<i className="fa fa-file-code-o" /> Docs
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
});
|
||||
<li>
|
||||
<a href="https://grafana.com/services/support" target="_blank">
|
||||
<i className="fa fa-support" /> Support Plans
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://community.grafana.com/" target="_blank">
|
||||
<i className="fa fa-comments-o" /> Community
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://grafana.com" target="_blank">
|
||||
{appName}
|
||||
</a>{' '}
|
||||
<span>
|
||||
v{buildVersion} (commit: {buildCommit})
|
||||
</span>
|
||||
</li>
|
||||
{newGrafanaVersionExists && (
|
||||
<li>
|
||||
<Tooltip placement="auto" content={newGrafanaVersion}>
|
||||
<a href="https://grafana.com/get" target="_blank">
|
||||
New version available!
|
||||
</a>
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Footer;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector';
|
||||
import { FilterInput } from '../FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
searchQuery: string;
|
||||
|
@ -22,16 +23,13 @@ export default class OrgActionBar extends PureComponent<Props> {
|
|||
return (
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={searchQuery}
|
||||
onChange={event => setSearchQuery(event.target.value)}
|
||||
placeholder="Filter by name or type"
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<FilterInput
|
||||
labelClassName="gf-form--has-input-icon"
|
||||
inputClassName="gf-form-input width-20"
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder={'Filter by name or type'}
|
||||
/>
|
||||
<LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => onSetLayoutMode(mode)} />
|
||||
</div>
|
||||
<div className="page-action-bar__spacer" />
|
||||
|
|
|
@ -7,20 +7,13 @@ exports[`Render should render component 1`] = `
|
|||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input width-20"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter by name or type"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input width-20"
|
||||
labelClassName="gf-form--has-input-icon"
|
||||
onChange={[MockFunction]}
|
||||
placeholder="Filter by name or type"
|
||||
value=""
|
||||
/>
|
||||
<LayoutSelector
|
||||
onLayoutModeChanged={[Function]}
|
||||
/>
|
||||
|
|
|
@ -33,9 +33,9 @@ class Page extends Component<Props> {
|
|||
updateTitle = () => {
|
||||
const title = this.getPageTitle;
|
||||
document.title = title ? title + ' - Grafana' : 'Grafana';
|
||||
}
|
||||
};
|
||||
|
||||
get getPageTitle () {
|
||||
get getPageTitle() {
|
||||
const { navModel } = this.props;
|
||||
if (navModel) {
|
||||
return getTitleFromNavModel(navModel) || undefined;
|
||||
|
@ -47,20 +47,21 @@ class Page extends Component<Props> {
|
|||
const { navModel } = this.props;
|
||||
const { buildInfo } = config;
|
||||
return (
|
||||
<div className="page-scrollbar-wrapper">
|
||||
<CustomScrollbar autoHeightMin={'100%'}>
|
||||
<div className="page-scrollbar-content">
|
||||
<PageHeader model={navModel} />
|
||||
{this.props.children}
|
||||
<Footer
|
||||
appName="Grafana"
|
||||
buildCommit={buildInfo.commit}
|
||||
buildVersion={buildInfo.version}
|
||||
newGrafanaVersion={buildInfo.latestVersion}
|
||||
newGrafanaVersionExists={buildInfo.hasUpdate} />
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
<div className="page-scrollbar-wrapper">
|
||||
<CustomScrollbar autoHeightMin={'100%'}>
|
||||
<div className="page-scrollbar-content">
|
||||
<PageHeader model={navModel} />
|
||||
{this.props.children}
|
||||
<Footer
|
||||
appName="Grafana"
|
||||
buildCommit={buildInfo.commit}
|
||||
buildVersion={buildInfo.version}
|
||||
newGrafanaVersion={buildInfo.latestVersion}
|
||||
newGrafanaVersionExists={buildInfo.hasUpdate}
|
||||
/>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ interface Props {
|
|||
}
|
||||
|
||||
class PageContents extends Component<Props> {
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.props;
|
||||
|
||||
|
|
|
@ -61,15 +61,14 @@ export default class PermissionsListItem extends PureComponent<Props> {
|
|||
{item.name} <ItemDescription item={item} />
|
||||
</td>
|
||||
<td>
|
||||
{item.inherited &&
|
||||
folderInfo && (
|
||||
<em className="muted no-wrap">
|
||||
Inherited from folder{' '}
|
||||
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
||||
{folderInfo.title}
|
||||
</a>{' '}
|
||||
</em>
|
||||
)}
|
||||
{item.inherited && folderInfo && (
|
||||
<em className="muted no-wrap">
|
||||
Inherited from folder{' '}
|
||||
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
||||
{folderInfo.title}
|
||||
</a>{' '}
|
||||
</em>
|
||||
)}
|
||||
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
|
||||
</td>
|
||||
<td className="query-keyword">Can</td>
|
||||
|
|
|
@ -5,6 +5,7 @@ import AsyncSelect from '@torkelo/react-select/lib/Async';
|
|||
import { TagOption } from './TagOption';
|
||||
import { TagBadge } from './TagBadge';
|
||||
import { components } from '@torkelo/react-select';
|
||||
import { escapeStringForRegex } from '../FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
tags: string[];
|
||||
|
@ -51,7 +52,7 @@ export class TagFilter extends React.Component<Props, any> {
|
|||
value: tags,
|
||||
styles: resetSelectStyles(),
|
||||
filterOption: (option, searchQuery) => {
|
||||
const regex = RegExp(searchQuery, 'i');
|
||||
const regex = RegExp(escapeStringForRegex(searchQuery), 'i');
|
||||
return regex.test(option.value);
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
|
||||
/*
|
||||
* Escapes `"` characters from string
|
||||
*/
|
||||
*/
|
||||
function escapeString(str: string): string {
|
||||
return str.replace('"', '"');
|
||||
}
|
||||
|
||||
/*
|
||||
* Determines if a value is an object
|
||||
*/
|
||||
*/
|
||||
export function isObject(value: any): boolean {
|
||||
const type = typeof value;
|
||||
return !!value && type === 'object';
|
||||
|
@ -20,7 +20,7 @@ export function isObject(value: any): boolean {
|
|||
* Gets constructor name of an object.
|
||||
* From http://stackoverflow.com/a/332429
|
||||
*
|
||||
*/
|
||||
*/
|
||||
export function getObjectName(object: object): string {
|
||||
if (object === undefined) {
|
||||
return '';
|
||||
|
@ -43,7 +43,7 @@ export function getObjectName(object: object): string {
|
|||
|
||||
/*
|
||||
* Gets type of an object. Returns "null" for null objects
|
||||
*/
|
||||
*/
|
||||
export function getType(object: object): string {
|
||||
if (object === null) {
|
||||
return 'null';
|
||||
|
@ -53,7 +53,7 @@ export function getType(object: object): string {
|
|||
|
||||
/*
|
||||
* Generates inline preview for a JavaScript object based on a value
|
||||
*/
|
||||
*/
|
||||
export function getValuePreview(object: object, value: string): string {
|
||||
const type = getType(object);
|
||||
|
||||
|
@ -78,7 +78,7 @@ export function getValuePreview(object: object, value: string): string {
|
|||
|
||||
/*
|
||||
* Generates inline preview for a JavaScript object
|
||||
*/
|
||||
*/
|
||||
let value = '';
|
||||
export function getPreview(obj: object): string {
|
||||
if (isObject(obj)) {
|
||||
|
@ -94,15 +94,15 @@ export function getPreview(obj: object): string {
|
|||
|
||||
/*
|
||||
* Generates a prefixed CSS class name
|
||||
*/
|
||||
*/
|
||||
export function cssClass(className: string): string {
|
||||
return `json-formatter-${className}`;
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a new DOM element with given type and class
|
||||
* TODO: move me to helpers
|
||||
*/
|
||||
* Creates a new DOM element with given type and class
|
||||
* TODO: move me to helpers
|
||||
*/
|
||||
export function createElement(type: string, className?: string, content?: Element | string): Element {
|
||||
const el = document.createElement(type);
|
||||
if (className) {
|
||||
|
|
|
@ -83,7 +83,7 @@ export class JsonExplorer {
|
|||
|
||||
/*
|
||||
* is formatter open?
|
||||
*/
|
||||
*/
|
||||
private get isOpen(): boolean {
|
||||
if (this._isOpen !== null) {
|
||||
return this._isOpen;
|
||||
|
@ -94,14 +94,14 @@ export class JsonExplorer {
|
|||
|
||||
/*
|
||||
* set open state (from toggler)
|
||||
*/
|
||||
*/
|
||||
private set isOpen(value: boolean) {
|
||||
this._isOpen = value;
|
||||
}
|
||||
|
||||
/*
|
||||
* is this a date string?
|
||||
*/
|
||||
*/
|
||||
private get isDate(): boolean {
|
||||
return (
|
||||
this.type === 'string' &&
|
||||
|
@ -111,14 +111,14 @@ export class JsonExplorer {
|
|||
|
||||
/*
|
||||
* is this a URL string?
|
||||
*/
|
||||
*/
|
||||
private get isUrl(): boolean {
|
||||
return this.type === 'string' && this.json.indexOf('http') === 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* is this an array?
|
||||
*/
|
||||
*/
|
||||
private get isArray(): boolean {
|
||||
return Array.isArray(this.json);
|
||||
}
|
||||
|
@ -126,21 +126,21 @@ export class JsonExplorer {
|
|||
/*
|
||||
* is this an object?
|
||||
* Note: In this context arrays are object as well
|
||||
*/
|
||||
*/
|
||||
private get isObject(): boolean {
|
||||
return isObject(this.json);
|
||||
}
|
||||
|
||||
/*
|
||||
* is this an empty object with no properties?
|
||||
*/
|
||||
*/
|
||||
private get isEmptyObject(): boolean {
|
||||
return !this.keys.length && !this.isArray;
|
||||
}
|
||||
|
||||
/*
|
||||
* is this an empty object or array?
|
||||
*/
|
||||
*/
|
||||
private get isEmpty(): boolean {
|
||||
return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray);
|
||||
}
|
||||
|
@ -148,14 +148,14 @@ export class JsonExplorer {
|
|||
/*
|
||||
* did we receive a key argument?
|
||||
* This means that the formatter was called as a sub formatter of a parent formatter
|
||||
*/
|
||||
*/
|
||||
private get hasKey(): boolean {
|
||||
return typeof this.key !== 'undefined';
|
||||
}
|
||||
|
||||
/*
|
||||
* if this is an object, get constructor function name
|
||||
*/
|
||||
*/
|
||||
private get constructorName(): string {
|
||||
return getObjectName(this.json);
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ export class JsonExplorer {
|
|||
/*
|
||||
* get type of this value
|
||||
* Possible values: all JavaScript primitive types plus "array" and "null"
|
||||
*/
|
||||
*/
|
||||
private get type(): string {
|
||||
return getType(this.json);
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ export class JsonExplorer {
|
|||
/*
|
||||
* get object keys
|
||||
* If there is an empty key we pad it wit quotes to make it visible
|
||||
*/
|
||||
*/
|
||||
private get keys(): string[] {
|
||||
if (this.isObject) {
|
||||
return Object.keys(this.json).map(key => (key ? key : '""'));
|
||||
|
|
|
@ -47,7 +47,8 @@ class BottomNavLinks extends PureComponent<Props> {
|
|||
<div className="sidemenu-org-switcher__org-current">Current Org:</div>
|
||||
</div>
|
||||
<div className="sidemenu-org-switcher__switch">
|
||||
<i className="fa fa-fw fa-random" />Switch
|
||||
<i className="fa fa-fw fa-random" />
|
||||
Switch
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -29,7 +29,8 @@ export class SideMenu extends PureComponent {
|
|||
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
|
||||
<i className="fa fa-bars" />
|
||||
<span className="sidemenu__close">
|
||||
<i className="fa fa-times" /> Close
|
||||
<i className="fa fa-times" />
|
||||
Close
|
||||
</span>
|
||||
</div>,
|
||||
<TopSection key="topsection" />,
|
||||
|
|
|
@ -57,7 +57,7 @@ export class Settings {
|
|||
isEnterprise: false,
|
||||
},
|
||||
viewersCanEdit: false,
|
||||
disableSanitizeHtml: false
|
||||
disableSanitizeHtml: false,
|
||||
};
|
||||
|
||||
_.extend(this, defaults, options);
|
||||
|
|
|
@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20;
|
|||
|
||||
export const PANEL_HEADER_HEIGHT = 27;
|
||||
export const PANEL_BORDER = 2;
|
||||
export const PANEL_OPTIONS_KEY_PREFIX = 'options-';
|
||||
|
|
|
@ -28,7 +28,7 @@ export function autofillEventFix($compile) {
|
|||
input.removeEventListener('animationstart', onAnimationStart);
|
||||
// input.removeEventListener('change', onChange);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -142,7 +142,7 @@ export function dropdownTypeahead2($compile) {
|
|||
const $input = $(inputTemplate);
|
||||
const $button = $(buttonTemplate);
|
||||
const timeoutId = {
|
||||
blur: null
|
||||
blur: null,
|
||||
};
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
export class Profiler {
|
||||
panelsRendered: number;
|
||||
enabled: boolean;
|
||||
|
|
|
@ -43,5 +43,5 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel)
|
|||
}
|
||||
|
||||
export const getTitleFromNavModel = (navModel: NavModel) => {
|
||||
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : '' }`;
|
||||
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : ''}`;
|
||||
};
|
||||
|
|
|
@ -27,7 +27,9 @@ export class AngularLoader {
|
|||
compiledElem.remove();
|
||||
},
|
||||
digest: () => {
|
||||
scope.$digest();
|
||||
if (!scope.$$phase) {
|
||||
scope.$digest();
|
||||
}
|
||||
},
|
||||
getScope: () => {
|
||||
return scope;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
const backendSrv = {
|
||||
get: jest.fn(),
|
||||
getDashboard: jest.fn(),
|
||||
|
@ -10,5 +9,3 @@ const backendSrv = {
|
|||
export function getBackendSrv() {
|
||||
return backendSrv;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {PasswordStrength} from '../components/PasswordStrength';
|
||||
import { PasswordStrength } from '../components/PasswordStrength';
|
||||
|
||||
describe('PasswordStrength', () => {
|
||||
|
||||
it('should have class bad if length below 4', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asd" />);
|
||||
expect(wrapper.find(".password-strength-bad")).toHaveLength(1);
|
||||
expect(wrapper.find('.password-strength-bad')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should have class ok if length below 8', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asdasd" />);
|
||||
expect(wrapper.find(".password-strength-ok")).toHaveLength(1);
|
||||
expect(wrapper.find('.password-strength-ok')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should have class good if length above 8', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asdaasdda" />);
|
||||
expect(wrapper.find(".password-strength-good")).toHaveLength(1);
|
||||
expect(wrapper.find('.password-strength-good')).toHaveLength(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -153,7 +153,7 @@ export function buildQueryTransaction(
|
|||
};
|
||||
}
|
||||
|
||||
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||
export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest;
|
||||
|
||||
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
|
||||
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue