mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of https://github.com/grafana/grafana
This commit is contained in:
commit
84af033281
@ -88,12 +88,9 @@ jobs:
|
||||
|
||||
test-frontend:
|
||||
docker:
|
||||
- image: circleci/node:6.11.4
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: install yarn
|
||||
command: 'sudo npm install -g yarn --quiet'
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
|
20
CHANGELOG.md
20
CHANGELOG.md
@ -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)
|
||||
* **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
|
||||
|
||||
* **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)
|
||||
* **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)
|
||||
|
||||
@ -56,6 +69,7 @@
|
||||
### New Features
|
||||
|
||||
* **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)
|
||||
* **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)
|
||||
* **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)
|
||||
|
||||
* **Permissions**: Important security fix for API keys with viewer role [#12343](https://github.com/grafana/grafana/issues/12343)
|
||||
|
17
ROADMAP.md
17
ROADMAP.md
@ -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.
|
||||
But it will give you an idea of our current vision and plan.
|
||||
|
||||
### Short term (1-2 months)
|
||||
|
||||
- Elasticsearch alerting
|
||||
- Crossplatform builds
|
||||
- Backend service refactorings
|
||||
- Explore UI
|
||||
- First login registration view
|
||||
|
||||
### Mid term (2-4 months)
|
||||
- Multi-Stat panel
|
||||
- Metrics & Log Explore UI
|
||||
|
||||
### Mid term (2-4 months)
|
||||
- React Panels
|
||||
- Change visualization (panel type) on the fly.
|
||||
- Templating Query Editor UI Plugin hook
|
||||
|
||||
### Long term (4 - 8 months)
|
||||
|
||||
- Alerting improvements (silence, per series tracking, etc)
|
||||
- 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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
* [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!
|
||||
* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets
|
||||
* [Security]({{< relref "#security" >}}) make your Grafana instance more secure
|
||||
* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
|
||||
* [InfluxDB]({{< relref "#influxdb" >}}) with support for a new function
|
||||
* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
|
||||
* [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements
|
||||
- [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here!
|
||||
- [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
|
||||
- [Security]({{< relref "#security" >}}) make your Grafana instance more secure
|
||||
- [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
|
||||
- [InfluxDB]({{< relref "#influxdb" >}}) now supports the `mode` function
|
||||
- [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
|
||||
- [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements
|
||||
|
||||
## 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>
|
||||
|
||||
## 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),
|
||||
MacOS/Darwin (x64) and Windows (x64) in both stable and nightly builds.
|
||||
Grafana v5.2 brings an improved build pipeline with cross-platform support. This enables native builds of Grafana for ARMv7 (x32) and ARM64 (x64).
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
## 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" >}}
|
||||
|
||||
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>
|
||||
|
||||
## Prometheus
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@ -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" >}}
|
||||
|
||||
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.
|
||||
This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit
|
||||
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.
|
||||
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.
|
||||
|
||||
<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" >}}
|
||||
|
||||
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.0 introduced support for dashboard folders and permissions. The import dashboard page have also got some general improvements
|
||||
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 has also got some general improvements
|
||||
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
|
||||
*Create Dashboard* and *Import Dashboard* is available in side navigation, dashboard search and manage dashboards/folder page for a
|
||||
user that has editor role in an organization or edit permission in at least one folder.
|
||||
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* 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 the edit permission in at least one folder.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
@ -44,6 +44,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
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
|
||||
|
||||
## Api Keys
|
||||
|
@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
|
||||
<h4>Provisioning</h4>
|
||||
<p>A guide to help you automate your Grafana setup & configuration.</p>
|
||||
</a>
|
||||
<a href="{{< relref "guides/whats-new-in-v5.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>What's new in v5.0</h4>
|
||||
<p>Article on all the new cool features and enhancements in v5.0</p>
|
||||
<a href="{{< relref "guides/whats-new-in-v5-2.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>What's new in v5.2</h4>
|
||||
<p>Article on all the new cool features and enhancements in v5.2</p>
|
||||
</a>
|
||||
<a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>Screencasts</h4>
|
||||
|
@ -15,10 +15,9 @@ weight = 1
|
||||
|
||||
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)
|
||||
<!--
|
||||
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 | [x86-64](https://grafana.com/grafana/download?platform=linux)
|
||||
Stable for Debian-based Linux | [ARM64](https://grafana.com/grafana/download?platform=arm)
|
||||
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
|
||||
installation.
|
||||
@ -27,17 +26,18 @@ installation.
|
||||
|
||||
|
||||
```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 dpkg -i grafana_5.1.4_amd64.deb
|
||||
```
|
||||
|
||||
<!-- ## Install Latest Beta
|
||||
Example:
|
||||
|
||||
```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 dpkg -i grafana_5.1.0-beta1_amd64.deb
|
||||
``` -->
|
||||
sudo dpkg -i grafana_5.1.4_amd64.deb
|
||||
```
|
||||
|
||||
## APT Repository
|
||||
|
||||
|
@ -11,6 +11,8 @@ weight = 4
|
||||
|
||||
# Installing on Mac
|
||||
|
||||
## Install using homebrew
|
||||
|
||||
Installation can be done using [homebrew](http://brew.sh/)
|
||||
|
||||
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`
|
||||
|
||||
## 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).
|
||||
|
||||
|
@ -15,42 +15,49 @@ weight = 2
|
||||
|
||||
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)
|
||||
<!--
|
||||
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 | [x86-64](https://grafana.com/grafana/download?platform=linux)
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [ARM64](https://grafana.com/grafana/download?platform=arm)
|
||||
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
|
||||
installation.
|
||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation.
|
||||
|
||||
## Install Stable
|
||||
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
```bash
|
||||
$ sudo yum install <rpm package url>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
$ 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
|
||||
$ 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`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
Example:
|
||||
|
||||
```bash
|
||||
$ 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
|
||||
$ 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
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
## Logging
|
||||
|
@ -12,11 +12,7 @@ weight = 3
|
||||
|
||||
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 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)
|
||||
-->
|
||||
Latest stable package for Windows | [x64](https://grafana.com/grafana/download?platform=windows)
|
||||
|
||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||
installation.
|
||||
|
@ -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": "v4.6", "path": "/v4.6", "archived": true },
|
||||
{ "version": "v4.5", "path": "/v4.5", "archived": true },
|
||||
|
@ -19,8 +19,8 @@ module.exports = function(config) {
|
||||
},
|
||||
|
||||
webpack: webpackTestConfig,
|
||||
webpackServer: {
|
||||
noInfo: true, // please don't spam the console when running in karma!
|
||||
webpackMiddleware: {
|
||||
stats: 'minimal',
|
||||
},
|
||||
|
||||
// list of files to exclude
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "5.1.3",
|
||||
"testing": "5.1.3"
|
||||
"stable": "5.2.0",
|
||||
"testing": "5.2.0"
|
||||
}
|
||||
|
35
package.json
35
package.json
@ -4,7 +4,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "5.2.0-pre1",
|
||||
"version": "5.3.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@ -16,11 +16,11 @@
|
||||
"@types/node": "^8.0.31",
|
||||
"@types/react": "^16.0.25",
|
||||
"@types/react-dom": "^16.0.3",
|
||||
"angular-mocks": "^1.6.6",
|
||||
"angular-mocks": "1.6.6",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"awesome-typescript-loader": "^4.0.0",
|
||||
"axios": "^0.17.1",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
@ -32,8 +32,9 @@
|
||||
"es6-shim": "^0.35.3",
|
||||
"expect.js": "~0.2.0",
|
||||
"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",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.1",
|
||||
"gaze": "^1.1.2",
|
||||
"glob": "~7.0.0",
|
||||
"grunt": "1.0.1",
|
||||
@ -56,7 +57,7 @@
|
||||
"grunt-webpack": "^3.0.2",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-harddisk-plugin": "^0.2.0",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^0.14.3",
|
||||
"jest": "^22.0.4",
|
||||
"jshint-stylish": "~2.2.1",
|
||||
@ -67,7 +68,7 @@
|
||||
"karma-phantomjs-launcher": "1.0.4",
|
||||
"karma-sinon": "^1.0.5",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^2.0.4",
|
||||
"karma-webpack": "^3.0.0",
|
||||
"lint-staged": "^6.0.0",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mobx-react-devtools": "^4.2.15",
|
||||
@ -89,21 +90,24 @@
|
||||
"style-loader": "^0.21.0",
|
||||
"systemjs": "0.20.19",
|
||||
"systemjs-plugin-css": "^0.1.36",
|
||||
"ts-loader": "^4.3.0",
|
||||
"ts-jest": "^22.4.6",
|
||||
"tslint": "^5.8.0",
|
||||
"tslint-loader": "^3.5.3",
|
||||
"typescript": "^2.6.2",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack": "^4.8.0",
|
||||
"webpack-bundle-analyzer": "^2.9.0",
|
||||
"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",
|
||||
"zone.js": "^0.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --progress --colors --config scripts/webpack/webpack.dev.js",
|
||||
"start": "webpack-dev-server --progress --colors --config scripts/webpack/webpack.hot.js",
|
||||
"watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
|
||||
"dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js",
|
||||
"start": "webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js",
|
||||
"watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js",
|
||||
"build": "grunt build",
|
||||
"test": "grunt test",
|
||||
"test:coverage": "grunt test --coverage=true",
|
||||
@ -135,8 +139,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"angular": "1.6.6",
|
||||
"angular-bindonce": "^0.3.1",
|
||||
"angular-native-dragdrop": "^1.2.2",
|
||||
"angular-bindonce": "0.3.1",
|
||||
"angular-native-dragdrop": "1.2.2",
|
||||
"angular-route": "1.6.6",
|
||||
"angular-sanitize": "1.6.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
@ -151,12 +155,14 @@
|
||||
"immutable": "^3.8.2",
|
||||
"jquery": "^3.2.1",
|
||||
"lodash": "^4.17.4",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"mobx": "^3.4.1",
|
||||
"mobx-react": "^4.3.5",
|
||||
"mobx-state-tree": "^1.3.1",
|
||||
"moment": "^2.18.1",
|
||||
"mousetrap": "^1.6.0",
|
||||
"mousetrap-global-bind": "^1.1.0",
|
||||
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
||||
"prismjs": "^1.6.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.2.0",
|
||||
@ -175,7 +181,8 @@
|
||||
"slate-react": "^0.12.4",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "^1.4.1"
|
||||
"tinycolor2": "^1.4.1",
|
||||
"uglifyjs-webpack-plugin": "^1.2.7"
|
||||
},
|
||||
"resolutions": {
|
||||
"caniuse-db": "1.0.30000772"
|
||||
|
@ -272,9 +272,9 @@ func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if dashboardID > 0 {
|
||||
guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
|
||||
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
|
||||
if dashboardID != 0 {
|
||||
guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
|
||||
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ type RouteRegister interface {
|
||||
|
||||
// Register iterates over all routes added to the RouteRegister
|
||||
// and add them to the `Router` pass as an parameter.
|
||||
Register(Router) *macaron.Router
|
||||
Register(Router)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (rr *routeRegister) Register(router Router) *macaron.Router {
|
||||
func (rr *routeRegister) Register(router Router) {
|
||||
for _, r := range rr.routes {
|
||||
// GET requests have to be added to macaron routing using Get()
|
||||
// Otherwise HEAD requests will not be allowed.
|
||||
@ -116,8 +116,6 @@ func (rr *routeRegister) Register(router Router) *macaron.Router {
|
||||
for _, g := range rr.groups {
|
||||
g.Register(router)
|
||||
}
|
||||
|
||||
return &macaron.Router{}
|
||||
}
|
||||
|
||||
func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {
|
||||
|
@ -308,9 +308,6 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
} else {
|
||||
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)
|
||||
|
||||
@ -334,11 +331,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
|
||||
if len(groupSearchResult.Entries) > 0 {
|
||||
for i := range groupSearchResult.Entries {
|
||||
if a.server.Attr.MemberOf == "dn" {
|
||||
memberOf = append(memberOf, groupSearchResult.Entries[i].DN)
|
||||
} else {
|
||||
memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
|
||||
}
|
||||
memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -356,7 +349,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
}
|
||||
|
||||
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
|
||||
if name == "DN" {
|
||||
if strings.ToLower(name) == "dn" {
|
||||
return result.Entries[n].DN
|
||||
}
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type AuthOptions struct {
|
||||
@ -34,6 +35,11 @@ func getApiKey(c *m.ReqContext) string {
|
||||
return key
|
||||
}
|
||||
|
||||
username, password, err := util.DecodeBasicAuthHeader(header)
|
||||
if err == nil && username == "api_key" {
|
||||
return password
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"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 err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil {
|
||||
if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
|
||||
ctx.Handle(407, "Proxy authentication required", err)
|
||||
return true
|
||||
}
|
||||
@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
|
||||
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, ",")
|
||||
sourceIP, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Compare allowed IP addresses to actual address
|
||||
for _, proxyIP := range proxies {
|
||||
if remoteAddr == strings.TrimSpace(proxyIP) {
|
||||
if sourceIP == strings.TrimSpace(proxyIP) {
|
||||
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)
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
|
||||
setting.BasicAuthEnabled = true
|
||||
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() {
|
||||
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) {
|
||||
|
||||
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) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
@ -473,7 +440,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext {
|
||||
func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
|
||||
sc.authHeader = authHeader
|
||||
return sc
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ export class ValueSelectDropdownCtrl {
|
||||
tagValuesPromise = this.$q.when(tag.values);
|
||||
}
|
||||
|
||||
tagValuesPromise.then(values => {
|
||||
return tagValuesPromise.then(values => {
|
||||
tag.values = values;
|
||||
tag.valuesText = values.join(' + ');
|
||||
_.each(this.options, option => {
|
||||
@ -132,7 +132,7 @@ export class ValueSelectDropdownCtrl {
|
||||
this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
|
||||
}
|
||||
|
||||
selectValue(option, event, commitChange, excludeOthers) {
|
||||
selectValue(option, event, commitChange?, excludeOthers?) {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
159
public/app/core/specs/value_select_dropdown.jest.ts
Normal file
159
public/app/core/specs/value_select_dropdown.jest.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,17 +1,17 @@
|
||||
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
|
||||
import '../annotations_srv';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import 'app/features/dashboard/time_srv';
|
||||
import { AnnotationsSrv } from '../annotations_srv';
|
||||
|
||||
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'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(ctx.createService('timeSrv'));
|
||||
beforeEach(() => {
|
||||
ctx.createService('annotationsSrv');
|
||||
});
|
||||
let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv);
|
||||
|
||||
describe('When translating the query result', () => {
|
||||
const annotationSource = {
|
||||
@ -30,11 +30,11 @@ describe('AnnotationsSrv', function() {
|
||||
let translatedAnnotations;
|
||||
|
||||
beforeEach(() => {
|
||||
translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations);
|
||||
translatedAnnotations = annotationsSrv.translateQueryResult(annotationSource, annotations);
|
||||
});
|
||||
|
||||
it('should set defaults', () => {
|
||||
expect(translatedAnnotations[0].source).to.eql(annotationSource);
|
||||
expect(translatedAnnotations[0].source).toEqual(annotationSource);
|
||||
});
|
||||
});
|
||||
});
|
67
public/app/features/dashboard/specs/viewstate_srv.jest.ts
Normal file
67
public/app/features/dashboard/specs/viewstate_srv.jest.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -179,4 +179,38 @@ describe('VariableSrv init', function() {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -209,7 +209,24 @@ export class VariableSrv {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
@ -1,160 +1,158 @@
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
|
||||
export class SeriesOverridesCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $element, popoverSrv) {
|
||||
$scope.overrideMenu = [];
|
||||
$scope.currentOverrides = [];
|
||||
$scope.override = $scope.override || {};
|
||||
/** @ngInject */
|
||||
export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
|
||||
$scope.overrideMenu = [];
|
||||
$scope.currentOverrides = [];
|
||||
$scope.override = $scope.override || {};
|
||||
|
||||
$scope.addOverrideOption = function(name, propertyName, values) {
|
||||
var option = {
|
||||
text: name,
|
||||
propertyName: propertyName,
|
||||
index: $scope.overrideMenu.lenght,
|
||||
values: values,
|
||||
submenu: _.map(values, function(value) {
|
||||
return { text: String(value), value: value };
|
||||
}),
|
||||
};
|
||||
|
||||
$scope.overrideMenu.push(option);
|
||||
$scope.addOverrideOption = function(name, propertyName, values) {
|
||||
var option = {
|
||||
text: name,
|
||||
propertyName: propertyName,
|
||||
index: $scope.overrideMenu.lenght,
|
||||
values: values,
|
||||
submenu: _.map(values, function(value) {
|
||||
return { text: String(value), value: value };
|
||||
}),
|
||||
};
|
||||
|
||||
$scope.setOverride = function(item, subItem) {
|
||||
// handle color overrides
|
||||
if (item.propertyName === 'color') {
|
||||
$scope.openColorSelector($scope.override['color']);
|
||||
$scope.overrideMenu.push(option);
|
||||
};
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$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.currentOverrides.push({
|
||||
name: option.text,
|
||||
propertyName: option.propertyName,
|
||||
value: String(value),
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
$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;
|
||||
}
|
||||
$scope.currentOverrides.push({
|
||||
name: option.text,
|
||||
propertyName: option.propertyName,
|
||||
value: String(value),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addOverrideOption('Bars', 'bars', [true, false]);
|
||||
$scope.addOverrideOption('Lines', 'lines', [true, false]);
|
||||
$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('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']);
|
||||
$scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
|
||||
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
|
||||
$scope.addOverrideOption('Dashes', 'dashes', [true, false]);
|
||||
$scope.addOverrideOption('Dash Length', 'dashLength', [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
]);
|
||||
$scope.addOverrideOption('Dash Space', 'spaceLength', [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
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();
|
||||
}
|
||||
$scope.addOverrideOption('Bars', 'bars', [true, false]);
|
||||
$scope.addOverrideOption('Lines', 'lines', [true, false]);
|
||||
$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('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']);
|
||||
$scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
|
||||
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
|
||||
$scope.addOverrideOption('Dashes', 'dashes', [true, false]);
|
||||
$scope.addOverrideOption('Dash Length', 'dashLength', [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
]);
|
||||
$scope.addOverrideOption('Dash Space', 'spaceLength', [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
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);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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>
|
||||
<label class="gf-form-label width-6">Font size</label>
|
||||
<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>
|
||||
@ -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>
|
||||
<label class="gf-form-label width-6">Font size</label>
|
||||
<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 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-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
|
||||
</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 max-width-21">
|
||||
<label class="gf-form-label width-8">Thresholds
|
||||
|
@ -198,8 +198,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
this.setValueMapping(data);
|
||||
}
|
||||
|
||||
canChangeFontSize() {
|
||||
return this.panel.gauge.show;
|
||||
canModifyText() {
|
||||
return !this.panel.gauge.show;
|
||||
}
|
||||
|
||||
setColoring(options) {
|
||||
@ -405,10 +405,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
elem = elem.find('.singlestat-panel');
|
||||
|
||||
function applyColoringThresholds(value, valueString) {
|
||||
if (!panel.colorValue) {
|
||||
return valueString;
|
||||
}
|
||||
|
||||
var color = getColorForValue(data, value);
|
||||
if (color) {
|
||||
return '<span style="color:' + color + '">' + valueString + '</span>';
|
||||
@ -426,15 +422,24 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
var body = '<div class="singlestat-panel-value-container">';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
display: inline-block;
|
||||
padding-right: 2px;
|
||||
&::after {
|
||||
content: " | ";
|
||||
content: ' | ';
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
@ -33,14 +33,23 @@
|
||||
li:last-child {
|
||||
&::after {
|
||||
padding-left: 0;
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-page {
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: $spacer;
|
||||
padding: 1rem 0 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.login-page {
|
||||
.footer {
|
||||
bottom: $spacer;
|
||||
position: absolute;
|
||||
padding: 5rem 0 1rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,8 +64,8 @@
|
||||
}
|
||||
|
||||
input + label::before {
|
||||
font-family: "FontAwesome";
|
||||
content: "\f096"; // square-o
|
||||
font-family: 'FontAwesome';
|
||||
content: '\f096'; // square-o
|
||||
color: $text-color-weak;
|
||||
transition: transform 0.4s;
|
||||
backface-visibility: hidden;
|
||||
@ -73,11 +73,11 @@
|
||||
}
|
||||
|
||||
input + label::after {
|
||||
content: "\f046"; // check-square-o
|
||||
content: '\f046'; // check-square-o
|
||||
color: $orange;
|
||||
text-shadow: $text-shadow-strong;
|
||||
|
||||
font-family: "FontAwesome";
|
||||
font-family: 'FontAwesome';
|
||||
transition: transform 0.4s;
|
||||
transform: rotateY(180deg);
|
||||
backface-visibility: hidden;
|
||||
@ -154,7 +154,8 @@ gf-form-switch[disabled] {
|
||||
.gf-form-switch input + label {
|
||||
cursor: default;
|
||||
pointer-events: none !important;
|
||||
&::before {
|
||||
&::before,
|
||||
&::after {
|
||||
color: $text-color-faint;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
$login-border: #8daac5;
|
||||
|
||||
.login {
|
||||
background-position: center;
|
||||
min-height: 85vh;
|
||||
height: 80vh;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-width: 100%;
|
||||
margin-left: 0;
|
||||
@ -95,7 +94,7 @@ select:-webkit-autofill:focus {
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
height: 320px;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.login-branding {
|
||||
@ -106,6 +105,7 @@ select:-webkit-autofill:focus {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 0;
|
||||
padding-top: 2rem;
|
||||
|
||||
.logo-icon {
|
||||
width: 70px;
|
||||
@ -127,7 +127,7 @@ select:-webkit-autofill:focus {
|
||||
|
||||
.login-inner-box {
|
||||
text-align: center;
|
||||
padding: 2rem 4rem;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -243,7 +243,7 @@ select:-webkit-autofill:focus {
|
||||
justify-content: space-between;
|
||||
|
||||
.login-divider-line {
|
||||
width: 110px;
|
||||
width: 100px;
|
||||
height: 10px;
|
||||
border-bottom: 1px solid $login-border;
|
||||
|
||||
@ -323,7 +323,10 @@ select:-webkit-autofill:focus {
|
||||
width: 35%;
|
||||
padding: 4rem 2rem;
|
||||
border-right: 1px solid $login-border;
|
||||
justify-content: flex-start;
|
||||
|
||||
.logo-icon {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-inner-box {
|
||||
@ -331,14 +334,18 @@ select:-webkit-autofill:focus {
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.login-branding {
|
||||
.logo-icon {
|
||||
width: 80px;
|
||||
.login-divider {
|
||||
.login-divider-line {
|
||||
width: 110px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.login {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
@ -373,10 +380,6 @@ select:-webkit-autofill:focus {
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.login {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = function(config) {
|
||||
module.exports = function (config) {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
@ -10,7 +10,10 @@ module.exports = function(config) {
|
||||
debug: {
|
||||
configFile: 'karma.conf.js',
|
||||
singleRun: false,
|
||||
browsers: ['Chrome']
|
||||
browsers: ['Chrome'],
|
||||
mime: {
|
||||
'text/x-typescript': ['ts', 'tsx']
|
||||
},
|
||||
},
|
||||
|
||||
test: {
|
||||
|
@ -1,37 +1,29 @@
|
||||
'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 {
|
||||
test: /\.scss$/,
|
||||
use: (extractSass || ExtractTextPlugin).extract({
|
||||
use: [
|
||||
{
|
||||
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',
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
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 } },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
const path = require('path');
|
||||
const { CheckerPlugin } = require('awesome-typescript-loader');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
@ -61,6 +61,8 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CheckerPlugin(),
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
checkSyntacticErrors: true,
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
@ -7,20 +7,17 @@ const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const extractSass = new ExtractTextPlugin({
|
||||
filename: "grafana.[name].css"
|
||||
});
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: "cheap-module-source-map",
|
||||
mode: 'development',
|
||||
|
||||
entry: {
|
||||
app: './public/app/index.ts',
|
||||
dark: './public/sass/grafana.dark.scss',
|
||||
light: './public/sass/grafana.light.scss',
|
||||
vendor: require('./dependencies'),
|
||||
},
|
||||
|
||||
output: {
|
||||
@ -48,15 +45,13 @@ module.exports = merge(common, {
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'awesome-typescript-loader',
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
useCache: true,
|
||||
transpileOnly: true
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
require('./sass.rule.js')({
|
||||
sourceMap: true, minimize: false, preserveUrl: false
|
||||
}, extractSass),
|
||||
require('./sass.rule.js')({ sourceMap: false, minimize: false, preserveUrl: false }),
|
||||
{
|
||||
test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
|
||||
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: [
|
||||
new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
|
||||
extractSass,
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "grafana.[name].css"
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||
template: path.resolve(__dirname, '../../public/views/index.template.html'),
|
||||
@ -80,9 +96,6 @@ module.exports = merge(common, {
|
||||
'NODE_ENV': JSON.stringify('development')
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
names: ['vendor', 'manifest'],
|
||||
}),
|
||||
// new BundleAnalyzerPlugin({
|
||||
// analyzerPort: 8889
|
||||
// })
|
||||
|
@ -42,20 +42,23 @@ module.exports = merge(common, {
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'awesome-typescript-loader',
|
||||
use: [{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
useCache: true,
|
||||
useBabel: true,
|
||||
babelOptions: {
|
||||
babelrc: false,
|
||||
plugins: [
|
||||
'syntax-dynamic-import',
|
||||
'react-hot-loader/babel'
|
||||
]
|
||||
}
|
||||
cacheDirectory: true,
|
||||
babelrc: false,
|
||||
plugins: [
|
||||
'syntax-dynamic-import',
|
||||
'react-hot-loader/babel'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true
|
||||
},
|
||||
}
|
||||
}],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
|
@ -1,21 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const common = require('./webpack.common.js');
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const ngAnnotatePlugin = require('ng-annotate-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, {
|
||||
mode: 'production',
|
||||
devtool: "source-map",
|
||||
|
||||
entry: {
|
||||
dark: './public/sass/grafana.dark.scss',
|
||||
light: './public/sass/grafana.light.scss',
|
||||
vendor: require('./dependencies'),
|
||||
},
|
||||
|
||||
module: {
|
||||
@ -35,49 +36,49 @@ module.exports = merge(common, {
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'awesome-typescript-loader',
|
||||
options: {
|
||||
errorsAsWarnings: false,
|
||||
},
|
||||
use: {
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
require('./sass.rule.js')({
|
||||
sourceMap: false, minimize: true, preserveUrl: false
|
||||
sourceMap: false, minimize: false, preserveUrl: false
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
devServer: {
|
||||
noInfo: true,
|
||||
stats: {
|
||||
chunks: false,
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
commons: {
|
||||
test: /[\\/]node_modules[\\/].*[jt]sx?$/,
|
||||
name: "vendor",
|
||||
chunks: "all"
|
||||
}
|
||||
}
|
||||
},
|
||||
minimizer: [
|
||||
new UglifyJsPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({})
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new ExtractTextPlugin({
|
||||
filename: 'grafana.[name].css',
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "grafana.[name].css"
|
||||
}),
|
||||
new ngAnnotatePlugin(),
|
||||
new UglifyJSPlugin({
|
||||
sourceMap: true,
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||
template: path.resolve(__dirname, '../../public/views/index.template.html'),
|
||||
inject: 'body',
|
||||
chunks: ['manifest', 'vendor', 'app'],
|
||||
}),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
names: ['vendor', 'manifest'],
|
||||
chunks: ['vendor', 'app'],
|
||||
}),
|
||||
function () {
|
||||
this.plugin("done", function (stats) {
|
||||
|
@ -3,29 +3,36 @@ const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
config = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: 'cheap-module-source-map',
|
||||
|
||||
externals: {
|
||||
'react/addons': true,
|
||||
'react/lib/ExecutionEnvironment': true,
|
||||
'react/lib/ReactContext': true,
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{ loader: "awesome-typescript-loader" }
|
||||
]
|
||||
use: {
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user