This commit is contained in:
Austin Winstanley 2018-06-29 03:32:18 +00:00
commit 84af033281
45 changed files with 3106 additions and 1672 deletions

View File

@ -88,12 +88,9 @@ jobs:
test-frontend: test-frontend:
docker: docker:
- image: circleci/node:6.11.4 - image: circleci/node:8
steps: steps:
- checkout - checkout
- run:
name: install yarn
command: 'sudo npm install -g yarn --quiet'
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "yarn.lock" }} key: dependency-cache-{{ checksum "yarn.lock" }}
- run: - run:

View File

@ -7,13 +7,26 @@
* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
# 5.2.0 (unreleased) # 5.2.1 (unreleased)
### Minor
* **UI**: Fix - Grafana footer overlapping page [#12430](https://github.com/grafana/grafana/issues/12430)
* **Auth Proxy**: Revert of "Whitelist proxy IP address instead of client IP address" introduced in 5.2.0-beta2 [#12444](https://github.com/grafana/grafana/pull/12444)
# 5.2.0-stable (2018-06-27)
### Minor ### Minor
* **Plugins**: Handle errors correctly when loading datasource plugin [#12383](https://github.com/grafana/grafana/pull/12383) thx [@rozetko](https://github.com/rozetko) * **Plugins**: Handle errors correctly when loading datasource plugin [#12383](https://github.com/grafana/grafana/pull/12383) thx [@rozetko](https://github.com/rozetko)
* **Render**: Enhance error message if phantomjs executable is not found [#11868](https://github.com/grafana/grafana/issues/11868) * **Render**: Enhance error message if phantomjs executable is not found [#11868](https://github.com/grafana/grafana/issues/11868)
* **Dashboard**: Set correct text in drop down when variable is present in url [#11968](https://github.com/grafana/grafana/issues/11968)
### 5.2.0-beta3 fixes
* **LDAP**: Handle "dn" ldap attribute more gracefully [#12385](https://github.com/grafana/grafana/pull/12385), reverts [#10970](https://github.com/grafana/grafana/pull/10970)
# 5.2.0-beta3 (2018-06-21) # 5.2.0-beta3 (2018-06-21)
@ -56,6 +69,7 @@
### New Features ### New Features
* **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95) * **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
* **Build**: Crosscompile and packages Grafana on arm, windows, linux and darwin [#11920](https://github.com/grafana/grafana/pull/11920), thx [@fg2it](https://github.com/fg2it)
* **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882) * **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882)
* **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541) * **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541)
@ -91,6 +105,10 @@
* **Dashboard list panel**: Search dashboards by folder [#11525](https://github.com/grafana/grafana/issues/11525) * **Dashboard list panel**: Search dashboards by folder [#11525](https://github.com/grafana/grafana/issues/11525)
* **Sidenav**: Always show server admin link in sidenav if grafana admin [#11657](https://github.com/grafana/grafana/issues/11657) * **Sidenav**: Always show server admin link in sidenav if grafana admin [#11657](https://github.com/grafana/grafana/issues/11657)
# 5.1.5 (2018-06-27)
* **Docker**: Config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170)
# 5.1.4 (2018-06-19) # 5.1.4 (2018-06-19)
* **Permissions**: Important security fix for API keys with viewer role [#12343](https://github.com/grafana/grafana/issues/12343) * **Permissions**: Important security fix for API keys with viewer role [#12343](https://github.com/grafana/grafana/issues/12343)

View File

@ -1,28 +1,21 @@
# Roadmap (2018-05-06) # Roadmap (2018-06-26)
This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change.
But it will give you an idea of our current vision and plan. But it will give you an idea of our current vision and plan.
### Short term (1-2 months) ### Short term (1-2 months)
- Multi-Stat panel
- Elasticsearch alerting - Metrics & Log Explore UI
- Crossplatform builds
- Backend service refactorings
- Explore UI
- First login registration view
### Mid term (2-4 months) ### Mid term (2-4 months)
- Multi-Stat panel
- React Panels - React Panels
- Change visualization (panel type) on the fly.
- Templating Query Editor UI Plugin hook - Templating Query Editor UI Plugin hook
### Long term (4 - 8 months) ### Long term (4 - 8 months)
- Alerting improvements (silence, per series tracking, etc) - Alerting improvements (silence, per series tracking, etc)
- Progress on React migration - Progress on React migration
- Change visualization (panel type) on the fly.
- Multi stat panel (vertical version of singlestat with bars/graph mode with big number etc)
- Repeat panel by query results
### In a distant future far far away ### In a distant future far far away

View File

@ -14,14 +14,14 @@ weight = -8
Grafana v5.2 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements. Grafana v5.2 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements.
* [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here! - [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here!
* [Cross platform build support]({{< relref "#cross-platform-build-support" >}}) enables native builds of Grafana for many more platforms! - [Native builds for ARM]({{< relref "#native-builds-for-arm" >}}) native builds of Grafana for many more platforms!
* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets - [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets
* [Security]({{< relref "#security" >}}) make your Grafana instance more secure - [Security]({{< relref "#security" >}}) make your Grafana instance more secure
* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements - [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
* [InfluxDB]({{< relref "#influxdb" >}}) with support for a new function - [InfluxDB]({{< relref "#influxdb" >}}) now supports the `mode` function
* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord - [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
* [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements - [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements
## Elasticsearch alerting ## Elasticsearch alerting
@ -32,16 +32,18 @@ the most requested features by our community and now it's finally here. Please t
<div class="clearfix"></div> <div class="clearfix"></div>
## Cross platform build support ## Native builds for ARM
Grafana v5.2 brings an improved build pipeline with cross platform support. This enables native builds of Grafana for ARMv7 (x32), ARM64 (x64), Grafana v5.2 brings an improved build pipeline with cross-platform support. This enables native builds of Grafana for ARMv7 (x32) and ARM64 (x64).
MacOS/Darwin (x64) and Windows (x64) in both stable and nightly builds. We've been longing for native ARM build support for ages. With the help from our amazing community this is now finally available.
Please try it out and let us know what you think.
We've been longing for native ARM build support for a long time. With the help from our amazing community this is now finally available. Another great addition with the improved build pipeline is that binaries for MacOS/Darwin (x64) and Windows (x64) are now automatically built and
published for both stable and nightly builds.
## Improved Docker image ## Improved Docker image
The Grafana docker image now includes support for Docker secrets which enables you to supply Grafana with configuration through files. More The Grafana docker image adds support for Docker secrets which enables you to supply Grafana with configuration through files. More
information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets). information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets).
## Security ## Security
@ -49,18 +51,18 @@ information in the [Installing using Docker documentation](/installation/docker/
{{< docs-imagebox img="/img/docs/v52/login_change_password.png" max-width="800px" class="docs-image--right" >}} {{< docs-imagebox img="/img/docs/v52/login_change_password.png" max-width="800px" class="docs-image--right" >}}
Starting from Grafana v5.2, when you login with the administrator account using the default password you'll be presented with a form to change the password. Starting from Grafana v5.2, when you login with the administrator account using the default password you'll be presented with a form to change the password.
By this we hope to encourage users to follow Grafana's best practices and change the default administrator password. We hope this encourages users to follow Grafana's best practices and change the default administrator password.
<div class="clearfix"></div> <div class="clearfix"></div>
## Prometheus ## Prometheus
The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate* The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate*
functions get consistent results, and thus avoid graphs jumping around on reload. functions get consistent results, and thus avoids graphs jumping around on reload.
## InfluxDB ## InfluxDB
The InfluxDB datasource now includes support for the *mode* function which allows to return the most frequent value in a list of field values. The InfluxDB datasource now includes support for the *mode* function which returns the most frequent value in a list of field values.
## Alerting ## Alerting
@ -72,9 +74,9 @@ By popular demand Grafana now includes support for an alert notification channel
{{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}} {{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}}
Starting from Grafana v5.2 a modified time range or variable are no longer saved by default. To save a modified Starting from Grafana v5.2, a modified time range or variable are no longer saved by default. To save a modified
time range or variable you'll need to actively select that when saving a dashboard, see screenshot. time range or variable, you'll need to actively select that when saving a dashboard, see screenshot.
This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit This should hopefully make it easier to have sane defaults for time and variables in dashboards and make it more explicit
when you actually want to overwrite those settings. when you actually want to overwrite those settings.
<div class="clearfix"></div> <div class="clearfix"></div>
@ -83,13 +85,13 @@ when you actually want to overwrite those settings.
{{< docs-imagebox img="/img/docs/v52/dashboard_import.png" max-width="800px" class="docs-image--right" >}} {{< docs-imagebox img="/img/docs/v52/dashboard_import.png" max-width="800px" class="docs-image--right" >}}
Grafana v5.2 adds support for specifying an existing folder or create a new one when importing a dashboard, a long awaited feature since Grafana v5.2 adds support for specifying an existing folder or creating a new one when importing a dashboard - a long-awaited feature since
Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page have also got some general improvements Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page has also got some general improvements
and should now make it more clear if a possible import will overwrite an existing dashboard, or not. and should now make it more clear if a possible import will overwrite an existing dashboard, or not.
This release also adds some improvements for those users only having editor or admin permissions in certain folders. Now the links to This release also adds some improvements for those users only having editor or admin permissions in certain folders. The links to
*Create Dashboard* and *Import Dashboard* is available in side navigation, dashboard search and manage dashboards/folder page for a *Create Dashboard* and *Import Dashboard* are now available in the side navigation, in dashboard search and on the manage dashboards/folder page for a
user that has editor role in an organization or edit permission in at least one folder. user that has editor role in an organization or the edit permission in at least one folder.
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -44,6 +44,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
The `Authorization` header value should be `Bearer <your api key>`. The `Authorization` header value should be `Bearer <your api key>`.
The API Token can also be passed as a Basic authorization password with the special username `api_key`:
curl example:
```bash
?curl http://api_key:eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk@localhost:3000/api/org
{"id":1,"name":"Main Org."}
```
# Auth HTTP resources / actions # Auth HTTP resources / actions
## Api Keys ## Api Keys

View File

@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
<h4>Provisioning</h4> <h4>Provisioning</h4>
<p>A guide to help you automate your Grafana setup & configuration.</p> <p>A guide to help you automate your Grafana setup & configuration.</p>
</a> </a>
<a href="{{< relref "guides/whats-new-in-v5.md" >}}" class="nav-cards__item nav-cards__item--guide"> <a href="{{< relref "guides/whats-new-in-v5-2.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>What's new in v5.0</h4> <h4>What's new in v5.2</h4>
<p>Article on all the new cool features and enhancements in v5.0</p> <p>Article on all the new cool features and enhancements in v5.2</p>
</a> </a>
<a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide"> <a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
<h4>Screencasts</h4> <h4>Screencasts</h4>

View File

@ -15,10 +15,9 @@ weight = 1
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable for Debian-based Linux | [grafana_5.1.4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb) Stable for Debian-based Linux | [x86-64](https://grafana.com/grafana/download?platform=linux)
<!-- Stable for Debian-based Linux | [ARM64](https://grafana.com/grafana/download?platform=arm)
Beta for Debian-based Linux | [grafana_5.1.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb) Stable for Debian-based Linux | [ARMv7](https://grafana.com/grafana/download?platform=arm)
-->
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation. installation.
@ -27,17 +26,18 @@ installation.
```bash ```bash
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb wget <debian package url>
sudo apt-get install -y adduser libfontconfig sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_5.1.4_amd64.deb sudo dpkg -i grafana_5.1.4_amd64.deb
``` ```
<!-- ## Install Latest Beta Example:
```bash ```bash
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb
sudo apt-get install -y adduser libfontconfig sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_5.1.0-beta1_amd64.deb sudo dpkg -i grafana_5.1.4_amd64.deb
``` --> ```
## APT Repository ## APT Repository

View File

@ -11,6 +11,8 @@ weight = 4
# Installing on Mac # Installing on Mac
## Install using homebrew
Installation can be done using [homebrew](http://brew.sh/) Installation can be done using [homebrew](http://brew.sh/)
Install latest stable: Install latest stable:
@ -75,3 +77,18 @@ If you want to manually install a plugin place it here: `/usr/local/var/lib/graf
The default sqlite database is located at `/usr/local/var/lib/grafana` The default sqlite database is located at `/usr/local/var/lib/grafana`
## Installing from binary tar file
Download [the latest `.tar.gz` file](https://grafana.com/get) and
extract it. This will extract into a folder named after the version you
downloaded. This folder contains all files required to run Grafana. There are
no init scripts or install scripts in this package.
To configure Grafana add a configuration file named `custom.ini` to the
`conf` folder and override any of the settings defined in
`conf/defaults.ini`.
Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
binary needs the working directory to be the root install directory (where the
binary and the `public` folder is located).

View File

@ -15,42 +15,49 @@ weight = 2
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm) Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [x86-64](https://grafana.com/grafana/download?platform=linux)
<!-- Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [ARM64](https://grafana.com/grafana/download?platform=arm)
Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm) Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [ARMv7](https://grafana.com/grafana/download?platform=arm)
-->
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation.
installation.
## Install Stable ## Install Stable
You can install Grafana using Yum directly. You can install Grafana using Yum directly.
```bash
$ sudo yum install <rpm package url>
```
Example:
```bash ```bash
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
``` ```
<!-- ## Install Beta Or install manually using `rpm`. First execute
```bash ```bash
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm $ wget <rpm package url>
``` --> ```
Or install manually using `rpm`. Example:
#### On CentOS / Fedora / Redhat:
```bash ```bash
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm $ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
$ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-5.1.4-1.x86_64.rpm
``` ```
#### On OpenSuse: ### On CentOS / Fedora / Redhat:
```bash ```bash
$ sudo rpm -i --nodeps grafana-5.1.4-1.x86_64.rpm $ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh <local rpm package>
```
### On OpenSuse:
```bash
$ sudo rpm -i --nodeps <local rpm package>
``` ```
## Install via YUM Repository ## Install via YUM Repository

View File

@ -21,7 +21,7 @@ the data source response.
To check this you should use Query Inspector (new in Grafana v4.5). The query Inspector shows query requests and responses. To check this you should use Query Inspector (new in Grafana v4.5). The query Inspector shows query requests and responses.
For more on the query insector read [this guide here](https://community.grafana.com/t/using-grafanas-query-inspector-to-troubleshoot-issues/2630). For For more on the query inspector read [this guide here](https://community.grafana.com/t/using-grafanas-query-inspector-to-troubleshoot-issues/2630). For
older versions of Grafana read the [how troubleshoot metric query issue](https://community.grafana.com/t/how-to-troubleshoot-metric-query-issues/50/2) article. older versions of Grafana read the [how troubleshoot metric query issue](https://community.grafana.com/t/how-to-troubleshoot-metric-query-issues/50/2) article.
## Logging ## Logging

View File

@ -12,11 +12,7 @@ weight = 3
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Latest stable package for Windows | [grafana-5.1.4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4.windows-x64.zip) Latest stable package for Windows | [x64](https://grafana.com/grafana/download?platform=windows)
<!--
Latest beta package for Windows | [grafana.5.1.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.windows-x64.zip)
-->
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation. installation.

View File

@ -1,5 +1,6 @@
[ [
{ "version": "v5.1", "path": "/", "archived": false, "current": true }, { "version": "v5.2", "path": "/", "archived": false, "current": true },
{ "version": "v5.1", "path": "/v5.1", "archived": true },
{ "version": "v5.0", "path": "/v5.0", "archived": true }, { "version": "v5.0", "path": "/v5.0", "archived": true },
{ "version": "v4.6", "path": "/v4.6", "archived": true }, { "version": "v4.6", "path": "/v4.6", "archived": true },
{ "version": "v4.5", "path": "/v4.5", "archived": true }, { "version": "v4.5", "path": "/v4.5", "archived": true },

View File

@ -19,8 +19,8 @@ module.exports = function(config) {
}, },
webpack: webpackTestConfig, webpack: webpackTestConfig,
webpackServer: { webpackMiddleware: {
noInfo: true, // please don't spam the console when running in karma! stats: 'minimal',
}, },
// list of files to exclude // list of files to exclude

View File

@ -1,4 +1,4 @@
{ {
"stable": "5.1.3", "stable": "5.2.0",
"testing": "5.1.3" "testing": "5.2.0"
} }

View File

@ -4,7 +4,7 @@
"company": "Grafana Labs" "company": "Grafana Labs"
}, },
"name": "grafana", "name": "grafana",
"version": "5.2.0-pre1", "version": "5.3.0-pre1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "http://github.com/grafana/grafana.git" "url": "http://github.com/grafana/grafana.git"
@ -16,11 +16,11 @@
"@types/node": "^8.0.31", "@types/node": "^8.0.31",
"@types/react": "^16.0.25", "@types/react": "^16.0.25",
"@types/react-dom": "^16.0.3", "@types/react-dom": "^16.0.3",
"angular-mocks": "^1.6.6", "angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",
"awesome-typescript-loader": "^4.0.0",
"axios": "^0.17.1", "axios": "^0.17.1",
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^0.1.19",
@ -32,8 +32,9 @@
"es6-shim": "^0.35.3", "es6-shim": "^0.35.3",
"expect.js": "~0.2.0", "expect.js": "~0.2.0",
"expose-loader": "^0.7.3", "expose-loader": "^0.7.3",
"extract-text-webpack-plugin": "^3.0.0", "extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"fork-ts-checker-webpack-plugin": "^0.4.1",
"gaze": "^1.1.2", "gaze": "^1.1.2",
"glob": "~7.0.0", "glob": "~7.0.0",
"grunt": "1.0.1", "grunt": "1.0.1",
@ -56,7 +57,7 @@
"grunt-webpack": "^3.0.2", "grunt-webpack": "^3.0.2",
"html-loader": "^0.5.1", "html-loader": "^0.5.1",
"html-webpack-harddisk-plugin": "^0.2.0", "html-webpack-harddisk-plugin": "^0.2.0",
"html-webpack-plugin": "^2.30.1", "html-webpack-plugin": "^3.2.0",
"husky": "^0.14.3", "husky": "^0.14.3",
"jest": "^22.0.4", "jest": "^22.0.4",
"jshint-stylish": "~2.2.1", "jshint-stylish": "~2.2.1",
@ -67,7 +68,7 @@
"karma-phantomjs-launcher": "1.0.4", "karma-phantomjs-launcher": "1.0.4",
"karma-sinon": "^1.0.5", "karma-sinon": "^1.0.5",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.4", "karma-webpack": "^3.0.0",
"lint-staged": "^6.0.0", "lint-staged": "^6.0.0",
"load-grunt-tasks": "3.5.2", "load-grunt-tasks": "3.5.2",
"mobx-react-devtools": "^4.2.15", "mobx-react-devtools": "^4.2.15",
@ -89,21 +90,24 @@
"style-loader": "^0.21.0", "style-loader": "^0.21.0",
"systemjs": "0.20.19", "systemjs": "0.20.19",
"systemjs-plugin-css": "^0.1.36", "systemjs-plugin-css": "^0.1.36",
"ts-loader": "^4.3.0",
"ts-jest": "^22.4.6", "ts-jest": "^22.4.6",
"tslint": "^5.8.0", "tslint": "^5.8.0",
"tslint-loader": "^3.5.3", "tslint-loader": "^3.5.3",
"typescript": "^2.6.2", "typescript": "^2.6.2",
"webpack": "^3.10.0", "webpack": "^4.8.0",
"webpack-bundle-analyzer": "^2.9.0", "webpack-bundle-analyzer": "^2.9.0",
"webpack-cleanup-plugin": "^0.5.1", "webpack-cleanup-plugin": "^0.5.1",
"webpack-dev-server": "2.11.1", "fork-ts-checker-webpack-plugin": "^0.4.2",
"webpack-cli": "^2.1.4",
"webpack-dev-server": "^3.1.0",
"webpack-merge": "^4.1.0", "webpack-merge": "^4.1.0",
"zone.js": "^0.7.2" "zone.js": "^0.7.2"
}, },
"scripts": { "scripts": {
"dev": "webpack --progress --colors --config scripts/webpack/webpack.dev.js", "dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js",
"start": "webpack-dev-server --progress --colors --config scripts/webpack/webpack.hot.js", "start": "webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js",
"watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", "watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js",
"build": "grunt build", "build": "grunt build",
"test": "grunt test", "test": "grunt test",
"test:coverage": "grunt test --coverage=true", "test:coverage": "grunt test --coverage=true",
@ -135,8 +139,8 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"angular": "1.6.6", "angular": "1.6.6",
"angular-bindonce": "^0.3.1", "angular-bindonce": "0.3.1",
"angular-native-dragdrop": "^1.2.2", "angular-native-dragdrop": "1.2.2",
"angular-route": "1.6.6", "angular-route": "1.6.6",
"angular-sanitize": "1.6.6", "angular-sanitize": "1.6.6",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
@ -151,12 +155,14 @@
"immutable": "^3.8.2", "immutable": "^3.8.2",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"mini-css-extract-plugin": "^0.4.0",
"mobx": "^3.4.1", "mobx": "^3.4.1",
"mobx-react": "^4.3.5", "mobx-react": "^4.3.5",
"mobx-state-tree": "^1.3.1", "mobx-state-tree": "^1.3.1",
"moment": "^2.18.1", "moment": "^2.18.1",
"mousetrap": "^1.6.0", "mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",
"optimize-css-assets-webpack-plugin": "^4.0.2",
"prismjs": "^1.6.0", "prismjs": "^1.6.0",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.2.0", "react": "^16.2.0",
@ -175,7 +181,8 @@
"slate-react": "^0.12.4", "slate-react": "^0.12.4",
"tether": "^1.4.0", "tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop/tarball/master", "tether-drop": "https://github.com/torkelo/drop/tarball/master",
"tinycolor2": "^1.4.1" "tinycolor2": "^1.4.1",
"uglifyjs-webpack-plugin": "^1.2.7"
}, },
"resolutions": { "resolutions": {
"caniuse-db": "1.0.30000772" "caniuse-db": "1.0.30000772"

View File

@ -272,9 +272,9 @@ func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
return false, nil return false, nil
} }
if dashboardID > 0 { if dashboardID != 0 {
guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit { if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err return false, err
} }
} }

View File

@ -42,7 +42,7 @@ type RouteRegister interface {
// Register iterates over all routes added to the RouteRegister // Register iterates over all routes added to the RouteRegister
// and add them to the `Router` pass as an parameter. // and add them to the `Router` pass as an parameter.
Register(Router) *macaron.Router Register(Router)
} }
type RegisterNamedMiddleware func(name string) macaron.Handler type RegisterNamedMiddleware func(name string) macaron.Handler
@ -101,7 +101,7 @@ func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handle
rr.groups = append(rr.groups, group) rr.groups = append(rr.groups, group)
} }
func (rr *routeRegister) Register(router Router) *macaron.Router { func (rr *routeRegister) Register(router Router) {
for _, r := range rr.routes { for _, r := range rr.routes {
// GET requests have to be added to macaron routing using Get() // GET requests have to be added to macaron routing using Get()
// Otherwise HEAD requests will not be allowed. // Otherwise HEAD requests will not be allowed.
@ -116,8 +116,6 @@ func (rr *routeRegister) Register(router Router) *macaron.Router {
for _, g := range rr.groups { for _, g := range rr.groups {
g.Register(router) g.Register(router)
} }
return &macaron.Router{}
} }
func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) { func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {

View File

@ -308,9 +308,6 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
} else { } else {
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult) filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
} }
if a.server.GroupSearchFilterUserAttribute == "dn" {
filter_replace = searchResult.Entries[0].DN
}
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1) filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
@ -334,11 +331,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
if len(groupSearchResult.Entries) > 0 { if len(groupSearchResult.Entries) > 0 {
for i := range groupSearchResult.Entries { for i := range groupSearchResult.Entries {
if a.server.Attr.MemberOf == "dn" { memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
memberOf = append(memberOf, groupSearchResult.Entries[i].DN)
} else {
memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
}
} }
break break
} }
@ -356,7 +349,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
} }
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string { func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
if name == "DN" { if strings.ToLower(name) == "dn" {
return result.Entries[n].DN return result.Entries[n].DN
} }
for _, attr := range result.Entries[n].Attributes { for _, attr := range result.Entries[n].Attributes {

View File

@ -9,6 +9,7 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
type AuthOptions struct { type AuthOptions struct {
@ -34,6 +35,11 @@ func getApiKey(c *m.ReqContext) string {
return key return key
} }
username, password, err := util.DecodeBasicAuthHeader(header)
if err == nil && username == "api_key" {
return password
}
return "" return ""
} }

View File

@ -2,6 +2,7 @@ package middleware
import ( import (
"fmt" "fmt"
"net"
"net/mail" "net/mail"
"reflect" "reflect"
"strings" "strings"
@ -28,7 +29,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
} }
// if auth proxy ip(s) defined, check if request comes from one of those // if auth proxy ip(s) defined, check if request comes from one of those
if err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil { if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
ctx.Handle(407, "Proxy authentication required", err) ctx.Handle(407, "Proxy authentication required", err)
return true return true
} }
@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
return nil return nil
} }
// Multiple ip addresses? Right-most IP address is the IP address of the most recent proxy
if strings.Contains(remoteAddr, ",") {
sourceIPs := strings.Split(remoteAddr, ",")
remoteAddr = strings.TrimSpace(sourceIPs[len(sourceIPs)-1])
}
remoteAddr = strings.TrimPrefix(remoteAddr, "[")
remoteAddr = strings.TrimSuffix(remoteAddr, "]")
proxies := strings.Split(setting.AuthProxyWhitelist, ",") proxies := strings.Split(setting.AuthProxyWhitelist, ",")
sourceIP, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return err
}
// Compare allowed IP addresses to actual address // Compare allowed IP addresses to actual address
for _, proxyIP := range proxies { for _, proxyIP := range proxies {
if remoteAddr == strings.TrimSpace(proxyIP) { if sourceIP == strings.TrimSpace(proxyIP) {
return nil return nil
} }
} }
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, remoteAddr) return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
} }

View File

@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) {
setting.BasicAuthEnabled = true setting.BasicAuthEnabled = true
authHeader := util.GetBasicAuthHeader("myUser", "myPass") authHeader := util.GetBasicAuthHeader("myUser", "myPass")
sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec() sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should init middleware context with user", func() { Convey("Should init middleware context with user", func() {
So(sc.context.IsSignedIn, ShouldEqual, true) So(sc.context.IsSignedIn, ShouldEqual, true)
@ -128,6 +128,28 @@ func TestMiddlewareContext(t *testing.T) {
}) })
}) })
middlewareScenario("Valid api key via Basic auth", func(sc *scenarioContext) {
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
return nil
})
authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9")
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should return 200", func() {
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("Should init middleware context", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
So(sc.context.OrgId, ShouldEqual, 12)
So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR)
})
})
middlewareScenario("UserId in session", func(sc *scenarioContext) { middlewareScenario("UserId in session", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
@ -293,61 +315,6 @@ func TestMiddlewareContext(t *testing.T) {
}) })
}) })
middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is not trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.1, 192.168.1.2")
sc.exec()
Convey("should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.1.2 is not from the authentication proxy")
})
})
middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is trusted", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
cmd.Result = &m.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.2, 192.168.1.1")
sc.exec()
Convey("Should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 33)
So(sc.context.OrgId, ShouldEqual, 4)
})
})
middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) { middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
setting.AuthProxyEnabled = true setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER" setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
@ -473,7 +440,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
return sc return sc
} }
func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext { func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
sc.authHeader = authHeader sc.authHeader = authHeader
return sc return sc
} }

View File

@ -93,7 +93,7 @@ export class ValueSelectDropdownCtrl {
tagValuesPromise = this.$q.when(tag.values); tagValuesPromise = this.$q.when(tag.values);
} }
tagValuesPromise.then(values => { return tagValuesPromise.then(values => {
tag.values = values; tag.values = values;
tag.valuesText = values.join(' + '); tag.valuesText = values.join(' + ');
_.each(this.options, option => { _.each(this.options, option => {
@ -132,7 +132,7 @@ export class ValueSelectDropdownCtrl {
this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length; this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
} }
selectValue(option, event, commitChange, excludeOthers) { selectValue(option, event, commitChange?, excludeOthers?) {
if (!option) { if (!option) {
return; return;
} }

View File

@ -0,0 +1,159 @@
import 'app/core/directives/value_select_dropdown';
import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown';
import q from 'q';
describe('SelectDropdownCtrl', () => {
let tagValuesMap: any = {};
ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn();
let ctrl;
describe('Given simple variable', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl(q);
ctrl.variable = {
current: { text: 'hej', value: 'hej' },
getValuesForTag: key => {
return Promise.resolve(tagValuesMap[key]);
},
};
ctrl.init();
});
it('Should init labelText and linkText', () => {
expect(ctrl.linkText).toBe('hej');
});
});
describe('Given variable with tags and dropdown is opened', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl(q);
ctrl.variable = {
current: { text: 'server-1', value: 'server-1' },
options: [
{ text: 'server-1', value: 'server-1', selected: true },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: key => {
return Promise.resolve(tagValuesMap[key]);
},
multi: true,
};
tagValuesMap.key1 = ['server-1', 'server-3'];
tagValuesMap.key2 = ['server-2', 'server-3'];
tagValuesMap.key3 = ['server-1', 'server-2', 'server-3'];
ctrl.init();
ctrl.show();
});
it('should init tags model', () => {
expect(ctrl.tags.length).toBe(3);
expect(ctrl.tags[0].text).toBe('key1');
});
it('should init options model', () => {
expect(ctrl.options.length).toBe(3);
});
it('should init selected values array', () => {
expect(ctrl.selectedValues.length).toBe(1);
});
it('should set linkText', () => {
expect(ctrl.linkText).toBe('server-1');
});
describe('after adititional value is selected', () => {
beforeEach(() => {
ctrl.selectValue(ctrl.options[2], {});
ctrl.commitChanges();
});
it('should update link text', () => {
expect(ctrl.linkText).toBe('server-1 + server-3');
});
});
describe('When tag is selected', () => {
beforeEach(async () => {
await ctrl.selectTag(ctrl.tags[0]);
ctrl.commitChanges();
});
it('should select tag', () => {
expect(ctrl.selectedTags.length).toBe(1);
});
it('should select values', () => {
expect(ctrl.options[0].selected).toBe(true);
expect(ctrl.options[2].selected).toBe(true);
});
it('link text should not include tag values', () => {
expect(ctrl.linkText).toBe('');
});
describe('and then dropdown is opened and closed without changes', () => {
beforeEach(() => {
ctrl.show();
ctrl.commitChanges();
});
it('should still have selected tag', () => {
expect(ctrl.selectedTags.length).toBe(1);
});
});
describe('and then unselected', () => {
beforeEach(async () => {
await ctrl.selectTag(ctrl.tags[0]);
});
it('should deselect tag', () => {
expect(ctrl.selectedTags.length).toBe(0);
});
});
describe('and then value is unselected', () => {
beforeEach(() => {
ctrl.selectValue(ctrl.options[0], {});
});
it('should deselect tag', () => {
expect(ctrl.selectedTags.length).toBe(0);
});
});
});
});
describe('Given variable with selected tags', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl(q);
ctrl.variable = {
current: {
text: 'server-1',
value: 'server-1',
tags: [{ text: 'key1', selected: true }],
},
options: [
{ text: 'server-1', value: 'server-1' },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: key => {
return Promise.resolve(tagValuesMap[key]);
},
multi: true,
};
ctrl.init();
ctrl.show();
});
it('should set tag as selected', () => {
expect(ctrl.tags[0].selected).toBe(true);
});
});
});

View File

@ -1,171 +0,0 @@
import { describe, beforeEach, it, expect, angularMocks, sinon } from 'test/lib/common';
import 'app/core/directives/value_select_dropdown';
describe('SelectDropdownCtrl', function() {
var scope;
var ctrl;
var tagValuesMap: any = {};
var rootScope;
var q;
beforeEach(angularMocks.module('grafana.core'));
beforeEach(
angularMocks.inject(function($controller, $rootScope, $q, $httpBackend) {
rootScope = $rootScope;
q = $q;
scope = $rootScope.$new();
ctrl = $controller('ValueSelectDropdownCtrl', { $scope: scope });
ctrl.onUpdated = sinon.spy();
$httpBackend.when('GET', /\.html$/).respond('');
})
);
describe('Given simple variable', function() {
beforeEach(function() {
ctrl.variable = {
current: { text: 'hej', value: 'hej' },
getValuesForTag: function(key) {
return q.when(tagValuesMap[key]);
},
};
ctrl.init();
});
it('Should init labelText and linkText', function() {
expect(ctrl.linkText).to.be('hej');
});
});
describe('Given variable with tags and dropdown is opened', function() {
beforeEach(function() {
ctrl.variable = {
current: { text: 'server-1', value: 'server-1' },
options: [
{ text: 'server-1', value: 'server-1', selected: true },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: function(key) {
return q.when(tagValuesMap[key]);
},
multi: true,
};
tagValuesMap.key1 = ['server-1', 'server-3'];
tagValuesMap.key2 = ['server-2', 'server-3'];
tagValuesMap.key3 = ['server-1', 'server-2', 'server-3'];
ctrl.init();
ctrl.show();
});
it('should init tags model', function() {
expect(ctrl.tags.length).to.be(3);
expect(ctrl.tags[0].text).to.be('key1');
});
it('should init options model', function() {
expect(ctrl.options.length).to.be(3);
});
it('should init selected values array', function() {
expect(ctrl.selectedValues.length).to.be(1);
});
it('should set linkText', function() {
expect(ctrl.linkText).to.be('server-1');
});
describe('after adititional value is selected', function() {
beforeEach(function() {
ctrl.selectValue(ctrl.options[2], {});
ctrl.commitChanges();
});
it('should update link text', function() {
expect(ctrl.linkText).to.be('server-1 + server-3');
});
});
describe('When tag is selected', function() {
beforeEach(function() {
ctrl.selectTag(ctrl.tags[0]);
rootScope.$digest();
ctrl.commitChanges();
});
it('should select tag', function() {
expect(ctrl.selectedTags.length).to.be(1);
});
it('should select values', function() {
expect(ctrl.options[0].selected).to.be(true);
expect(ctrl.options[2].selected).to.be(true);
});
it('link text should not include tag values', function() {
expect(ctrl.linkText).to.be('');
});
describe('and then dropdown is opened and closed without changes', function() {
beforeEach(function() {
ctrl.show();
ctrl.commitChanges();
rootScope.$digest();
});
it('should still have selected tag', function() {
expect(ctrl.selectedTags.length).to.be(1);
});
});
describe('and then unselected', function() {
beforeEach(function() {
ctrl.selectTag(ctrl.tags[0]);
rootScope.$digest();
});
it('should deselect tag', function() {
expect(ctrl.selectedTags.length).to.be(0);
});
});
describe('and then value is unselected', function() {
beforeEach(function() {
ctrl.selectValue(ctrl.options[0], {});
});
it('should deselect tag', function() {
expect(ctrl.selectedTags.length).to.be(0);
});
});
});
});
describe('Given variable with selected tags', function() {
beforeEach(function() {
ctrl.variable = {
current: {
text: 'server-1',
value: 'server-1',
tags: [{ text: 'key1', selected: true }],
},
options: [
{ text: 'server-1', value: 'server-1' },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: function(key) {
return q.when(tagValuesMap[key]);
},
multi: true,
};
ctrl.init();
ctrl.show();
});
it('should set tag as selected', function() {
expect(ctrl.tags[0].selected).to.be(true);
});
});
});

View File

@ -1,17 +1,17 @@
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
import '../annotations_srv'; import '../annotations_srv';
import helpers from 'test/specs/helpers';
import 'app/features/dashboard/time_srv'; import 'app/features/dashboard/time_srv';
import { AnnotationsSrv } from '../annotations_srv';
describe('AnnotationsSrv', function() { describe('AnnotationsSrv', function() {
var ctx = new helpers.ServiceTestContext(); let $rootScope = {
onAppEvent: jest.fn(),
};
let $q;
let datasourceSrv;
let backendSrv;
let timeSrv;
beforeEach(angularMocks.module('grafana.core')); let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv);
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.createService('timeSrv'));
beforeEach(() => {
ctx.createService('annotationsSrv');
});
describe('When translating the query result', () => { describe('When translating the query result', () => {
const annotationSource = { const annotationSource = {
@ -30,11 +30,11 @@ describe('AnnotationsSrv', function() {
let translatedAnnotations; let translatedAnnotations;
beforeEach(() => { beforeEach(() => {
translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations); translatedAnnotations = annotationsSrv.translateQueryResult(annotationSource, annotations);
}); });
it('should set defaults', () => { it('should set defaults', () => {
expect(translatedAnnotations[0].source).to.eql(annotationSource); expect(translatedAnnotations[0].source).toEqual(annotationSource);
}); });
}); });
}); });

View File

@ -0,0 +1,67 @@
//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
import 'app/features/dashboard/view_state_srv';
import config from 'app/core/config';
import { DashboardViewState } from '../view_state_srv';
describe('when updating view state', () => {
let location = {
replace: jest.fn(),
search: jest.fn(),
};
let $scope = {
onAppEvent: jest.fn(() => {}),
dashboard: {
meta: {},
panels: [],
},
};
let $rootScope = {};
let viewState;
beforeEach(() => {
config.bootData = {
user: {
orgId: 1,
},
};
});
describe('to fullscreen true and edit true', () => {
beforeEach(() => {
location.search = jest.fn(() => {
return { fullscreen: true, edit: true, panelId: 1 };
});
viewState = new DashboardViewState($scope, location, {}, $rootScope);
});
it('should update querystring and view state', () => {
var updateState = { fullscreen: true, edit: true, panelId: 1 };
viewState.update(updateState);
expect(location.search).toHaveBeenCalledWith({
edit: true,
editview: null,
fullscreen: true,
orgId: 1,
panelId: 1,
});
expect(viewState.dashboard.meta.fullscreen).toBe(true);
expect(viewState.state.fullscreen).toBe(true);
});
});
describe('to fullscreen false', () => {
beforeEach(() => {
viewState = new DashboardViewState($scope, location, {}, $rootScope);
});
it('should remove params from query string', () => {
viewState.update({ fullscreen: true, panelId: 1, edit: true });
viewState.update({ fullscreen: false });
expect(viewState.dashboard.meta.fullscreen).toBe(false);
expect(viewState.state.fullscreen).toBe(null);
});
});
});

View File

@ -1,65 +0,0 @@
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
import 'app/features/dashboard/view_state_srv';
import config from 'app/core/config';
describe('when updating view state', function() {
var viewState, location;
var timeSrv = {};
var templateSrv = {};
var contextSrv = {
user: {
orgId: 19,
},
};
beforeEach(function() {
config.bootData = {
user: {
orgId: 1,
},
};
});
beforeEach(angularMocks.module('grafana.services'));
beforeEach(
angularMocks.module(function($provide) {
$provide.value('timeSrv', timeSrv);
$provide.value('templateSrv', templateSrv);
$provide.value('contextSrv', contextSrv);
})
);
beforeEach(
angularMocks.inject(function(dashboardViewStateSrv, $location, $rootScope) {
$rootScope.onAppEvent = function() {};
$rootScope.dashboard = {
meta: {},
panels: [],
};
viewState = dashboardViewStateSrv.create($rootScope);
location = $location;
})
);
describe('to fullscreen true and edit true', function() {
it('should update querystring and view state', function() {
var updateState = { fullscreen: true, edit: true, panelId: 1 };
viewState.update(updateState);
expect(location.search()).to.eql({
fullscreen: true,
edit: true,
panelId: 1,
orgId: 1,
});
expect(viewState.dashboard.meta.fullscreen).to.be(true);
expect(viewState.state.fullscreen).to.be(true);
});
});
describe('to fullscreen false', function() {
it('should remove params from query string', function() {
viewState.update({ fullscreen: true, panelId: 1, edit: true });
viewState.update({ fullscreen: false });
expect(viewState.dashboard.meta.fullscreen).to.be(false);
expect(viewState.state.fullscreen).to.be(null);
});
});
});

View File

@ -179,4 +179,38 @@ describe('VariableSrv init', function() {
expect(variable.options[2].selected).to.be(false); expect(variable.options[2].selected).to.be(false);
}); });
}); });
describeInitScenario('when template variable is present in url multiple times using key/values', scenario => {
scenario.setup(() => {
scenario.variables = [
{
name: 'apps',
type: 'query',
multi: true,
current: { text: 'Val1', value: 'val1' },
options: [
{ text: 'Val1', value: 'val1' },
{ text: 'Val2', value: 'val2' },
{ text: 'Val3', value: 'val3', selected: true },
],
},
];
scenario.urlParams['var-apps'] = ['val2', 'val1'];
});
it('should update current value', function() {
var variable = ctx.variableSrv.variables[0];
expect(variable.current.value.length).to.be(2);
expect(variable.current.value[0]).to.be('val2');
expect(variable.current.value[1]).to.be('val1');
expect(variable.current.text).to.be('Val2 + Val1');
expect(variable.options[0].selected).to.be(true);
expect(variable.options[1].selected).to.be(true);
});
it('should set options that are not in value to selected false', function() {
var variable = ctx.variableSrv.variables[0];
expect(variable.options[2].selected).to.be(false);
});
});
}); });

View File

@ -209,7 +209,24 @@ export class VariableSrv {
return op.text === urlValue || op.value === urlValue; return op.text === urlValue || op.value === urlValue;
}); });
option = option || { text: urlValue, value: urlValue }; let defaultText = urlValue;
let defaultValue = urlValue;
if (!option && _.isArray(urlValue)) {
defaultText = [];
for (let n = 0; n < urlValue.length; n++) {
let t = _.find(variable.options, op => {
return op.value === urlValue[n];
});
if (t) {
defaultText.push(t.text);
}
}
}
option = option || { text: defaultText, value: defaultValue };
return variable.setValue(option); return variable.setValue(option);
}); });
} }

View File

@ -1,160 +1,158 @@
import _ from 'lodash'; import _ from 'lodash';
import angular from 'angular'; import angular from 'angular';
export class SeriesOverridesCtrl { /** @ngInject */
/** @ngInject */ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
constructor($scope, $element, popoverSrv) { $scope.overrideMenu = [];
$scope.overrideMenu = []; $scope.currentOverrides = [];
$scope.currentOverrides = []; $scope.override = $scope.override || {};
$scope.override = $scope.override || {};
$scope.addOverrideOption = function(name, propertyName, values) { $scope.addOverrideOption = function(name, propertyName, values) {
var option = { var option = {
text: name, text: name,
propertyName: propertyName, propertyName: propertyName,
index: $scope.overrideMenu.lenght, index: $scope.overrideMenu.lenght,
values: values, values: values,
submenu: _.map(values, function(value) { submenu: _.map(values, function(value) {
return { text: String(value), value: value }; return { text: String(value), value: value };
}), }),
};
$scope.overrideMenu.push(option);
}; };
$scope.setOverride = function(item, subItem) { $scope.overrideMenu.push(option);
// handle color overrides };
if (item.propertyName === 'color') {
$scope.openColorSelector($scope.override['color']); $scope.setOverride = function(item, subItem) {
// handle color overrides
if (item.propertyName === 'color') {
$scope.openColorSelector($scope.override['color']);
return;
}
$scope.override[item.propertyName] = subItem.value;
// automatically disable lines for this series and the fill below to series
// can be removed by the user if they still want lines
if (item.propertyName === 'fillBelowTo') {
$scope.override['lines'] = false;
$scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false });
}
$scope.updateCurrentOverrides();
$scope.ctrl.render();
};
$scope.colorSelected = function(color) {
$scope.override['color'] = color;
$scope.updateCurrentOverrides();
$scope.ctrl.render();
};
$scope.openColorSelector = function(color) {
var fakeSeries = { color: color };
popoverSrv.show({
element: $element.find('.dropdown')[0],
position: 'top center',
openOn: 'click',
template: '<series-color-picker series="series" onColorChange="colorSelected" />',
model: {
autoClose: true,
colorSelected: $scope.colorSelected,
series: fakeSeries,
},
onClose: function() {
$scope.ctrl.render();
},
});
};
$scope.removeOverride = function(option) {
delete $scope.override[option.propertyName];
$scope.updateCurrentOverrides();
$scope.ctrl.refresh();
};
$scope.getSeriesNames = function() {
return _.map($scope.ctrl.seriesList, function(series) {
return series.alias;
});
};
$scope.updateCurrentOverrides = function() {
$scope.currentOverrides = [];
_.each($scope.overrideMenu, function(option) {
var value = $scope.override[option.propertyName];
if (_.isUndefined(value)) {
return; return;
} }
$scope.currentOverrides.push({
$scope.override[item.propertyName] = subItem.value; name: option.text,
propertyName: option.propertyName,
// automatically disable lines for this series and the fill below to series value: String(value),
// can be removed by the user if they still want lines
if (item.propertyName === 'fillBelowTo') {
$scope.override['lines'] = false;
$scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false });
}
$scope.updateCurrentOverrides();
$scope.ctrl.render();
};
$scope.colorSelected = function(color) {
$scope.override['color'] = color;
$scope.updateCurrentOverrides();
$scope.ctrl.render();
};
$scope.openColorSelector = function(color) {
var fakeSeries = { color: color };
popoverSrv.show({
element: $element.find('.dropdown')[0],
position: 'top center',
openOn: 'click',
template: '<series-color-picker series="series" onColorChange="colorSelected" />',
model: {
autoClose: true,
colorSelected: $scope.colorSelected,
series: fakeSeries,
},
onClose: function() {
$scope.ctrl.render();
},
}); });
}; });
};
$scope.removeOverride = function(option) { $scope.addOverrideOption('Bars', 'bars', [true, false]);
delete $scope.override[option.propertyName]; $scope.addOverrideOption('Lines', 'lines', [true, false]);
$scope.updateCurrentOverrides(); $scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
$scope.ctrl.refresh(); $scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}; $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']);
$scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
$scope.getSeriesNames = function() { $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
return _.map($scope.ctrl.seriesList, function(series) { $scope.addOverrideOption('Dashes', 'dashes', [true, false]);
return series.alias; $scope.addOverrideOption('Dash Length', 'dashLength', [
}); 1,
}; 2,
3,
$scope.updateCurrentOverrides = function() { 4,
$scope.currentOverrides = []; 5,
_.each($scope.overrideMenu, function(option) { 6,
var value = $scope.override[option.propertyName]; 7,
if (_.isUndefined(value)) { 8,
return; 9,
} 10,
$scope.currentOverrides.push({ 11,
name: option.text, 12,
propertyName: option.propertyName, 13,
value: String(value), 14,
}); 15,
}); 16,
}; 17,
18,
$scope.addOverrideOption('Bars', 'bars', [true, false]); 19,
$scope.addOverrideOption('Lines', 'lines', [true, false]); 20,
$scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); ]);
$scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); $scope.addOverrideOption('Dash Space', 'spaceLength', [
$scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']); 1,
$scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames()); 2,
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); 3,
$scope.addOverrideOption('Dashes', 'dashes', [true, false]); 4,
$scope.addOverrideOption('Dash Length', 'dashLength', [ 5,
1, 6,
2, 7,
3, 8,
4, 9,
5, 10,
6, 11,
7, 12,
8, 13,
9, 14,
10, 15,
11, 16,
12, 17,
13, 18,
14, 19,
15, 20,
16, ]);
17, $scope.addOverrideOption('Points', 'points', [true, false]);
18, $scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]);
19, $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);
20, $scope.addOverrideOption('Color', 'color', ['change']);
]); $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
$scope.addOverrideOption('Dash Space', 'spaceLength', [ $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]);
1, $scope.addOverrideOption('Transform', 'transform', ['negative-Y']);
2, $scope.addOverrideOption('Legend', 'legend', [true, false]);
3, $scope.updateCurrentOverrides();
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
]);
$scope.addOverrideOption('Points', 'points', [true, false]);
$scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]);
$scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);
$scope.addOverrideOption('Color', 'color', ['change']);
$scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
$scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]);
$scope.addOverrideOption('Transform', 'transform', ['negative-Y']);
$scope.addOverrideOption('Legend', 'legend', [true, false]);
$scope.updateCurrentOverrides();
}
} }
angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl); angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl);

View File

@ -0,0 +1,42 @@
import '../series_overrides_ctrl';
import { SeriesOverridesCtrl } from '../series_overrides_ctrl';
describe('SeriesOverridesCtrl', () => {
let popoverSrv = {};
let $scope;
beforeEach(() => {
$scope = {
ctrl: {
refresh: jest.fn(),
render: jest.fn(),
seriesList: [],
},
render: jest.fn(() => {}),
};
SeriesOverridesCtrl($scope, {}, popoverSrv);
});
describe('When setting an override', () => {
beforeEach(() => {
$scope.setOverride({ propertyName: 'lines' }, { value: true });
});
it('should set override property', () => {
expect($scope.override.lines).toBe(true);
});
it('should update view model', () => {
expect($scope.currentOverrides[0].name).toBe('Lines');
expect($scope.currentOverrides[0].value).toBe('true');
});
});
describe('When removing overide', () => {
it('click should include option and value index', () => {
$scope.setOverride(1, 0);
$scope.removeOverride({ propertyName: 'lines' });
expect($scope.currentOverrides.length).toBe(0);
});
});
});

View File

@ -1,55 +0,0 @@
import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common';
import '../series_overrides_ctrl';
import helpers from 'test/specs/helpers';
describe('SeriesOverridesCtrl', function() {
var ctx = new helpers.ControllerTestContext();
var popoverSrv = {};
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(
ctx.providePhase({
popoverSrv: popoverSrv,
})
);
beforeEach(
angularMocks.inject(function($rootScope, $controller) {
ctx.scope = $rootScope.$new();
ctx.scope.ctrl = {
refresh: sinon.spy(),
render: sinon.spy(),
seriesList: [],
};
ctx.scope.render = function() {};
ctx.controller = $controller('SeriesOverridesCtrl', {
$scope: ctx.scope,
});
})
);
describe('When setting an override', function() {
beforeEach(function() {
ctx.scope.setOverride({ propertyName: 'lines' }, { value: true });
});
it('should set override property', function() {
expect(ctx.scope.override.lines).to.be(true);
});
it('should update view model', function() {
expect(ctx.scope.currentOverrides[0].name).to.be('Lines');
expect(ctx.scope.currentOverrides[0].value).to.be('true');
});
});
describe('When removing overide', function() {
it('click should include option and value index', function() {
ctx.scope.setOverride(1, 0);
ctx.scope.removeOverride({ propertyName: 'lines' });
expect(ctx.scope.currentOverrides.length).to.be(0);
});
});
});

View File

@ -29,7 +29,7 @@
<input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur> <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
<label class="gf-form-label width-6">Font size</label> <label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="ctrl.canChangeFontSize()"></select> <select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
</div> </div>
</div> </div>
</div> </div>
@ -39,7 +39,7 @@
<input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur> <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
<label class="gf-form-label width-6">Font size</label> <label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="ctrl.canChangeFontSize()"></select> <select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
</div> </div>
</div> </div>
<div class="gf-form"> <div class="gf-form">
@ -58,6 +58,10 @@
<gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch> <gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch> <gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
</div> </div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label-class="width-6" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-6" label="Postfix" checked="ctrl.panel.colorPostfix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
</div>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form max-width-21"> <div class="gf-form max-width-21">
<label class="gf-form-label width-8">Thresholds <label class="gf-form-label width-8">Thresholds

View File

@ -198,8 +198,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
this.setValueMapping(data); this.setValueMapping(data);
} }
canChangeFontSize() { canModifyText() {
return this.panel.gauge.show; return !this.panel.gauge.show;
} }
setColoring(options) { setColoring(options) {
@ -405,10 +405,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
elem = elem.find('.singlestat-panel'); elem = elem.find('.singlestat-panel');
function applyColoringThresholds(value, valueString) { function applyColoringThresholds(value, valueString) {
if (!panel.colorValue) {
return valueString;
}
var color = getColorForValue(data, value); var color = getColorForValue(data, value);
if (color) { if (color) {
return '<span style="color:' + color + '">' + valueString + '</span>'; return '<span style="color:' + color + '">' + valueString + '</span>';
@ -426,15 +422,24 @@ class SingleStatCtrl extends MetricsPanelCtrl {
var body = '<div class="singlestat-panel-value-container">'; var body = '<div class="singlestat-panel-value-container">';
if (panel.prefix) { if (panel.prefix) {
var prefix = applyColoringThresholds(data.value, panel.prefix); var prefix = panel.prefix;
if (panel.colorPrefix) {
prefix = applyColoringThresholds(data.value, panel.prefix);
}
body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix); body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix);
} }
var value = applyColoringThresholds(data.value, data.valueFormatted); var value = data.valueFormatted;
if (panel.colorValue) {
value = applyColoringThresholds(data.value, value);
}
body += getSpan('singlestat-panel-value', panel.valueFontSize, value); body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
if (panel.postfix) { if (panel.postfix) {
var postfix = applyColoringThresholds(data.value, panel.postfix); var postfix = panel.postfix;
if (panel.colorPostfix) {
postfix = applyColoringThresholds(data.value, panel.postfix);
}
body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix); body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix);
} }

View File

@ -25,7 +25,7 @@
display: inline-block; display: inline-block;
padding-right: 2px; padding-right: 2px;
&::after { &::after {
content: " | "; content: ' | ';
padding-left: 2px; padding-left: 2px;
} }
} }
@ -33,14 +33,23 @@
li:last-child { li:last-child {
&::after { &::after {
padding-left: 0; padding-left: 0;
content: ""; content: '';
} }
} }
} }
.login-page { .login-page {
.footer { .footer {
position: absolute; padding: 1rem 0 1rem 0;
bottom: $spacer; }
}
@include media-breakpoint-up(md) {
.login-page {
.footer {
bottom: $spacer;
position: absolute;
padding: 5rem 0 1rem 0;
}
} }
} }

View File

@ -64,8 +64,8 @@
} }
input + label::before { input + label::before {
font-family: "FontAwesome"; font-family: 'FontAwesome';
content: "\f096"; // square-o content: '\f096'; // square-o
color: $text-color-weak; color: $text-color-weak;
transition: transform 0.4s; transition: transform 0.4s;
backface-visibility: hidden; backface-visibility: hidden;
@ -73,11 +73,11 @@
} }
input + label::after { input + label::after {
content: "\f046"; // check-square-o content: '\f046'; // check-square-o
color: $orange; color: $orange;
text-shadow: $text-shadow-strong; text-shadow: $text-shadow-strong;
font-family: "FontAwesome"; font-family: 'FontAwesome';
transition: transform 0.4s; transition: transform 0.4s;
transform: rotateY(180deg); transform: rotateY(180deg);
backface-visibility: hidden; backface-visibility: hidden;
@ -154,7 +154,8 @@ gf-form-switch[disabled] {
.gf-form-switch input + label { .gf-form-switch input + label {
cursor: default; cursor: default;
pointer-events: none !important; pointer-events: none !important;
&::before { &::before,
&::after {
color: $text-color-faint; color: $text-color-faint;
text-shadow: none; text-shadow: none;
} }

View File

@ -1,9 +1,8 @@
$login-border: #8daac5; $login-border: #8daac5;
.login { .login {
background-position: center;
min-height: 85vh; min-height: 85vh;
height: 80vh; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
min-width: 100%; min-width: 100%;
margin-left: 0; margin-left: 0;
@ -95,7 +94,7 @@ select:-webkit-autofill:focus {
position: relative; position: relative;
justify-content: center; justify-content: center;
z-index: 1; z-index: 1;
height: 320px; min-height: 320px;
} }
.login-branding { .login-branding {
@ -106,6 +105,7 @@ select:-webkit-autofill:focus {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-grow: 0; flex-grow: 0;
padding-top: 2rem;
.logo-icon { .logo-icon {
width: 70px; width: 70px;
@ -127,7 +127,7 @@ select:-webkit-autofill:focus {
.login-inner-box { .login-inner-box {
text-align: center; text-align: center;
padding: 2rem 4rem; padding: 2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -243,7 +243,7 @@ select:-webkit-autofill:focus {
justify-content: space-between; justify-content: space-between;
.login-divider-line { .login-divider-line {
width: 110px; width: 100px;
height: 10px; height: 10px;
border-bottom: 1px solid $login-border; border-bottom: 1px solid $login-border;
@ -323,7 +323,10 @@ select:-webkit-autofill:focus {
width: 35%; width: 35%;
padding: 4rem 2rem; padding: 4rem 2rem;
border-right: 1px solid $login-border; border-right: 1px solid $login-border;
justify-content: flex-start;
.logo-icon {
width: 80px;
}
} }
.login-inner-box { .login-inner-box {
@ -331,14 +334,18 @@ select:-webkit-autofill:focus {
padding: 1rem 2rem; padding: 1rem 2rem;
} }
.login-branding { .login-divider {
.logo-icon { .login-divider-line {
width: 80px; width: 110px;
} }
} }
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
.login {
min-height: 100vh;
}
.login-content { .login-content {
flex: 1 0 100%; flex: 1 0 100%;
} }
@ -373,10 +380,6 @@ select:-webkit-autofill:focus {
} }
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
.login {
min-height: 100vh;
}
.login-form-input { .login-form-input {
min-width: 300px; min-width: 300px;
} }

View File

@ -1,4 +1,4 @@
module.exports = function(config) { module.exports = function (config) {
'use strict'; 'use strict';
return { return {
@ -10,7 +10,10 @@ module.exports = function(config) {
debug: { debug: {
configFile: 'karma.conf.js', configFile: 'karma.conf.js',
singleRun: false, singleRun: false,
browsers: ['Chrome'] browsers: ['Chrome'],
mime: {
'text/x-typescript': ['ts', 'tsx']
},
}, },
test: { test: {

View File

@ -1,37 +1,29 @@
'use strict'; 'use strict';
const ExtractTextPlugin = require("extract-text-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = function (options, extractSass) { module.exports = function(options) {
return { return {
test: /\.scss$/, test: /\.scss$/,
use: (extractSass || ExtractTextPlugin).extract({ use: [
use: [ MiniCssExtractPlugin.loader,
{ {
loader: 'css-loader', loader: 'css-loader',
options: {
importLoaders: 2,
url: options.preserveUrl,
sourceMap: options.sourceMap,
minimize: options.minimize,
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap,
config: { path: __dirname + '/postcss.config.js' }
}
},
{ loader: 'sass-loader', options: { sourceMap: options.sourceMap } }
],
fallback: [{
loader: 'style-loader',
options: { options: {
sourceMap: true importLoaders: 2,
} url: options.preserveUrl,
}] sourceMap: options.sourceMap,
}) minimize: options.minimize,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap,
config: { path: __dirname + '/postcss.config.js' },
},
},
{ loader: 'sass-loader', options: { sourceMap: options.sourceMap } },
],
}; };
} };

View File

@ -1,5 +1,5 @@
const path = require('path'); const path = require('path');
const { CheckerPlugin } = require('awesome-typescript-loader'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = { module.exports = {
target: 'web', target: 'web',
@ -61,6 +61,8 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new CheckerPlugin(), new ForkTsCheckerWebpackPlugin({
checkSyntacticErrors: true,
}),
] ]
}; };

View File

@ -7,20 +7,17 @@ const webpack = require('webpack');
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin"); const ExtractTextPlugin = require("extract-text-webpack-plugin");
const CleanWebpackPlugin = require('clean-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const extractSass = new ExtractTextPlugin({
filename: "grafana.[name].css"
});
module.exports = merge(common, { module.exports = merge(common, {
devtool: "cheap-module-source-map", devtool: "cheap-module-source-map",
mode: 'development',
entry: { entry: {
app: './public/app/index.ts', app: './public/app/index.ts',
dark: './public/sass/grafana.dark.scss', dark: './public/sass/grafana.dark.scss',
light: './public/sass/grafana.light.scss', light: './public/sass/grafana.light.scss',
vendor: require('./dependencies'),
}, },
output: { output: {
@ -48,15 +45,13 @@ module.exports = merge(common, {
test: /\.tsx?$/, test: /\.tsx?$/,
exclude: /node_modules/, exclude: /node_modules/,
use: { use: {
loader: 'awesome-typescript-loader', loader: 'ts-loader',
options: { options: {
useCache: true, transpileOnly: true
}, },
} },
}, },
require('./sass.rule.js')({ require('./sass.rule.js')({ sourceMap: false, minimize: false, preserveUrl: false }),
sourceMap: true, minimize: false, preserveUrl: false
}, extractSass),
{ {
test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
loader: 'file-loader' loader: 'file-loader'
@ -64,9 +59,30 @@ module.exports = merge(common, {
] ]
}, },
optimization: {
splitChunks: {
cacheGroups: {
manifest: {
chunks: "initial",
test: "vendor",
name: "vendor",
enforce: true
},
vendor: {
chunks: "initial",
test: "vendor",
name: "vendor",
enforce: true
}
}
}
},
plugins: [ plugins: [
new CleanWebpackPlugin('../../public/build', { allowExternal: true }), new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
extractSass, new MiniCssExtractPlugin({
filename: "grafana.[name].css"
}),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index.template.html'), template: path.resolve(__dirname, '../../public/views/index.template.html'),
@ -80,9 +96,6 @@ module.exports = merge(common, {
'NODE_ENV': JSON.stringify('development') 'NODE_ENV': JSON.stringify('development')
} }
}), }),
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest'],
}),
// new BundleAnalyzerPlugin({ // new BundleAnalyzerPlugin({
// analyzerPort: 8889 // analyzerPort: 8889
// }) // })

View File

@ -42,20 +42,23 @@ module.exports = merge(common, {
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
exclude: /node_modules/, exclude: /node_modules/,
use: { use: [{
loader: 'awesome-typescript-loader', loader: 'babel-loader',
options: { options: {
useCache: true, cacheDirectory: true,
useBabel: true, babelrc: false,
babelOptions: { plugins: [
babelrc: false, 'syntax-dynamic-import',
plugins: [ 'react-hot-loader/babel'
'syntax-dynamic-import', ]
'react-hot-loader/babel' }
] },
} {
loader: 'ts-loader',
options: {
transpileOnly: true
}, },
} }],
}, },
{ {
test: /\.scss$/, test: /\.scss$/,

View File

@ -1,21 +1,22 @@
'use strict'; 'use strict';
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js'); const common = require('./webpack.common.js');
const webpack = require('webpack'); const webpack = require('webpack');
const path = require('path'); const path = require('path');
const ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(common, { module.exports = merge(common, {
mode: 'production',
devtool: "source-map", devtool: "source-map",
entry: { entry: {
dark: './public/sass/grafana.dark.scss', dark: './public/sass/grafana.dark.scss',
light: './public/sass/grafana.light.scss', light: './public/sass/grafana.light.scss',
vendor: require('./dependencies'),
}, },
module: { module: {
@ -35,49 +36,49 @@ module.exports = merge(common, {
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: {
{ loader: 'ts-loader',
loader: 'awesome-typescript-loader', options: {
options: { transpileOnly: true
errorsAsWarnings: false,
},
}, },
] },
}, },
require('./sass.rule.js')({ require('./sass.rule.js')({
sourceMap: false, minimize: true, preserveUrl: false sourceMap: false, minimize: false, preserveUrl: false
}) })
] ]
}, },
devServer: { optimization: {
noInfo: true, splitChunks: {
stats: { cacheGroups: {
chunks: false, commons: {
test: /[\\/]node_modules[\\/].*[jt]sx?$/,
name: "vendor",
chunks: "all"
}
}
}, },
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true
}),
new OptimizeCSSAssetsPlugin({})
]
}, },
plugins: [ plugins: [
new ExtractTextPlugin({ new MiniCssExtractPlugin({
filename: 'grafana.[name].css', filename: "grafana.[name].css"
}), }),
new ngAnnotatePlugin(), new ngAnnotatePlugin(),
new UglifyJSPlugin({
sourceMap: true,
}),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index.template.html'), template: path.resolve(__dirname, '../../public/views/index.template.html'),
inject: 'body', inject: 'body',
chunks: ['manifest', 'vendor', 'app'], chunks: ['vendor', 'app'],
}),
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest'],
}), }),
function () { function () {
this.plugin("done", function (stats) { this.plugin("done", function (stats) {

View File

@ -3,29 +3,36 @@ const merge = require('webpack-merge');
const common = require('./webpack.common.js'); const common = require('./webpack.common.js');
config = merge(common, { config = merge(common, {
mode: 'development',
devtool: 'cheap-module-source-map', devtool: 'cheap-module-source-map',
externals: { externals: {
'react/addons': true, 'react/addons': true,
'react/lib/ExecutionEnvironment': true, 'react/lib/ExecutionEnvironment': true,
'react/lib/ReactContext': true, 'react/lib/ReactContext': true,
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: {
{ loader: "awesome-typescript-loader" } loader: 'ts-loader',
] options: {
transpileOnly: true,
},
},
}, },
] ],
}, },
plugins: [ plugins: [
new webpack.SourceMapDevToolPlugin({ new webpack.SourceMapDevToolPlugin({
filename: null, // if no value is provided the sourcemap is inlined filename: null, // if no value is provided the sourcemap is inlined
test: /\.(ts|js)($|\?)/i // process .js and .ts files only test: /\.(ts|js)($|\?)/i, // process .js and .ts files only
}), }),
] ],
}); });
module.exports = config; module.exports = config;

3181
yarn.lock

File diff suppressed because it is too large Load Diff