Merge branch 'master' into toggle-tempvars-controls
commit
590a33bce8
|
@ -0,0 +1,8 @@
|
|||
[bumpversion]
|
||||
current_version = 1.3.0
|
||||
files = README.md server/swagger.json
|
||||
|
||||
[bumpversion:file:ui/package.json]
|
||||
search = "version": "{current_version}"
|
||||
replace = "version": "{new_version}"
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -1,12 +1,30 @@
|
|||
## v1.3.2 [unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### Features
|
||||
|
||||
### UI Improvements
|
||||
1. [#1508](https://github.com/influxdata/chronograf/pull/1508): The enter and escape keys now perform as expected when renaming dashboard headers.
|
||||
|
||||
## v1.3.1 [unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
1. [#1450](https://github.com/influxdata/chronograf/pull/1450): Fix infinite spinner when using "/chronograf" as a basepath
|
||||
1. [#1428](https://github.com/influxdata/chronograf/issues/1428): Prevent Query Templates dropdown from appearing in the Cell Editor Overlay
|
||||
1. [#1458](https://github.com/influxdata/chronograf/pull/1458): New versions of Chronograf will automatically clear localStorage settings until further notice.
|
||||
1. [#1455](https://github.com/influxdata/chronograf/issues/1455): Fix backwards sort arrows in tables
|
||||
1. [#1423](https://github.com/influxdata/chronograf/issues/1423): Make logout nav item consistent with design
|
||||
1. [#1426](https://github.com/influxdata/chronograf/issues/1426): Fix graph loading spinner
|
||||
1. [#1485](https://github.com/influxdata/chronograf/pull/1485): Filter out any template variable values that are empty, whitespace, or duplicates
|
||||
1. [#1484](https://github.com/influxdata/chronograf/pull/1484): Allow user to select SingleStat as Visualization Type before adding any queries and continue to be able to click Add Query
|
||||
1. [#1349](https://github.com/influxdata/chronograf/pull/1349): Add query for windows uptime
|
||||
|
||||
### Features
|
||||
1. [#1477](https://github.com/influxdata/chronograf/pull/1477): Add ability to log alerts
|
||||
1. [#1491](https://github.com/influxdata/chronograf/pull/1491): Update go vendoring to dep and committed vendor directory
|
||||
1. [#1500](https://github.com/influxdata/chronograf/pull/1500): Add autocomplete functionality to Template Variable dropdowns
|
||||
1. [#1498](https://github.com/influxdata/chronograf/pull/1498): Notify user via UI when local settings are cleared
|
||||
|
||||
### UI Improvements
|
||||
1. [#1451](https://github.com/influxdata/chronograf/pull/1451): Refactor scrollbars to support non-webkit browsers
|
||||
|
@ -15,6 +33,7 @@
|
|||
1. [#1464](https://github.com/influxdata/chronograf/pull/1464): Make Template Variables Manager more space efficient
|
||||
1. [#1464](https://github.com/influxdata/chronograf/pull/1464): Add page spinners to pages that did not have them
|
||||
1. [#1464](https://github.com/influxdata/chronograf/pull/1464): Denote which source is connected in the Sources table
|
||||
1. [#1478](https://github.com/influxdata/chronograf/pull/1478): InfluxDB dashboard uses milliseconds rather than nanoseconds
|
||||
|
||||
## v1.3.0 [2017-05-09]
|
||||
|
||||
|
|
19
Godeps
19
Godeps
|
@ -1,19 +0,0 @@
|
|||
github.com/NYTimes/gziphandler 6710af535839f57c687b62c4c23d649f9545d885
|
||||
github.com/Sirupsen/logrus 3ec0642a7fb6488f65b06f9040adc67e3990296a
|
||||
github.com/boltdb/bolt 5cc10bbbc5c141029940133bb33c9e969512a698
|
||||
github.com/bouk/httprouter ee8b3818a7f51fbc94cc709b5744b52c2c725e91
|
||||
github.com/dgrijalva/jwt-go 24c63f56522a87ec5339cc3567883f1039378fdb
|
||||
github.com/elazarl/go-bindata-assetfs 9a6736ed45b44bf3835afeebb3034b57ed329f3e
|
||||
github.com/gogo/protobuf 6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b
|
||||
github.com/google/go-github 1bc362c7737e51014af7299e016444b654095ad9
|
||||
github.com/google/go-querystring 9235644dd9e52eeae6fa48efd539fdc351a0af53
|
||||
github.com/influxdata/influxdb af72d9b0e4ebe95be30e89b160f43eabaf0529ed
|
||||
github.com/influxdata/kapacitor 5408057e5a3493d3b5bd38d5d535ea45b587f8ff
|
||||
github.com/influxdata/usage-client 6d3895376368aa52a3a81d2a16e90f0f52371967
|
||||
github.com/jessevdk/go-flags 4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa
|
||||
github.com/satori/go.uuid b061729afc07e77a8aa4fad0a2fd840958f1942a
|
||||
github.com/sergi/go-diff 1d28411638c1e67fe1930830df207bef72496ae9
|
||||
github.com/tylerb/graceful 50a48b6e73fcc75b45e22c05b79629a67c79e938
|
||||
golang.org/x/net 749a502dd1eaf3e5bfd4f8956748c502357c0bbe
|
||||
golang.org/x/oauth2 1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5
|
||||
google.golang.org/api bc20c61134e1d25265dd60049f5735381e79b631
|
|
@ -0,0 +1,132 @@
|
|||
memo = "bac138180cd86a0ae604cd3aa7b6ba300673478c880882bd58a4bd7f8bff518d"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/NYTimes/gziphandler"
|
||||
packages = ["."]
|
||||
revision = "6710af535839f57c687b62c4c23d649f9545d885"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/boltdb/bolt"
|
||||
packages = ["."]
|
||||
revision = "5cc10bbbc5c141029940133bb33c9e969512a698"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/bouk/httprouter"
|
||||
packages = ["."]
|
||||
revision = "ee8b3818a7f51fbc94cc709b5744b52c2c725e91"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "24c63f56522a87ec5339cc3567883f1039378fdb"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "259d2a102b871d17f30e3cd9881a642961a1e486"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/elazarl/go-bindata-assetfs"
|
||||
packages = ["."]
|
||||
revision = "9a6736ed45b44bf3835afeebb3034b57ed329f3e"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["gogoproto","jsonpb","plugin/compare","plugin/defaultcheck","plugin/description","plugin/embedcheck","plugin/enumstringer","plugin/equal","plugin/face","plugin/gostring","plugin/marshalto","plugin/oneofcheck","plugin/populate","plugin/size","plugin/stringer","plugin/testgen","plugin/union","plugin/unmarshal","proto","protoc-gen-gogo","protoc-gen-gogo/descriptor","protoc-gen-gogo/generator","protoc-gen-gogo/grpc","protoc-gen-gogo/plugin","vanity","vanity/command"]
|
||||
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "8ee79997227bf9b34611aee7946ae64735e6fd93"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-github"
|
||||
packages = ["github"]
|
||||
revision = "1bc362c7737e51014af7299e016444b654095ad9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-querystring"
|
||||
packages = ["query"]
|
||||
revision = "9235644dd9e52eeae6fa48efd539fdc351a0af53"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"]
|
||||
revision = "af72d9b0e4ebe95be30e89b160f43eabaf0529ed"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
packages = ["client/v1","influxdb","models","pipeline","services/k8s/client","tick","tick/ast","tick/stateful","udf"]
|
||||
revision = "5408057e5a3493d3b5bd38d5d535ea45b587f8ff"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/usage-client"
|
||||
packages = ["v1"]
|
||||
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jessevdk/go-flags"
|
||||
packages = ["."]
|
||||
revision = "4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jteeuwen/go-bindata"
|
||||
packages = ["."]
|
||||
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "ff09b135c25aae272398c51a07235b90a75aa4f0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "b061729afc07e77a8aa4fad0a2fd840958f1942a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sergi/go-diff"
|
||||
packages = ["diffmatchpatch"]
|
||||
revision = "1d28411638c1e67fe1930830df207bef72496ae9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/tylerb/graceful"
|
||||
packages = ["."]
|
||||
revision = "50a48b6e73fcc75b45e22c05b79629a67c79e938"
|
||||
version = "v1.2.13"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp"]
|
||||
revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [".","github","heroku","internal"]
|
||||
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "f3918c30c5c2cb527c0b071a27c35120a6c0719a"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/api"
|
||||
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","oauth2/v2"]
|
||||
revision = "bc20c61134e1d25265dd60049f5735381e79b631"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
|
@ -0,0 +1,77 @@
|
|||
required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","github.com/gogo/protobuf/jsonpb","github.com/gogo/protobuf/protoc-gen-gogo","github.com/gogo/protobuf/gogoproto"]
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/NYTimes/gziphandler"
|
||||
revision = "6710af535839f57c687b62c4c23d649f9545d885"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/Sirupsen/logrus"
|
||||
revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/boltdb/bolt"
|
||||
revision = "5cc10bbbc5c141029940133bb33c9e969512a698"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/bouk/httprouter"
|
||||
revision = "ee8b3818a7f51fbc94cc709b5744b52c2c725e91"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
revision = "24c63f56522a87ec5339cc3567883f1039378fdb"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/elazarl/go-bindata-assetfs"
|
||||
revision = "9a6736ed45b44bf3835afeebb3034b57ed329f3e"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/google/go-github"
|
||||
revision = "1bc362c7737e51014af7299e016444b654095ad9"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
revision = "af72d9b0e4ebe95be30e89b160f43eabaf0529ed"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
version = "^1.2.0"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/influxdata/usage-client"
|
||||
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/jessevdk/go-flags"
|
||||
revision = "4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/jteeuwen/go-bindata"
|
||||
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
revision = "b061729afc07e77a8aa4fad0a2fd840958f1942a"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/sergi/go-diff"
|
||||
revision = "1d28411638c1e67fe1930830df207bef72496ae9"
|
||||
|
||||
[[dependencies]]
|
||||
name = "github.com/tylerb/graceful"
|
||||
version = "^1.2.13"
|
||||
|
||||
[[dependencies]]
|
||||
name = "golang.org/x/net"
|
||||
revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe"
|
||||
|
||||
[[dependencies]]
|
||||
name = "golang.org/x/oauth2"
|
||||
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
|
||||
|
||||
[[dependencies]]
|
||||
name = "google.golang.org/api"
|
||||
revision = "bc20c61134e1d25265dd60049f5735381e79b631"
|
21
Makefile
21
Makefile
|
@ -2,11 +2,10 @@
|
|||
|
||||
VERSION ?= $(shell git describe --always --tags)
|
||||
COMMIT ?= $(shell git rev-parse --short=8 HEAD)
|
||||
GDM := $(shell command -v gdm 2> /dev/null)
|
||||
GOBINDATA := $(shell go list -f {{.Root}} github.com/jteeuwen/go-bindata 2> /dev/null)
|
||||
YARN := $(shell command -v yarn 2> /dev/null)
|
||||
|
||||
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go')
|
||||
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go' -not -path "./vendor/*" )
|
||||
UISOURCES := $(shell find ui -type f -not \( -path ui/build/\* -o -path ui/node_modules/\* -prune \) )
|
||||
|
||||
LDFLAGS=-ldflags "-s -X main.version=${VERSION} -X main.commit=${COMMIT}"
|
||||
|
@ -70,16 +69,11 @@ canned/bin_gen.go: canned/*.json
|
|||
|
||||
dep: .jsdep .godep
|
||||
|
||||
.godep: Godeps
|
||||
ifndef GDM
|
||||
@echo "Installing GDM"
|
||||
go get github.com/sparrc/gdm
|
||||
endif
|
||||
.godep:
|
||||
ifndef GOBINDATA
|
||||
@echo "Installing go-bindata"
|
||||
go get -u github.com/jteeuwen/go-bindata/...
|
||||
endif
|
||||
gdm restore
|
||||
@touch .godep
|
||||
|
||||
.jsdep: ui/yarn.lock
|
||||
|
@ -90,16 +84,18 @@ else
|
|||
@touch .jsdep
|
||||
endif
|
||||
|
||||
gen: bolt/internal/internal.proto
|
||||
gen: internal.pb.go
|
||||
|
||||
internal.pb.go: bolt/internal/internal.proto
|
||||
go generate -x ./bolt/internal
|
||||
|
||||
test: jstest gotest gotestrace
|
||||
|
||||
gotest:
|
||||
go test ./...
|
||||
go test `go list ./... | grep -v /vendor/`
|
||||
|
||||
gotestrace:
|
||||
go test -race ./...
|
||||
go test -race `go list ./... | grep -v /vendor/`
|
||||
|
||||
jstest:
|
||||
cd ui && npm test
|
||||
|
@ -117,8 +113,5 @@ clean:
|
|||
rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go
|
||||
@rm -f .godep .jsdep .jssrc .dev-jssrc .bindata
|
||||
|
||||
continuous:
|
||||
while true; do if fswatch -e "\.git" -r --one-event .; then echo "#-> Starting build: `date`"; make dev; pkill -9 chronograf; make run-dev & echo "#-> Build complete."; fi; sleep 0.5; done
|
||||
|
||||
ctags:
|
||||
ctags -R --languages="Go" --exclude=.git --exclude=ui .
|
||||
|
|
11
README.md
11
README.md
|
@ -110,8 +110,7 @@ Change the default root path of the Chronograf server with the `--basepath` opti
|
|||
|
||||
## Versions
|
||||
|
||||
The most recent version of Chronograf is v1.3.0.
|
||||
We will be iterating quickly based on user feedback and recommend using the [nightly builds](https://www.influxdata.com/downloads/) for the time being.
|
||||
The most recent version of Chronograf is [v1.3.0](https://www.influxdata.com/downloads/).
|
||||
|
||||
Spotted a bug or have a feature request?
|
||||
Please open [an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||
|
@ -136,16 +135,16 @@ By default, chronograf runs on port `8888`.
|
|||
|
||||
|
||||
### With Docker
|
||||
To get started right away with Docker, you can pull down our latest alpha:
|
||||
To get started right away with Docker, you can pull down our latest release:
|
||||
|
||||
```sh
|
||||
docker pull quay.io/influxdb/chronograf:latest
|
||||
docker pull chronograf:1.3.0
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
* Chronograf works with go 1.7.x, node 6.x/7.x, and yarn 0.18+.
|
||||
* Chronograf requires [Kapacitor](https://github.com/influxdata/kapacitor) 1.1.x+ to create and store alerts.
|
||||
* Chronograf works with go 1.8.x, node 6.x/7.x, and yarn 0.18+.
|
||||
* Chronograf requires [Kapacitor](https://github.com/influxdata/kapacitor) 1.2.x+ to create and store alerts.
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install)
|
||||
1. [Install Node and NPM](https://nodejs.org/en/download/)
|
||||
|
|
|
@ -13,16 +13,18 @@
|
|||
"name": "InfluxDB - Query Performance",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT non_negative_derivative(max(\"queryDurationNs\"), 1s) AS \"duration\" FROM \"influxdb_queryExecutor\"",
|
||||
"query": "SELECT non_negative_derivative(max(\"queryDurationNs\"), 1s) / 1000000 AS \"duration_ms\" FROM \"influxdb_queryExecutor\"",
|
||||
"label": "ms",
|
||||
"groupbys": [],
|
||||
"wheres": []
|
||||
},
|
||||
{
|
||||
"query": "SELECT non_negative_derivative(max(\"queriesExecuted\"), 1s) AS \"queries_executed\" FROM \"influxdb_queryExecutor\"",
|
||||
"query": "SELECT non_negative_derivative(max(\"queriesExecuted\"), 1s) / 1000000 AS \"queries_executed_ms\" FROM \"influxdb_queryExecutor\"",
|
||||
"label": "ms",
|
||||
"groupbys": [],
|
||||
"wheres": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ machine:
|
|||
services:
|
||||
- docker
|
||||
environment:
|
||||
DOCKER_TAG: chronograf-20170208
|
||||
DOCKER_TAG: chronograf-20170516
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
|
|
|
@ -1,15 +1,40 @@
|
|||
## Creating a release
|
||||
The release process is handled via our [circle.yml](https://github.com/influxdata/chronograf/blob/master/circle.yml).
|
||||
|
||||
A release tag of the format `1.1.0-beta1` needs to be added. Afterwhich, circle
|
||||
A release tag of the format `1.3.0` needs to be added. Afterwhich, circle
|
||||
will build our packages for all of our platforms.
|
||||
|
||||
### Bumpversion
|
||||
We use [bumpversion](https://github.com/peritus/bumpversion) to help us
|
||||
remember all the places to increment our version number.
|
||||
|
||||
To install:
|
||||
|
||||
```sh
|
||||
pip install --upgrade bumpversion
|
||||
```
|
||||
|
||||
To use to increment third number (e.g. 1.3.0 -> 1.3.1):
|
||||
|
||||
```sh
|
||||
bumpversion patch
|
||||
```
|
||||
|
||||
|
||||
To increment minor number (e.g. 1.3.5 -> 1.4.0):
|
||||
|
||||
```sh
|
||||
bumpversion minor
|
||||
```
|
||||
|
||||
The behavior of `bumpversion` is controlled by .bumpversion.cfg
|
||||
|
||||
### Creating Release tag
|
||||
You can create a release tag from [Github](https://github.com/influxdata/chronograf/releases)
|
||||
or create an annotated tag:
|
||||
|
||||
```sh
|
||||
git tag -a 1.1.0-beta1 -m "Release 1.1.0-beta1"
|
||||
git tag -a 1.3.0 -m "Release 1.3.0"
|
||||
git push --tags
|
||||
```
|
||||
|
||||
|
@ -22,7 +47,7 @@ git push --tags
|
|||
* armel
|
||||
* static_i386
|
||||
* static_amd64
|
||||
* OS X
|
||||
* OS X
|
||||
* amd64
|
||||
* Windows
|
||||
* amd64
|
|
@ -18,9 +18,10 @@ RUN pip install boto requests python-jose --upgrade
|
|||
RUN gem install fpm
|
||||
|
||||
# Install node
|
||||
RUN wget -q https://nodejs.org/dist/latest-v6.x/node-v6.9.5-linux-x64.tar.gz; \
|
||||
tar -xvf node-v6.9.5-linux-x64.tar.gz -C / --strip-components=1; \
|
||||
rm -f node-v6.9.5-linux-x64.tar.gz
|
||||
ENV NODE_VERSION v6.10.3
|
||||
RUN wget -q https://nodejs.org/dist/latest-v6.x/node-${NODE_VERSION}-linux-x64.tar.gz; \
|
||||
tar -xvf node-${NODE_VERSION}-linux-x64.tar.gz -C / --strip-components=1; \
|
||||
rm -f node-${NODE_VERSION}-linux-x64.tar.gz
|
||||
|
||||
# Update npm
|
||||
RUN cd $(npm root -g)/npm \
|
||||
|
@ -34,7 +35,7 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
|||
|
||||
# Install go
|
||||
ENV GOPATH /root/go
|
||||
ENV GO_VERSION 1.7.5
|
||||
ENV GO_VERSION 1.8.1
|
||||
ENV GO_ARCH amd64
|
||||
RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \
|
||||
tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz ; \
|
||||
|
|
|
@ -39,7 +39,7 @@ CONFIGURATION_FILES = [
|
|||
LOGROTATE_DIR + '/chronograf',
|
||||
]
|
||||
|
||||
PACKAGE_LICENSE = "MIT"
|
||||
PACKAGE_LICENSE = "AGPLv3"
|
||||
PACKAGE_URL = "https://github.com/influxdata/chronograf"
|
||||
MAINTAINER = "contact@influxdb.com"
|
||||
VENDOR = "InfluxData"
|
||||
|
|
|
@ -21,6 +21,10 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) {
|
|||
return chronograf.QueryConfig{}, err
|
||||
}
|
||||
|
||||
if itsDashboardTime {
|
||||
influxQL = strings.Replace(influxQL, "now() - 15m", ":dashboardTime:", 1)
|
||||
}
|
||||
|
||||
raw := chronograf.QueryConfig{
|
||||
RawText: &influxQL,
|
||||
Fields: []chronograf.Field{},
|
||||
|
|
|
@ -21,7 +21,7 @@ func kapaHandler(handler string) (string, error) {
|
|||
return "email", nil
|
||||
case "http":
|
||||
return "post", nil
|
||||
case "alerta", "sensu", "slack", "email", "talk", "telegram", "post", "tcp", "exec":
|
||||
case "alerta", "sensu", "slack", "email", "talk", "telegram", "post", "tcp", "exec", "log":
|
||||
return handler, nil
|
||||
default:
|
||||
return "", fmt.Errorf("Unsupported alert handler %s", handler)
|
||||
|
|
|
@ -95,6 +95,31 @@ func TestAlertServices(t *testing.T) {
|
|||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Test log",
|
||||
rule: chronograf.AlertRule{
|
||||
AlertNodes: []chronograf.KapacitorNode{
|
||||
{
|
||||
Name: "log",
|
||||
Args: []string{"/tmp/alerts.log"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `alert()
|
||||
.log('/tmp/alerts.log')
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Test log no argument",
|
||||
rule: chronograf.AlertRule{
|
||||
AlertNodes: []chronograf.KapacitorNode{
|
||||
{
|
||||
Name: "log",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Test tcp no argument with other services",
|
||||
rule: chronograf.AlertRule{
|
||||
|
|
|
@ -493,6 +493,7 @@ func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) {
|
|||
extractTalk(t, rule)
|
||||
extractTelegram(t, rule)
|
||||
extractTCP(t, rule)
|
||||
extractLog(t, rule)
|
||||
extractExec(t, rule)
|
||||
}
|
||||
return nil
|
||||
|
@ -801,6 +802,23 @@ func extractTCP(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
rule.AlertNodes = append(rule.AlertNodes, alert)
|
||||
}
|
||||
|
||||
func extractLog(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.LogHandlers == nil {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "log")
|
||||
log := node.LogHandlers[0]
|
||||
alert := chronograf.KapacitorNode{
|
||||
Name: "log",
|
||||
}
|
||||
|
||||
if log.FilePath != "" {
|
||||
alert.Args = []string{log.FilePath}
|
||||
}
|
||||
|
||||
rule.AlertNodes = append(rule.AlertNodes, alert)
|
||||
}
|
||||
|
||||
func extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.ExecHandlers == nil {
|
||||
return
|
||||
|
|
|
@ -59,12 +59,13 @@ func TestReverse(t *testing.T) {
|
|||
.slack()
|
||||
.victorOps()
|
||||
.email('howdy@howdy.com')
|
||||
.log('/tmp/alerts.log')
|
||||
`),
|
||||
|
||||
want: chronograf.AlertRule{
|
||||
Name: "name",
|
||||
Trigger: "threshold",
|
||||
Alerts: []string{"victorops", "smtp", "slack"},
|
||||
Alerts: []string{"victorops", "smtp", "slack", "log"},
|
||||
AlertNodes: []chronograf.KapacitorNode{
|
||||
{
|
||||
Name: "victorops",
|
||||
|
@ -76,6 +77,10 @@ func TestReverse(t *testing.T) {
|
|||
{
|
||||
Name: "slack",
|
||||
},
|
||||
{
|
||||
Name: "log",
|
||||
Args: []string{"/tmp/alerts.log"},
|
||||
},
|
||||
},
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: "greater than",
|
||||
|
|
|
@ -34,15 +34,16 @@ const AlertsTable = React.createClass({
|
|||
|
||||
filterAlerts(searchTerm, newAlerts) {
|
||||
const alerts = newAlerts || this.props.alerts
|
||||
const filterText = searchTerm.toLowerCase()
|
||||
const filteredAlerts = alerts.filter(h => {
|
||||
if (h.host === null || h.name === null || h.level === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
h.name.toLowerCase().search(searchTerm.toLowerCase()) !== -1 ||
|
||||
h.host.toLowerCase().search(searchTerm.toLowerCase()) !== -1 ||
|
||||
h.level.toLowerCase().search(searchTerm.toLowerCase()) !== -1
|
||||
h.name.toLowerCase().includes(filterText) ||
|
||||
h.host.toLowerCase().includes(filterText) ||
|
||||
h.level.toLowerCase().includes(filterText)
|
||||
)
|
||||
})
|
||||
this.setState({searchTerm, filteredAlerts})
|
||||
|
|
|
@ -8,12 +8,27 @@ class DashboardEditHeader extends Component {
|
|||
const {dashboard: {name}} = props
|
||||
this.state = {name}
|
||||
this.handleChange = ::this.handleChange
|
||||
this.handleFormSubmit = ::this.handleFormSubmit
|
||||
this.handleKeyUp = ::this.handleKeyUp
|
||||
}
|
||||
|
||||
handleChange(name) {
|
||||
this.setState({name})
|
||||
}
|
||||
|
||||
handleFormSubmit(e) {
|
||||
e.preventDefault()
|
||||
const name = e.target.name.value
|
||||
this.props.onSave(name)
|
||||
}
|
||||
|
||||
handleKeyUp(e) {
|
||||
const {onCancel} = this.props
|
||||
if (e.key === 'Escape') {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {onSave, onCancel} = this.props
|
||||
const {name} = this.state
|
||||
|
@ -21,15 +36,18 @@ class DashboardEditHeader extends Component {
|
|||
return (
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<form className="page-header__left" onSubmit={this.handleFormSubmit}>
|
||||
<input
|
||||
className="page-header--editing"
|
||||
name="name"
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
placeholder="Name this Dashboard"
|
||||
onChange={e => this.handleChange(e.target.value)}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<ConfirmButtons item={name} onConfirm={onSave} onCancel={onCancel} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,8 @@ const TemplateControlBar = ({
|
|||
<Dropdown
|
||||
items={items}
|
||||
buttonSize="btn-xs"
|
||||
selected={selectedText || 'Loading...'}
|
||||
useAutoComplete={true}
|
||||
selected={selectedText || '(No values)'}
|
||||
onChoose={item =>
|
||||
onSelectTemplate(id, [item].map(x => omit(x, 'text')))}
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, {PropTypes, Component} from 'react'
|
|||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import uniq from 'lodash/uniq'
|
||||
|
||||
import OnClickOutside from 'react-onclickoutside'
|
||||
import classnames from 'classnames'
|
||||
|
||||
|
@ -23,6 +25,8 @@ import generateTemplateVariableQuery
|
|||
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
|
||||
const compact = values => uniq(values).filter(value => /\S/.test(value))
|
||||
|
||||
const RowValues = ({
|
||||
selectedType,
|
||||
values = [],
|
||||
|
@ -30,7 +34,7 @@ const RowValues = ({
|
|||
onStartEdit,
|
||||
autoFocusTarget,
|
||||
}) => {
|
||||
const _values = values.map(({value}) => value).join(', ')
|
||||
const _values = values.map(v => v.value).join(', ')
|
||||
|
||||
if (selectedType === 'csv') {
|
||||
return (
|
||||
|
@ -293,7 +297,8 @@ class RowWrapper extends Component {
|
|||
} else {
|
||||
parsedData = await this.runTemplateVariableQuery(source, queryConfig)
|
||||
}
|
||||
onRunQuerySuccess(template, queryConfig, parsedData, tempVar)
|
||||
|
||||
onRunQuerySuccess(template, queryConfig, compact(parsedData), tempVar)
|
||||
} catch (error) {
|
||||
onRunQueryFailure(error)
|
||||
}
|
||||
|
|
|
@ -115,8 +115,9 @@ const MeasurementList = React.createClass({
|
|||
)
|
||||
}
|
||||
|
||||
const filterText = this.state.filterText.toLowerCase()
|
||||
const measurements = this.state.measurements.filter(m =>
|
||||
m.match(this.state.filterText)
|
||||
m.toLowerCase().includes(filterText)
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -101,6 +101,7 @@ const QueryBuilder = React.createClass({
|
|||
config={query}
|
||||
onUpdate={this.handleEditRawText}
|
||||
templates={templates}
|
||||
isInDataExplorer={isInDataExplorer}
|
||||
/>
|
||||
{this.renderLists()}
|
||||
</div>
|
||||
|
|
|
@ -162,9 +162,12 @@ class QueryEditor extends Component {
|
|||
if (matched && !_.isEmpty(templates)) {
|
||||
// maintain cursor poition
|
||||
const start = this.editor.selectionStart
|
||||
|
||||
const end = this.editor.selectionEnd
|
||||
const filterText = matched[0].substr(1).toLowerCase()
|
||||
|
||||
const filteredTemplates = templates.filter(t =>
|
||||
t.tempVar.includes(matched[0].substring(1))
|
||||
t.tempVar.toLowerCase().includes(filterText)
|
||||
)
|
||||
|
||||
const found = filteredTemplates.find(
|
||||
|
@ -241,15 +244,19 @@ class QueryEditor extends Component {
|
|||
}
|
||||
|
||||
renderStatus(status) {
|
||||
const {isInDataExplorer} = this.props
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
{isInDataExplorer
|
||||
? <Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -258,12 +265,14 @@ class QueryEditor extends Component {
|
|||
return (
|
||||
<div className="query-editor--status">
|
||||
<LoadingDots />
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
{isInDataExplorer
|
||||
? <Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -286,23 +295,26 @@ class QueryEditor extends Component {
|
|||
/>
|
||||
{status.error || status.warn || status.success}
|
||||
</span>
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
{isInDataExplorer
|
||||
? <Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
QueryEditor.propTypes = {
|
||||
query: string.isRequired,
|
||||
onUpdate: func.isRequired,
|
||||
config: shape().isRequired,
|
||||
isInDataExplorer: bool,
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
tempVar: string.isRequired,
|
||||
|
|
|
@ -52,7 +52,8 @@ const TagListItem = React.createClass({
|
|||
return <div>no tag values</div>
|
||||
}
|
||||
|
||||
const filtered = tagValues.filter(v => v.match(this.state.filterText))
|
||||
const filterText = this.state.filterText.toLowerCase()
|
||||
const filtered = tagValues.filter(v => v.toLowerCase().includes(filterText))
|
||||
|
||||
return (
|
||||
<div className="query-builder--sub-list">
|
||||
|
|
|
@ -43,7 +43,7 @@ const VisView = ({
|
|||
if (cellType === 'single-stat') {
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
queries={[queries[0]]}
|
||||
queries={queries.length ? [queries[0]] : []}
|
||||
autoRefresh={autoRefresh}
|
||||
templates={templates}
|
||||
/>
|
||||
|
|
|
@ -5,17 +5,24 @@ import _ from 'lodash'
|
|||
export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
|
||||
return proxy({
|
||||
source: proxyLink,
|
||||
query: 'select mean(usage_user) from cpu where cpu = \'cpu-total\' and time > now() - 10m group by host; select mean("load1") from "system" where time > now() - 10m group by host; select mean("Percent_Processor_Time") from win_cpu where time > now() - 10m group by host; select mean("Processor_Queue_Length") from win_system where time > now() - 10s group by host; select non_negative_derivative(mean(uptime)) as deltaUptime from "system" where time > now() - 10m group by host, time(1m) fill(0); show tag values from /win_system|system/ with key = "host"',
|
||||
query: `select mean(usage_user) from cpu where cpu = "cpu-total" and time > now() - 10m group by host;
|
||||
select mean("load1") from "system" where time > now() - 10m group by host;
|
||||
select non_negative_derivative(mean(uptime)) as deltaUptime from "system" where time > now() - 10m group by host, time(1m) fill(0);
|
||||
select mean("Percent_Processor_Time") from win_cpu where time > now() - 10m group by host;
|
||||
select mean("Processor_Queue_Length") from win_system where time > now() - 10s group by host;
|
||||
select non_negative_derivative(mean("System_Up_Time")) as deltaUptime from "telegraf"."autogen"."win_uptime" where time > now() - 10m group by host, time(1m) fill(0);
|
||||
show tag values from /win_system|system/ with key = "host"`,
|
||||
db: telegrafDB,
|
||||
}).then(resp => {
|
||||
const hosts = {}
|
||||
const precision = 100
|
||||
const cpuSeries = _.get(resp, ['data', 'results', '0', 'series'], [])
|
||||
const loadSeries = _.get(resp, ['data', 'results', '1', 'series'], [])
|
||||
const winCPUSeries = _.get(resp, ['data', 'results', '2', 'series'], [])
|
||||
const winLoadSeries = _.get(resp, ['data', 'results', '3', 'series'], [])
|
||||
const uptimeSeries = _.get(resp, ['data', 'results', '4', 'series'], [])
|
||||
const allHostsSeries = _.get(resp, ['data', 'results', '5', 'series'], [])
|
||||
const uptimeSeries = _.get(resp, ['data', 'results', '2', 'series'], [])
|
||||
const winCPUSeries = _.get(resp, ['data', 'results', '3', 'series'], [])
|
||||
const winLoadSeries = _.get(resp, ['data', 'results', '4', 'series'], [])
|
||||
const winUptimeSeries = _.get(resp, ['data', 'results', '5', 'series'], [])
|
||||
const allHostsSeries = _.get(resp, ['data', 'results', '6', 'series'], [])
|
||||
|
||||
allHostsSeries.forEach(s => {
|
||||
const hostnameIndex = s.columns.findIndex(col => col === 'value')
|
||||
|
@ -64,6 +71,12 @@ export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
|
|||
Math.round(s.values[0][meanIndex] * precision) / precision
|
||||
})
|
||||
|
||||
winUptimeSeries.forEach(s => {
|
||||
const uptimeIndex = s.columns.findIndex(col => col === 'deltaUptime')
|
||||
hosts[s.tags.host].deltaUptime =
|
||||
s.values[s.values.length - 1][uptimeIndex]
|
||||
})
|
||||
|
||||
return hosts
|
||||
})
|
||||
}
|
||||
|
|
|
@ -35,20 +35,21 @@ const HostsTable = React.createClass({
|
|||
},
|
||||
|
||||
filter(allHosts, searchTerm) {
|
||||
const filterText = searchTerm.toLowerCase()
|
||||
return allHosts.filter(h => {
|
||||
const apps = h.apps ? h.apps.join(', ') : ''
|
||||
// search each tag for the presence of the search term
|
||||
let tagResult = false
|
||||
if (h.tags) {
|
||||
tagResult = Object.keys(h.tags).reduce((acc, key) => {
|
||||
return acc || h.tags[key].search(searchTerm) !== -1
|
||||
return acc || h.tags[key].toLowerCase().includes(filterText)
|
||||
}, false)
|
||||
} else {
|
||||
tagResult = false
|
||||
}
|
||||
return (
|
||||
h.name.search(searchTerm) !== -1 ||
|
||||
apps.search(searchTerm) !== -1 ||
|
||||
h.name.toLowerCase().includes(filterText) ||
|
||||
apps.toLowerCase().includes(filterText) ||
|
||||
tagResult
|
||||
)
|
||||
})
|
||||
|
|
|
@ -40,6 +40,8 @@ import 'src/style/chronograf.scss'
|
|||
|
||||
import {HEARTBEAT_INTERVAL} from 'shared/constants'
|
||||
|
||||
const errorsQueue = []
|
||||
|
||||
const rootNode = document.getElementById('react-root')
|
||||
|
||||
const basepath = rootNode.dataset.basepath || ''
|
||||
|
@ -48,7 +50,7 @@ const browserHistory = useRouterHistory(createHistory)({
|
|||
basename: basepath, // this is written in when available by the URL prefixer middleware
|
||||
})
|
||||
|
||||
const store = configureStore(loadLocalStorage(), browserHistory)
|
||||
const store = configureStore(loadLocalStorage(errorsQueue), browserHistory)
|
||||
const {dispatch} = store
|
||||
|
||||
browserHistory.listen(() => {
|
||||
|
@ -67,6 +69,7 @@ const history = syncHistoryWithStore(browserHistory, store)
|
|||
|
||||
const Root = React.createClass({
|
||||
componentWillMount() {
|
||||
this.flushErrorsQueue()
|
||||
this.checkAuth()
|
||||
},
|
||||
|
||||
|
@ -99,6 +102,14 @@ const Root = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
flushErrorsQueue() {
|
||||
if (errorsQueue.length) {
|
||||
errorsQueue.forEach(errorText => {
|
||||
dispatch(errorThrown({status: 0, auth: null}, errorText, 'warning'))
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
|
|
|
@ -74,12 +74,13 @@ export const RULE_MESSAGE_TEMPLATES = {
|
|||
},
|
||||
}
|
||||
|
||||
export const DEFAULT_ALERTS = ['http', 'tcp', 'exec']
|
||||
export const DEFAULT_ALERTS = ['http', 'tcp', 'exec', 'log']
|
||||
|
||||
export const DEFAULT_ALERT_LABELS = {
|
||||
http: 'URL:',
|
||||
tcp: 'Address:',
|
||||
exec: 'Add Command (Arguments separated by Spaces):',
|
||||
log: 'File',
|
||||
smtp: 'Email Addresses (Separated by Spaces):',
|
||||
slack: 'Send alerts to Slack channel:',
|
||||
alerta: 'Paste Alerta TICKscript:',
|
||||
|
@ -88,6 +89,7 @@ export const DEFAULT_ALERT_PLACEHOLDERS = {
|
|||
http: 'Ex: http://example.com/api/alert',
|
||||
tcp: 'Ex: exampleendpoint.com:5678',
|
||||
exec: 'Ex: woogie boogie',
|
||||
log: 'Ex: /tmp/alerts.log',
|
||||
smtp: 'Ex: benedict@domain.com delaney@domain.com susan@domain.com',
|
||||
slack: '#alerts',
|
||||
alerta: 'alerta()',
|
||||
|
@ -97,6 +99,7 @@ export const ALERT_NODES_ACCESSORS = {
|
|||
http: rule => _.get(rule, 'alertNodes[0].args[0]', ''),
|
||||
tcp: rule => _.get(rule, 'alertNodes[0].args[0]', ''),
|
||||
exec: rule => _.get(rule, 'alertNodes[0].args', []).join(' '),
|
||||
log: rule => _.get(rule, 'alertNodes[0].args[0]', ''),
|
||||
smtp: rule => _.get(rule, 'alertNodes[0].args', []).join(' '),
|
||||
slack: rule => _.get(rule, 'alertNodes[0].properties[0].args', ''),
|
||||
alerta: rule =>
|
||||
|
|
|
@ -81,6 +81,7 @@ export default function rules(state = {}, action) {
|
|||
switch (alertType) {
|
||||
case 'http':
|
||||
case 'tcp':
|
||||
case 'log':
|
||||
alertNodesByType = [
|
||||
{
|
||||
name: alertType,
|
||||
|
|
|
@ -1,10 +1,30 @@
|
|||
export const loadLocalStorage = () => {
|
||||
export const loadLocalStorage = errorsQueue => {
|
||||
try {
|
||||
const serializedState = localStorage.getItem('state')
|
||||
|
||||
return JSON.parse(serializedState) || {}
|
||||
} catch (err) {
|
||||
console.error(`Loading persisted state failed: ${err}`) // eslint-disable-line no-console
|
||||
const state = JSON.parse(serializedState)
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
if (state.VERSION !== VERSION) {
|
||||
const errorText =
|
||||
'New version of Chronograf detected. Local settings cleared.'
|
||||
|
||||
console.log(errorText) // eslint-disable-line no-console
|
||||
errorsQueue.push(errorText)
|
||||
|
||||
window.localStorage.removeItem('state')
|
||||
return {}
|
||||
}
|
||||
|
||||
delete state.VERSION
|
||||
|
||||
return state || {}
|
||||
} catch (error) {
|
||||
const errorText = `Loading local settings failed: ${error}`
|
||||
|
||||
console.error(errorText) // eslint-disable-line no-console
|
||||
errorsQueue.push(errorText)
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +45,7 @@ export const saveToLocalStorage = ({
|
|||
queryConfigs,
|
||||
timeRange,
|
||||
dataExplorer,
|
||||
VERSION, // eslint-disable-line no-undef
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const errorThrown = (error, altText) => ({
|
||||
export const errorThrown = (error, altText, alertType) => ({
|
||||
type: 'ERROR_THROWN',
|
||||
error,
|
||||
altText,
|
||||
alertType,
|
||||
})
|
||||
|
|
|
@ -32,7 +32,7 @@ export const handleSuccess = (data, query, editQueryStatus) => {
|
|||
}
|
||||
|
||||
export const handleError = (error, query, editQueryStatus) => {
|
||||
const message = _.get(error, ['data', 'message'], error)
|
||||
const message = _.get(error, ['data', 'message'], error.toString())
|
||||
|
||||
// 400 from chrono server = fail
|
||||
editQueryStatus(query.id, {error: message})
|
||||
|
|
|
@ -3,13 +3,19 @@ import {Link} from 'react-router'
|
|||
import classnames from 'classnames'
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import {DROPDOWN_MENU_MAX_HEIGHT, DROPDOWN_MENU_ITEM_THRESHOLD} from 'shared/constants/index'
|
||||
import {
|
||||
DROPDOWN_MENU_MAX_HEIGHT,
|
||||
DROPDOWN_MENU_ITEM_THRESHOLD,
|
||||
} from 'shared/constants/index'
|
||||
|
||||
class Dropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
searchTerm: '',
|
||||
filteredItems: this.props.items,
|
||||
highlightedItemIndex: null,
|
||||
}
|
||||
|
||||
this.handleClickOutside = ::this.handleClickOutside
|
||||
|
@ -17,6 +23,10 @@ class Dropdown extends Component {
|
|||
this.handleSelection = ::this.handleSelection
|
||||
this.toggleMenu = ::this.toggleMenu
|
||||
this.handleAction = ::this.handleAction
|
||||
this.handleFilterChange = ::this.handleFilterChange
|
||||
this.applyFilter = ::this.applyFilter
|
||||
this.handleFilterKeyPress = ::this.handleFilterKeyPress
|
||||
this.handleHighlight = ::this.handleHighlight
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -24,6 +34,7 @@ class Dropdown extends Component {
|
|||
buttonSize: 'btn-sm',
|
||||
buttonColor: 'btn-info',
|
||||
menuWidth: '100%',
|
||||
useAutoComplete: false,
|
||||
}
|
||||
|
||||
handleClickOutside() {
|
||||
|
@ -42,10 +53,21 @@ class Dropdown extends Component {
|
|||
this.props.onChoose(item)
|
||||
}
|
||||
|
||||
handleHighlight(itemIndex) {
|
||||
this.setState({highlightedItemIndex: itemIndex})
|
||||
}
|
||||
|
||||
toggleMenu(e) {
|
||||
if (e) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
if (!this.state.isOpen) {
|
||||
this.setState({
|
||||
searchTerm: '',
|
||||
filteredItems: this.props.items,
|
||||
highlightedItemIndex: null,
|
||||
})
|
||||
}
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
|
@ -54,21 +76,94 @@ class Dropdown extends Component {
|
|||
action.handler(item)
|
||||
}
|
||||
|
||||
handleFilterKeyPress(e) {
|
||||
const {filteredItems, highlightedItemIndex} = this.state
|
||||
|
||||
if (e.key === 'Enter' && filteredItems.length) {
|
||||
this.setState({isOpen: false})
|
||||
this.props.onChoose(filteredItems[highlightedItemIndex])
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
this.setState({isOpen: false})
|
||||
}
|
||||
if (e.key === 'ArrowUp' && highlightedItemIndex > 0) {
|
||||
this.setState({highlightedItemIndex: highlightedItemIndex - 1})
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
if (highlightedItemIndex < filteredItems.length - 1) {
|
||||
this.setState({highlightedItemIndex: highlightedItemIndex + 1})
|
||||
}
|
||||
if (highlightedItemIndex === null && filteredItems.length) {
|
||||
this.setState({highlightedItemIndex: 0})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleFilterChange(e) {
|
||||
if (e.target.value === null || e.target.value === '') {
|
||||
this.setState({
|
||||
searchTerm: '',
|
||||
filteredItems: this.props.items,
|
||||
highlightedItemIndex: null,
|
||||
})
|
||||
} else {
|
||||
this.setState({searchTerm: e.target.value}, () =>
|
||||
this.applyFilter(this.state.searchTerm)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
applyFilter(searchTerm) {
|
||||
const {items} = this.props
|
||||
const filterText = searchTerm.toLowerCase()
|
||||
const matchingItems = items.filter(item =>
|
||||
item.text.toLowerCase().includes(filterText)
|
||||
)
|
||||
|
||||
this.setState({
|
||||
filteredItems: matchingItems,
|
||||
highlightedItemIndex: 0,
|
||||
})
|
||||
}
|
||||
|
||||
renderShortMenu() {
|
||||
const {actions, addNew, items, menuWidth, menuLabel} = this.props
|
||||
const {
|
||||
actions,
|
||||
addNew,
|
||||
items,
|
||||
menuWidth,
|
||||
menuLabel,
|
||||
useAutoComplete,
|
||||
selected,
|
||||
} = this.props
|
||||
const {filteredItems, highlightedItemIndex} = this.state
|
||||
const menuItems = useAutoComplete ? filteredItems : items
|
||||
|
||||
return (
|
||||
<ul className="dropdown-menu" style={{width: menuWidth}}>
|
||||
{menuLabel
|
||||
? <li className="dropdown-header">{menuLabel}</li>
|
||||
: null
|
||||
}
|
||||
{items.map((item, i) => {
|
||||
<ul
|
||||
className={classnames('dropdown-menu', {
|
||||
'dropdown-menu--no-highlight': useAutoComplete,
|
||||
})}
|
||||
style={{width: menuWidth}}
|
||||
>
|
||||
{menuLabel ? <li className="dropdown-header">{menuLabel}</li> : null}
|
||||
{menuItems.map((item, i) => {
|
||||
if (item.text === 'SEPARATOR') {
|
||||
return <li key={i} role="separator" className="divider" />
|
||||
}
|
||||
return (
|
||||
<li className="dropdown-item" key={i}>
|
||||
<a href="#" onClick={() => this.handleSelection(item)}>
|
||||
<li
|
||||
className={classnames('dropdown-item', {
|
||||
highlight: i === highlightedItemIndex,
|
||||
active: item.text === selected,
|
||||
})}
|
||||
key={i}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => this.handleSelection(item)}
|
||||
onMouseOver={() => this.handleHighlight(i)}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
{actions.length > 0
|
||||
|
@ -78,8 +173,7 @@ class Dropdown extends Component {
|
|||
<button
|
||||
key={action.text}
|
||||
className="dropdown-item__action"
|
||||
onClick={e =>
|
||||
this.handleAction(e, action, item)}
|
||||
onClick={e => this.handleAction(e, action, item)}
|
||||
>
|
||||
<span
|
||||
title={action.text}
|
||||
|
@ -105,21 +199,44 @@ class Dropdown extends Component {
|
|||
}
|
||||
|
||||
renderLongMenu() {
|
||||
const {actions, addNew, items, menuWidth, menuLabel} = this.props
|
||||
const {
|
||||
actions,
|
||||
addNew,
|
||||
items,
|
||||
menuWidth,
|
||||
menuLabel,
|
||||
useAutoComplete,
|
||||
selected,
|
||||
} = this.props
|
||||
const {filteredItems, highlightedItemIndex} = this.state
|
||||
const menuItems = useAutoComplete ? filteredItems : items
|
||||
|
||||
return (
|
||||
<ul className="dropdown-menu" style={{width: menuWidth, height: DROPDOWN_MENU_MAX_HEIGHT}}>
|
||||
<ul
|
||||
className={classnames('dropdown-menu', {
|
||||
'dropdown-menu--no-highlight': useAutoComplete,
|
||||
})}
|
||||
style={{width: menuWidth, height: DROPDOWN_MENU_MAX_HEIGHT}}
|
||||
>
|
||||
<FancyScrollbar autoHide={false}>
|
||||
{menuLabel
|
||||
? <li className="dropdown-header">{menuLabel}</li>
|
||||
: null
|
||||
}
|
||||
{items.map((item, i) => {
|
||||
{menuLabel ? <li className="dropdown-header">{menuLabel}</li> : null}
|
||||
{menuItems.map((item, i) => {
|
||||
if (item.text === 'SEPARATOR') {
|
||||
return <li key={i} role="separator" className="divider" />
|
||||
}
|
||||
return (
|
||||
<li className="dropdown-item" key={i}>
|
||||
<a href="#" onClick={() => this.handleSelection(item)}>
|
||||
<li
|
||||
className={classnames('dropdown-item', {
|
||||
highlight: i === highlightedItemIndex,
|
||||
active: item.text === selected,
|
||||
})}
|
||||
key={i}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => this.handleSelection(item)}
|
||||
onMouseOver={() => this.handleHighlight(i)}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
{actions.length > 0
|
||||
|
@ -129,8 +246,7 @@ class Dropdown extends Component {
|
|||
<button
|
||||
key={action.text}
|
||||
className="dropdown-item__action"
|
||||
onClick={e =>
|
||||
this.handleAction(e, action, item)}
|
||||
onClick={e => this.handleAction(e, action, item)}
|
||||
>
|
||||
<span
|
||||
title={action.text}
|
||||
|
@ -164,33 +280,57 @@ class Dropdown extends Component {
|
|||
iconName,
|
||||
buttonSize,
|
||||
buttonColor,
|
||||
useAutoComplete,
|
||||
} = this.props
|
||||
const {isOpen} = this.state
|
||||
const {isOpen, searchTerm, filteredItems} = this.state
|
||||
const menuItems = useAutoComplete ? filteredItems : items
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={this.handleClick}
|
||||
className={classnames(`dropdown ${className}`, {open: isOpen})}
|
||||
>
|
||||
<div className={`btn dropdown-toggle ${buttonSize} ${buttonColor}`}>
|
||||
{iconName
|
||||
? <span className={classnames('icon', {[iconName]: true})} />
|
||||
: null}
|
||||
<span className="dropdown-selected">{selected}</span>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{(isOpen && items.length < DROPDOWN_MENU_ITEM_THRESHOLD)
|
||||
{useAutoComplete && isOpen
|
||||
? <div
|
||||
className={`dropdown-autocomplete dropdown-toggle ${buttonSize} ${buttonColor}`}
|
||||
>
|
||||
<input
|
||||
ref="dropdownAutoComplete"
|
||||
className="dropdown-autocomplete--input"
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
placeholder="Filter items..."
|
||||
spellCheck={false}
|
||||
onChange={this.handleFilterChange}
|
||||
onKeyDown={this.handleFilterKeyPress}
|
||||
value={searchTerm}
|
||||
/>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
: <div className={`btn dropdown-toggle ${buttonSize} ${buttonColor}`}>
|
||||
{iconName
|
||||
? <span className={classnames('icon', {[iconName]: true})} />
|
||||
: null}
|
||||
<span className="dropdown-selected">{selected}</span>
|
||||
<span className="caret" />
|
||||
</div>}
|
||||
{isOpen && menuItems.length < DROPDOWN_MENU_ITEM_THRESHOLD
|
||||
? this.renderShortMenu()
|
||||
: null}
|
||||
{(isOpen && items.length >= DROPDOWN_MENU_ITEM_THRESHOLD)
|
||||
{isOpen && menuItems.length >= DROPDOWN_MENU_ITEM_THRESHOLD
|
||||
? this.renderLongMenu()
|
||||
: null}
|
||||
{isOpen && !menuItems.length
|
||||
? <ul className="dropdown-menu">
|
||||
<li className="dropdown-empty">No matching items</li>
|
||||
</ul>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, shape, string, func} = PropTypes
|
||||
const {arrayOf, bool, shape, string, func} = PropTypes
|
||||
|
||||
Dropdown.propTypes = {
|
||||
actions: arrayOf(
|
||||
|
@ -218,6 +358,7 @@ Dropdown.propTypes = {
|
|||
buttonColor: string,
|
||||
menuWidth: string,
|
||||
menuLabel: string,
|
||||
useAutoComplete: bool,
|
||||
}
|
||||
|
||||
export default OnClickOutside(Dropdown)
|
||||
|
|
|
@ -17,7 +17,7 @@ const errorsMiddleware = store => next => action => {
|
|||
const {auth: {me}} = store.getState()
|
||||
|
||||
if (action.type === 'ERROR_THROWN') {
|
||||
const {error: {status, auth}, altText} = action
|
||||
const {error: {status, auth}, altText, alertType = 'error'} = action
|
||||
|
||||
if (status === HTTP_FORBIDDEN) {
|
||||
const wasSessionTimeout = me !== null
|
||||
|
@ -26,7 +26,7 @@ const errorsMiddleware = store => next => action => {
|
|||
|
||||
if (wasSessionTimeout) {
|
||||
store.dispatch(
|
||||
notify('error', 'Session timed out. Please login again.')
|
||||
notify(alertType, 'Session timed out. Please login again.')
|
||||
)
|
||||
|
||||
allowNotifications = false
|
||||
|
@ -35,9 +35,9 @@ const errorsMiddleware = store => next => action => {
|
|||
}, notificationsBlackoutDuration)
|
||||
}
|
||||
} else if (altText) {
|
||||
store.dispatch(notify('error', altText))
|
||||
store.dispatch(notify(alertType, altText))
|
||||
} else {
|
||||
store.dispatch(notify('error', 'Cannot communicate with server.'))
|
||||
store.dispatch(notify(alertType, 'Cannot communicate with server.'))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,30 +12,16 @@ $dropdown-menu-max-height: 270px;
|
|||
Generic width modifiers
|
||||
Use instead of creating new classes if possible
|
||||
*/
|
||||
.dropdown .dropdown-toggle {
|
||||
.dropdown .dropdown-toggle,
|
||||
.dropdown .dropdown-autocomplete {
|
||||
width: 120px; /* Default width */
|
||||
}
|
||||
.dropdown {
|
||||
&-80 .dropdown-toggle {width: 80px;}
|
||||
&-90 .dropdown-toggle {width: 90px;}
|
||||
&-100 .dropdown-toggle {width: 100px;}
|
||||
&-110 .dropdown-toggle {width: 110px;}
|
||||
&-120 .dropdown-toggle {width: 120px;}
|
||||
&-130 .dropdown-toggle {width: 130px;}
|
||||
&-140 .dropdown-toggle {width: 140px;}
|
||||
&-150 .dropdown-toggle {width: 150px;}
|
||||
&-160 .dropdown-toggle {width: 160px;}
|
||||
&-170 .dropdown-toggle {width: 170px;}
|
||||
&-180 .dropdown-toggle {width: 180px;}
|
||||
&-190 .dropdown-toggle {width: 190px;}
|
||||
&-200 .dropdown-toggle {width: 200px;}
|
||||
&-210 .dropdown-toggle {width: 210px;}
|
||||
&-220 .dropdown-toggle {width: 220px;}
|
||||
&-230 .dropdown-toggle {width: 230px;}
|
||||
&-240 .dropdown-toggle {width: 240px;}
|
||||
&-250 .dropdown-toggle {width: 250px;}
|
||||
@for $i from 8 through 30 {
|
||||
&-#{$i * 10} .dropdown-toggle,
|
||||
&-#{$i * 10} .dropdown-autocomplete { width: #{$i * 10}px; }
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
position: relative;
|
||||
text-align: left;
|
||||
|
@ -65,7 +51,54 @@ $dropdown-menu-max-height: 270px;
|
|||
.dropdown .dropdown-toggle.btn-xs {
|
||||
height: 22px !important;
|
||||
line-height: 22px !important;
|
||||
padding: 0 9px !important;
|
||||
padding: 0 9px;
|
||||
}
|
||||
|
||||
/*
|
||||
AutoComplete Field
|
||||
----------------------------------------------
|
||||
*/
|
||||
.dropdown-autocomplete {
|
||||
position: relative;
|
||||
padding: 0 !important;
|
||||
|
||||
&.btn-xs {height: 22px;}
|
||||
&.btn-sm {height: 30px;}
|
||||
&.btn-md {height: 36px;}
|
||||
&.btn-lg {height: 50px;}
|
||||
}
|
||||
.dropdown-autocomplete--input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: $g20-white;
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
|
||||
.btn-xs & {padding: 0 18px 0 9px; font-size: 12px;}
|
||||
.btn-sm & {padding: 0 18px 0 9px; font-size: 13px;}
|
||||
.btn-md & {padding: 0 34px 0 17px; font-size: 14px;}
|
||||
.btn-lg & {padding: 0 48px 0 24px; font-size: 18px;}
|
||||
|
||||
&::-webkit-input-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; }
|
||||
&::-moz-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; }
|
||||
&:-ms-input-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; }
|
||||
&:-moz-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; }
|
||||
|
||||
&:focus {
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
.dropdown-empty {
|
||||
padding: 7px 9px;
|
||||
font-size: 13px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
font-weight: 500;
|
||||
line-height: 15px;
|
||||
@include no-user-select();
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -86,6 +119,9 @@ $dropdown-menu-max-height: 270px;
|
|||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.active {
|
||||
@include gradient-h($c-sapphire, $c-pool);
|
||||
}
|
||||
&:hover {
|
||||
@include gradient-h($c-laser, $c-pool);
|
||||
}
|
||||
|
@ -99,7 +135,7 @@ $dropdown-menu-max-height: 270px;
|
|||
padding: 7px 9px;
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
font-weight: 500;
|
||||
font-weight: 500 !important;
|
||||
color: $c-yeti !important;
|
||||
background-color: transparent;
|
||||
transition:
|
||||
|
@ -118,6 +154,16 @@ $dropdown-menu-max-height: 270px;
|
|||
@include gradient-h($c-ocean, $c-pool);
|
||||
}
|
||||
}
|
||||
li.dropdown-item.highlight {
|
||||
&, &:hover {
|
||||
@include gradient-h($c-laser, $c-pool);
|
||||
}
|
||||
> a {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown.dropdown-kapacitor .dropdown-toggle {
|
||||
color: $c-rainforest !important;
|
||||
|
@ -137,6 +183,16 @@ $dropdown-menu-max-height: 270px;
|
|||
color: $g20-white !important;
|
||||
}
|
||||
}
|
||||
li.dropdown-item.highlight {
|
||||
&, &:hover {
|
||||
@include gradient-h($c-laser, $c-rainforest);
|
||||
}
|
||||
> a {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown.dropdown-chronograf .dropdown-menu {
|
||||
@include custom-scrollbar($c-comet, $c-potassium);
|
||||
|
@ -151,8 +207,36 @@ $dropdown-menu-max-height: 270px;
|
|||
color: $g20-white !important;
|
||||
}
|
||||
}
|
||||
li.dropdown-item.highlight {
|
||||
&, &:hover {
|
||||
@include gradient-h($c-laser, $c-comet);
|
||||
}
|
||||
> a {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dropdown Menu (only js highlighting, works with autocomplete feature)
|
||||
----------------------------------------------
|
||||
*/
|
||||
.dropdown-menu.dropdown-menu--no-highlight {
|
||||
li.dropdown-item {
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
li.dropdown-item.highlight {
|
||||
&, &:hover {
|
||||
@include gradient-h($c-laser, $c-pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
Dropdown Header
|
||||
----------------------------------------------
|
||||
|
@ -256,4 +340,4 @@ $dropdown-menu-max-height: 270px;
|
|||
background-color: $g2-kevlar !important;
|
||||
border-color: $c-pool !important;
|
||||
box-shadow: 0 0 6px 0px $c-pool !important
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,10 +80,13 @@ $template-control-dropdown-min-width: 118px;
|
|||
}
|
||||
.dropdown-menu {
|
||||
@include gradient-h($c-star,$c-pool);
|
||||
@include custom-scrollbar-round($c-pool,$c-laser);
|
||||
|
||||
li.dropdown-item {
|
||||
&:hover {@include gradient-h($c-comet,$c-pool);}
|
||||
&, &:hover {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
&.active {@include gradient-h($c-amethyst,$c-pool);}
|
||||
}
|
||||
li.dropdown-item > a {
|
||||
&, &:focus {background: none;}
|
||||
|
@ -91,6 +94,14 @@ $template-control-dropdown-min-width: 118px;
|
|||
font-size: 12px;
|
||||
font-family: $code-font;
|
||||
}
|
||||
li.dropdown-item.highlight {
|
||||
&, &:hover {@include gradient-h($c-comet,$c-pool);}
|
||||
> a {
|
||||
color: $g20-white;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.template-control--label {
|
||||
|
|
Loading…
Reference in New Issue